GESP C++一级真题“交朋友”:数组下标与去重逻辑全解析

发布时间:2026/10/3 9:53:54
GESP C++一级真题“交朋友”:数组下标与去重逻辑全解析 GESP 2026年3月的C一级考完好几个孩子跑来找我说“交朋友”这道题样例一看就明白但一提交就心里打鼓为什么有人答案翻了一倍为什么三个人互相写却不计数这种“看着简单、拿不准”的题恰恰是GESP一级最真实的考法。这篇博文不搞虚的直接用考生回忆还原题面给出一份能提交运行的C题解再把去重逻辑、易错点和自测方法全部摊开讲。打算考GESP一级的同学、辅导孩子的家长、带竞赛班的老师都可以拿来做参考。说明一下题目文字根据当天多位考生的回忆整理不是官方标准原文中文表达和样例细节可能与原卷存在细微差异但考点和解题思路是一致的。1. 真题长什么样先把“交朋友”完整还原1.1 GESP C一级到底考什么GESP由中国计算机学会主办是面向青少年的编程能力等级认证分成一到八级C一级是很多孩子接触算法竞赛的第一道坎。它的考试范围其实相当朴素顺序结构、分支结构、循环结构、一维数组再加上最基础的输入输出。没有贪心没有递归没有结构体甚至连函数都不强制要求掌握。可为什么每年一级通过率依然让人揪心因为GESP命题有个习惯把简单的知识点放进一个很长、很生活化的故事里让学生先做“阅读理解”再写代码。“交朋友”就是非常典型的一题。很多家长误解C一级主要考语法其实一级更看重“能不能把自然语言描述翻译成程序逻辑”。比如“如果A写了B且B也写了A才算一对朋友”这句话听起来谁都会读可放到判题机里就变成“先判断再计数的条件语句”。“交朋友”这道题表面是数组题实质是一道考察关系建模和循环边界的题目。下面这张表可以帮助你理解考点落点GESP一级大纲考点“交朋友”的实际落点一维数组用下标代表学号存储每位同学写下的学号循环结构遍历每一位同学逐一检查朋友关系分支判断判断是否满足“你写我、我也写你”计数变量用ans统计成功结成的好朋友对数1.2 题面整理考生回忆版题目名称叫“交朋友”。大意如下班级里有 n 位同学学号分别为 1 到 n。老师组织了一次交朋友活动每位同学在一张纸条上写下自己最想交朋友的另一位同学的学号。如果 A 同学写的学号是 B并且 B 同学写的学号也是 A那么 A 和 B 就算成功交上了朋友。请你统计全班一共有多少对成功交上朋友输入格式 第一行是一个正整数 n表示班级人数。 第二行有 n 个正整数第 i 个数表示第 i 位同学写下的学号。输出格式 输出一个整数表示成功交朋友的对数。数据范围2 ≤ n ≤ 10000并且保证每位同学写下的学号不等于自己的学号。我给出一份考场回忆版的样例输入 7 2 1 4 6 3 3 2 输出 1用刚才的条件人工核对一下1号同学写了2号2号同学写了1号两人互相选择这是第一对成功的朋友。3号写4号4号写6号6号写3号三个人虽然形成了一个环但没有任何两个人互相写对方所以不能算朋友对。5号写了3号3号没有写5号7号写了2号2号没有写7号这两个都是单向箭头也不能算。所以最终答案是1。1.3 读题阶段千万别漏的4个信息读题是第一道关卡很多孩子代码写错不是因为不会写而是漏看了题面里的关键信息。我总结出四个点第一学号从1开始。这意味着数组下标最好也从1开始用让want[1]对应1号同学而不是从0开始再加一减一平白增加出错概率。第二计数的单位是“对”不是“个”。一对成功朋友包含两个人但题目要的是有多少对。如果最后数出来4指的是4对不是8个人。第三必须“双向奔赴”。A写B、B写A才算一对。单向箭头不算三人环也不算案例里的三人循环就是专门用来排除“单箭头”和“环”这两种错误理解的。第四n最大到10000。这个范围说明O(n²)的双重循环在数据上限之下勉强能跑但没必要。最优做法是O(n)一趟扫描这也是标准答案该有的样子。2. 核心解题思路用数组下标代替学号2.1 为什么说这道题考的是“关系建模”刚学数组的孩子很容易把数组当成“一堆数字的容器”却忽略了数组最值钱的能力用下标表达位置。在“交朋友”里学号天生就是下标第i位同学写下的学号正好可以存进want[i]。这一步想通了整道题就完成了一半。这道题的底层逻辑很像图论里的“有向边”每个同学是一条箭头箭头指向他想交朋友的人。一对成功的友谊在图上就是两条方向相反的边也就是A指向B、B指向A形成一个长度最短的环。一年级不要求懂图论术语但这个场景你能想象成“互加好友”你关注了对方对方也关注了你系统才会提示你们成为好友。程序里没有“系统提示”只有我们写下的条件判断。想明白这一点很多变式题也就通了。如果题目改成“统计谁收到的好友申请最多”那就是另外开一个计数器数组如果改成“统计至少被一个人写到过的同学有多少”那就是用一个bool数组做标记。一题吃透能带出一片。2.2 判断“互相做朋友”的那行条件核心逻辑一句话对于当前第 i 位同学记他写下的学号为 j也就是 want[i] j。这时只需要检查一件事want[j] 是不是等于 i。如果相等说明 j 同学写下的学号正好也是 i两个人双向选择答案加一。写成代码就是int j want[i]; if (want[j] i) { ans; }千万不要把判断条件写成 want[i] j因为 want[i] 本来就是 j这个条件恒成立没有意义。判断得是“对方有没有写我”而不是“我有没有写对方”。这一点看上去简单实际考试里写反的人不在少数。你可以类比递纸条你递纸条给同桌不算友谊必须同桌也回你一张纸条写着你的名字这才成立。这里还要补充一个细节题目保证每个人写的学号不等于自己所以不会出现“我写我自己”的情况。这样一来我们甚至不需要额外判断 j 是否等于 i因为输入数据已经帮我们保证掉了。看清题目给的每一条限制条件就是出题人在帮你降低思考成本。2.3 去重的三种写法以及我推荐第一种的原因如果直接遍历每一个 i只要满足 want[want[i]] i 就计数你会发现答案比真实值多了一倍。原因很好理解1号写2号、2号写1号这一对遍历到 i1 时会满足条件遍历到 i2 时也会满足条件。于是一对朋友被数了两次。去重是这道题真正的分水岭。我至少见过三种可行的写法第一种只让学号小的一方来“认领”这对朋友。判断时加上一个条件 i want[i]也就是当前学号小于对方学号才计数。比如枚举到1号时1 2 成立计数枚举到2号时2 1 不成立不计数。每对朋友只有较小编号的那一方会进入判断天然避免重复。第二种使用标记数组 visited。初始化全为 false当发现一对互选且两人都未被标记时计数并把两个人的标记都改成 true。这个写法直观但需要额外开一个bool数组多一个变量要维护。第三种统计完所有互选关系之后输出 ans / 2。因为每对朋友会被数两次总计数一定是偶数除以2就是正确答案。这个写法最简练但我个人不太建议初学者在考场用因为它的正确性依赖于“每对恰好被数两次”这个前提少想一层就容易被绕晕。我推荐第一种原因不只是代码短更在于它的逻辑最贴合“一对”这个概念一对朋友只需要一个人代表发言。考试时间有限少维护一个数组就少一分出错的可能。3. 参考题解与逐行讲解一份能直接提交的C代码3.1 完整C代码可直接提交#include iostream using namespace std; const int MAXN 10005; int want[MAXN]; int main() { int n; cin n; for (int i 1; i n; i) { cin want[i]; } int ans 0; for (int i 1; i n; i) { int j want[i]; // i号同学想和j号同学交朋友 if (i j want[j] i) { // 只让编号小的一方计数 ans; // 并且j号同学也想和i号交朋友 } } cout ans endl; return 0; }这份代码已经去掉调试输出可以直接粘贴到评测环境提交。如果你想在本地Dev-C或者VSCode里跑建立一个cpp文件输入样例数据输出结果应该是1。3.2 逐段拆解数组、循环、核心if先看数组声明。int want[MAXN];放在 main 外面也就是全局变量。全局变量的好处是不需要手动初始化程序启动时自动全是0。虽然这题里每个下标都会被读入覆盖从0开始的地方用不到但写在全局能省掉初始化这一步也避免局部数组因未初始化而产生随机垃圾值。读入部分循环从 i1 开始到 in 结束。为什么要从1开始因为学号是1到n这样 want[1] 对应1号同学want[n] 对应n号同学。如果习惯从0开始就得写 want[i1] 或者后面所有地方都减1式子一长就容易乱。我见过太多孩子因为数组下标问题样例能过但换一组数据就崩。核心条件看这一行if (i j want[j] i)第一个条件i j是在做去重第二个条件want[j] i是在判断“对方是否也想和我交朋友”。注意逻辑与运算符是两个条件同时成立才计数。如果你把顺序调换先判断want[j] i再判断i j结果一样但把不变量写在前面会让思路更顺。还有一个新手总问的问题为什么不判断 j 是否越界因为输入数据保证了 want[i] 的范围是1到n所以 j 一定在1到n之间want[j]的访问永远是安全的。这也是题目给数据范围的意义所在以后刷题也要养成先看范围再写代码的习惯。3.3 样例手推从输入到输出的完整过程我习惯在讲题的时候把循环整个走一遍因为看十遍抽象解释不如看一张跟踪表。针对回忆版样例want数组读入后的状态是want[1] 2want[2] 1want[3] 4want[4] 6want[5] 3want[6] 3want[7] 2核心循环从 i1 到 i7 逐一判断ijwant[i]是否满足 ijwant[j] 的值want[j] 是否等于 i是否计数12是want[2]1是是ans变为121否21不成立不判断不判断否34是want[4]6否否46是want[6]3否否53否53不成立不判断不判断否63否63不成立不判断不判断否72否72不成立不判断不判断否最后 ans 等于1输出1。这张表建议你自己也在草稿纸上画一遍。尤其注意 i1 和 i2 的关系这一对之所以只计数一次完全是因为 ij 这个条件把第二次判断挡掉了。很多孩子就是在这里翻倍的。3.4 时间与空间复杂度评估这段代码只用了一层循环遍历数组时间复杂度是 O(n)也就是处理时间和 n 成正比。n10000 时判断次数只有大约一万次对计算机来说瞬间完成。空间上只开了一个长度为 n5 的 int 数组O(n) 级别约40KB内存可以忽略不计。也许有人会问一级考试又不卡你时间为什么还要分析复杂度因为GESP越到后面级别越看重算法效率。从一级开始养成估算复杂度的习惯后面学二级、三级时就不会一头雾水。退一步讲就算这题你写双重循环也能过可一旦养成不看范围就写暴力解的习惯将来遇到 n100000 的题就会吃大亏。好习惯从“交朋友”这道简单题开始养。4. 考场高频失误与实用避坑技巧4.1 最常见错误为什么很多人的答案恰好是两倍我在各种考后交流里听到最多的说法是“样例过了但交上去只有部分分甚至零分。”而这类错误里出现频率最高的就是重复计数。具体表现是程序确实把所有“双向关系”都找出来了却把每对关系数了两遍最终答案正好是正确值的两倍。原因前面说过枚举到1号时发现1号和2号互选计数一次枚举到2号时又发现2号和1号互选又计数一次。一个苹果被左手数了一次、右手又数了一次自然变两个。这里提供两个快速自查方法。第一用最小数据 n2、输入“2 1”测试如果输出是2而不是1那一定有重复计数问题。第二在代码里加一行临时输出把每次满足 want[want[i]]i 的 i 打印出来看看是不是“1 2”都被打印了。如果成对出现说明没去重。第二个常见错误是误判“环”。有的孩子统计的时候把3号写4号、4号写6号、6号写3号的三人环也当成某种配对甚至输出2或者3。这说明对“互相选择”的理解还不够牢固。三人环里没有任何两个人满足双向选择即便每个人都“有箭指向别人”也不能构成一对朋友。判题机不会替你做人类社交理解它只认代码里的布尔条件。第三个常见错误是把数组下标从0开始导致学号和位置错位。输入数据是2 1 4 6 3 3 2如果你存在 want[0] 到 want[6]那么 want[1] 其实存的是第二个数“1”和“1号同学写了什么”完全对不上。建议所有涉及学号的题目一律从下标1开始存省心省力。4.2 3组自测数据考前必跑代码写完之后别急着提交先自己跑三组数据。这三组数据是我每次带学生练这道题都会用的覆盖面足够能筛出大部分低级错误。第一组最小规模测试输入 2 2 1 预期输出 1如果这里输出2说明去重逻辑没生效。第二组三人循环测试输入 3 2 3 1 预期输出 01号写2号2号写3号3号写1号。这是全环结构没有双向选择。如果输出1或者3说明你错误理解了配对规则把环当成了朋友关系。第三组混合场景测试输入 4 2 1 2 3 预期输出 11号写2号、2号写1号是一对互选3号写2号是单向4号写3号是单向所以答案是1。注意3号写“2号”时如果2号已经和1号配对是否会影响计数不会题目没有规定一个人只能被一个人写所以3号照样写2号但这不会产生新的一对。这道题考察的是“互相写”不是“一对一匹配”。这三组数据必须在本地编译器上跑确认输出和预期一致后再提交。考场上虽然不能运行代码但平时养成的这种自测习惯会让你在写代码的时候更有把握减少“交上去再后悔”的情况。4.3 本地调试建议从Dev-C到VSCode很多刚入门的孩子还在用学校机房里的Dev-C这没问题一级考试的代码量完全够用。但如果你想提高调试效率我建议尝试VSCode加C插件。VSCode安装C/C扩展后可以在代码左侧打断点然后逐行执行实时观察want数组的变化。看到 want[1]2、want[2]1 在内存里真实存在比干想一百遍都有用。调试时最常用的观察方式有两种。一是“监视”窗口添加 want 数组展开看每一位存的数字二是在核心if前临时加一句cout i j want[j] endl;把关键中间值打出来。输出信息能直接暴露逻辑错误比如你发现 want[j] 是3而不是1那就能立刻定位到是数组读入错了还是下标理解错了。这一步做完记得把调试输出删掉再提交最终代码。顺便说一句不管用哪个编译器都要开启警告提示。如果代码里出现未使用变量、数组越界等隐患编译器会用警告提醒你。很多低级错误其实编译器早就告诉你了只是你一直没看。5. 从“交朋友”延伸出去一级备考还能怎么练5.1 变式一统计最受欢迎的同学“交朋友”的底层逻辑稍微改一改就能变成另一道经典一级题。假设题目问哪位同学被最多人选为“最想交朋友的人”解法是用一个计数数组 cnt读入每个 want[i] 后执行cnt[want[i]];最后遍历 cnt 找到最大值对应的下标。这个变式其实把“关系判断”换成“频次统计”考的还是数组下标与计数思想。我带学生练这道变式时会让他们先默写“交朋友”的数组读入部分再改三行代码。你会发现只要理解了“数组下标代表学号”这个核心逻辑后面加多少个计数数组都顺理成章。反过来讲如果“交朋友”这道题本身就让你的数组概念摇摇欲坠那变式也会跟着出问题。所以基础题永远值得反复练不要觉得简单就跳过。5.2 变式二统计至少被写到一次的同学还有一道常见的变式有多少位同学被至少一位同学写到了这题不需要判断双向只需要用 bool 数组标记每读到一个 want[i]就把对应的同学标记为 true。最后数一遍 true 的个数。它比“交朋友”少了去重逻辑却多了对标记数组的考察。这个变式的价值在于帮你分清“标记”和“计数”的差别。标记数组记录的是“有没有”计数数组记录的是“有多少次”。GESP一级经常用这两类数组设置考点平时把差别搞明白考场上看到题面就不会发怵。我在实际教学里发现能把“有没有”和“多少次”分清楚的孩子做这类生活化题目的正确率会明显高出一截。5.3 给备考GESP C一级的几条实在建议第一真题永远是第一优先级。GESP每年考好几次往期一级真题、蓝桥杯省赛初级组题目、电子学会等级考试一级题目都可以作为辅助练习。它们风格各异但底层考点高度重合多刷几套就能找到规律。第二学会“手推样例”。很多孩子写代码很快但从不验证结果样例一跑不过就开始瞎改。正确的做法是先用手推算出样例的预期输出再用代码跑两面对不上说明思路有问题。第三控制考试节奏。一级题量不大但生活化描述读起来费时间建议先读题面最后两三行的输入输出格式再回头读故事这样能快速抓住题目要什么。说句实在话一级通过并不难难的是“看错题”和“小错误不断”这两件事。与其大量刷难题不如把“交朋友”这种典型题练到闭着眼睛都能写对。数组下标、循环范围、计数器这三个基本功过关后面二级、三级的路会好走很多。个人经验分享与结尾我带学生复盘这道题时最深的感受是大部分孩子不是不会而是太自信。样例里全是双向互选他们就觉得“单向箭头也要数一下”或者“反正数两遍无所谓”结果在去重这种一分钟就能解决的问题上栽跟头。考场上只要多花10秒把样例手推一遍判断“1号写2号、2号写1号”时到底应该计数几次这类错误就能挡掉一大半。最后再分享一个小习惯无论题目多简单提交之前都用最小数据和特殊情况各跑一遍。n2的边界用例三人环的干扰用例加起来不超过两分钟却能帮你拿稳那些本该拿到的分。编程这条路比的往往不是谁会惊艳的算法而是谁少犯低级错误。