28 KiB
CSP-J 2026 第一轮试卷:参考答案与详细解析
根据用户提供的《CSP-J-2026-试卷与答案.pdf》整理,题号及选项对应该文件。本文对全部 43 题逐题推导,所得答案与文件卷末参考答案一致。判断题中 A 表示正确,B 表示错误。本文沿用所提供文件的标题,不另行认定其官方来源。
一、答案速查
| 题号 | 答案 | 题号 | 答案 | 题号 | 答案 |
|---|---|---|---|---|---|
| 1 | B | 16 | A(正确) | 31 | B |
| 2 | D | 17 | B(错误) | 32 | D |
| 3 | C | 18 | A(正确) | 33 | C |
| 4 | C | 19 | A | 34 | D |
| 5 | B | 20 | C | 35 | B |
| 6 | D | 21 | C | 36 | D |
| 7 | D | 22 | A(正确) | 37 | B |
| 8 | C | 23 | B(错误) | 38 | C |
| 9 | B | 24 | B(错误) | 39 | B |
| 10 | A | 25 | B | 40 | D |
| 11 | A | 26 | A | 41 | C |
| 12 | D | 27 | C | 42 | A |
| 13 | C | 28 | B(错误) | 43 | D |
| 14 | A | 29 | A(正确) | ||
| 15 | B | 30 | A(正确) |
分值:第 1~15 题每题 2 分;第 16 题 1 分;第 17、18、22、23、24、28、29、30 题每题 1.5 分;其余每题 3 分,共 100 分。
二、单项选择题(第 1~15 题)
第 1 题:精确存储大整数
答案:B,long long。
需要精确存储的整数是 $10^{18}+1$。
按竞赛中通常采用的类型规格,32 位有符号 int 的最大值为 $2^{31}-1=2147483647$,无法容纳这个数;64 位有符号 long long 的最大值为 $2^{63}-1\approx9.22\times10^{18}$,能够精确存储。
float 和 double 虽然可以表示数量级很大的数,但有效精度有限。常见的 IEEE 754 double 有 53 位二进制有效精度,能够连续精确表示的非负整数范围只到 $2^{53}$。在 10^{18} 附近,相邻可表示数的间距是 128,不能精确表示 $10^{18}+1$;float 的精度更低。
易错点:表示范围大,不等于能精确存储范围内的每一个整数。
第 2 题:十六进制转八进制
答案:D,1365。
先转为十进制:
(2F5)_{16}=2\times16^2+15\times16+5=757。
再检查:
(1365)_8=1\times8^3+3\times8^2+6\times8+5=757。
也可以利用二进制中转:每个十六进制位转成 4 位二进制,再从右向左按 3 位分组。
2F5₁₆ = 0010 1111 0101₂
= 001 011 110 101₂
= 1 3 6 5₈
第 3 题:整数除法与取余
答案:C,7。
a=7、b=3,整数除法 7/3=2,取余 7%3=1。
a / b * b + a % b
= 2 * 3 + 1
= 7
正整数除法满足:被除数 = 商 × 除数 + 余数。
第 4 题:判断合法出栈序列
答案:C,3、1、2、4。
栈遵循“后进先出”。要先弹出 3,必须先依次压入 1、2、3。弹出 3 后,栈中从底到顶是 1、2,此时必须先弹出 2,才能弹出 1,因此不可能紧接着输出 1。
其他选项均可实现:
- A:压入 1、2,弹出 2;压入 3、4,依次弹出 4、3、1。
- B:每压入一个数就立即弹出。
- D:压入并弹出 1;压入 2、3、4,再依次弹出 4、3、2。
第 5 题:完全二叉树的叶子数
答案:B,50。
将完全二叉树的结点按层序编号为 1~100。编号为 i 的结点,其左孩子编号为 $2i$。
当 2i\le100 时有孩子,即编号 1~50 的结点不是叶子;编号 51~100 的结点都是叶子,共 50 个。
一般地,含 N 个结点的完全二叉树叶子数为:
N-\left\lfloor\frac N2\right\rfloor=\left\lceil\frac N2\right\rceil。
第 6 题:3 或 5 的倍数之和
答案:D,2418。
条件中的 || 表示满足任意一个条件即可,因此应求 1~100 中所有 3 或 5 的倍数之和。
3 的倍数之和:
3(1+2+\cdots+33)=3\times\frac{33\times34}{2}=1683。
5 的倍数之和:
5(1+2+\cdots+20)=1050。
15 的倍数被重复计算,需要减去:
15(1+2+\cdots+6)=315。
最终结果为 $1683+1050-315=2418$。这就是容斥原理。
第 7 题:每次上 1、2、3 级台阶
答案:D,81。
设 f(i) 为到达第 i 级台阶的方法数。按照最后一步走了几级分类:
f(i)=f(i-1)+f(i-2)+f(i-3)。
边界为 $f(0)=1$,表示不走也是到达地面的一种方式;也可以直接从 f(1)=1,f(2)=2,f(3)=4 开始推。
台阶数 i |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
方法数 f(i) |
1 | 1 | 2 | 4 | 7 | 13 | 24 | 44 | 81 |
第 8 题:BFS 的入队次序
答案:C,14 个。
坐标按 (行,列) 表示,起点为 (0,0),终点为 (3,4)。必须同时遵守“队首先出”和邻居按“上、下、左、右”检查。
首次入队的完整顺序,直到 E 入队为止:
| 入队编号 | 格子坐标 | 由哪个格子发现 |
|---|---|---|
| 1 | (0,0),S | 初始入队 |
| 2 | (1,0) | (0,0) |
| 3 | (0,1) | (0,0) |
| 4 | (2,0) | (1,0) |
| 5 | (1,1) | (1,0) |
| 6 | (0,2) | (0,1) |
| 7 | (2,1) | (2,0) |
| 8 | (1,2) | (1,1) |
| 9 | (2,2) | (2,1) |
| 10 | (3,2) | (2,2) |
| 11 | (4,2) | (3,2) |
| 12 | (3,3) | (3,2) |
| 13 | (4,1) | (4,2) |
| 14 | (3,4),E | (3,3) |
关键细节:处理 (3,2) 时,先发现下面的 (4,2),再发现右边的 (3,3)。所以在 (3,3) 发现 E 之前,(4,2) 已经出队,并使 (4,1) 入队。
易错点:题目统计累计入队过多少格,不是当前队列长度,也不是最短路径上的格子数。
第 9 题:最大公约数条件
答案:B,6 个。
因为 $\gcd(n,60)=6$,所以令 $n=6t$,则:
\gcd(6t,60)=6\gcd(t,10)=6,
即 $\gcd(t,10)=1$。由 $1\le n\le100$,可得 $1\le t\le16$。
在 1~16 中排除 2 或 5 的倍数,剩下:
t=1,3,7,9,11,13。
对应 $n=6,18,42,54,66,78$,共 6 个。
第 10 题:最少硬币数
答案:A,3 枚。
可以用 $4+4+1=9$,因此最多需要 3 枚。
1 枚显然不行;2 枚可能组成的金额为 $2,5,7,8,10,12$,没有 9,因此最少为 3 枚。
**易错点:每次选最大面值的贪心策略在这里不成立。**按该策略会选 $6+1+1+1$,需要 4 枚。
第 11 题:指针与数组下标
答案:A,14,13。
初始数组为 [1,3,5,7,9],p=a+2,即 p 指向 a[2]。
第一条赋值:
*(p - 1) = p[0] + p[2];
等价于 a[1]=a[2]+a[4]=5+9=14。
第二条赋值:
p[1] = *(a + 1) - a[0];
等价于 a[3]=a[1]-a[0]=14-1=13。注意这里的 a[1] 已被上一条语句修改。
所以输出 14,13。
第 12 题:二分查找的最坏比较次数
答案:D,10 次。
每比较一个中间元素,就将候选区间缩小到大约原来的一半。按照题目通常的计数方式,一次与中间元素的比较算一次:
\lfloor\log_2 1000\rfloor+1=10。
也可以观察最坏情况下的非空候选区间长度:
1000 → 500 → 250 → 125 → 62 → 31 → 15 → 7 → 3 → 1
共需要检查 10 个中间元素。注意题目不是统计具体代码里 ==、< 等运算符的执行总次数。
第 13 题:由前缀和求原数组
答案:C,58。
已知 $S[i]=3i^2+i$,利用:
a[i]=S[i]-S[i-1]。
因此:
a[10]=(3\times10^2+10)-(3\times9^2+9)=310-252=58。
不要把 S[10] 直接当成 a[10]:前者是前十项之和。
第 14 题:到所有点的距离和最小
答案:A,37。
数轴上到各点距离之和,在中位数处取得最小值。七个点已经按从小到大排序,其中位数为第 4 个数 7。
取 $P=7$:
6+4+3+0+3+8+13=37。
也可以配对证明:对 (1,20)、(3,15)、(4,10),距离和分别至少为 19、12、6;P=7 同时达到这三个下界,且到中间点 7 的距离为 0,所以最小值为 $19+12+6=37$。
第 15 题:度数和与边数
答案:B,18。
无向图中每条边给两个端点各贡献 1 的度数,因此度数总和等于边数的 2 倍。
2|E|=4\times3+(10-4)\times4=36,\qquad |E|=18。
三、阅读程序(第 16~33 题)
第一组:除以 2 与二进制数位(第 16~21 题)
程序整体含义
初始 x=1,y=1。每轮循环无论奇偶,x 都增加 1;只有当前 n 为奇数时,y 才增加 1。随后执行 n/=2。
对非负整数而言,反复执行 n%2、n/=2,相当于从低位到高位依次读取二进制数位。
x= 1 + 循环次数。对正数,这就是 1 + 二进制位数。y= 1 + 原数二进制中 1 的个数。- 输入 0 时不进入循环,输出
1 1。
第 16 题
答案:A,正确。
输入 3,二进制为 11。
| 时刻 | 本轮开始的 n | 操作后的 x | 操作后的 y | n/=2 后 |
|---|---|---|---|---|
| 初始 | 3 | 1 | 1 | — |
| 第 1 轮 | 3,奇数 | 2 | 2 | 1 |
| 第 2 轮 | 1,奇数 | 3 | 3 | 0 |
最终输出 3 3。
第 17 题
答案:B,错误。
删除奇数分支中的 ++x 后,x 只在处理二进制位 0 时增加,y 只在处理二进制位 1 时增加。0 的数量与 1 的数量不一定相等。
例如输入 1,唯一一轮是奇数分支,只增加 y,输出为 1 2,两个数不相等。
第 18 题
答案:A,正确。
原程序每轮一定增加 x,增加 y 时也一定增加 x,而二者初始相等,所以始终有 x>=y。
从二进制角度看,1 的数量不可能超过总位数。输入 0 时二者均为 1,结论也成立。
第 19 题
答案:A,陷入死循环。
当条件改为 while(n>=0),n 变成 0 后仍满足循环条件,而 0/2 永远是 0,无法退出。
按题目算法层面的讨论,应选死循环。严格按 C++ 语言语义,若无限运行使有符号变量 x 溢出,还会涉及未定义行为,但这不改变本题要求识别的循环条件错误。
第 20 题
答案:C,4 3。
输入 6,二进制为 110,共有 3 位,其中有两个 1。所以 x=1+3=4,y=1+2=3。
逐轮也可得到:
初始:n=6,x=1,y=1
第1轮:6为偶数,x=2,y=1,n=3
第2轮:3为奇数,x=3,y=2,n=1
第3轮:1为奇数,x=4,y=3,n=0
第 21 题
答案:C,31 次。
第二个数恰好为 2,要求原数二进制中恰好有一个 1,即原数为 2 的非负整数次幂。
在 0\le n\le2^{31}-1 范围内,这些数是:
2^0,2^1,\ldots,2^{30},
共 31 个。0 的输出第二个数为 1,不符合条件;2^{31} 超出范围。
第二组:高精度加法及输出细节(第 22~27 题)
程序整体含义
输入字符串逆序保存到数组中:a[0]、b[0] 是个位,a[1]、b[1] 是十位,以此类推。数组为全局变量,未赋值的位置初始为 0。
令 L=max(a_len,b_len)。程序从 i=0 算到 i=L,多计算一位以容纳最高位进位,然后固定输出 c[L] 到 c[0]。
**原程序总是输出 L+1 个字符,不会主动去除前导零。**正常加法中每个 c[i] 都在 0~9 之间。
第 22 题
答案:A,正确。
123+456=579,但 L=3,程序输出 c[3],c[2],c[1],c[0]。最高位 c[3]=0,所以输出 0579。
第 23 题
答案:B,错误。
即使输入没有前导零,只要最高位没有产生进位,多输出的那一位仍然是 0。例如 123 456 的输出就是 0579,足以否定“一定不会含有前导零”。
第 24 题
答案:B,错误。
删除 +carry[i] 会使进位无法参与下一位的计算,但结果不一定严格变小。
例如输入 1 2,本来就没有进位,修改前后都输出 03,结果相同。因此“一定比原来的结果小”不成立。
第 25 题
答案:B,013023。
计算 $12345+678=13023$,原输入最大长度为 5,所以输出 6 位,最前面补出一个 0。
| 数位 | 两个数的本位及进位 | 本位结果 | 向高位进位 |
|---|---|---|---|
| 个位 | 5+8+0=13 | 3 | 1 |
| 十位 | 4+7+1=12 | 2 | 1 |
| 百位 | 3+6+1=10 | 0 | 1 |
| 千位 | 2+0+1=3 | 3 | 0 |
| 万位 | 1+0+0=1 | 1 | 0 |
| 额外最高位 | 0+0+0=0 | 0 | 0 |
从高位到低位输出为 013023。
第 26 题
答案:A,01010。
将进位条件改为 c[i]>10 后,恰好等于 10 时不会进位,也不会减去 10。
对输入 95 15:
| i | 计算结果 | 是否大于 10 | 最终 c[i] | 下一位收到的进位 |
|---|---|---|---|---|
| 0 | 5+5+0=10 | 否 | 10 | 0 |
| 1 | 9+1+0=10 | 否 | 10 | 0 |
| 2 | 0+0+0=0 | 否 | 0 | 0 |
输出顺序是 0、10、10,直接连接后为 01010。
易错点:数组元素是整数,不保证始终只有一位。cout << 10 会输出两个字符。
第 27 题
答案:C,长度一定为 n+1,且第一个字符为 '0'。
两个输入均为 n 位数,所以 L=n,原程序固定输出 n+1 位。由于和小于 $10^n$,不存在进入第 n 位的进位,因此 c[n]=0,输出的第一个字符一定为 '0'。
第三组:质数判断与深度优先搜索(第 28~33 题)
程序整体含义
check_prime(x) 判断 x 是否为质数。若 x 是合数,则一定有一个不超过 \sqrt{x} 的因子,因此试除到 i*i<=x 即可。
search_result(x) 的逻辑为:
- 如果 x 不是质数,立即停止这条搜索分支。
- 如果 x 是质数且
x>=n,输出 x,并停止向后添加数字。 - 如果 x 是质数且
x<n,依次尝试在其末尾添加 0~9。
所以只有每个非空十进制前缀都是质数的数,才能到达输出位置。遍历方式是深度优先搜索,并不按数值大小排序。
第 28 题
答案:B,错误。
输入 10 时,只有起始数字 2、3、5、7 可以继续扩展。一旦得到两位质数,就达到阈值并输出。
全部输出为:
23
29
31
37
53
59
71
73
79
共 9 行,不是 10 行。比如 11 虽然是质数,但首位 1 不是质数,对应分支早已被剪去。
第 29 题
答案:A,正确。
主函数一定调用 search_result(5)。5 是质数,且当 n<=5 时满足 5>=n,所以该调用会直接输出 5。
第 30 题
答案:A,正确。
把添加数字范围改为 1,3,5,7,9,只是跳过了末位为偶数的扩展。
扩展得到的数至少有两位,如果末位是 0、2、4、6、8,则它是大于 2 的偶数,必定不是质数。在原程序中,这些分支也会立即返回,既不会输出,也不会继续扩展。
因此删除这些无效分支后,输出内容和顺序都不变。
第 31 题
答案:B,29。
输入 24 时,先从首位 2 开始搜索:
- 20、21、22 不是质数,返回。
- 23 是质数,但小于 24,所以继续扩展。
- 230~239 中只有 233、239 是质数,依次输出这两个数。
- 返回两位数这一层,24~28 均非质数,随后访问 29。
- 29 是质数且不小于 24,因此第 3 行输出 29。
前几行是:
233
239
29
31
37
...
易错点:深度优先会先完成 23 的整条分支,再访问 29。
第 32 题
答案:D。
对于输出的任意一个至少两位的数 x,它必然由上一层 search_result(x/10) 添加末位得到。上一层只有通过质数检查,才能继续递归。因此删去 x 的末位后得到的数必定是质数。
其余选项:
- A 错:输入 24 时,233、239 出现在 29 前面,并非从小到大。
- B 错:输入 23 时输出 9 行,输入 24 时输出 10 行;原来输出的 23 被两个后代 233、239 替代,行数增加。
- C 错:例如输入 10 时会输出 29、31、59、71、79,个位并不只可能是 3 或 7。
第 33 题
答案:C,14 行。
输入 200 时,一位和两位的质数前缀都小于阈值。可继续扩展的两位前缀就是第 28 题列出的 9 个数。
| 两位质数前缀 | 添加一位后仍为质数的数 | 数量 |
|---|---|---|
| 23 | 233、239 | 2 |
| 29 | 293 | 1 |
| 31 | 311、313、317 | 3 |
| 37 | 373、379 | 2 |
| 53 | 无 | 0 |
| 59 | 593、599 | 2 |
| 71 | 719 | 1 |
| 73 | 733、739 | 2 |
| 79 | 797 | 1 |
这些三位数均不小于 200,所以输出后停止扩展。总行数为 $2+1+3+2+0+2+1+2+1=14$。
四、完善程序(第 34~43 题)
第一组:将 mn 进制转换成 n 进制(第 34~38 题)
1. 数组究竟表示什么
这里 mn 表示 $m\times n$。数组 b 以低位在前的顺序,保存已读入部分所代表数值的 n 进制表示:
V=b[0]+b[1]n+b[2]n^2+\cdots+b[len-1]n^{len-1}。
b[0] 是个位,b[len-1] 是最高位。每轮进位结束后,各位都在 0~n-1 之间;进位前可以暂时超出这个范围。
2. 每读入一位,要执行哪种运算
从高到低读入原数的新数位 x,相当于:
V_{新}=V_{旧}\times(mn)+x。
这与读十进制数时“旧数乘 10 再加新数字”是同一个道理。当前用 n 进制保存数值,不会改变原输入的进制是 mn 这一事实。
第 34 题:①
答案:D,b[j - 1] * m。
for (int j = len; j >= 1; j--)
b[j] = b[j - 1] * m;
这行同时做了两件事:将数字从第 j-1 位放到第 j 位,并将数字本身乘以 m。
假设旧数位是 c,它原来代表 $c n^{j-1}$,现在代表:
(cm)n^j=(c n^{j-1})\times mn。
**为什么不能再写 * n?**因为位置从 j-1 变成 j,位权已经从 n^{j-1} 变成 $n^j$,相当于自动乘了一次 n。就像十进制的 3 从个位移到十位,数字仍为 3,但代表的数值已经是 30。
若写成 b[j]=b[j-1]*m*n,结合位置变化,实际乘的是 $mn^2$,多乘了一次 n。
**为什么倒序移动?**从高位向低位赋值,能够保留还没被读取的旧数位;若从低位往高位操作,会提前覆盖后面需要使用的数据。
第 35 题:②
答案:B,x。
整体高移一位后,最低位空出,相当于为 0。将新读入的 x 加到最低位,直接写:
b[0] = x;
x 可能大于等于 n,但后面的循环会统一处理进位。
第 36 题:③
答案:D,b[j] / n。
n 进制中,每满 n 就向高一位进 1,所以需要向高一位增加 b[j]/n:
b[j + 1] += b[j] / n;
必须使用 +=,因为高一位可能已有数值,进位应该在原有值上累加。
第 37 题:④
答案:B,b[j] % n。
进位后,本位留下除以 n 的余数:
b[j] = b[j] % n;
商送到高位,余数留在本位。进位循环由低位向高位进行,能继续处理进位引起的连锁变化。
如果向当前最高位之外又进了一位,len++ 会让循环继续处理这个新数位。
第 38 题:⑤
答案:C,len > 1 && b[len - 1] == 0。
while (len > 1 && b[len - 1] == 0)
len--;
每次读入都先增加 len,但实际数值未必需要这么多位,因此最后需要删除最高位多余的零。
- 应检查
b[len-1],因为它是最高位;b[0]是最低位,末尾的零不能随意删除。 - 必须限制
len>1,保证 A=0 时仍保留一位 0。如果使用len>0,可能把唯一的一位零也删掉,导致没有输出。
3. 完整示例
取 n=2、m=3,输入的原数是六进制 1 4:
(14)_6=1\times6+4=10=(1010)_2。
读完首位 1 后,有效数位是 [1]。实际数组可能还带有最高位的零,这些零不影响数值,最后会统一去除。
读入下一位 4,移动并乘以 m,再写入最低位,得到尚未进位的有效部分 [4,3],表示 $4+3\times2=10$。
[4,3] 第0位为4,向高位进2,本位留0
[0,5] 第1位为5,向高位进2,本位留1
[0,1,2] 第2位为2,向高位进1,本位留0
[0,1,0,1] 所有数位均合法
数组低位在前,倒序输出就是 1 0 1 0。
4. 补全后的完整程序
#include <iostream>
constexpr int N = 100005;
long long b[N];
int main() {
long long n, m, d;
std::cin >> n >> m >> d;
int len = 1;
for (int i = 0; i < d; i++) {
long long x;
std::cin >> x;
for (int j = len; j >= 1; j--)
b[j] = b[j - 1] * m;
b[0] = x;
len++;
for (int j = 0; j < len; j++) {
if (b[j] >= n) {
b[j + 1] += b[j] / n;
b[j] %= n;
if (j + 1 == len) len++;
}
}
}
while (len > 1 && b[len - 1] == 0) len--;
for (int i = len - 1; i >= 0; i--)
std::cout << b[i] << ' ';
return 0;
}
理解程序时,抓住“读入一位 → 旧数乘 mn 并加 x → 按 n 进制整理进位”。题干虽然称为“逐位除以 n”,但整个读入过程用上述递推式来解释更清楚。
第二组:枚举连续分段,使平均值极差最小(第 39~43 题)
1. 题意与递归状态
字符串中的每个十六进制字符分别代表一个整数。例如 016A 对应 [0,1,6,10],不是把整串当成一个十六进制整数参与运算。
在 n-1 个内部间隙中选择至少一个切分位置,将序列分成至少两段。对每段求平均值,目标是让“最大段平均值 - 最小段平均值”尽可能小。
函数:
split(int l, int cnt, double mnb, double mxb)
各参数含义如下:
| 参数 | 含义 |
|---|---|
| l | 下一段从第 l 个数开始 |
| cnt | 已选的内部切分位置数量,即切了几刀 |
| mnb | 已确定各段平均值的最小值 |
| mxb | 已确定各段平均值的最大值 |
每次枚举当前段的右端点 r,当前段为 [l,r],然后递归处理从 r+1 开始的剩余部分。
当 l>n 时,所有数已经分完。只有 cnt>0 才满足至少切一刀的要求;此时用 mxb-mnb 更新最优答案。
第 39 题:①,十六进制字符转数值
答案:B,c - (c < 'A' ? '0' : 'A' - 10)。
在题设 ASCII 编码和字符范围下:
'0'~'9'均小于'A',用c-'0'得到 0~9。'A'~'F'用c-'A'+10得到 10~15。
选项 B 中,字母分支是:
c - ('A' - 10) // 等价于 c - 'A' + 10
例如 'A' 得到 10,'F' 得到 15。
其他选项:A 使用 c<'9',会把字符 '9' 错分到字母分支;C 把两个分支颠倒;D 将字母分支中的加 10 错写成减 10。
第 40 题:②,枚举当前段的右端点
答案:D,int r = l; r <= n; ++r。
段 [l,r] 至少包含一个元素,因此最小右端点为 l;最后一段必须能包含第 n 个元素,所以最大右端点为 n。每个位置都可能是右端点,必须逐个枚举。
- A 从 l+1 开始,会漏掉长度为 1 的段。
- B 限制 r<n,无法正常选出结束于 n 的最后一段。
- C 每次增加 2,会遗漏部分段长。
循环中的 sum+=a[r] 不断扩展当前段,使 sum 始终等于 a[l]+...+a[r]。
第 41 题:③,正确计算平均值
答案:C,sum * 1.0 / (r - l + 1)。
区间 [l,r] 共包含 r-l+1 个元素,平均值应为总和除以元素个数。
sum*1.0 先将运算转换为浮点运算,避免整数除法丢失小数部分。
例如总和为 3、长度为 2:
3 / 2 * 1.0 // 先进行整数除法,结果为 1.0
3 * 1.0 / 2 // 进行浮点除法,结果为 1.5
因此 A 错。B 的分母和运算位置错误,且 r=l 时分母为 0;D 少算了当前段最后一个元素。
第 42 题:④,递归参数更新
答案:A。
r + 1, cnt + (r < n), min(mnb, nwb), max(mxb, nwb)
四个参数分别解释如下:
- 下一段从 r+1 开始,保证不重叠、不遗漏。
- 只有 r<n 时,r 后面才是一个内部切分位置,才应增加 cnt。
- 新的最小平均值是
min(mnb,nwb)。 - 新的最大平均值是
max(mxb,nwb)。
**为什么不能用 cnt+(r<=n)?**循环内总有 r<=n,这会连整个序列末尾也算作一次切分。这样不切分的整段 [1,n] 也能通过 cnt>0 的检查,而唯一一段的最大和最小平均值相等,极差为 0,会让程序错误地对所有输入都得到 0。
例如输入 2 09,唯一合法方案是 [0] | [9],正确极差为 9;不切分时极差为 0,但这不符合题意。
第 43 题:⑤,递归初始状态
答案:D,1, 0, 1e100, -1e100。
split(1, 0, 1e100, -1e100);
- 数组从下标 1 开始保存数据,所以 l=1。
- 尚未选择内部切分位置,所以 cnt=0。
- 最小值初始设为很大的数,这样遇到第一个平均值时,
min会选中它。 - 最大值初始设为很小的数,这样第一次
max也会选中真实平均值。
实际平均值在 0~15 之间,所以这两个初值足以充当正、负无穷的替代值。
2. 手算示例:016A
数值序列为 [0,1,6,10],共有 3 个内部间隙,至少切一刀,所以合法方案共 2^3-1=7 个。
| 分段方案 | 各段平均值 | 极差 |
|---|---|---|
[0] | [1,6,10] |
0、17/3 | 17/3 |
[0,1] | [6,10] |
1/2、8 | 15/2 |
[0,1,6] | [10] |
7/3、10 | 23/3 |
[0] | [1] | [6,10] |
0、1、8 | 8 |
[0] | [1,6] | [10] |
0、7/2、10 | 10 |
[0,1] | [6] | [10] |
1/2、6、10 | 19/2 |
[0] | [1] | [6] | [10] |
0、1、6、10 | 10 |
最小极差为 $17/3$,保留六位小数输出 5.666667。
3. 补全后的完整程序
#include <algorithm>
#include <iomanip>
#include <iostream>
using namespace std;
constexpr int N = 25;
int n, a[N];
char s[N];
double ans = 1e100;
int value(char c) {
return c - (c < 'A' ? '0' : 'A' - 10);
}
void split(int l, int cnt, double mnb, double mxb) {
if (l > n) {
if (cnt == 0) return;
ans = min(ans, mxb - mnb);
return;
}
int sum = 0;
for (int r = l; r <= n; ++r) {
sum += a[r];
double nwb = sum * 1.0 / (r - l + 1);
split(r + 1, cnt + (r < n),
min(mnb, nwb), max(mxb, nwb));
}
}
int main() {
cin >> n >> (s + 1);
for (int i = 1; i <= n; ++i)
a[i] = value(s[i]);
split(1, 0, 1e100, -1e100);
cout << fixed << setprecision(6) << ans;
return 0;
}
代码按常见竞赛 C++14/C++17 环境书写;读取 s+1 的语句含义与原题一致。原文件在输出语句处结束了代码展示,这里补齐了 return 0; 和主函数结束括号。
4. 为什么枚举不会漏解
任意一个合法连续分段方案,都唯一确定第一段右端点、第二段右端点,直到最后一段右端点 n。递归在每层遍历所有可能的右端点,因此每个方案都能被访问一次,也不会重复。
n-1 个内部间隙各有“切”或“不切”两种选择,共 2^{n-1} 个完整方案,排除一刀不切后为 2^{n-1}-1 个合法方案。代码通过累计 sum 和维护最值,使每条递归边的工作量为常数,总时间复杂度为 $O(2^n)$,递归栈空间为 $O(n)$。n≤20 时适合直接枚举。
五、复习重点
| 知识点 | 对应题号 | 需要掌握的结论或方法 |
|---|---|---|
| 整数范围与浮点精度 | 1 | 存得下某数量级,不代表能精确表示每个整数 |
| 进制与数位权重 | 2、16~21、34~38 | 除 2 读取二进制位;移动数位改变位权 |
| 栈、树、图 | 4、5、8、15 | 后进先出、完全二叉树编号、BFS 队列顺序、握手定理 |
| 递推与计数 | 6、7、9 | 容斥、按最后一步分类、最大公约数化简 |
| 最优性判断 | 10、14 | 贪心需要依据;距离和最小取中位数 |
| 数组与指针 | 11 | 指针偏移对应数组下标,注意赋值顺序 |
| 二分与前缀和 | 12、13 | 区间反复减半;原项等于相邻前缀和之差 |
| 高精度加法 | 22~27 | 低位先算、进位条件、固定输出长度 |
| DFS 与剪枝 | 28~33 | 区分递归访问顺序和数值顺序,跟踪提前返回 |
| 连续分段枚举 | 39~43 | 状态含义、内部切分计数、浮点除法、最值初值 |
做程序题时,先确定变量含义与循环不变量,再手算一个小例子;看到“一定”“所有”等断言时,优先检查 0、最小输入、无进位和边界恰好相等等情形。