试卷解析 · 理解每一步
2025 CCF CSP-S 第一轮(提高级 C++)
CSPS-2025-R1
试卷解析总览,可直接查看每题答案与解析。
第 1 题(单选题,2 分)
有 5 个红色球和 5 个蓝色球,它们除了颜色之外完全相同。将这 10 个球排成一排,要求任意两个蓝色球都不能相邻,有多少种不同的排列方法?
正确答案C
解析详情
先排好 5 个红球,形成“空位 红 空位 红 空位 红 空位 红 空位 红 空位”,共 6 个空位,包含两端。
蓝球不能相邻,因此每个空位最多放 1 个蓝球。把 5 个蓝球放入 6 个空位,相当于选择唯一不放蓝球的空位。
同色球完全相同,不需要对它们再排列,方案数为 ,选 C。
第 2 题(单选题,2 分)
在 KMP 算法中,对于模式串 P="abacaba",其 next 数组 (next[i] 定义为模式串 P[0..i] 最长公共前后缀的长度,且数组下标从 0 开始) 的值是什么?
正确答案A
解析详情
依题目定义,只比较真前缀和真后缀,不能把整个字符串算进去。
前四个前缀依次为 a、ab、aba、abac,其最长相同真前后缀依次为空、空、a、空,长度为 0、0、1、0。
后面三个前缀 abaca、abacab、abacaba 的最长相同真前后缀分别为 a、ab、aba,长度为 1、2、3。
故 next 数组为 {0,0,1,0,1,2,3},选 A。
左右滑动表格,可查看完整内容。
| i | P[0..i] | 最长相同真前后缀 | next[i] |
|---|---|---|---|
| 0 | a | 空 | 0 |
| 1 | ab | 空 | 0 |
| 2 | aba | a | 1 |
| 3 | abac | 空 | 0 |
| 4 | abaca | a | 1 |
| 5 | abacab | ab | 2 |
| 6 | abacaba | aba | 3 |
只比较真前缀和真后缀,整串本身不计。
第 3 题(单选题,2 分)
对一个大小为 16 (下标 0-15) 的数组上构造满线段树,查询区间 [3, 11] 时,最少需要访问多少个树结点(包括路径上的父结点和完全包含在查询区间内的结点)?
正确答案B
解析详情
从根区间 [0,15] 出发,只进入与 [3,11] 相交的子区间;若结点区间被完全覆盖,就直接取该结点的信息,不再向下。
左侧依次访问 [0,7]、[0,3]、[2,3]、[3,3],并访问完整覆盖的 [4,7],共 5 个结点。
右侧访问 [8,15] 和完整覆盖的 [8,11],共 2 个结点。加上根 [0,15],总数为 ,选 B。
最终参与合并的区间只有 [3,3]、[4,7]、[8,11],但题目还要求计入到达它们的父结点。
左右滑动表格,可查看完整内容。
| 顺序 | 区间 | 处理 |
|---|---|---|
| 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 树(包括根节点)共有多少个结点?
正确答案D
解析详情
根结点表示空串。每个非根结点对应一个不同的非空前缀,公共前缀只建一次。
以 c 开头的单词贡献 c、ca、cat、car、cart、cas、case,共 7 个结点;cart 复用 car,case 复用 ca。
以 d 开头的单词贡献 d、do、dog,共 3 个结点;插入 do 时,它的路径已由 dog 建好,只需标记单词结束。
加上根结点,得到 ,选 D。
第 5 题(单选题,2 分)
对于一个包含 n 个结点和 m 条边的有向无环图 (DAG),其拓扑排序的结果有多少种可能?
正确答案D
解析详情
拓扑序要求每条有向边的起点排在终点之前,实际有多少种顺序取决于边造成的先后约束。
若图是一条覆盖所有顶点的有向链,拓扑序只有 1 种;若图没有边,任意顶点排列都合法,共有 种。
例如 时共有 6 种,既不恒为 1,也不受“最多 n 种”限制,更不等于 n−m,所以选 D。
第 6 题(单选题,2 分)
在一个大小为 13 的哈希表中,使用闭散列法的线性探查来解决冲突。哈希函数为 H(key)=key mod 13,依次插入关键字 18, 26, 35, 9, 68, 74,插入 74 后,它最终被放置在哪个索引位置?
正确答案D
解析详情
依次计算余数并插入:18 放在 5,26 放在 0,35 放在 9。
插入 9 时,位置 9 已被 35 占用,向后探查到空位 10;68 的余数为 3,放在 3。
74 的余数为 9。位置 9、10 已占用,下一位置 11 为空,因此最终放在 11,选 D。
第 7 题(单选题,2 分)
一个包含 8 个顶点的完全图(顶点的编号为 1 到 8),任意两点之间的边权重等于两顶点编号的差的绝对值。例如,顶点 3 和 7 之间的边权重为 |7 - 3| = 4。该图的最小生成树总权重是多少?
正确答案A
解析详情
8 个顶点的生成树恰有 7 条边。不同顶点编号至少相差 1,因此每条边权至少为 1,总权重至少为 7。
选取 (1,2)、(2,3)、…、(7,8),得到连接所有顶点的一条链,没有环,且 7 条边权均为 1。
该构造达到下界,所以最小生成树总权重正好为 7,选 A。
第 8 题(单选题,2 分)
如果一棵二叉搜索树的后序遍历序列是 2, 5, 4, 8, 12, 10, 6,那么该树的前序遍历是什么?
正确答案A
解析详情
后序遍历是“左、右、根”,因此末尾的 6 是整棵树的根。
二叉搜索树左子树的值小于 6,得到后序序列 2,5,4,根为 4,左右孩子分别为 2、5;右子树后序为 8,12,10,根为 10,左右孩子为 8、12。
改按前序“根、左、右”访问:先 6,再 4,2,5,最后 10,8,12,得到选项 A。
- 左子树:4
- 左孩子:2
- 右孩子:5
- 右子树:10
- 左孩子:8
- 右孩子:12
分支和叶子均按从左到右排列;前序先访问父结点。
第 9 题(单选题,2 分)
一个 0-1 背包问题,背包容量为 20,现有 5 个物品,其重量和价值分别为 7, 5, 4, 3, 6 和 15, 12, 9, 7, 13。装入背包的物品能获得的最大总价值是多少?
正确答案D
解析详情
五件物品总重量为 ,总价值为 。容量为 20,因此至少要舍弃重量 5,且希望舍弃的价值最小。
只舍弃一件时,可舍弃重量 5、6 或 7 的物品,相应损失价值 12、13、15;最小损失为 12。重量 3 或 4 的单件不足以满足要求。
若舍弃至少两件,价值损失至少是两件最低价值之和 ,不会优于损失 12。
所以舍弃第 2 件,保留第 1、3、4、5 件,重量为 20、价值为 ,选 D。
第 10 题(单选题,2 分)
在一棵以结点 1 为根的树中,结点 12 和结点 18 的最近公共祖先 (LCA) 是结点 4。那么下列哪个结点的 LCA 组合是不可能出现的?
正确答案D
解析详情
LCA(12,18)=4 说明结点 4 同时是 12 和 18 的祖先,且没有更深的公共祖先。
因此把其中一个结点换成 4,或把 4 加入共同求 LCA 的结点集合,最近公共祖先仍为 4。
但根结点 1 的祖先只有它自己。1 与任意结点的最近公共祖先都只能是 1,所以 LCA(12,1)=1,不可能等于 4,选 D。
第 11 题(单选题,2 分)
递归关系式 描述了某个分治算法的时间复杂度。请问该算法的时间复杂度是多少?
正确答案C
解析详情
第 0 层只有一个规模 n 的问题,非递归工作量为 。
第 j 层有 个规模为 的子问题,该层工作量为 。
各层之和不超过 ,叶子层的线性开销也被包含,故选 C。
第 12 题(单选题,2 分)
在一个初始为空的最小堆 (min-heap) 中,依次插入元素 20, 12, 15, 8, 10, 5。然后连续执行两次"删除最小值" (delete-min) 操作,请问此时堆顶元素是什么?
正确答案A
解析详情
最小堆只保证堆顶为当前最小值,不要求整个数组有序。
插入所有元素后,集合为 {5,8,10,12,15,20},第一次删除取走 5,调整后堆顶为 8。
第二次删除再取走 8,剩余元素的最小值为 10,因此堆顶为 10,选 A。
第 13 题(单选题,2 分)
1 到 1000 之间,不能被 2、3、5 中任意一个数整除的整数有多少个?
正确答案A
解析详情
先数能被 2、3、5 中至少一个整除的数。三个集合大小分别为 500、333、200。
被两个集合重复计入的数分别是 6、10、15 的倍数,个数为 166、100、66,需减去;30 的倍数共 33 个,被多减一次,要加回来。
并集大小为 。
题目求并集的补集,故有 个,选 A。
第 14 题(单选题,2 分)
斐波那契数列的定义为 F(0)=0, F(1)=1, F(n)=F(n−1)+F(n−2)。使用朴素递归方法计算 F(n) 的时间复杂度是指数级的。而使用动态规划(或迭代)方法的时间复杂度是线性的。适应这种巨大差异的根本原因是?
正确答案C
解析详情
朴素递归计算 F(n) 时会计算 F(n−1) 和 F(n−2),而 F(n−1) 内部又会计算 F(n−2),同一状态被反复求解。
例如求 F(5),F(3) 既在 F(4) 的递归中出现,又作为 F(5) 的直接子问题出现;这种重复会继续向下扩展。
动态规划保存已算出的值,让 F(0) 到 F(n) 每个状态只算一次,每次转移只需常数次操作,时间变成 ,选 C。
第 15 题(单选题,2 分)
有 5 个独立的、不可抢占的任务 A1, A2, A3, A4, A5 需要在一台机器上执行(从时间 0 开始执行),每个任务都有对应的处理时长和截止时刻,按顺序分别为 3,4,2,5,1 和 5,10,3,15,11。如果某一个任务超时,相应的惩罚等于其处理时长。为了最小化总惩罚,应该优先执行哪个任务?
正确答案B
解析详情
按截止时刻从早到晚排为 A3、A1、A2、A5、A4,对应截止时刻为 3、5、10、11、15。
从时刻 0 连续执行,完成时刻依次为 2、5、9、10、15,均不晚于各自截止时刻,总惩罚为 0,已达到非负惩罚的最小值。
若先做 A5,则做完 A3 最早为时刻 3,再做 A1 最早为时刻 6,A1 超时;若为了 A1 先执行它,A3 又会超时。先做 A4 更会使 A3 错过截止时刻。
所以给定选项中应优先执行 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。
正确答案正确
解析详情
flag 保证每个数只用一次;第 15 行禁止后一个数恰为前一个数加 1,而不是禁止相差 1 的所有情况。
n=3 的六个排列中,123、231、312 分别含有 1→2 或 2→3,会被剪枝;132、213、321 都合法。
因此恰有 3 个排列走到 k=n+1 的计数位置,输出 3,判断正确。
第 17 题(判断题,1.5 分)
在 dfs 函数运行过程中,k 的取值会满足 1≤k≤n+1。
正确答案正确
解析详情
主函数从 dfs(1) 开始,每次递归只调用 dfs(k+1),所以 k 不会小于 1。
当 k≤n 时,当前层尝试填写 p[k];填完第 n 个位置后,下一层到达 k=n+1。
此时先执行 ++ans,再立即 return,不会继续调用 dfs(n+2)。所以所有调用均满足 。
第 18 题(判断题,1.5 分)
删除第 19 行的 "flag[i]=false",对答案不会产生影响。
正确答案错误
解析详情
flag[i]=true 表示数字 i 被当前搜索路径占用。递归返回后,需要恢复为 false,让其他分支还能选择它。
若删除恢复语句,已经离开的路径仍会占住数字。例如 n=2 时,先选 1 后,2 因相邻递增被禁,分支失败;回到根时 1 仍被标记。
随后尝试以 2 开头,也无法再选 1,于是输出 0;正常程序会计入排列 21,输出 1。因此删除该行会影响答案。
第 19 题(单选题,3 分)
当输入的 n=4 的时候,程序输出的答案为 ( )。
正确答案A
解析详情
n=4 时共有 个排列,需要排除含有连续片段 12、23、34 的排列。
固定任意一个禁对,把它粘成一个整体,与另外两个数字排列,有 种;三个禁对共先减去 。
同时包含任意两个禁对时,粘合后均剩两个整体,有 种;共有 对,因此加回 6。同时包含三个禁对只有 1234,再减去 1。
合法数为 ,选 A。
第 20 题(单选题,3 分)
如果因为某些问题,导致程序运行第 25 行的 dfs 函数之前,数组 p 的初值并不全为 0,则对程序的影响是 ( )。
正确答案D
解析详情
当 k=1 时,条件 k>1 为假,&& 短路,不会读取 p[0]。
当 k>1 时,程序只读取 p[k−1]。这个元素已在上一层递归进入当前层之前,由 p[k−1]=i 写入了本次选择的值。
每个真正用到的 p 元素都已由当前路径赋值,原来的内容不会影响剪枝或答案,因此选 D。
第 21 题(单选题,3 分)
假如删去第 14 行的 "if(flag[i])continue",输入 3,得到的输出答案是 ( )。
正确答案C
解析详情
删除 flag 检查后,同一个数字可以重复使用,但相邻的 1→2 和 2→3 仍被禁止。
记长度为 t、末项分别为 1、2、3 的序列数为 a、b、c。添加 1 没有限制;添加 2 时前项不能为 1;添加 3 时前项不能为 2,因此下一层为 (a+b+c,b+c,a+c)。
长度 1 的计数为 (1,1,1),长度 2 为 (3,2,2),长度 3 为 (7,4,5)。
总数为 ,选 C。
左右滑动表格,可查看完整内容。
| 长度 | 末项 1 | 末项 2 | 末项 3 | 总数 |
|---|---|---|---|---|
| 1 | 1 | 1 | 1 | 3 |
| 2 | 3 | 2 | 2 | 7 |
| 3 | 7 | 4 | 5 | 16 |
每行来自上一行;仍禁止 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。
正确答案正确
解析详情
t=1 时从高度 1 依次检查到 5;前四次不碎,第五次碎并确认答案,共调用 check 5 次。
t=2 时,满足 的最小 w 为 3。先检查 3,不碎;更新后检查 5,碎,此时 ti=2。
内层从 5−2+1=4 开始,只检查 4,结果不碎,因而确定临界高度就是已检查过的 5。检查顺序为 3、5、4,共 3 次,判断正确。
左右滑动表格,可查看完整内容。
| 调用次数 | 测试高度 | 反馈 | 下一步 |
|---|---|---|---|
| 1 | 3 | 不碎 | 端点升到 5 |
| 2 | 5 | 碎 | 检查区间内部 |
| 3 | 4 | 不碎 | 确定 k=5 |
碎掉第一枚蛋后改为顺序查找;assert_ans 不计为猜测。
第 23 题(判断题,1.5 分)
不管输入的 n 和 k 具体为多少,t=2 时的猜测数总是小于等于 t=1 时的猜测数。
正确答案错误
解析详情
给出反例 n=6、k=1。guess1 第一次检查高度 1 就碎,直接确定答案,只需 1 次。
guess2 先取 w=3,检查高度 3 会碎,再从区间下端检查高度 1,又碎并确认答案,共 2 次。
因此 guess2 并非对每个输入都比 guess1 用更少的猜测,判断错误;它改进的是最坏情况的量级。
第 24 题(判断题,1.5 分)
不管 t=1 或 t=2,程序都一定会得到正确结果。
正确答案正确
解析详情
check(h) 在 h<k 时不碎,在 h≥k 时碎。guess1 从 1 递增检查,第一次碎的位置必为 k,并立即返回。
guess2 先检查分段端点,步长依次为 w、w−1、…;三角形和至少为 n,所以最终一定能遇到不低于 k 的端点 nh。
第一次碎后,前一个已测端点以下都安全。程序在线性扫描的范围内从低到高寻找 k;若都不碎,则 k=nh。最后一次端点被截到 n 时,扫描起点可能略早,但不会漏掉真正的 k。
端点测试最多打碎一个蛋,线性阶段一旦打碎第二个就立即返回,因此不会耗尽鸡蛋后还继续寻找,判断正确。
第 25 题(单选题,3 分)
函数 guess1 在运行过程中,cnt_broken 的值最多为 ( )。
正确答案B
解析详情
guess1 从高度 1 开始逐层调用 check;在 i<k 时,cnt_broken 保持 0。
到 i=k 时,check 返回 true,同时 cnt_broken 增加到 1,随后执行 assert_ans(i) 并立即 return。
由于输入保证 k≤n,这次命中必会发生,此后不再调用 check,所以最大值为 1,选 B。
第 26 题(单选题,3 分)
函数 guess2 在运行过程中,最多使用的猜测次数的量级为 ( )。
正确答案C
解析详情
设最多允许 w 次猜测。第一段可覆盖 w 层:若第一下就碎,余下最多 w−1 次顺序试低层;若不碎,已经用掉一次,第二段只能再覆盖 w−1 层。
这样 w 次可覆盖的层数为 ,程序选择其不小于 n 的最小整数 w。
若第 r 次端点测试打碎鸡蛋,当时段长为 w−r+1,最多再检查 w−r 个内部位置,总次数不超过 w。
由 得 ,故猜测次数量级为 ,选 C。
第 27 题(单选题,3 分)
当输入的 n=100 的时候,代码中 t=1 和 t=2 分别需要的猜测次数最多分别为 ( )。
正确答案A
解析详情
guess1 在 k=100 时需要逐层检查 1 到 100,最坏 100 次。
guess2 中 ,而 ,所以 w=14,最坏次数不超过 14。
该上界能达到:取 k=14,先检查 14 并打碎第一枚蛋,再检查 1 到 13,均不碎,最终确认 14。总共 次。
两种策略最坏次数分别为 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());"后,代码输出的结果不会受到影响。
正确答案错误
解析详情
ans1 已排序。程序倒序访问 ans2,并让 las 只向右移动去寻找 −ans2[i];要使目标值单调不减,ans2 必须先升序排序。
反例取 n=3、m=2,各 (k,p) 依次为 (1,1)、(−2,1)、(1,1)。ans1=[1,2],DFS 生成的 ans2=[−1,0,−3,−2]。
若不排序,倒序先处理 −2,las 移到值 2 并计入一次;再处理 −3,las 越过数组末尾,之后遇到 −1 时无法回退,漏掉与 1 的匹配。
正确解数为 2,删除排序后仅输出 1,因此判断错误。
第 29 题(判断题,1.5 分)
假设计算过程中不发生溢出,函数 mpow(x, k) 的功能是求出 的取值。( )
正确答案正确
解析详情
把初始指数写成二进制。每轮检查最低位,若为 1,就把当前 x 乘入 ans;之后指数右移一位,x 平方。
循环中的 x 依次代表原底数的 次幂,恰好与指数的各二进制位对应。
例如 ,会乘入原底数的 1、4、8 次幂,结果为 。指数为 0 时循环不执行,返回 1。
因此在非负整数指数且计算不溢出的题设下,函数返回所求幂,判断正确。
第 30 题(判断题,1.5 分)
代码中第 39 行到第 50 行的目的是为了将 ans1 数组进行"去重"操作。( )
正确答案正确
解析详情
排序后,相同部分和会连续出现。newcnt1 表示目前保留的不同值个数,ans1[newcnt1−1] 是最后一个已保留值。
若扫描到相同值,只增加对应频次;若不同,就把新值写到 ans1[newcnt1],再增加 newcnt1 并设置频次为 1。
例如 [−2,−2,1,1,1,4] 被压成有效前缀 [−2,1,4],频次为 [2,3,1],最后 cnt1=3。
这完成了逻辑去重,因此判断正确;vector 的物理长度未缩短,但后续只访问前 cnt1 个位置。
第 31 题(单选题,3 分)
当输入为"3 15 1 2 -1 2 1 2"时,输出结果为 ( )
正确答案B
解析详情
输入得到 n=3、m=15,三项系数与指数分别为 (1,2)、(−1,2)、(1,2),要求 ,即 。
在正整数 1 到 15 内,忽略两条直角边的顺序,勾股数组为 (3,4,5)、(6,8,10)、(5,12,13)、(9,12,15)。前两项是直角边,第三项是斜边。
每组中斜边对应 x₂,两条不同的直角边可分别分配给 x₁、x₃,形成 2 个有序解。
所以共 组,程序输出 8,选 B。
第 32 题(单选题,3 分)
记程序结束前 p 数组元素的最大值为 P,则该代码的时间复杂度是 ( )
正确答案D
解析详情
DFS 将变量分为前后两半,每个变量枚举 1 到 m。按选项常用的“每半约 n/2 个变量”记法,每半状态数约为 。
每条搜索转移调用 mpow 计算次幂,最大指数为 P,快速幂开销为 ;枚举部分的开销相应为 。
两组部分和都要排序,开销为 ;随后对 ans1 做频次压缩,再用单调双指针配对,合计只需线性扫描。代码并没有调用 lower_bound 或 upper_bound。
两类主要开销相加,得到选项 D:。
严格处理奇数 n 时,两半大小分别是 、,应以较大的 表示状态规模;这不改变本题对排序和快速幂两项开销的考查。
第 33 题(单选题,3 分)
本题所求的是 ( )。
正确答案D
解析详情
dfs 的 l 表示当前变量编号,i 从 1 到 m 枚举它的取值,将 加入部分和 v。
ans1 保存前半变量的所有部分和,ans2 保存后半变量的所有部分和。每组左右赋值唯一对应一组完整的变量赋值。
程序寻找两半之和为 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 分)
①处应填( )
01-1false正确答案A
解析详情
定义 d[u][0] 为到达 u 且尚未使用免费机会的最小费用,d[u][1] 为到达 u 且已经使用的最小费用。同一顶点必须区分这两种状态。
出发时位于 s、费用为 0、免费机会未用,所以 d[s][0]=0,队列条目也必须为 {0,s,0}。
因此①填 0,选 A,使入队状态与距离初始化一致。
第 35 题(单选题,3 分)
②处应填( )
d[u][!used]d[u][used]d[t][used]INF正确答案B
解析详情
同一个 (u,used) 状态可能先以较大距离入队,后来又找到更短路径,因此队列中可能保留旧条目。
弹出条目的 dist 应与同一个状态的当前记录 d[u][used] 比较。若 dist 更大,说明旧条目已过期,可以跳过。
例如 d[u][0] 已由 10 降到 6,之后弹出的 {10,u,0} 就无需再次扩展。②应填 d[u][used],选 B。
第 36 题(单选题,3 分)
③处应填( )
d[v][1]d[v][used]d[u][used]d[v][0]正确答案B
解析详情
这里按原价 w 经过边 u→v,没有使用新的免费机会,所以状态 used 保持不变。
候选费用为 d[u][used]+w,目标为 d[v][used];只有候选费用更小时,才更新这个目标并入队。
因此比较右侧和赋值左侧的③都填 d[v][used],选 B。used=0 和 used=1 均能进行这种付费转移。
第 37 题(单选题,3 分)
④处应填( )
d[v][0]d[v][1]d[u][0]d[u][1]正确答案C
解析详情
该分支的前提 used==0 表示免费机会尚未使用。此时把边 u→v 的本次费用设为 0,到达 v 后机会已用完。
因此跨层转移为 ,候选费用只包含到达 u 的费用,不再加 w。
④出现于比较与赋值的源表达式位置,所以应填 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 分)
⑤处应填( )
d[t][1]d[t][0]min(d[t][0], d[t][1])d[t][0] + d[t][1]正确答案C
解析详情
到终点 t 时,合法情况包括没有使用免费机会,以及恰好使用一次机会;它们分别对应 d[t][0]、d[t][1]。
这两者代表不同路径方案的最优费用,应取较小者,而不是求和,所以⑤填 min(d[t][0],d[t][1]),选 C。
例如 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 分)
①处应填( )
(1<<w) < ncount_patterns(w, k) < ncount_patterns(k, w) < ncomb(w, k) < n正确答案B
解析详情
w 次测试的反馈可写为长度 w 的二进制码;缺陷线属于某批次则该位为 1。为满足最多 k 次退货,每个码中 1 的数量不得超过 k。
恰有 t 个 1 时,选择其所在位置有 种,因此可用码字数为 ,即 count_patterns(w,k)。
为了区分 n 条生产线,必须至少有 n 个不同码。若数量还小于 n,就继续增加 w,因此①填 count_patterns(w,k)<n,选 B。
例如 n=4、k=1 时,w=2 只有 3 个可用码;w=3 有 000、100、010、001 共 4 个,足以唯一编码。
第 40 题(单选题,3 分)
②处应填( )
next_permutation(bits.begin(), bits.end())prev_permutation(bits.begin(), bits.end())next_permutation(bits.begin(), bits.begin()+ones)prev_permutation(bits.begin(), bits.begin()+ones)正确答案B
解析详情
fill 将前 ones 个位置设为 1,其余为 0,初始 bits 形如 1100,是该组二进制多重集合的字典序最大排列。
从最大排列出发,需要用 prev_permutation 遍历更小排列。例如两个 1、两个 0 时,依次得到 1100、1010、1001、0110、0101、0011。
排列范围必须包含整个 bits;只排列前 ones 个位置会一直是全 1,无法改变选中的测试批次。故②填 prev_permutation(bits.begin(),bits.end()),选 B。
第 41 题(单选题,3 分)
③处应填( )
(j>>i) & 1(i>>j) & 1code[i][j] == 1code[j][i] == 1正确答案D
解析详情
code[j] 是第 j 条生产线的整段码字,code[j][i] 表示它是否参加第 i 批测试。
构造 plan[i] 时,外层 i 固定测试批次,内层 j 枚举生产线,应加入所有 code[j][i]==1 的生产线,故③选 D。
这样若缺陷线为 j,各批次收到的反馈就恰好是 code[j],可在后续解码中唯一定位。
左右滑动表格,可查看完整内容。
| 生产线 j | code[j] | 加入的批次 |
|---|---|---|
| 0 | 000 | 不加入任何批次 |
| 1 | 100 | 第 0 批 |
| 2 | 010 | 第 1 批 |
| 3 | 001 | 第 2 批 |
列按第 0、1、2 批排列;每行至多一个 1,每条线的反馈不同。
第 42 题(单选题,3 分)
④处应填( )
(signature >> i) & 1(signature >> i) ^ 1signature | (1 << i)(signature >> i) | 1正确答案A
解析详情
signature 的最低位代表第 0 批测试,第 i 位代表第 i 批测试。先右移 i 位,把目标位移到最低位。
再与 1 按位与,屏蔽所有更高位,只保留 0 或 1。因此④填 (signature>>i)&1,选 A。
例如 signature=5,二进制为 101;i=0、1、2 时提取结果依次为 1、0、1,写入 sig_bits 后就还原了三批反馈。
第 43 题(单选题,3 分)
⑤处应填( )
is_permutation(code[j].begin(), code[j].end(), sig_bits.begin())code[j] == sig_bitsplan[j] == sig_bitscode[j][i] == sig_bits[i]正确答案B
解析详情
每条生产线 j 都被分配了不同的码字 code[j];若它是缺陷线,各测试批次产生的反馈就应与该码字完全相同。
把 signature 拆成 sig_bits 后,逐条查找 code[j]==sig_bits,向量相等会比较长度及每个对应位置,匹配的 j 就是答案,选 B。
仅比较是否互为排列不够,例如 100 和 010 虽然都有一个 1,却表示不同批次退货,对应不同生产线。