GESP 客观题评测系统

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

CSPS-2025-R1

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

一、单项选择题

1 题(单选题2 分)

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

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

正确答案C

解析详情

【答案】C

【考点】插空法

【解析】 先排好 5 个红球,会形成首尾及相邻红球之间共 6 个空位。为使蓝球不相邻,每个空位至多放 1 个蓝球;从 6 个空位中选 5 个放蓝球,共有 C(6,5)=6 种。

【易错点】 红、蓝球各自同色同质,不能把同色球当成互不相同的球排列。

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

解析详情

【答案】A

【考点】KMP 前缀函数

【解析】 逐个考察前缀,"a"、"ab"、"aba"、"abac" 的最长公共前后缀长度依次为 0、0、1、0;继续加入 "a"、"b"、"a" 后长度依次为 1、2、3。因此 next={0,0,1,0,1,2,3}。

【易错点】 公共前后缀必须是真前缀和真后缀,不能把整个 P[0..i] 计入。

3 题(单选题2 分)

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

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

正确答案B

解析详情

【答案】B

【考点】线段树区间分解

【解析】 [3,11] 可由 [3,3]、[4,7]、[8,11] 三个完整结点覆盖。连同它们到根的不同祖先 [2,3]、[0,3]、[0,7]、[8,15]、[0,15],共需访问 3+5=8 个结点。

【易错点】 只数最终覆盖区间的结点会漏掉查询路径上的父结点。

4 题(单选题2 分)

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

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

正确答案D

解析详情

【答案】D

【考点】Trie 树结点计数

【解析】 Trie 的结点对应所有不同前缀(含空前缀)。这些前缀为 ""、c、ca、cat、car、cart、cas、case、d、do、dog,共 11 个。

【易错点】 多个单词的公共前缀只建立一次,同时不要漏算根结点。

5 题(单选题2 分)

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

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

正确答案D

解析详情

【答案】D

【考点】DAG 拓扑排序

【解析】 拓扑序数量取决于边所规定的偏序关系:链式 DAG 只有 1 种,而无边 DAG 有 n! 种。它既不恒为 1,也不受 n 或 n-m 这样的给定数量限制。

【易错点】 存在拓扑序不等于拓扑序唯一;只有偏序足够严格时才可能唯一。

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

【考点】哈希表线性探查

【解析】 前五个关键字依次占据 5、0、9、10、3。74 mod 13=9,而位置 9、10 已被占用,线性向后探查到第一个空位 11,因此 74 放在索引 11。

【易错点】 发生冲突后要连续探查,不能只向后移动一次。

7 题(单选题2 分)

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

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

正确答案A

解析详情

【答案】A

【考点】最小生成树

【解析】 选择边 (1,2),(2,3),...,(7,8) 可连通全部 8 个顶点,每条边权均为 1,总权重为 7。生成树必须有 7 条边且每条边权至少为 1,所以不可能更小。

【易错点】 完全图边多不代表最小生成树需要更多边,8 个顶点的生成树始终只有 7 条边。

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

【考点】二叉搜索树遍历还原

【解析】 后序末项 6 是根;小于 6 的 2,5,4 构成左子树,其根为 4、孩子为 2 和 5;大于 6 的 8,12,10 构成右子树,其根为 10、孩子为 8 和 12。按根—左—右得到前序 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

解析详情

【答案】D

【考点】0-1 背包

【解析】 选择第 1、3、4、5 个物品,重量为 7+4+3+6=20,价值为 15+9+7+13=44。按容量逐项做 0-1 背包转移后,dp[20] 的最大值即为 44。

【易错点】 每件物品最多选一次,不能按单位价值重复装入。

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 是整棵树的根,也是结点 12 的祖先,因此 LCA(12,1) 必为 1,不可能是 4。已知 LCA(12,18)=4,则 4 是二者的公共祖先,前三种关系均可成立。

【易错点】 当一个结点本身是另一个结点的祖先时,二者的 LCA 就是该祖先结点。

11 题(单选题2 分)

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

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

正确答案C

解析详情

【答案】C

【考点】主定理

【解析】 a=2、b=2,故 n^(log_b a)=n;而合并代价 f(n)=Θ(n²) 多项式级地大于 n。并且 2f(n/2)=n²/2 满足正则条件,所以 T(n)=Θ(n²)。

【易错点】 不能看到两个子问题就直接多乘一个 log n,主导项是每层的 n² 合并代价。

12 题(单选题2 分)

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

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

正确答案A

解析详情

【答案】A

【考点】最小堆

【解析】 全部插入后,堆中元素集合为 {5,8,10,12,15,20},堆顶是 5。连续两次 delete-min 删除 5 和 8,剩余元素中的最小值 10 成为堆顶。

【易错点】 删除最小值后会重新调整堆,不能按插入顺序确定新堆顶。

13 题(单选题2 分)

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

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

正确答案A

解析详情

【答案】A

【考点】容斥原理

【解析】 能被 2、3、5 至少一个整除的数有 500+333+200-166-100-66+33=734 个。因此三个数都不能整除的整数有 1000-734=266 个。

【易错点】 两两公倍数被重复计算,必须减去 6、10、15 的倍数并补回 30 的倍数。

14 题(单选题2 分)

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

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

正确答案C

解析详情

【答案】C

【考点】动态规划与重叠子问题

【解析】 朴素递归会反复计算同一个状态,例如求 F(n-1) 和 F(n-2) 时都会再次求 F(n-3)。动态规划把每个 F(i) 只计算一次并复用结果,因而从指数级降为 O(n)。

【易错点】 性能差异的主因是重复计算,而不是递归调用栈本身的常数开销。

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

【考点】截止时间优先调度

【解析】 A3 的截止时刻最早为 3,处理时长为 2,应最先执行并在时刻 2 完成。按截止时刻排序 A3,A1,A2,A5,A4,完成时刻为 2,5,9,10,15,均不超时,总惩罚为 0。

【易错点】 只按处理时长排序会忽略截止时刻,短任务 A5 并不是最紧迫的任务。

二、阅读程序(1)

#include <algorithm>
#include <cstdio>
#include <cstring>
bool flag[27];
int n;
int p[27];
int ans = 0;
void dfs(int k) {
    if (k == n + 1){
        ++ ans;
        return;
    }
    for (int i = 1; i <= n; ++i) {
        if (flag[i]) continue;
        if (k > 1 && i == p[k - 1] + 1) continue;
        p[k] = i;
        flag[i] = true;
        dfs(k + 1);
        flag[i] = false;
    }
    return;
}
int main() {
    scanf("%d", &n);
    dfs(1);
    printf("%d\n", ans);
    return 0;
}

16 题(判断题1 分)

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

正确答案正确

解析详情

【答案】正确

【考点】回溯计数与变量追踪

【解析】 程序枚举 1,2,3 的全排列,并通过 `p[k-1] == i - 1` 排除相邻位置满足后一个数等于前一个数加 1 的情况(例如 12, 23 连续出现)。当 n=3 时,1~3的全排列共6个:123(含12, 23不合法), 132(合法), 213(合法), 231(含23不合法), 312(含12不合法), 321(合法)。合法排列恰好为 132、213、321,共 3 个,所以 ans 递增三次,输出 3。

【易错点】 限制条件 `p[k-1] == i - 1` 只针对排列中相邻的两个位置,并不是禁止所有数值连续的元素出现在任意位置。

17 题(判断题1.5 分)

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

正确答案正确

解析详情

【答案】正确

【考点】递归边界与调用树推导

【解析】 主程序首次调用 `dfs(1)`,传入初始参数 k=1。在函数内部,每向下一层递归,k 值增加 1。当递归到达深度 k=n+1 时,触发基础情况(Base Case),程序会立即执行 `ans++` 并 `return` 返回,不再进入更深层次。因此,k 最大的取值恰好在判断终止条件的这一层,也就是 n+1。运行过程中 k 的范围确切为 1 到 n+1。

【易错点】 不要把排列数组 `p` 最后一层可填写的位置 n 与函数的递归终止参数 n+1 混淆。

18 题(判断题1.5 分)

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

正确答案错误

解析详情

【答案】错误

【考点】回溯法的状态重置机制

【解析】 `flag` 数组用于标记数字 `i` 是否已经被放入当前排列中。在递归树回退到上一层时,必须将当前层的状态撤销,即执行 `flag[i] = false`,从而使得数字 `i` 能够被用于排列的其他位置。如果不恢复标记,数字一旦被使用就永远处于被选中状态,后续所有分支都无法再次选取该数字,导致最终只能搜索到一条到达底部的路径或者提前因找不到可用数字而结束,输出结果会远小于正确答案。

【易错点】 忽略了深度优先搜索(DFS)中为了遍历全部可能的分支,必须进行“回溯并恢复现场”的硬性要求。

19 题(单选题3 分)

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

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

正确答案A

解析详情

【答案】A

【考点】排列容斥

【解析】 4! 个排列中,令事件 Ai 表示 i 后紧邻 i+1。用容斥:24-3×6+(2+2+2)-1=11,其中两事件交集均有 2 种,三个事件同时发生只有 1234 一种。

【易错点】 同时出现 12 和 23 时应把 123 看成一个整体,不能把两个限制独立相乘。

20 题(单选题3 分)

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

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

正确答案D

解析详情

【答案】D

【考点】递归状态覆盖

【解析】 dfs(1) 在进入下一层前总会执行 p[k]=i;而 k=1 时不会读取 p[0]。因此搜索过程中实际会读取的 p[k-1] 都已经由上一层赋值,p 的初值不影响结果。

【易错点】 判断未初始化数据是否有影响,要看它是否会在被覆盖前被读取。

21 题(单选题3 分)

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

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

正确答案C

解析详情

【答案】C

【考点】序列计数与容斥

【解析】 删除 flag 判断后,每个位置可独立取 1~3,共 27 个序列。相邻两处分别有 6 个序列满足“后项=前项+1”,二者交集只有 123,故合法数为 27-6-6+1=16。

【易错点】 删除去重限制后枚举的是可重复序列,不再是 3 个数的排列。

二、阅读程序(2)

#include <algorithm>
#include <cstdio>
#include <cstring>
#define ll long long
int cnt_broken = 0;
int cnt_check = 0;
int n, k;
inline bool check(int h) {
    printf("now check:%d\n", h);
    ++cnt_check;
    if (cnt_broken == 2) {
        printf("You have no egg!\n");
        return false;
    }
    if (h >= k) {
        ++cnt_broken;
        return true;
    } else {
        return false;
    }
}
inline bool assert_ans(int h) {
    if (h == k) {
        printf("You are Right using %d checks\n", cnt_check);
        return true;
    } else {
        printf("Wrong answer!\n");
        return false;
    }
}
inline void guess1(int n) {
    for (int i = 1; i <= n; ++i) {
        if (check(i)) {
            assert_ans(i);
            return;
        }
    }
}
inline void guess2(int n) {
    int w = 0;
    for (w = 1; w * (w + 1) / 2 < n; ++w)
        ;
    for (int ti = w, nh = w; --ti, nh += ti, nh = std::min(nh, n)) {
        if (check(nh)) {
            for (int j = nh - ti + 1; j < nh; ++j) {
                if (check(j)) {
                    assert_ans(j);
                    return;
                }
            }
            assert_ans(nh);
            return;
        }
    }
}
int main() {
    scanf("%d%d", &n, &k);
    int t;
    scanf("%d", &t);
    if (t == 1) {
        guess1(n);
    } else {
        guess2(n);
    }
    return 0;
}

> (注意:下述的"猜测数"为调用 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 时按递减步长分段定位,针对 k=5 依次检查到对应的分段端点并在段内确认,共需 3 次猜测。

【易错点】 猜测次数只统计 check 的调用,assert_ans 不计入。

23 题(判断题1.5 分)

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

正确答案错误

解析详情

【答案】错误

【考点】算法最坏情况与实例比较

【解析】 guess2 的 O(√n) 是最坏情况优势,不保证每个具体 k 都比顺序猜测少。例如 k 很小时,guess1 很快就在低层命中,而 guess2 仍要先检查较高的分段端点。

【易错点】 渐进复杂度更优不等于对所有输入实例的实际调用次数都更少。

24 题(判断题1.5 分)

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

正确答案正确

解析详情

【答案】正确

【考点】顺序查找与两阶段查找

【解析】 guess1 从低到高首次触发时高度就是 k。guess2 先用递减步长确定 k 所在区间,再在该区间从低到高检查,因此也能把首次触发位置确定为 k。

【易错点】 第二种方法改变的是检查顺序和次数,不改变 check(h) 在 h≥k 时为真的判定边界。

25 题(单选题3 分)

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

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

正确答案B

解析详情

【答案】B

【考点】循环提前返回

【解析】 guess1 中 check(i) 第一次返回 true 时 cnt_broken 加 1,随后立即调用 assert_ans 并 return。函数不会继续执行下一次 check,所以 cnt_broken 最大为 1。

【易错点】 不要忽略 if 分支中的 return,否则会误以为还能继续打破第二个蛋。

26 题(单选题3 分)

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

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

正确答案C

解析详情

【答案】C

【考点】两蛋问题的递减步长策略

【解析】 w 取满足 w(w+1)/2≥n 的最小整数,因此 w=Θ(√n)。外层至多检查 w 个分段端点,触发后段内线性检查也不超过 w 次,最多猜测次数为 O(√n)。

【易错点】 该策略的分段长度逐次减小,不是每段都固定扫描 √n 次。

27 题(单选题3 分)

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

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

正确答案A

解析详情

【答案】A

【考点】最坏情况猜测次数

【解析】 guess1 最坏要从 1 检查到 100,共 100 次。guess2 取最小 w 使 w(w+1)/2≥100;13×14/2=91<100,而 14×15/2=105,因此最坏为 14 次。

【易错点】 w 应向上取到覆盖 n,不能因 13 已接近 √(2n) 就取 13。

二、阅读程序(3)

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <vector>
#define ll long long
int n, m;
std::vector<int> k, p;
std::vector<int> ans1, ans2;
int cnt1, cnt2;
inline int mpow(int x, int k) {
    int ans = 1;
    for (; k; k >>= 1, x = x * x) {
        if (k & 1)
            ans = ans * x;
    }
    return ans;
}
inline void dfs(std::vector<int>& ans, int& cnt, int l, int r, int v) {
    if (l > r) {
        ++cnt;
        ans.push_back(v);
        return;
    }
    for (int i = 1; i <= m; ++i) {
        dfs(ans, cnt, l + 1, r, v + k[i] * mpow(i, p[l]));
    }
    return;
}
std::vector<int> cntans1;
int main() {
    scanf("%d", &n, &m);
    k.resize(n + 1);
    p.resize(n + 1);
    for (int i = 1; i <= n; ++i) {
        scanf("%d%d", &k[i], &p[i]);
    }
    dfs(ans1, cnt1, 1, n >> 1, 0);
    dfs(ans2, cnt2, (n >> 1) + 1, n, 0);
    std::sort(ans1.begin(), ans1.end());
    int newcnt1 = 1;
    cntans1.push_back(1);
    for (int i = 1; i < cnt1; ++i) {
        if (ans1[i] == ans1[newcnt1 - 1]) {
            ++cntans1[newcnt1 - 1];
        } else {
            ans1[newcnt1++] = ans1[i];
            cntans1.push_back(1);
        }
    }
    cnt1 = newcnt1;
    std::sort(ans2.begin(), ans2.end());
    int las = 0;
    ll ans = 0;
    for (int i = cnt2 - 1; i >= 0; --i) {
        for (; las < cnt1 && ans1[las] + ans2[i] < 0; ++las)
            ;
        if (las < cnt1 && ans1[las] + ans2[i] == 0) {
            ans += cntans1[las];
        }
    }
    printf("%lld\n", ans);
    return 0;
}

28 题(判断题1.5 分)

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

正确答案错误

解析详情

【答案】错误

【考点】双指针计数

【解析】 后续从 ans2 的末尾向前遍历,并让 las 在已排序的 ans1 中单调右移,这依赖 ans2 也按升序排列。删除排序后 ans2 次序无序,las 不能回退,会漏计或错计配对。

【易错点】 双指针正确性的前提是两侧数据具有与指针移动方向一致的单调性。

29 题(判断题1.5 分)

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

正确答案正确

解析详情

【答案】正确

【考点】快速幂

【解析】 每轮将 k 右移一位并把 x 平方;当当前二进制位为 1 时把 x 乘入 ans。这正是按 k 的二进制展开计算 x^k,未溢出时结果正确。

【易错点】 x 在循环中不断平方,但 ans 只在 k 的对应位为 1 时累乘。

30 题(判断题1.5 分)

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

正确答案正确

解析详情

【答案】正确

【考点】排序去重与频次统计

【解析】 ans1 排序后,相同值连续出现;循环把不同值压到 ans1 前部,并在 cntans1 中记录每个值的出现次数,最后令 cnt1 等于不同值个数。因此这段代码完成了带频次的去重。

【易错点】 这里不是简单删除重复值,还必须保留重复次数供最终答案累加。

31 题(单选题3 分)

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

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

正确答案B

解析详情

【答案】B

【考点】双向搜索与状态推导

【解析】 这是典型的中途相遇法(Meet-in-the-Middle)解方程。方程为 x12x22+x32=0x_1^2 - x_2^2 + x_3^2 = 0,其中 xi[1,15]x_i \in [1, 15]。变形为 x12+x32=x22x_1^2 + x_3^2 = x_2^2。在 [1,15][1, 15] 范围内的勾股数有 (3,4,5), (6,8,10), (5,12,13), (9,12,15)。每组勾股数可以产生 2 组解(交换 x1x_1x3x_3),即 2×4=82 \times 4 = 8 组解。因此输出 8。

【易错点】 容易遗漏 x1x_1x3x_3 的对称性,误认为只有 4 组解,或者在列举勾股数时漏掉比例放大的数对。

32 题(单选题3 分)

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

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

正确答案D

解析详情

【答案】D

【考点】折半搜索算法复杂度

【解析】 折半搜索将 nn 个变量分为两半,前后各 n/2n/2 个变量。前半部分搜索产生 mn/2m^{n/2} 种状态并存入数组,随后通过 `std::sort` 排序,排序的复杂度为 O(mn/2log(mn/2))O(m^{n/2} \log (m^{n/2}))。后半部分也是 mn/2m^{n/2} 种状态,每次在前半段的结果中使用 `std::lower_bound` 和 `std::upper_bound` 进行二分查找,查找次数涉及元素大小比较。如果涉及大整数运算或内部二分时间,总体复杂度需加上快速幂等操作带来的 logP\log P 开销,因此最严谨的时间复杂度应为 O(mn/2(log(mn/2)+logP))O(m^{n/2}(\log (m^{n/2}) + \log P))

【易错点】 容易忽略快速幂 `mpow` 函数在每次计算次幂时的 O(logpi)O(\log p_i) 的时间复杂度开销,导致错选 C。

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

【考点】折半搜索方程计数

【解析】 dfs 对每个变量枚举 1~m,并分别计算两半的 Σki·xi^pi;随后统计两半和为 0 的组合。因此所求是 xi∈[1,m] 时方程 Σki·xi^pi=0 的解数。

【易错点】 枚举循环从 1 开始,取值范围不包含 0。

三、完善程序(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

【考点】分层图最短路状态初始化

【解析】 初始位于 s,免费边尚未使用,所以状态应为 used_freebie=0。它与已设置的 d[s][0]=0 对应,因此压入队列的是 {0,s,0}。

【易错点】 状态 1 表示免费机会已经用掉,不能作为起点的初始状态。

35 题(单选题3 分)

②处应填( )

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

正确答案B

解析详情

【答案】B

【考点】Dijkstra 过期状态判断

【解析】 优先队列弹出的状态由顶点 u 和 used 共同确定,其当前最短距离是 d[u][used]。若 dist>d[u][used],说明这是旧的较差记录,应跳过。

【易错点】 分层图中同一顶点有两个状态,不能只按顶点或相反状态比较距离。

36 题(单选题3 分)

③处应填( )

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

正确答案B

解析详情

【答案】B

【考点】Dijkstra 的同层松弛

【解析】 ③位于按原边权行走的转移中,免费边使用状态 `used` 不变,因此应比较并更新目标状态 `d[v][used]`,对应选项 B。

【易错点】 这一步尚未使用免费边,不能把目标固定写成 `d[v][1]`。

37 题(单选题3 分)

④处应填( )

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

正确答案C

解析详情

【答案】C

【考点】免费边的分层图转移

【解析】 ④只在 `used == 0` 时执行。免费经过边 `(u,v)` 后,应令 `d[v][1] = d[u][0]`,所以空缺表达式是 `d[u][0]`,对应选项 C。

【易错点】 空缺表示转移前的源状态距离;目标状态 `d[v][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

【考点】分层图答案合并

【解析】 题目允许“至多”使用一次免费边,所以到达 t 时既可以未使用,也可以已经使用。最终费用应取两个状态的较小值 min(d[t][0],d[t][1])。

【易错点】 “至多一次”包含零次,不能强制答案只取已使用免费边的状态。

三、完善程序(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

【考点】组合计数与最小测试数

【解析】 w 次测试且退货不超过 k 次时,可用反馈码数量为 Σ(t=0..min(w,k))C(w,t),即 count_patterns(w,k)。只要该数量小于 n 就无法给每条生产线分配唯一编码,因此应继续增加 w。

【易错点】 限制的是码字中 1 的个数不超过 k,不能直接使用全部 2^w 个二进制码。

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

【考点】全排列枚举

【解析】 bits 初始形如 11...100...0,是含固定个数 1 的字典序最大排列。要依次枚举其余不同排列,应调用 prev_permutation(bits.begin(),bits.end())。

【易错点】 从字典序最大排列调用 next_permutation 会立即结束,无法枚举全部组合。

41 题(单选题3 分)

③处应填( )

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

正确答案D

解析详情

【答案】D

【考点】编码矩阵转测试方案

【解析】 code[j][i] 表示生产线 j 的编码在第 i 位是否为 1。第 i 批测试应加入所有该位为 1 的生产线,因此条件应为 code[j][i]==1。

【易错点】 code 的第一维是生产线编号 j,第二维才是测试批次 i。

42 题(单选题3 分)

④处应填( )

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

正确答案A

解析详情

【答案】A

【考点】位运算取位

【解析】 signature 的第 i 位代表第 i 批测试结果。先右移 i 位,再与 1 按位与,即 (signature>>i)&1,可判断该位是否为 1。

【易错点】 右移后必须用 &1 屏蔽更高位,不能用异或或按位或代替。

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

【考点】二进制编码解码

【解析】 每条生产线 j 都分配了唯一反馈码 code[j],实际反馈被拆成 sig_bits。逐条比较 code[j]==sig_bits,完全相等的那一行所对应的 j 就是缺陷生产线。

【易错点】 需要逐位完全相等,排列相同但位序不同并不代表同一测试反馈。