【每日一题】上海计算机学会2023年6月月赛 乙组T1 两数归零(map 二分查找)

文章讲述了如何解决一个关于给定n个整数的两数归零问题,通过暴力枚举、二分查找以及使用C++STL中的map数据结构来优化解决方案。暴力枚举方法时间复杂度为O(n^2),而二分查找和map方法则显著提高效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目描述

这是上海计算机学会竞赛P817:两数归零
标签 m a p map map、二分查找
给定 n n n 个整数 a 1 , a 2 , a 3 , . . . , a n a_1,a_2,a_3,...,a_n a1,a2,a3,...,an,请统计有多少二元组 i , j i,j i,j 满足 i < j i<j i<j 且满足 a i + a j = 0 a_i+a_j=0 ai+aj=0
数据范围: 1 < = n < = 300000 1<=n<= 300000 1<=n<=300000 − 1 , 000 , 000 , 000 ≤ a i ≤ 1 , 000 , 000 , 000 −1,000,000,000≤a_i≤1,000,000,000 1,000,000,000ai1,000,000,000

暴力枚举解法

很多同学看到题目就想着直接两层循环模拟,这样的时间复杂度是 O ( n 2 ) O(n^2) O(n2) 即最坏情况: 9 ∗ 1 0 10 9*10^{10} 91010,很明显会超时,这种方式能拿 60 60 60 分。
60 60 60 分代码

#include <bits/stdc++.h>
using namespace std;

int a[300005];

int main() {
    int n, ans = 0;
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = 1; i <= n; i++) {
        for (int j = i + 1; j <= n; j++) {
            if (a[i] + a[j] == 0) ans++;
        }
    }
    cout << ans << endl;
    return 0;
}

二分查找解法

我们课上讲过,可以通过二分去找到某个数字在有序序列中第一次出现的下标位置最后一次出现的下标位置,那么这个区间长度就是这个数字在这个序列中出现的次数。

当然手写二分,这也太累了吧。所以我们决定调用 S T L STL STL 中的二分查找函数。

l o w e r _ b o u n d ( a , a + n , v a l ) lower\_bound(a,a+n,val) lower_bound(a,a+n,val):找到有序序列 a a a 中大于等于 v a l val val 的第一个位置

u p p e r _ b o u n d ( a , a + n , v a l ) upper\_bound(a,a+n,val) upper_bound(a,a+n,val):找到有序序列 a a a 中大于 v a l val val 的第一个位置

通过位置差值获取 v a l val val在这个有序序列中的个数,那么回到这道题,我们可以先给 a a a数组排序,然后遍历每一个数 a i a_i ai,然后通过以上讲解的方式去得到这个有序序列中 − a i -a_i ai的个数,然后把个数进行一个累计。

当然,因为题目中要求 i < j i<j i<j,我们直接这样找会有重复的情况出现,比如第 1 1 1项和第 3 3 3项之和为 0 0 0,我们遍历到第 3 3 3项的时候又会把第 1 1 1项之和为 0 0 0的情况算上去,所以最终累计的计数要除以 2 2 2

代码

#include <bits/stdc++.h>
using namespace std;

int a[300005];

int main() {
    int n, ans = 0;
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    sort(a + 1, a + 1 + n);

    for (int i = 1; i <= n; i++) {
        int l = lower_bound(a + 1, a + 1 + n, -a[i]) - a;
        int r = upper_bound(a + 1, a + 1 + n, -a[i]) - a;
        ans += r - l;
    }

    cout << ans / 2 << endl;
    return 0;
}

m a p 解法 map解法 map解法

直接从前往后遍历一遍,到第 i i i个数 a i a_i ai的时候,看看前面 − a i -a_i ai的个数,把计数加上去,最后就是答案了。该题 a i a_i ai数据范围比较大,并且有负数,所以我们需要用 m a p map map维护一下。
代码

#include <bits/stdc++.h>
using namespace std;

int a[300005];
map<int, int> m;

int main() {
    int n, ans = 0;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        ans += m[-a[i]];
        m[a[i]]++;
    }
    cout << ans << endl;
    return 0;
}
<think>首先,用户的问题是关于上海计算机学会20251C++乙组T2题目的解题思路,题目是“元音切片”。用户提供了参考引用,其中引用[1]详细描述了题目的代码和逻辑,引用[2]似乎是一些个人笔记,不相关,所以我应该忽略引用[2]。 用户指令要求: - 所有行内数学表达式使用$...$格式。 - 独立公式使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 在回答中引用的段落末尾自然地添加引用标识,如[^1]。 回答结构: - 结构清晰,帮助用户逐步解决问题。 - 保证回答真实可靠,基于提供的引用。 关键点: - 题目是关于字符串处理,具体是元音和辅音的计数。 - 引用[1]中的代码和描述是核心:题目要求处理字符串,将元音序列分成每k个元音一组,计算分割方案数。 - 逻辑:遍历字符串,计数元音;当遇到一组k个元音时,考虑与上一组之间的辅音数量,分割方案数基于辅音数量(方案数为辅音数加一),使用乘法原理累乘这些方案数。 - 代码中:ans 初始为1,cnt 计数元音,x 计数辅音,flag 标记第一组元音。 解题思路步骤: 1. **理解问题**:给定字符串,包含元音(a,e,i,o,u)和辅音。要求将元音序列分成连续的段,每段恰好k个元音。计算所有可能的分割方案数。 2. **关键观察**:分割点只能在元音组之间,具体是每两段k元音之间的辅音序列中,可以选择任意位置作为分割点(包括两端),所以方案数为辅音数量加一。 3. **算法**: - 遍历字符串。 - 维护当前元音计数 cnt。 - 当 cnt 达到 k 时重置为0,表示完成一组。 - 在每组开始时(cnt==1),如果不是第一组,则计算与上一组之间的辅音数量 x,ans 乘上 (x+1),并取模。 - 辅音计数 x 只在元音组之间计数。 4. **边界处理**:第一组元音不需要分割,所以用 flag 标记是否已开始第一组。 5. **复杂度**:O(n) 时间,一次遍历。 在回答中,我需要: - 用中文解释。 - 使用数学表达式:例如,方案数计算为 $(x + 1)$。 - 生成相关问题:在最后添加相关问题部分。 - 添加引用:由于引用[1]提供了代码和描述,我应该在解释中自然地引用它。 回答结构: 1. **问题概述**:简述题目要求。 2. **解题思路**:逐步解释逻辑。 3. **代码解释**:简要解释引用[1]中的代码关键部分。 4. **总结**:强调核心思想。 5. **相关问题**:生成2-3个相关问题。 相关问题想法: - 如何优化这个算法? - 如果k不是固定的,如何处理? - 类似字符串分割问题的其他应用。 - 错误处理或边界情况。 确保LaTeX正确: - 行内数学:例如,方案数为 $(x + 1)$。 - 独立公式:可能不需要,因为问题不涉及复杂数学。如果有,比如概率计算,但这里没有。 最终回答: - 以用户可见层开始。 - 保持专业和帮助性。</think>### 元音切片解题思路 用户查询的是上海计算机学会20251C++乙组T2“元音切片”题目的解题思路。这是一个字符串处理问题,要求计算将字符串中的元音序列分割成每段恰好$k$个元音的所有可能方案数(结果对$1000000007$取模)。元音指字母 `a`, `e`, `i`, `o`, `u`,其他字母为辅音。下面我将基于参考引用[1]中的代码和描述,逐步解释解题思路,确保逻辑清晰可靠。 #### 1. **问题理解与关键观察** - **输入**:字符串$s$(长度$n$),整数$k$(每段元音的数量)。 - **输出**:所有分割方案数(模$1000000007$)。分割规则:元音序列必须被分成连续段,每段恰好$k$个元音;分割点只能在元音组之间的辅音序列中选择。 - **核心观察**: - 分割方案只取决于元音组之间的辅音序列。对于每两段$k$元音之间的辅音序列(长度为$x$),分割点可以是辅音之间的任意间隙(包括两端),因此方案数为$(x + 1)$。 - 整体方案数使用乘法原理:将所有元音组之间的方案数相乘,即 $\prod (x_i + 1)$,其中$x_i$是第$i$组与第$i+1$组元音之间的辅音数量。 - 第一组元音之前的分割不需要计算方案(作为起始点)。 - **数学表示**:设元音组数为$m$,则总方案数为: $$ \text{ans} = \prod_{i=1}^{m-1} (x_i + 1) $$ 其中$x_i$是第$i$组和第$i+1$组之间的辅音数量[^1]。 #### 2. **算法步骤** - **初始化**: - `ans = 1`(方案数初始值,乘法原理的基数)。 - `cnt = 0`(当前元音计数,用于跟踪是否凑够$k$个元音)。 - `x = 0`(当前两段元音之间的辅音数量)。 - `flag = false`(标记是否遇到第一组元音;初始未开始)。 - **遍历字符串**(每个字符): - **如果是元音**: - `cnt++`(元音计数加一)。 - 如果 `cnt == 1`(表示新元音组的开始): - 如果 `flag == false`,则设置 `flag = true`(标记第一组开始)。 - 否则(非第一组),将方案数更新:`ans = ans * (x + 1) % 1000000007`(乘法原理,乘上当前辅音序列的方案数)。 - 重置 `x = 0`(辅音计数归零,准备下一段)。 - 如果 `cnt == k`,则重置 `cnt = 0`(完成一组$k$元音)。 - **如果是辅音**: - 如果 `cnt == 0`(表示当前处于两段元音之间),则 `x++`(辅音计数加一)。 - **输出结果**:遍历结束后,`ans` 即为总方案数。 #### 3. **代码解释(基于引用[1])** 引用[1]中的C++代码实现了上述逻辑。以下是关键部分解析: ```cpp #include<bits/stdc++.h> using namespace std; int main() { cin >> t; // 测试用例数 while(t--) { cin >> n >> k >> s; // 输入字符串长度n, 分段大小k, 字符串s long long ans = 1; int cnt = 0, x = 0; bool flag = 0; for(int i = 0; i < n; i++) { if (s[i] == 'a' || s[i] == 'e' || s[i] == 'i' || s[i] == 'o' || s[i] == 'u') { // 元音判断 cnt++; if (cnt == 1) { if (flag == 0) flag = 1; // 第一组开始 else { ans = ans * (x + 1) % 1000000007; // 乘法原理更新方案数 } x = 0; // 重置辅音计数 } cnt %= k; // 当cnt==k时重置为0 } else { // 辅音 if (cnt == 0) x++; // 只计数两段元音之间的辅音 } } cout << ans << endl; } return 0; } ``` - **时间复杂度**:$O(n)$,一次遍历字符串。 - **空间复杂度**:$O(1)$,仅用常数额外变量。 - **边界处理**: - 如果字符串中元音总数不足$k$个,则无法分割,方案数为0(但代码中`ans`初始为1,需确保输入合法)。 - 第一组元音忽略方案计算,避免多余乘法。 - 取模操作确保结果不溢出[^1]。 #### 4. **核心思想总结** - **乘法原理**:分割方案由独立的选择点决定,每个元音组间的方案数$(x_i + 1)$ 相乘。 - **贪心遍历**:通过计数元音和辅音,动态识别分割点,无需额外存储。 - **实际应用**:类似问题可用于文本处理或数据压缩,如固定长度模式的分割。 如果您有具体输入案例需要测试或优化建议,我可以进一步解释! ### 相关问题 1. 如果$k$值不固定(如每段元音数可变),如何修改算法? 2. 如何处理字符串中包含大写字母或特殊字符的情况? 3. 这个算法在哪些实际场景中(如自然语言处理)有应用?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值