先说个考场现象。每到GESP一级的编程题环节总有考生在《交朋友》这类题上卡住——不是不会写循环而是读了三遍题不知道该从哪儿下手。2026年3月这场认证第三部分的编程题1交朋友题面讲的是“同学参加兴趣社团同社团就能交朋友”听起来像个人际关系问题实际只要把“朋友关系”翻译成“编号相等”整道题就变成了一节数组课。这篇文章会把这个翻译过程掰开揉碎讲清楚从题面还原、思路推导、完整代码到考场细节一次性说透。无论你是第一次考GESP的学生还是带人刷题的教练都可以拿这篇当讲义用。1. 题目还原与考点定位1.1 完整题面与输入输出先把我手里这版《交朋友》的题面完整还原一下。真实考场题干的表述可能略有差异但核心逻辑非常稳定题目描述 某班有 n 名同学1 ≤ n ≤ 1000每位同学报名参加了一个兴趣社团。 社团编号是 1 到 1000 之间的正整数。 规定如果两位同学参加了同一个社团他们就可以互相成为朋友。 任务 1统计这个班级里一共有多少对朋友 2找出人数最多的社团。如果有多个社团的人数并列最多输出编号最小的那个社团编号及其人数。 输入格式 第一行一个正整数 n表示同学人数。 第二行 n 个正整数 a[1], a[2], ..., a[n]表示每位同学参加的社团编号。 输出格式 第一行一个整数表示朋友总对数。 第二行两个整数用空格隔开表示人数最多的社团编号和人数。 样例输入 8 3 1 2 3 2 1 1 3 样例输出 7 1 3样例怎么来的我帮你拆一遍。8个同学参加的社团依次是3、1、2、3、2、1、1、3。数一下编号1出现3次编号2出现2次编号3出现3次。编号1社团有3人任意两人都能成为朋友所以有 C(3,2)3 对编号2社团有2人有1对编号3社团有3人也有3对。总朋友对数 3 1 3 7。人数最多的社团是编号1和编号3都是3人按题意输出编号更小的1所以第二行输出“1 3”。这类题在GESP一级里非常典型它不考复杂算法考的是你能不能把一个带生活背景的描述准确地翻译成数组下标和循环语句。1.2 考点定位它命中了哪些一级考点GESP C一级的考纲重点落在三块顺序与分支结构、循环结构、一维数组的简单应用。《交朋友》这题正好踩在一维数组和循环的交叉点上。我把题面关键词和对应考点整理成了下面这张表考前对照着看会很有感觉题干里的说法计算机思维翻译每位同学参加一个社团一个一维数组下标代表同学值代表社团编号同一个社团的人能交朋友统计每个社团编号出现了多少次一共有多少对朋友次数为k的社团贡献 k×(k−1)÷2 对哪个社团人数最多遍历计数数组找最大值并列时输出编号最小比较时用 而不是 很多同学做不出这道题不是因为不会数组而是没有完成上面这行翻译。题目中文里通篇没有出现“数字出现次数”这种词它用“社团”“朋友”做了包装你要做的是把包装纸撕掉看到里面那个统计问题。这一层意识比多背十段代码都管用。2. 核心思路把“交朋友”翻译成计数数组2.1 为什么不能只用暴力枚举第一次看到这道题最朴素的想法是枚举每一对同学for (int i 1; i n; i) { for (int j i 1; j n; j) { if (a[i] a[j]) ans; } }两层循环n1000的时候要做大约50万次比较完全跑得动。如果题目只给到1000的规模暴力枚举确实能AC。但我不建议你在考场上用这个写法原因有两个一是代码容易写错。你需要在循环里判断 ij还要保证每个朋友对只被数一次稍不注意就会重复计数。二是思路没有延展性。GESP一级虽然n不大但以后你还要考二级、三级题目数据范围会大得多。如果n变成100000暴力枚举要做大约50亿次比较直接超时。到时候再改思路就晚了。所以正确做法是换一个角度不数人和人之间的关系而是数编号出现的次数。开一个数组cnt[1005]让cnt[x]表示“参加编号为 x 的社团的人数”。读入每个同学的社团编号 x就执行cnt[x]。读完之后cnt[1]、cnt[2]、cnt[3]分别是1、2、3一眼就能看出每个社团有几个人。2.2 组合数公式怎么来的、怎么算不踩坑知道了某个社团有 k 个人这 k 个人之间能产生多少对朋友答案是 k×(k−1)÷2。我经常跟学生这么解释这个公式假设社团里有4个人。第1个人往后看后面有3个人都能交朋友第2个人只需要看后面的2个人因为和前面那个已经数过了第3个人再看后面1个第4个人后面没人了。加起来就是3216也就是4×3÷26。推广到 k 个人就是 (k−1)(k−2)...1正好等于 k×(k−1)÷2。这个式子写进C有讲究。很多同学会写成cnt[i] / 2 * (cnt[i] - 1)一旦cnt[i]是奇数就出错。比如 k3正确结果是3×2÷23但3 / 2 * 2在整数除法下等于1×22直接WA。正确写法一定是先乘后除ans cnt[i] * (cnt[i] - 1) / 2;如果是担心溢出再在最前面乘一个1LLans 1LL * cnt[i] * (cnt[i] - 1) / 2;这样当数据变大时也不用担心int装不下。2.3 边读边统计原始数组可以省掉这题读完所有a[i]之后我们还需要再用到原始数据吗不需要。我们只关心每个编号出现的次数。所以代码里根本没有必要开一个a[n1]数组先存起来完全可以边读边统计int x; for (int i 1; i n; i) { cin x; cnt[x]; }少开一个数组就少一个越界风险也少一遍循环。这个“边读边处理”的思路在竞赛里很常用遇到“只统计不回溯”的题目基本都能这么干。如果题目后面还要求按原顺序输出每个同学的信息那就必须保留数组了——但《交朋友》不涉及。3. 考场上最容易丢分的几个细节3.1 并列时输出编号最小用 还是 决定命运这是《交朋友》这题最阴险的一个点。假设现在社团1有3人社团3也有3人。题目说“人数并列最多时输出编号最小的那个”正确输出是1。用更新if (cnt[i] maxCnt) { maxCnt cnt[i]; maxId i; }遍历到1的时候maxCnt从-1变成3maxId变成1遍历到3的时候cnt[3]等于3但不大于maxCnt所以不会更新maxId最终输出1正确。如果你手一滑用了if (cnt[i] maxCnt) { maxCnt cnt[i]; maxId i; }遍历到3的时候33成立maxId被改成3最终输出3错误。不要小看这一个符号的差别。我在不少模拟赛里见过有人因为这题WA掉下来一对答案才发现是和的问题。你问我怎么记只要题面出现“编号最小/最大”这种限定就用只有题面明确说“任意一个都可以”时才可以用。3.2 long long 是习惯不是摆设GESP一级的n最大只有1000朋友对数最多也就是 C(1000,2)499500int完全装得下。但我在写代码时还是用了1LL * cnt[i] * (cnt[i] - 1)。为什么因为这是一个习惯问题。等以后你做到二级、三级数据范围一上来int很容易溢出。与其到时候满世界找哪里出了错不如从一开始就养成“计数、求和、乘积”用 long long 的习惯。这类问题有个特性小样例全部通过一换大样例就输出负数或者奇怪的大数。原因基本就是int溢出。3.3 数组大小按值域开不是按人数开这是一级新手最高发的错误看到 n≤1000就开cnt[1001]然后下标用cnt[a[i]]结果一跑就崩溃或者随机内存错误。注意cnt数组的下标是社团编号不是同学人数。社团编号范围是1到1000所以至少要开cnt[1005]。如果你老惦记着“n最大1000”就把数组开成1001刚好够用但非常悬——下标访问到1000的时候要求数组至少能装下标0到1000也就是11个元素都没有不对是1001个元素下标0到1000。所以cnt[1001]恰好够用但建议多开几个宁可浪费也不能越界int cnt[1005];如果题目范围更大比如社团编号到10^5就开cnt[100005]如果编号大到10^9数组开不了就要用map或者离散化——那是后面级别的内容现阶段你记住“数组大小跟着值域走不是跟着输入个数走”就够了。3.4 输出格式与提交规范线下认证判题时输出必须和标准答案逐字符一致。这里有三条硬规矩不要输出任何提示文字比如“答案是”或“朋友对数为”多余的文字会让判题直接判错。换行用\n而不是endl。本题数据量小用 endl 没什么问题但养成用\n的习惯以后数据量大了会省很多时间。输出完第二行后要不要再换行一般建议养成输出最后也换行的习惯大多数评测系统不卡最后的换行符但统一处理最稳妥。4. 参考代码与样例推演4.1 可以直接用的完整代码下面这段代码是我在模拟环境里实际跑通过的一版直接用没有问题#include iostream using namespace std; int cnt[1005]; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin n; for (int i 1; i n; i) { int x; cin x; cnt[x]; } long long friendCount 0; int maxCnt -1; int maxId 0; for (int i 1; i 1000; i) { if (cnt[i] 0) { friendCount 1LL * cnt[i] * (cnt[i] - 1) / 2; } if (cnt[i] maxCnt) { maxCnt cnt[i]; maxId i; } } cout friendCount \n; cout maxId maxCnt \n; return 0; }两点说明。第一有的同学喜欢写#include bits/stdc.h在大部分竞赛环境里都能用但这题只用到cin/cout写#include iostream更稳妥。第二maxCnt初始化为 -1 而不是 0 是故意的这样能保证第一个遇到的正数次数一定被选中。就算所有社团人数都是0这种极端情况题目不会出现逻辑也不会出错。4.2 样例是怎么一步步算出来的拿样例数据走一遍你会更清楚代码的执行过程。初始cnt全为0。依次读入第1个数3cnt[3]变为1第2个数1cnt[1]变为1第3个数2cnt[2]变为1第4个数3cnt[3]变为2第5个数2cnt[2]变为2第6个数1cnt[1]变为2第7个数1cnt[1]变为3第8个数3cnt[3]变为3最后cnt[1]3, cnt[2]2, cnt[3]3。进入统计循环。i1时cnt[1]3累加 3×2÷23同时3 -1更新maxCnt3, maxId1。i2时cnt[2]2累加 1得到42 3不成立max不更新。i3时cnt[3]3累加 3得到73 3不成立maxId仍然是1。循环结束输出7 1 3看到没有前面强调的在这里刚好让并列的编号1胜出。如果你改成第一次输出就会变成3 3样例直接挂掉。4.3 换汤不换药的变形题《交朋友》最常见的变形是把生活场景换掉核心代码一个字都不用动。比如改成统计“生日在同一个月的人有多少对”输入12个月份改成统计“参加同一个社团的兴趣小组”输入社团编号改成“每个人有一个幸运数字统计幸运数字相同的人的对数”编号范围变成1到100只问朋友对数不找人数最多的社团那就只保留第一问删掉第二问的更新逻辑。本质都是同一道题。你在考场上如果发现题目里出现“相同”“配对”“最多”这些词就可以往计数数组上联想。注意变形题有时会改变值域范围比如幸运数字范围是0到999那么数组要开到cnt[1000]下标从0开始循环。这些都是小调整核心思想没变。5. 平时怎么练这类统计题5.1 先学会翻译题干再动手写码我给学生训练时要求每做一道带生活描述的题先在草稿纸上写三行翻译写不出来不许碰键盘输入数据用什么结构存数组还是变量要统计什么用什么做数组下标输出要比较什么用还是《交朋友》的三行翻译就是存社团编号用一维数组统计每个编号出现次数下标就是编号比较最大人数并列取最小编号用。这个习惯的价值在于它逼着你把模糊的文字描述变成清晰的数据逻辑。很多同学代码写不出来不是不会语法而是脑子里根本没有这一层数据抽象。从今天开始每次做题都写这“三行翻译”坚持一个月你对题目的敏感度会明显不一样。5.2 同类型变式训练清单如果没有更多真题可刷我建议你拿这几类变式题练手它们都在一级难度范围里输入n个1到100之间的整数统计出现次数最多的数并列输出最小的。输入一个字符串统计哪个字母出现次数最多。输入n个同学的身高单位厘米范围80到200统计相同身高的人有多少对。输入n个评委给选手打的分1到10的整数统计每个分数出现几次并输出中位数对应的分数段。第2题会用到字符转下标的小技巧cnt[s[i] - a]。第3题是《交朋友》的纯换皮。第4题则是把统计结果用于二次比较。每一道都是在练“计数数组”这个核心技能刷完你再看这类题就会觉得它们长得一模一样。5.3 考场自检头尾两端数据试一遍代码写完样例过了先别急着交。用最短时间和最极端的数据各测一组。最短的数据是 n1输入 1 7此时朋友对数一定是0人数最多社团是7输出是0 7 1如果代码输出第一行不是0说明组合数累积逻辑有问题。最极端的数据是所有人都报同一个社团比如 n5全是2输入 5 2 2 2 2 2输出应该是10 2 5因为 C(5,2)10。如果输出不是10检查一下是不是把社团编号当成人数用了。这两组极端数据跑对了代码基本就稳了。再加上题面里的样例一共三组测试足够覆盖一道一级大题的大部分隐藏bug。最后再分享一个考场小习惯。C里friend是保留字不能用来当变量名有的同学题目刚看到“交朋友”上来就写int friend编译直接报错。变量命名这种事看着小但考场上很影响心态。建议统一用friendCount、maxId这类名字既清楚又安全。做编程题审题、建模、编码、测试四步每一步都稳一点分数自然就稳了。