2 条题解
-
0
#include <bits/stdc++.h> using namespace std; int main(){ int n; cin >> n; int arr[105]; // 建议开稍微大一点的固定数组,不要用 int a[n] for(int i = 0; i < n; i++){ cin >> arr[i]; // 修正1:用 i 做下标 } int cnt = 0; // 第一层:枚举每一个目标数 arr[k] for(int k = 0; k < n; k++){ bool found = false; // 标记:是否找到了组成 arr[k] 的两个数 // 第二层:枚举第一个加数 arr[i] for(int i = 0; i < n; i++){ // 第三层:枚举第二个加数 arr[j] // 修正问题:a<n 是可以的,只要 j 从 i+1 开始即可 for(int j = i + 1; j < n; j++){ // 修正3:比较的是数值 arr[...],而不是下标 k, i, j if(arr[k] == arr[i] + arr[j]){ cnt++; found = true; // 标记找到了 break; // 跳出最内层循环 (j循环) } } if(found) break; // 修正4:如果在内层找到了,也要跳出中层循环 (i循环),去测试下一个目标数 } } cout << cnt << endl; return 0; } -
0
P2141 这道题(NOIP 2014 普及组 T1)是一道非常经典的枚举(暴力)题目。虽然题目简单,但有一个非常容易踩的“坑”,导致很多同学拿不到满分。
下面我给你梳理一下思路和注意事项:
1. 题目核心意图
我们要统计的是:集合中有多少个数,可以由集合中另外两个不同的数相加得到。 关键点:求的是“有多少个这样的数”,而不是“有多少对加数”。
2. 思路一:三重循环(最直观,不容易错)
因为数据范围很小,,这意味着 的复杂度大约是 次运算,计算机完全可以在 1 秒内跑完。
我们可以按照逻辑直接翻译:
- 第一层循环:枚举每一个数,把它当作“被测验的目标数”(记为
target)。 - 第二层循环:枚举数组中的一个加数(记为
a)。 - 第三层循环:枚举数组中的另一个加数(记为
b)。- 判断条件:如果
a + b == target,且a、b、target是三个不同位置的数(题目保证集合中数各不相同,所以只要下标不同即可)。 - 计数并跳出:一旦找到了这样的一对
a和b,说明这个target是符合条件的。计数器 +1,并且立刻break跳出内层循环(或者是跳出针对当前target的寻找)。- 为什么要跳出? 因为我们只关心
target能不能被算出来,不关心它能被几种方式算出来。比如 ,也是 ,只要算出一次,5 这个数就算 1 个答案。
- 为什么要跳出? 因为我们只关心
- 判断条件:如果
3. 思路二:两重循环 + 标记数组(更快,更像高手的写法)
如果你想优化一下,可以用空间换时间(利用桶/标记的思想):
- 先用一个布尔数组(或者 int 数组)
exist,标记哪些数字在输入的集合中出现过。比如输入有 3,那么exist[3] = 1。 - 双重循环枚举所有可能的加数对
a[i]和a[j]()。 - 计算和
sum = a[i] + a[j]。 - 判断
sum是否存在于原集合中(直接查exist[sum]是否为 true)。 - 关键去重:
- 如果
sum存在,我们不能直接ans++。因为可能有 和 ,这样 5 就会被算两次。 - 你需要再开一个
counted数组,记录这个数是否已经被统计过了。 - 或者,如果你用思路一的方法,就不会有这个问题。
- 如果
4. 最大的坑(一定要看!)
很多同学写成下面的逻辑,结果导致样例都过不去或者不得分:
循环 i (第一个加数) 循环 j (第二个加数) 如果 (a[i] + a[j] 在数组里) 计数器 ans++错误原因: 假如集合是
{1, 2, 3, 4, 5}。- ,你让 ans+1。
- ,你又让 ans+1。
- 结果“5”这个数字被你贡献了 2 次答案。但题目问的是“有多少个数”,5 只能算 1 个。
5. 总结建议
对于 的数据,我推荐你使用思路一,逻辑最清晰:
- 读入数据。
- 外层循环枚举每一个数作为“目标”。
- 内层两重循环去尝试拼凑这个目标。
- 拼凑成功就
ans++并break(处理下一个目标)。
祝你 AC!快去试着写代码吧。
但是这个target是两个另外两个元素之和是不确定的,应该怎么处理?
你的困惑点非常棒,这正是这道题逻辑的核心所在!
既然“不知道哪两个数相加等于哪个数”,我们就不能通过“算”直接得出答案,而是要通过**“试”(枚举)**来解决。
计算机最擅长的就是把所有可能性都试一遍。我们可以采用**“固定嫌疑人,寻找证人”**的思路。
核心思路:固定目标法(三重循环)
既然我们不知道数组里哪个数是“和”,那我们就轮流假设每一个数都是那个“和”(Target),然后去数组里找有没有另外两个数加起来等于它。
具体的步骤演示:
假设输入数组是
a[] = {1, 2, 3, 4}。第一轮:假设
a[0](也就是 1) 是那个 Target- 我们需要在剩下的数里找:有没有两个数相加等于 1?
- 我们遍历
1+2,1+3,2+3... 发现都没有。 - 结论:1 不是我们要找的数。
第二轮:假设
a[1](也就是 2) 是那个 Target- 我们找有没有两个数相加等于 2?(注意题目说要是正整数集合)
- 遍历发现没有。
- 结论:2 也不符合。
第三轮:假设
a[2](也就是 3) 是那个 Target- 我们开始尝试找加数:
- 试
1 + 2?等于 3!Bingo! - 既然找到了,说明 3 是符合条件的。
- 计数器 +1。
- 重点:立刻停止针对 3 的查找(Break),转而去测试下一个数。如果不停止,后面可能还有别的组合也能算出 3,就会重复计数。
- 试
第四轮:假设
a[3](也就是 4) 是那个 Target- 试
1 + 2= 3 (不等于4) - 试
1 + 3= 4 (Bingo!) - 计数器 +1。Break。
代码逻辑结构
为了实现上面这个过程,我们需要三层循环(就像我之前提到的思路一):
// 1. 外层循环:枚举每一个“嫌疑人” (Target) for (int k = 0; k < n; k++) { // 在这里加一个标记,防止同一个数被多次统计 // 其实只要找到一次就可以直接 break 跳出内层循环 // 2. 中层循环:枚举第一个加数 for (int i = 0; i < n; i++) { // 3. 内层循环:枚举第二个加数 for (int j = i + 1; j < n; j++) { // j从i+1开始,避免重复且避免自己加自己 // 这里的 a[k] 就是我们要验证的 Target // a[i] 和 a[j] 就是两个加数 // 必须保证加数和结果不是同一个数(题目要求不同的数) // 虽然 a[i]+a[j]==a[k] 且全是正整数时,k肯定不等于i或j,但为了逻辑严谨可以不管它, // 只要关注下标 i, j, k 不重复即可,或者直接看数值。 if (a[i] + a[j] == a[k]) { ans++; // 找到了!记一笔 goto next_target; // 找到了就不找了,立刻测试下一个嫌疑人 (用 break 加 标记 也可以) } } } next_target:; // 标签位置 }总结
不要去想“谁和谁加等于谁”,而是要**“拿出一个数,问问剩下的数能不能拼出它”**。
- Target 不确定? 用外层循环
k把它变得确定。 - 加数不确定? 用内层循环
i和j把所有组合都试一遍。
试着把这个逻辑写成代码吧!记得处理好**“找到一个组合就停止,不要重复统计同一个 Target”**这个细节。
这么写对吗?
#include <bits/stdc++.h> using namespace std; int main(){ int n; cin>>n; int a[n]={0}; for(int i=0;i<n;i++){ cin>>a[n]; } int cnt=0; for(int t=0;t<n;t++){ for(int a=0;a<n-1;a++){//可以写a<n吗? for(int b=a+1;b<n;b++){ if(t==a+b){ cnt++; break;//找到了就及时break避免多余检查 } } } } cout<<cnt<<endl; }同学,这份代码有4个非常严重的逻辑错误,会导致无法运行或者答案错误。
别急,我们一个个来修:
1. 致命错误:输入数组下标越界
cin >> a[n];错误原因:
- 数组下标是从
0到n-1的。a[n]这个位置已经越界了(超出数组范围)。 - 而且你在循环里一直往同一个位置
a[n]读入数据,前面的数据全被覆盖了,数组里并没有存下所有的数。
修改:
cin >> a[i]; // 用循环变量 i 作为下标2. 致命错误:变量名冲突(Shadowing)
int a[n] = {0}; // 这里定义了数组叫 a // ... for(int a=0; a<n-1; a++){ // 这里定义了循环变量叫 a错误原因:
- 你在
for循环里定义了一个整型变量a,这会**屏蔽(遮挡)**掉外面的数组a。 - 在这个循环内部,当你写
a时,编译器认为它是那个循环变量(整数),而不是数组。这会导致你无法访问数组里的元素。
修改: 请老老实实遵循 OI 界的传统,循环变量用
i,j,k。- 外层
t(Target) 改为k(习惯用法,或者保持i也行)。 - 中层
a改为i。 - 内层
b改为j。
3. 逻辑错误:比较的是下标,不是数值
if(t == a + b)错误原因:
- 这里
t,a,b在你的代码里是下标(也就是第几个数)。 - 题目要求的是**“集合中的数”**相加,而不是它们的“位置编号”相加。
修改: 应该去数组里取值比较:
if( arr[k] == arr[i] + arr[j] ) // 假设数组名改成了 arr,循环变量改成了 k, i, j4. 逻辑错误:
break跳出不完全(会导致重复计数)if(match){ cnt++; break; // 只能跳出最内层的 b 循环 }错误原因:
break只能跳出一层循环。- 假如目标数是
10,数组里有2, 8和3, 7。- 当找到
2+8=10时,cnt加 1,跳出b循环。 - 但是!中间的
a循环还在继续。它会继续走到3,然后内层循环又找到7。 3+7=10,cnt又加 1。- 结果:数字
10被统计了两次,答案偏大。
- 当找到
修改: 你需要一个机制来彻底停止对当前 Target 的检查。可以用一个
bool标记。
回答你的小问题
//可以写a<n吗?回答:可以。 虽然写
a < n在逻辑上没错(因为内层b是从a+1开始的,如果a到了n-1,内层循环根本不会执行),但通常为了严谨和微小的效率,写a < n也没问题,不会导致报错,仅仅是多一次无效判断。
修改后的正确代码参考
我把变量名改成了
arr以免混淆,循环变量用了i, j, k。#include <bits/stdc++.h> using namespace std; int main(){ int n; cin >> n; int arr[105]; // 建议开稍微大一点的固定数组,不要用 int a[n] for(int i = 0; i < n; i++){ cin >> arr[i]; // 修正1:用 i 做下标 } int cnt = 0; // 第一层:枚举每一个目标数 arr[k] for(int k = 0; k < n; k++){ bool found = false; // 标记:是否找到了组成 arr[k] 的两个数 // 第二层:枚举第一个加数 arr[i] for(int i = 0; i < n; i++){ // 第三层:枚举第二个加数 arr[j] // 修正问题:a<n 是可以的,只要 j 从 i+1 开始即可 for(int j = i + 1; j < n; j++){ // 修正3:比较的是数值 arr[...],而不是下标 k, i, j if(arr[k] == arr[i] + arr[j]){ cnt++; found = true; // 标记找到了 break; // 跳出最内层循环 (j循环) } } if(found) break; // 修正4:如果在内层找到了,也要跳出中层循环 (i循环),去测试下一个目标数 } } cout << cnt << endl; return 0; } - 第一层循环:枚举每一个数,把它当作“被测验的目标数”(记为
- 1
信息
- ID
- 3584
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者