GESP 客观题评测系统

试卷解析 · 理解每一步

2025 CCF CSP-S 第一轮(提高级 C++)

CSPS-2025-R1

试卷解析总览,可直接查看每题答案与解析。

一、单项选择题

第 1 题(单选题,2 分)

有 5 个红色球和 5 个蓝色球,它们除了颜色之外完全相同。将这 10 个球排成一排,要求任意两个蓝色球都不能相邻,有多少种不同的排列方法?

A.
25
B.
30
C.
6
D.
120

正确答案C

解析详情
答案 C插空法与相同元素计数
思路与推导
  1. 先排好 5 个红球,形成“空位 红 空位 红 空位 红 空位 红 空位 红 空位”,共 6 个空位,包含两端。

  2. 蓝球不能相邻,因此每个空位最多放 1 个蓝球。把 5 个蓝球放入 6 个空位,相当于选择唯一不放蓝球的空位。

  3. 同色球完全相同,不需要对它们再排列,方案数为 (65)=6\binom{6}{5}=6,选 C。

第 2 题(单选题,2 分)

在 KMP 算法中,对于模式串 P="abacaba",其 next 数组 (next[i] 定义为模式串 P[0..i] 最长公共前后缀的长度,且数组下标从 0 开始) 的值是什么?

A.
{0, 0, 1, 0, 1, 2, 3}
B.
{0, 1, 2, 3, 4, 5, 6}
C.
{0, 0, 1, 1, 2, 2, 3}
D.
{0, 0, 0, 0, 1, 2, 3}

正确答案A

解析详情
答案 AKMP 前缀函数
思路与推导
  1. 依题目定义,只比较真前缀和真后缀,不能把整个字符串算进去。

  2. 前四个前缀依次为 a、ab、aba、abac,其最长相同真前后缀依次为空、空、a、空,长度为 0、0、1、0。

  3. 后面三个前缀 abaca、abacab、abacaba 的最长相同真前后缀分别为 a、ab、aba,长度为 1、2、3。

  4. 故 next 数组为 {0,0,1,0,1,2,3},选 A。

图解 · 看懂这一步逐个前缀求 next

左右滑动表格,可查看完整内容。

iP[0..i]最长相同真前后缀next[i]
0a空0
1ab空0
2abaa1
3abac空0
4abacaa1
5abacabab2
6abacabaaba3

只比较真前缀和真后缀,整串本身不计。

第 3 题(单选题,2 分)

对一个大小为 16 (下标 0-15) 的数组上构造满线段树,查询区间 [3, 11] 时,最少需要访问多少个树结点(包括路径上的父结点和完全包含在查询区间内的结点)?

A.
7
B.
8
C.
9
D.
10

正确答案B

解析详情
答案 B线段树区间查询路径
思路与推导
  1. 从根区间 [0,15] 出发,只进入与 [3,11] 相交的子区间;若结点区间被完全覆盖,就直接取该结点的信息,不再向下。

  2. 左侧依次访问 [0,7]、[0,3]、[2,3]、[3,3],并访问完整覆盖的 [4,7],共 5 个结点。

  3. 右侧访问 [8,15] 和完整覆盖的 [8,11],共 2 个结点。加上根 [0,15],总数为 1+5+2=81+5+2=8,选 B。

  4. 最终参与合并的区间只有 [3,3]、[4,7]、[8,11],但题目还要求计入到达它们的父结点。

图解 · 看懂这一步查询实际进入的 8 个结点

左右滑动表格,可查看完整内容。

顺序区间处理
1[0,15]部分相交,分两侧
2[0,7]部分相交
3[0,3]只进右侧
4[2,3]只进右侧
5[3,3]完整覆盖
6[4,7]完整覆盖
7[8,15]只进左侧
8[8,11]完整覆盖

按先左后右的访问顺序;完整覆盖后立即停止下探。

第 4 题(单选题,2 分)

将字符串 "cat", "car", "cart", "case", "dog", "do" 插入一个空的 Trie 树(前缀树)中,构造完成 Trie 树(包括根节点)共有多少个结点?

A.
8
B.
9
C.
10
D.
11

正确答案D

解析详情
答案 DTrie 的共享前缀
思路与推导
  1. 根结点表示空串。每个非根结点对应一个不同的非空前缀,公共前缀只建一次。

  2. 以 c 开头的单词贡献 c、ca、cat、car、cart、cas、case,共 7 个结点;cart 复用 car,case 复用 ca。

  3. 以 d 开头的单词贡献 d、do、dog,共 3 个结点;插入 do 时,它的路径已由 dog 建好,只需标记单词结束。

  4. 加上根结点,得到 1+7+3=111+7+3=11,选 D。

第 5 题(单选题,2 分)

对于一个包含 n 个结点和 m 条边的有向无环图 (DAG),其拓扑排序的结果有多少种可能?

A.
只有 1 种
B.
最多 n 种
C.
等于 n-m 种
D.
以上都不对

正确答案D

解析详情
答案 D拓扑序与偏序约束
思路与推导
  1. 拓扑序要求每条有向边的起点排在终点之前,实际有多少种顺序取决于边造成的先后约束。

  2. 若图是一条覆盖所有顶点的有向链,拓扑序只有 1 种;若图没有边,任意顶点排列都合法,共有 n!n! 种。

  3. 例如 n=3,m=0n=3,m=0 时共有 6 种,既不恒为 1,也不受“最多 n 种”限制,更不等于 n−m,所以选 D。

第 6 题(单选题,2 分)

在一个大小为 13 的哈希表中,使用闭散列法的线性探查来解决冲突。哈希函数为 H(key)=key mod 13,依次插入关键字 18, 26, 35, 9, 68, 74,插入 74 后,它最终被放置在哪个索引位置?

A.
5
B.
7
C.
9
D.
11

正确答案D

解析详情
答案 D哈希冲突与线性探查
思路与推导
  1. 依次计算余数并插入:18 放在 5,26 放在 0,35 放在 9。

  2. 插入 9 时,位置 9 已被 35 占用,向后探查到空位 10;68 的余数为 3,放在 3。

  3. 74 的余数为 9。位置 9、10 已占用,下一位置 11 为空,因此最终放在 11,选 D。

第 7 题(单选题,2 分)

一个包含 8 个顶点的完全图(顶点的编号为 1 到 8),任意两点之间的边权重等于两顶点编号的差的绝对值。例如,顶点 3 和 7 之间的边权重为 |7 - 3| = 4。该图的最小生成树总权重是多少?

A.
7
B.
8
C.
9
D.
10

正确答案A

解析详情
答案 A最小生成树的下界与构造
思路与推导
  1. 8 个顶点的生成树恰有 7 条边。不同顶点编号至少相差 1,因此每条边权至少为 1,总权重至少为 7。

  2. 选取 (1,2)、(2,3)、…、(7,8),得到连接所有顶点的一条链,没有环,且 7 条边权均为 1。

  3. 该构造达到下界,所以最小生成树总权重正好为 7,选 A。

第 8 题(单选题,2 分)

如果一棵二叉搜索树的后序遍历序列是 2, 5, 4, 8, 12, 10, 6,那么该树的前序遍历是什么?

A.
6, 4, 2, 5, 10, 8, 12
B.
6, 4, 5, 2, 10, 12, 8
C.
2, 4, 5, 6, 8, 10, 12
D.
12, 8, 10, 5, 2, 4, 6

正确答案A

解析详情
答案 A二叉搜索树与遍历还原
思路与推导
  1. 后序遍历是“左、右、根”,因此末尾的 6 是整棵树的根。

  2. 二叉搜索树左子树的值小于 6,得到后序序列 2,5,4,根为 4,左右孩子分别为 2、5;右子树后序为 8,12,10,根为 10,左右孩子为 8、12。

  3. 改按前序“根、左、右”访问:先 6,再 4,2,5,最后 10,8,12,得到选项 A。

图解 · 看懂这一步由后序还原的二叉搜索树
6
  • 左子树:4
    • 左孩子:2
    • 右孩子:5
  • 右子树:10
    • 左孩子:8
    • 右孩子:12

分支和叶子均按从左到右排列;前序先访问父结点。

第 9 题(单选题,2 分)

一个 0-1 背包问题,背包容量为 20,现有 5 个物品,其重量和价值分别为 7, 5, 4, 3, 6 和 15, 12, 9, 7, 13。装入背包的物品能获得的最大总价值是多少?

A.
43
B.
41
C.
45
D.
44

正确答案D

解析详情
答案 D0-1 背包与补集优化
思路与推导
  1. 五件物品总重量为 7+5+4+3+6=257+5+4+3+6=25,总价值为 15+12+9+7+13=5615+12+9+7+13=56。容量为 20,因此至少要舍弃重量 5,且希望舍弃的价值最小。

  2. 只舍弃一件时,可舍弃重量 5、6 或 7 的物品,相应损失价值 12、13、15;最小损失为 12。重量 3 或 4 的单件不足以满足要求。

  3. 若舍弃至少两件,价值损失至少是两件最低价值之和 7+9=167+9=16,不会优于损失 12。

  4. 所以舍弃第 2 件,保留第 1、3、4、5 件,重量为 20、价值为 56−12=4456-12=44,选 D。

第 10 题(单选题,2 分)

在一棵以结点 1 为根的树中,结点 12 和结点 18 的最近公共祖先 (LCA) 是结点 4。那么下列哪个结点的 LCA 组合是不可能出现的?

A.
LCA(12, 4) = 4
B.
LCA(18, 4) = 4
C.
LCA(12, 18, 4) = 4
D.
LCA(12, 1) = 4

正确答案D

解析详情
答案 D最近公共祖先与根结点
思路与推导
  1. LCA(12,18)=4 说明结点 4 同时是 12 和 18 的祖先,且没有更深的公共祖先。

  2. 因此把其中一个结点换成 4,或把 4 加入共同求 LCA 的结点集合,最近公共祖先仍为 4。

  3. 但根结点 1 的祖先只有它自己。1 与任意结点的最近公共祖先都只能是 1,所以 LCA(12,1)=1,不可能等于 4,选 D。

第 11 题(单选题,2 分)

递归关系式 T(n)=2T(n/2)+O(n2)T(n) = 2T(n/2) + O(n^{2}) 描述了某个分治算法的时间复杂度。请问该算法的时间复杂度是多少?

A.
O(n)O(n)
B.
O(nlog⁡n)O(n \log n)
C.
O(n2)O(n^{2})
D.
O(n2log⁡n)O(n^{2}\log n)

正确答案C

解析详情
答案 C递归树与几何级数
思路与推导
  1. 第 0 层只有一个规模 n 的问题,非递归工作量为 O(n2)O(n^2)。

  2. 第 j 层有 2j2^j 个规模为 n/2jn/2^j 的子问题,该层工作量为 O(2j(n/2j)2)=O(n2/2j)O(2^j(n/2^j)^2)=O(n^2/2^j)。

  3. 各层之和不超过 O(n2(1+1/2+1/4+⋯ ))=O(n2)O(n^2(1+1/2+1/4+\cdots))=O(n^2),叶子层的线性开销也被包含,故选 C。

第 12 题(单选题,2 分)

在一个初始为空的最小堆 (min-heap) 中,依次插入元素 20, 12, 15, 8, 10, 5。然后连续执行两次"删除最小值" (delete-min) 操作,请问此时堆顶元素是什么?

A.
10
B.
12
C.
15
D.
20

正确答案A

解析详情
答案 A最小堆与删除最小值
思路与推导
  1. 最小堆只保证堆顶为当前最小值,不要求整个数组有序。

  2. 插入所有元素后,集合为 {5,8,10,12,15,20},第一次删除取走 5,调整后堆顶为 8。

  3. 第二次删除再取走 8,剩余元素的最小值为 10,因此堆顶为 10,选 A。

第 13 题(单选题,2 分)

1 到 1000 之间,不能被 2、3、5 中任意一个数整除的整数有多少个?

A.
266
B.
267
C.
333
D.
734

正确答案A

解析详情
答案 A容斥原理
思路与推导
  1. 先数能被 2、3、5 中至少一个整除的数。三个集合大小分别为 500、333、200。

  2. 被两个集合重复计入的数分别是 6、10、15 的倍数,个数为 166、100、66,需减去;30 的倍数共 33 个,被多减一次,要加回来。

  3. 并集大小为 500+333+200−166−100−66+33=734500+333+200-166-100-66+33=734。

  4. 题目求并集的补集,故有 1000−734=2661000-734=266 个,选 A。

第 14 题(单选题,2 分)

斐波那契数列的定义为 F(0)=0, F(1)=1, F(n)=F(n−1)+F(n−2)。使用朴素递归方法计算 F(n) 的时间复杂度是指数级的。而使用动态规划(或迭代)方法的时间复杂度是线性的。适应这种巨大差异的根本原因是?

A.
递归函数调用栈开销过大
B.
操作系统对递归深度有限制
C.
朴素递归中存在大量的重叠子问题未被重复利用
D.
动态规划使用了更少的数据存储空间

正确答案C

解析详情
答案 C重叠子问题与动态规划
思路与推导
  1. 朴素递归计算 F(n) 时会计算 F(n−1) 和 F(n−2),而 F(n−1) 内部又会计算 F(n−2),同一状态被反复求解。

  2. 例如求 F(5),F(3) 既在 F(4) 的递归中出现,又作为 F(5) 的直接子问题出现;这种重复会继续向下扩展。

  3. 动态规划保存已算出的值,让 F(0) 到 F(n) 每个状态只算一次,每次转移只需常数次操作,时间变成 O(n)O(n),选 C。

第 15 题(单选题,2 分)

有 5 个独立的、不可抢占的任务 A1, A2, A3, A4, A5 需要在一台机器上执行(从时间 0 开始执行),每个任务都有对应的处理时长和截止时刻,按顺序分别为 3,4,2,5,1 和 5,10,3,15,11。如果某一个任务超时,相应的惩罚等于其处理时长。为了最小化总惩罚,应该优先执行哪个任务?

A.
处理时间最短的任务 A5
B.
截止时间最早的任务 A3
C.
处理时间最长的任务 A4
D.
任一任务都可以

正确答案B

解析详情
答案 B截止时间调度与最优性验证
思路与推导
  1. 按截止时刻从早到晚排为 A3、A1、A2、A5、A4,对应截止时刻为 3、5、10、11、15。

  2. 从时刻 0 连续执行,完成时刻依次为 2、5、9、10、15,均不晚于各自截止时刻,总惩罚为 0,已达到非负惩罚的最小值。

  3. 若先做 A5,则做完 A3 最早为时刻 3,再做 A1 最早为时刻 6,A1 超时;若为了 A1 先执行它,A3 又会超时。先做 A4 更会使 A3 错过截止时刻。

  4. 所以给定选项中应优先执行 A3,选 B。这里是用具体可行安排证明本题最优,不是宣称该规则能解决所有带惩罚的调度问题。

二、阅读程序(1)

 1 | #include <algorithm>
 2 | #include <cstdio>
 3 | #include <cstring>
 4 | bool flag[27];
 5 | int n;
 6 | int p[27];
 7 | int ans = 0;
 8 | void dfs(int k) {
 9 |     if (k == n + 1){
10 |         ++ ans;
11 |         return;
12 |     }
13 |     for (int i = 1; i <= n; ++i) {
14 |         if (flag[i]) continue;
15 |         if (k > 1 && i == p[k - 1] + 1) continue;
16 |         p[k] = i;
17 |         flag[i] = true;
18 |         dfs(k + 1);
19 |         flag[i] = false;
20 |     }
21 |     return;
22 | }
23 | int main() {
24 |     scanf("%d", &n);
25 |     dfs(1);
26 |     printf("%d\n", ans);
27 |     return 0;
28 | }

第 16 题(判断题,1 分)

当输入的 n=3 的时候,程序输出的答案为 3。

正确答案正确

解析详情
答案 正确排列搜索与相邻约束
思路与推导
  1. flag 保证每个数只用一次;第 15 行禁止后一个数恰为前一个数加 1,而不是禁止相差 1 的所有情况。

  2. n=3 的六个排列中,123、231、312 分别含有 1→2 或 2→3,会被剪枝;132、213、321 都合法。

  3. 因此恰有 3 个排列走到 k=n+1 的计数位置,输出 3,判断正确。

第 17 题(判断题,1.5 分)

在 dfs 函数运行过程中,k 的取值会满足 1≤k≤n+1。

正确答案正确

解析详情
答案 正确递归参数与终止条件
思路与推导
  1. 主函数从 dfs(1) 开始,每次递归只调用 dfs(k+1),所以 k 不会小于 1。

  2. 当 k≤n 时,当前层尝试填写 p[k];填完第 n 个位置后,下一层到达 k=n+1。

  3. 此时先执行 ++ans,再立即 return,不会继续调用 dfs(n+2)。所以所有调用均满足 1≤k≤n+11\le k\le n+1。

第 18 题(判断题,1.5 分)

删除第 19 行的 "flag[i]=false",对答案不会产生影响。

正确答案错误

解析详情
答案 错误回溯与状态恢复
思路与推导
  1. flag[i]=true 表示数字 i 被当前搜索路径占用。递归返回后,需要恢复为 false,让其他分支还能选择它。

  2. 若删除恢复语句,已经离开的路径仍会占住数字。例如 n=2 时,先选 1 后,2 因相邻递增被禁,分支失败;回到根时 1 仍被标记。

  3. 随后尝试以 2 开头,也无法再选 1,于是输出 0;正常程序会计入排列 21,输出 1。因此删除该行会影响答案。

第 19 题(单选题,3 分)

当输入的 n=4 的时候,程序输出的答案为 ( )。

A.
11
B.
12
C.
24
D.
9

正确答案A

解析详情
答案 A相邻禁对的容斥计数
思路与推导
  1. n=4 时共有 4!=244!=24 个排列,需要排除含有连续片段 12、23、34 的排列。

  2. 固定任意一个禁对,把它粘成一个整体,与另外两个数字排列,有 3!=63!=6 种;三个禁对共先减去 3×63\times6。

  3. 同时包含任意两个禁对时,粘合后均剩两个整体,有 2!=22!=2 种;共有 (32)\binom{3}{2} 对,因此加回 6。同时包含三个禁对只有 1234,再减去 1。

  4. 合法数为 24−18+6−1=1124-18+6-1=11,选 A。

第 20 题(单选题,3 分)

如果因为某些问题,导致程序运行第 25 行的 dfs 函数之前,数组 p 的初值并不全为 0,则对程序的影响是 ( )。

A.
输出的答案比原答案要小
B.
无法确定输出的答案
C.
程序可能陷入死循环
D.
没有影响

正确答案D

解析详情
答案 D先赋值后使用与短路求值
思路与推导
  1. 当 k=1 时,条件 k>1 为假,&& 短路,不会读取 p[0]。

  2. 当 k>1 时,程序只读取 p[k−1]。这个元素已在上一层递归进入当前层之前,由 p[k−1]=i 写入了本次选择的值。

  3. 每个真正用到的 p 元素都已由当前路径赋值,原来的内容不会影响剪枝或答案,因此选 D。

第 21 题(单选题,3 分)

假如删去第 14 行的 "if(flag[i])continue",输入 3,得到的输出答案是 ( )。

A.
27
B.
3
C.
16
D.
12

正确答案C

解析详情
答案 C有重复序列的状态计数
思路与推导
  1. 删除 flag 检查后,同一个数字可以重复使用,但相邻的 1→2 和 2→3 仍被禁止。

  2. 记长度为 t、末项分别为 1、2、3 的序列数为 a、b、c。添加 1 没有限制;添加 2 时前项不能为 1;添加 3 时前项不能为 2,因此下一层为 (a+b+c,b+c,a+c)。

  3. 长度 1 的计数为 (1,1,1),长度 2 为 (3,2,2),长度 3 为 (7,4,5)。

  4. 总数为 7+4+5=167+4+5=16,选 C。

图解 · 看懂这一步允许重复后按末项计数

左右滑动表格,可查看完整内容。

长度末项 1末项 2末项 3总数
11113
23227
374516

每行来自上一行;仍禁止 1→2 和 2→3。

二、阅读程序(2)

 1 | #include <algorithm>
 2 | #include <cstdio>
 3 | #include <cstring>
 4 | #define ll long long
 5 | int cnt_broken = 0;
 6 | int cnt_check = 0;
 7 | int n, k;
 8 | inline bool check(int h) {
 9 |     printf("now check:%d\n", h);
10 |     ++cnt_check;
11 |     if (cnt_broken == 2) {
12 |         printf("You have no egg!\n");
13 |         return false;
14 |     }
15 |     if (h >= k) {
16 |         ++cnt_broken;
17 |         return true;
18 |     } else {
19 |         return false;
20 |     }
21 | }
22 | inline bool assert_ans(int h) {
23 |     if (h == k) {
24 |         printf("You are Right using %d checks\n", cnt_check);
25 |         return true;
26 |     } else {
27 |         printf("Wrong answer!\n");
28 |         return false;
29 |     }
30 | }
31 | inline void guess1(int n) {
32 |     for (int i = 1; i <= n; ++i) {
33 |         if (check(i)) {
34 |             assert_ans(i);
35 |             return;
36 |         }
37 |     }
38 | }
39 | inline void guess2(int n) {
40 |     int w = 0;
41 |     for (w = 1; w * (w + 1) / 2 < n; ++w)
42 |         ;
43 |     for (int ti = w, nh = w;; --ti, nh += ti, nh = std::min(nh, n)) {
44 |         if (check(nh)) {
45 |             for (int j = nh - ti + 1; j < nh; ++j) {
46 |                 if (check(j)) {
47 |                     assert_ans(j);
48 |                     return;
49 |                 }
50 |             }
51 |             assert_ans(nh);
52 |             return;
53 |         }
54 |     }
55 | }
56 | int main() {
57 |     scanf("%d%d", &n, &k);
58 |     int t;
59 |     scanf("%d", &t);
60 |     if (t == 1) {
61 |         guess1(n);
62 |     } else {
63 |         guess2(n);
64 |     }
65 |     return 0;
66 | }

> (注意:下述的"猜测数"为调用 check 函数的次数 (即 cnt_check 的值);"猜测正确"的含义为 assert_ans 函数 return true (执行第 25 行所在分支)的情况;所有输入保证 1 ≤ k ≤ n。)

第 22 题(判断题,1.5 分)

当输入为 "6 5 1" 时,猜测次数为 5;当输入为 "6 5 2" 时,猜测次数为 3。

正确答案正确

解析详情
答案 正确两种猜测策略的执行过程
思路与推导
  1. t=1 时从高度 1 依次检查到 5;前四次不碎,第五次碎并确认答案,共调用 check 5 次。

  2. t=2 时,满足 w(w+1)/2≥6w(w+1)/2\ge6 的最小 w 为 3。先检查 3,不碎;更新后检查 5,碎,此时 ti=2。

  3. 内层从 5−2+1=4 开始,只检查 4,结果不碎,因而确定临界高度就是已检查过的 5。检查顺序为 3、5、4,共 3 次,判断正确。

图解 · 看懂这一步n=6、k=5 时的 guess2

左右滑动表格,可查看完整内容。

调用次数测试高度反馈下一步
13不碎端点升到 5
25碎检查区间内部
34不碎确定 k=5

碎掉第一枚蛋后改为顺序查找;assert_ans 不计为猜测。

第 23 题(判断题,1.5 分)

不管输入的 n 和 k 具体为多少,t=2 时的猜测数总是小于等于 t=1 时的猜测数。

正确答案错误

解析详情
答案 错误最坏复杂度与具体输入比较
思路与推导
  1. 给出反例 n=6、k=1。guess1 第一次检查高度 1 就碎,直接确定答案,只需 1 次。

  2. guess2 先取 w=3,检查高度 3 会碎,再从区间下端检查高度 1,又碎并确认答案,共 2 次。

  3. 因此 guess2 并非对每个输入都比 guess1 用更少的猜测,判断错误;它改进的是最坏情况的量级。

第 24 题(判断题,1.5 分)

不管 t=1 或 t=2,程序都一定会得到正确结果。

正确答案正确

解析详情
答案 正确单调边界与两枚鸡蛋的正确性
思路与推导
  1. check(h) 在 h<k 时不碎,在 h≥k 时碎。guess1 从 1 递增检查,第一次碎的位置必为 k,并立即返回。

  2. guess2 先检查分段端点,步长依次为 w、w−1、…;三角形和至少为 n,所以最终一定能遇到不低于 k 的端点 nh。

  3. 第一次碎后,前一个已测端点以下都安全。程序在线性扫描的范围内从低到高寻找 k;若都不碎,则 k=nh。最后一次端点被截到 n 时,扫描起点可能略早,但不会漏掉真正的 k。

  4. 端点测试最多打碎一个蛋,线性阶段一旦打碎第二个就立即返回,因此不会耗尽鸡蛋后还继续寻找,判断正确。

第 25 题(单选题,3 分)

函数 guess1 在运行过程中,cnt_broken 的值最多为 ( )。

A.
0
B.
1
C.
2
D.
n

正确答案B

解析详情
答案 B首次命中与提前返回
思路与推导
  1. guess1 从高度 1 开始逐层调用 check;在 i<k 时,cnt_broken 保持 0。

  2. 到 i=k 时,check 返回 true,同时 cnt_broken 增加到 1,随后执行 assert_ans(i) 并立即 return。

  3. 由于输入保证 k≤n,这次命中必会发生,此后不再调用 check,所以最大值为 1,选 B。

第 26 题(单选题,3 分)

函数 guess2 在运行过程中,最多使用的猜测次数的量级为 ( )。

A.
O(n)O(n)
B.
O(n2)O(n^{2})
C.
O(n)O(\sqrt{n})
D.
O(log⁡n)O(\log n)

正确答案C

解析详情
答案 C递减步长与最坏次数平衡
思路与推导
  1. 设最多允许 w 次猜测。第一段可覆盖 w 层:若第一下就碎,余下最多 w−1 次顺序试低层;若不碎,已经用掉一次,第二段只能再覆盖 w−1 层。

  2. 这样 w 次可覆盖的层数为 w+(w−1)+⋯+1=w(w+1)/2w+(w-1)+\cdots+1=w(w+1)/2,程序选择其不小于 n 的最小整数 w。

  3. 若第 r 次端点测试打碎鸡蛋,当时段长为 w−r+1,最多再检查 w−r 个内部位置,总次数不超过 w。

  4. 由 w(w+1)/2≥nw(w+1)/2\ge n 得 w=Θ(n)w=\Theta(\sqrt n),故猜测次数量级为 O(n)O(\sqrt n),选 C。

第 27 题(单选题,3 分)

当输入的 n=100 的时候,代码中 t=1 和 t=2 分别需要的猜测次数最多分别为 ( )。

A.
100, 14
B.
100, 13
C.
99, 14
D.
99, 13

正确答案A

解析详情
答案 A最坏次数的上界与达到上界的输入
思路与推导
  1. guess1 在 k=100 时需要逐层检查 1 到 100,最坏 100 次。

  2. guess2 中 13×14/2=91<10013\times14/2=91<100,而 14×15/2=105≥10014\times15/2=105\ge100,所以 w=14,最坏次数不超过 14。

  3. 该上界能达到:取 k=14,先检查 14 并打碎第一枚蛋,再检查 1 到 13,均不碎,最终确认 14。总共 1+13=141+13=14 次。

  4. 两种策略最坏次数分别为 100、14,选 A。

二、阅读程序(3)

 1 | #include <algorithm>
 2 | #include <cstdio>
 3 | #include <cstring>
 4 | #include <vector>
 5 | #define ll long long
 6 | int n, m;
 7 | std::vector<int> k, p;
 8 | std::vector<int> ans1, ans2;
 9 | int cnt1, cnt2;
10 | inline int mpow(int x, int k) {
11 |     int ans = 1;
12 |     for (; k; k >>= 1, x = x * x) {
13 |         if (k & 1)
14 |             ans = ans * x;
15 |     }
16 |     return ans;
17 | }
18 | inline void dfs(std::vector<int>& ans, int& cnt, int l, int r, int v) {
19 |     if (l > r) {
20 |         ++cnt;
21 |         ans.push_back(v);
22 |         return;
23 |     }
24 |     for (int i = 1; i <= m; ++i) {
25 |         dfs(ans, cnt, l + 1, r, v + k[l] * mpow(i, p[l]));
26 |     }
27 |     return;
28 | }
29 | std::vector<int> cntans1;
30 | int main() {
31 |     scanf("%d%d", &n, &m);
32 |     k.resize(n + 1);
33 |     p.resize(n + 1);
34 |     for (int i = 1; i <= n; ++i) {
35 |         scanf("%d%d", &k[i], &p[i]);
36 |     }
37 |     dfs(ans1, cnt1, 1, n >> 1, 0);
38 |     dfs(ans2, cnt2, (n >> 1) + 1, n, 0);
39 |     std::sort(ans1.begin(), ans1.end());
40 |     int newcnt1 = 1;
41 |     cntans1.push_back(1);
42 |     for (int i = 1; i < cnt1; ++i) {
43 |         if (ans1[i] == ans1[newcnt1 - 1]) {
44 |             ++cntans1[newcnt1 - 1];
45 |         } else {
46 |             ans1[newcnt1++] = ans1[i];
47 |             cntans1.push_back(1);
48 |         }
49 |     }
50 |     cnt1 = newcnt1;
51 |     std::sort(ans2.begin(), ans2.end());
52 |     int las = 0;
53 |     ll ans = 0;
54 |     for (int i = cnt2 - 1; i >= 0; --i) {
55 |         for (; las < cnt1 && ans1[las] + ans2[i] < 0; ++las)
56 |             ;
57 |         if (las < cnt1 && ans1[las] + ans2[i] == 0) {
58 |             ans += cntans1[las];
59 |         }
60 |     }
61 |     printf("%lld\n", ans);
62 |     return 0;
63 | }

第 28 题(判断题,1.5 分)

删除第 51 行的"std::sort(ans2.begin(), ans2.end());"后,代码输出的结果不会受到影响。

正确答案错误

解析详情
答案 错误排序与单调双指针
思路与推导
  1. ans1 已排序。程序倒序访问 ans2,并让 las 只向右移动去寻找 −ans2[i];要使目标值单调不减,ans2 必须先升序排序。

  2. 反例取 n=3、m=2,各 (k,p) 依次为 (1,1)、(−2,1)、(1,1)。ans1=[1,2],DFS 生成的 ans2=[−1,0,−3,−2]。

  3. 若不排序,倒序先处理 −2,las 移到值 2 并计入一次;再处理 −3,las 越过数组末尾,之后遇到 −1 时无法回退,漏掉与 1 的匹配。

  4. 正确解数为 2,删除排序后仅输出 1,因此判断错误。

第 29 题(判断题,1.5 分)

假设计算过程中不发生溢出,函数 mpow(x, k) 的功能是求出 xkx^{k} 的取值。( )

正确答案正确

解析详情
答案 正确二进制快速幂
思路与推导
  1. 把初始指数写成二进制。每轮检查最低位,若为 1,就把当前 x 乘入 ans;之后指数右移一位,x 平方。

  2. 循环中的 x 依次代表原底数的 1,2,4,8,…1,2,4,8,\ldots 次幂,恰好与指数的各二进制位对应。

  3. 例如 13=(1101)213=(1101)_2,会乘入原底数的 1、4、8 次幂,结果为 x1+4+8=x13x^{1+4+8}=x^{13}。指数为 0 时循环不执行,返回 1。

  4. 因此在非负整数指数且计算不溢出的题设下,函数返回所求幂,判断正确。

第 30 题(判断题,1.5 分)

代码中第 39 行到第 50 行的目的是为了将 ans1 数组进行"去重"操作。( )

正确答案正确

解析详情
答案 正确排序压缩与频次保留
思路与推导
  1. 排序后,相同部分和会连续出现。newcnt1 表示目前保留的不同值个数,ans1[newcnt1−1] 是最后一个已保留值。

  2. 若扫描到相同值,只增加对应频次;若不同,就把新值写到 ans1[newcnt1],再增加 newcnt1 并设置频次为 1。

  3. 例如 [−2,−2,1,1,1,4] 被压成有效前缀 [−2,1,4],频次为 [2,3,1],最后 cnt1=3。

  4. 这完成了逻辑去重,因此判断正确;vector 的物理长度未缩短,但后续只访问前 cnt1 个位置。

第 31 题(单选题,3 分)

当输入为"3 15 1 2 -1 2 1 2"时,输出结果为 ( )

A.
4
B.
8
C.
0
D.
10

正确答案B

解析详情
答案 B折半搜索与勾股数计数
思路与推导
  1. 输入得到 n=3、m=15,三项系数与指数分别为 (1,2)、(−1,2)、(1,2),要求 x12−x22+x32=0x_1^2-x_2^2+x_3^2=0,即 x12+x32=x22x_1^2+x_3^2=x_2^2。

  2. 在正整数 1 到 15 内,忽略两条直角边的顺序,勾股数组为 (3,4,5)、(6,8,10)、(5,12,13)、(9,12,15)。前两项是直角边,第三项是斜边。

  3. 每组中斜边对应 x₂,两条不同的直角边可分别分配给 x₁、x₃,形成 2 个有序解。

  4. 所以共 4×2=84\times2=8 组,程序输出 8,选 B。

第 32 题(单选题,3 分)

记程序结束前 p 数组元素的最大值为 P,则该代码的时间复杂度是 ( )

A.
O(n)O(n)
B.
O(mnlog⁡mn)O(m^{n}\log m^{n})
C.
O(mn/2log⁡mn/2)O(m^{n/2}\log m^{n/2})
D.
O(mn/2(log⁡mn/2+log⁡P))O\left(m^{n/2}\left(\log m^{n/2}+\log P\right)\right)

正确答案D

解析详情
答案 D折半枚举、快速幂与排序复杂度
思路与推导
  1. DFS 将变量分为前后两半,每个变量枚举 1 到 m。按选项常用的“每半约 n/2 个变量”记法,每半状态数约为 S=mn/2S=m^{n/2}。

  2. 每条搜索转移调用 mpow 计算次幂,最大指数为 P,快速幂开销为 O(log⁡P)O(\log P);枚举部分的开销相应为 O(Slog⁡P)O(S\log P)。

  3. 两组部分和都要排序,开销为 O(Slog⁡S)O(S\log S);随后对 ans1 做频次压缩,再用单调双指针配对,合计只需线性扫描。代码并没有调用 lower_bound 或 upper_bound。

  4. 两类主要开销相加,得到选项 D:O(mn/2(log⁡mn/2+log⁡P))O(m^{n/2}(\log m^{n/2}+\log P))。

  5. 严格处理奇数 n 时,两半大小分别是 ⌊n/2⌋\lfloor n/2\rfloor、⌈n/2⌉\lceil n/2\rceil,应以较大的 m⌈n/2⌉m^{\lceil n/2\rceil} 表示状态规模;这不改变本题对排序和快速幂两项开销的考查。

第 33 题(单选题,3 分)

本题所求的是 ( )。

A.
满足 a,b,c∈[1,m]a,b,c \in [1,m] 的整数方程 a3+b3=c3a^{3}+b^{3}=c^{3} 的解的数量
B.
满足 a,b,c∈[1,m]a,b,c \in [1,m] 的整数方程 a2+b2=c2a^{2}+b^{2}=c^{2} 的解的数量
C.
满足 xi∈[0,m]x_i \in [0,m] 的整数方程 ∑i=1nkixipi=0\sum_{i=1}^{n} k_i x_i^{p_i}=0 的解的数量
D.
满足 xi∈[1,m]x_i \in [1,m] 的整数方程 ∑i=1nkixipi=0\sum_{i=1}^{n} k_i x_i^{p_i}=0 的解的数量

正确答案D

解析详情
答案 D折半搜索的状态含义
思路与推导
  1. dfs 的 l 表示当前变量编号,i 从 1 到 m 枚举它的取值,将 kliplk_l i^{p_l} 加入部分和 v。

  2. ans1 保存前半变量的所有部分和,ans2 保存后半变量的所有部分和。每组左右赋值唯一对应一组完整的变量赋值。

  3. 程序寻找两半之和为 0 的配对,并按频次累加,因此统计的是 xi∈[1,m]x_i\in[1,m] 时 ∑i=1nkixipi=0\sum_{i=1}^{n}k_i x_i^{p_i}=0 的解数,选 D。

三、完善程序(1)

1. (特殊最短路)给定一个含 N 个点 M 条边的带权无向图,边权非负。起点为 S,终点为 T。对于一条 S 到 T 的路径,可以在整条路径中,至多选择一条边作为"免费边";当第一次经过这条被选中的边时,费用视为 0;如果之后再次经过该边,则仍按其原始权重计费。点和边均可重复经过。求从 S 到 T 的最小总费用。 以下代码求解了上述问题,试补全程序。

#include <algorithm>
#include <iostream>
#include <queue>
#include <vector>
using namespace std;

const long long INF = 1e18;

struct Edge {
    int to;
    int weight;
};

struct State {
    long long dist;
    int u;
    int used_freebie; // 0 for not used, 1 for used
    bool operator>(const State &other) const {
        return dist > other.dist;
    }
};

int main() {
    int n, m, s, t;
    cin >> n >> m >> s >> t;

    vector<vector<Edge>> adj(n + 1);
    for (int i = 0; i < m; ++i) {
        int u, v, w;
        cin >> u >> v >> w;
        adj[u].push_back({v, w});
        adj[v].push_back({u, w});
    }

    vector<vector<long long>> d(n + 1, vector<long long>(2, INF));
    priority_queue<State, vector<State>, greater<State>> pq;
    d[s][0] = 0;
    pq.push({0, s, ①});

    while (!pq.empty()) {
        State current = pq.top();
        pq.pop();

        long long dist = current.dist;
        int u = current.u;
        int used = current.used_freebie;

        if (dist > ②) {
            continue;
        }

        for (const auto &edge : adj[u]) {
            int v = edge.to;
            int w = edge.weight;

            if (d[u][used] + w < ③) {
                ③ = d[u][used] + w;
                pq.push({d[v][used], v, used});
            }

            if (used == 0) {
                if (④ < d[v][1]) {
                    d[v][1] = ④;
                    pq.push({d[v][1], v, 1});
                }
            }
        }
    }

    cout << ⑤ << endl;
    return 0;
}

第 34 题(单选题,3 分)

①处应填( )

A.
0
B.
1
C.
-1
D.
false

正确答案A

解析详情
答案 A分层图的状态定义与初始化
思路与推导
  1. 定义 d[u][0] 为到达 u 且尚未使用免费机会的最小费用,d[u][1] 为到达 u 且已经使用的最小费用。同一顶点必须区分这两种状态。

  2. 出发时位于 s、费用为 0、免费机会未用,所以 d[s][0]=0,队列条目也必须为 {0,s,0}。

  3. 因此①填 0,选 A,使入队状态与距离初始化一致。

第 35 题(单选题,3 分)

②处应填( )

A.
d[u][!used]
B.
d[u][used]
C.
d[t][used]
D.
INF

正确答案B

解析详情
答案 BDijkstra 的过期队列条目
思路与推导
  1. 同一个 (u,used) 状态可能先以较大距离入队,后来又找到更短路径,因此队列中可能保留旧条目。

  2. 弹出条目的 dist 应与同一个状态的当前记录 d[u][used] 比较。若 dist 更大,说明旧条目已过期,可以跳过。

  3. 例如 d[u][0] 已由 10 降到 6,之后弹出的 {10,u,0} 就无需再次扩展。②应填 d[u][used],选 B。

第 36 题(单选题,3 分)

③处应填( )

A.
d[v][1]
B.
d[v][used]
C.
d[u][used]
D.
d[v][0]

正确答案B

解析详情
答案 B付费经过边时的同层松弛
思路与推导
  1. 这里按原价 w 经过边 u→v,没有使用新的免费机会,所以状态 used 保持不变。

  2. 候选费用为 d[u][used]+w,目标为 d[v][used];只有候选费用更小时,才更新这个目标并入队。

  3. 因此比较右侧和赋值左侧的③都填 d[v][used],选 B。used=0 和 used=1 均能进行这种付费转移。

第 37 题(单选题,3 分)

④处应填( )

A.
d[v][0]
B.
d[v][1]
C.
d[u][0]
D.
d[u][1]

正确答案C

解析详情
答案 C使用一次免费机会的跨层转移
思路与推导
  1. 该分支的前提 used==0 表示免费机会尚未使用。此时把边 u→v 的本次费用设为 0,到达 v 后机会已用完。

  2. 因此跨层转移为 d[v][1]←min⁡(d[v][1],d[u][0])d[v][1]\gets\min(d[v][1],d[u][0]),候选费用只包含到达 u 的费用,不再加 w。

  3. ④出现于比较与赋值的源表达式位置,所以应填 d[u][0],选 C。之后只能做第 1 层内的付费转移,不能再次免费。

图解 · 看懂这一步两层状态的三种合法转移

左右滑动表格,可查看完整内容。

转移源状态目标状态本次费用
尚未用机会,付费(u,0)(v,0)w
已经用机会,付费(u,1)(v,1)w
本次使用免费机会(u,0)(v,1)0

u→v 原权为 w;免费机会只可在 0→1 时使用一次。

第 38 题(单选题,3 分)

⑤处应填( )

A.
d[t][1]
B.
d[t][0]
C.
min(d[t][0], d[t][1])
D.
d[t][0] + d[t][1]

正确答案C

解析详情
答案 C至多一次操作的终态合并
思路与推导
  1. 到终点 t 时,合法情况包括没有使用免费机会,以及恰好使用一次机会;它们分别对应 d[t][0]、d[t][1]。

  2. 这两者代表不同路径方案的最优费用,应取较小者,而不是求和,所以⑤填 min(d[t][0],d[t][1]),选 C。

  3. 例如 s=t 时,空路径费用为 0 且无需用免费机会;同时保留未使用状态能自然处理这一情况。

三、完善程序(2)

2. 工厂打算通过客户反馈来间接测试生产线,从而找到存在缺陷的生产线。工厂有 n 条生产线(编号 0~n−1),已知其中有一个生产线存在缺陷。每一轮测试为:从若干生产线的产品取样混合成一个批次发给客户。若该批次中包含缺陷生产线的产品,客户将要发送货(结果应为 1),否则正常收货(记为 0)。受售后压力限制,在所有发货批次中,最多只能有 k 次退货(即结果为 1 的次数 ≤k)。工厂的目标是,设计最少的间接测试数 w(发货总批次),保证根据客户收到或退货的反馈结果,唯一确定存在缺陷的生产线。

以下程序实现了工厂的目标,包含两部分:

i) 确定 w 的最小值,并设计最优测试方案; ii) 根据测试结果推断存在缺陷的生产线。该程序确定 w 最小值的方法为:由于不同的生产线故障时,测试应当返回不同的结果,因此 w 较测试的可能性系数不应少于生产线数量。

`test_subset()` 函数为抽象测试接口,输入所有批次的方案并返回一个二进制编码;该编码表示为每批次的检测结果(即最低位第 1 批次、最高位第 w 批次);其实现在此处未给出。

试补全程序。

**程序代码**

#include <algorithm>
#include <cstddef>
#include <iostream>
#include <vector>
using namespace std;

long long comb(int w, int i) {
    if (i < 0 || i > w) {
        return 0;
    }
    long long res = 1;
    for (int t = 1; t <= i; ++t) {
        res = res * (w - t + 1) / t;
    }
    return res;
}

// 计算长度为 w、1 的个数 ≤ k 的码字总数
long long count_patterns(int w, int k) {
    long long total = 0;
    for (int t = 0; t <= min(w, k); ++t) {
        total += comb(w, t);
    }
    return total;
}

// 抽象测试接口
int test_subset(const vector<vector<int>> &plan);

int solve(int n, int k) {
    // === 第 1 步:求最小 w ===
    int w = 1;
    while (①) {
        ++w;
    }
    cout << w << endl;

    // === 第 2 步:生成测试方案 ===
    vector<vector<int>> code(n, vector<int>(w, 0));
    int idx = 0;
    for (int ones = 0; ones <= k && idx < n; ++ones) {
        vector<int> bits(w, 0);
        fill(bits.begin(), bits.begin() + ones, 1);
        do {
            for (int b = 0; b < w; ++b) {
                code[idx][b] = bits[b];
            }
            ++idx;
            if (idx >= n) {
                break;
            }
        } while (②);
    }

    vector<vector<int>> plan(w);
    for (int i = 0; i < w; ++i) {
        for (int j = 0; j < n; ++j) {
            if (③) {
                plan[i].push_back(j);
            }
        }
    }

    // === 第 3 步:调用测试接口 ===
    int signature = test_subset(plan);

    // === 第 4 步:结果解码 ===
    vector<int> sig_bits(w, 0);
    for (int i = 0; i < w; ++i) {
        if (④) {
            sig_bits[i] = 1;
        }
    }

    for (int j = 0; j < n; ++j) {
        if (⑤) {
            return j;
        }
    }
}

int main() {
    int n, k;
    cin >> n >> k;
    int ans = solve(n, k);
    cout << ans << endl;
    return 0;
}

第 39 题(单选题,3 分)

①处应填( )

A.
(1<<w) < n
B.
count_patterns(w, k) < n
C.
count_patterns(k, w) < n
D.
comb(w, k) < n

正确答案B

解析详情
答案 B受限二进制编码与信息数量
思路与推导
  1. w 次测试的反馈可写为长度 w 的二进制码;缺陷线属于某批次则该位为 1。为满足最多 k 次退货,每个码中 1 的数量不得超过 k。

  2. 恰有 t 个 1 时,选择其所在位置有 (wt)\binom{w}{t} 种,因此可用码字数为 ∑t=0min⁡(w,k)(wt)\sum_{t=0}^{\min(w,k)}\binom{w}{t},即 count_patterns(w,k)。

  3. 为了区分 n 条生产线,必须至少有 n 个不同码。若数量还小于 n,就继续增加 w,因此①填 count_patterns(w,k)<n,选 B。

  4. 例如 n=4、k=1 时,w=2 只有 3 个可用码;w=3 有 000、100、010、001 共 4 个,足以唯一编码。

第 40 题(单选题,3 分)

②处应填( )

A.
next_permutation(bits.begin(), bits.end())
B.
prev_permutation(bits.begin(), bits.end())
C.
next_permutation(bits.begin(), bits.begin()+ones)
D.
prev_permutation(bits.begin(), bits.begin()+ones)

正确答案B

解析详情
答案 B固定 1 的数量与逆字典序枚举
思路与推导
  1. fill 将前 ones 个位置设为 1,其余为 0,初始 bits 形如 1100,是该组二进制多重集合的字典序最大排列。

  2. 从最大排列出发,需要用 prev_permutation 遍历更小排列。例如两个 1、两个 0 时,依次得到 1100、1010、1001、0110、0101、0011。

  3. 排列范围必须包含整个 bits;只排列前 ones 个位置会一直是全 1,无法改变选中的测试批次。故②填 prev_permutation(bits.begin(),bits.end()),选 B。

第 41 题(单选题,3 分)

③处应填( )

A.
(j>>i) & 1
B.
(i>>j) & 1
C.
code[i][j] == 1
D.
code[j][i] == 1

正确答案D

解析详情
答案 D码字矩阵与测试方案的转置关系
思路与推导
  1. code[j] 是第 j 条生产线的整段码字,code[j][i] 表示它是否参加第 i 批测试。

  2. 构造 plan[i] 时,外层 i 固定测试批次,内层 j 枚举生产线,应加入所有 code[j][i]==1 的生产线,故③选 D。

  3. 这样若缺陷线为 j,各批次收到的反馈就恰好是 code[j],可在后续解码中唯一定位。

图解 · 看懂这一步n=4、k=1 的测试方案

左右滑动表格,可查看完整内容。

生产线 jcode[j]加入的批次
0000不加入任何批次
1100第 0 批
2010第 1 批
3001第 2 批

列按第 0、1、2 批排列;每行至多一个 1,每条线的反馈不同。

第 42 题(单选题,3 分)

④处应填( )

A.
(signature >> i) & 1
B.
(signature >> i) ^ 1
C.
signature | (1 << i)
D.
(signature >> i) | 1

正确答案A

解析详情
答案 A右移与按位与取单个位
思路与推导
  1. signature 的最低位代表第 0 批测试,第 i 位代表第 i 批测试。先右移 i 位,把目标位移到最低位。

  2. 再与 1 按位与,屏蔽所有更高位,只保留 0 或 1。因此④填 (signature>>i)&1,选 A。

  3. 例如 signature=5,二进制为 101;i=0、1、2 时提取结果依次为 1、0、1,写入 sig_bits 后就还原了三批反馈。

第 43 题(单选题,3 分)

⑤处应填( )

A.
is_permutation(code[j].begin(), code[j].end(), sig_bits.begin())
B.
code[j] == sig_bits
C.
plan[j] == sig_bits
D.
code[j][i] == sig_bits[i]

正确答案B

解析详情
答案 B唯一编码与逐位匹配解码
思路与推导
  1. 每条生产线 j 都被分配了不同的码字 code[j];若它是缺陷线,各测试批次产生的反馈就应与该码字完全相同。

  2. 把 signature 拆成 sig_bits 后,逐条查找 code[j]==sig_bits,向量相等会比较长度及每个对应位置,匹配的 j 就是答案,选 B。

  3. 仅比较是否互为排列不够,例如 100 和 010 虽然都有一个 1,却表示不同批次退货,对应不同生产线。