历年CSP-S初赛真题解析 | 2025年CSP-S初赛
欢迎大家订阅我的专栏:算法题解:C++与Python实现!
本专栏旨在帮助大家从基础到进阶 ,逐步提升编程能力,助力信息学竞赛备战!
专栏特色
1.经典算法练习:根据信息学竞赛大纲,精心挑选经典算法题目,提供清晰的代码实现与详细指导,帮助您夯实算法基础。
2.系统化学习路径:按照算法类别和难度分级,从基础到进阶,循序渐进,帮助您全面提升编程能力与算法思维。
适合人群:
- 准备参加蓝桥杯、GESP、CSP-J、CSP-S等信息学竞赛的学生
- 希望系统学习C++/Python编程的初学者
- 想要提升算法与编程能力的编程爱好者
附上汇总贴:历年CSP-S初赛真题解析 | 汇总-CSDN博客
考情分析
一、单项选择题(共15题)
| 题号 | 考察知识点 |
|---|---|
| 1 | 组合数学 — 插空法(不相邻排列问题) |
| 2 | 字符串算法 — KMP算法,\(\text{next}\)数组(前缀函数)计算 |
| 3 | 数据结构 — 线段树,区间查询的节点访问分析 |
| 4 | 数据结构 — Trie树(前缀树)的构建与节点计数 |
| 5 | 图论 — 拓扑排序,DAG的性质与方案数分析 |
| 6 | 数据结构 — 哈希表,闭散列法,线性探查冲突处理 |
| 7 | 图论 — 最小生成树(MST),Kruskal/Prim算法的应用 |
| 8 | 数据结构 — 二叉搜索树(BST),后序遍历重建树,前序遍历输出 |
| 9 | 动态规划 — 0-1背包问题,状态转移与最优解分析 |
| 10 | 数据结构 — 最近公共祖先(LCA),树的深度与祖先性质 |
| 11 | 算法分析 — 分治算法,递推关系式,时间复杂度分析(主定理) |
| 12 | 数据结构 — 最小堆(\(\min\)-heap),插入与删除最小值操作 |
| 13 | 数学 — 容斥原理,整数整除计数问题 |
| 14 | 算法思想 — 动态规划 vs 朴素递归,重叠子问题与最优子结构 |
| 15 | 贪心算法 — 任务调度,截止时间与惩罚最小化(类似经典调度问题) |
二、阅读程序题(共18题,分3段代码)
程序一:DFS排列计数(第16-21题)
考察算法: 深度优先搜索(DFS),排列生成,回溯法
代码思路:
- 使用DFS生成 \(1 \sim n\) 的排列
- 剪枝条件:\(k>1 \ \&\& \ i == p[k-1]+1\),即禁止相邻位置数字连续递增(\(i\) 不能等于前一个数 \(+1\))
- \(\text{flag}[]\) 数组标记数字是否已使用,实现回溯
| 题号 | 考察知识点 |
|---|---|
| 16 | DFS枚举结果验证,\(n=3\) 时合法排列为 \(\{132, 213, 321\}\) |
| 17 | DFS递归过程分析,\(k\) 的取值范围 \([1, n+1]\) |
| 18 | 回溯法的核心机制 — \(\text{flag}[i]=\text{false}\) 的撤销操作必要性 |
| 19 | 枚举计数,\(n=4\) 时排除不合法排列后剩余 \(11\) 种 |
| 20 | 数组初始化状态的影响分析 — \(p[]\) 在递归中会被覆盖,初值无关 |
| 21 | 去掉 \(\text{flag}[i]\) 判断后的影响分析:允许重复选数字,需重新计算合法方案数 |
程序二:鸡蛋掉落问题(第22-27题)
考察算法: 经典"鸡蛋掉落"问题的两种策略:线性扫描 vs 数学优化(等差数列分块)
代码思路:
- \(\text{guess1}\):线性扫描,从 \(1\) 到 \(n\) 逐层测试,鸡蛋在第 \(k\) 层破碎
- \(\text{guess2}\):优化策略,将 \(n\) 层楼分成 \(w\) 个块(块大小递减:\(w, w-1, w-2, \ldots\)),先按块测试,再在目标块内线扫
- 核心条件:\(\frac{w(w+1)}{2} \ge n\),确保覆盖所有楼层
- 数学本质:等差数列求和,使最坏情况下总测试次数最小化
| 题号 | 考察知识点 |
|---|---|
| 22 | 两种策略的具体模拟:\(\text{guess1}\) 测试 \(5\) 次,\(\text{guess2}\) 测试 \(3\) 次 |
| 23 | 边界情况分析:\(k=1, n=2\) 时 \(\text{guess2}\) 反而更差(\(2\) 次 \(> 1\) 次) |
| 24 | 程序正确性保证:两种策略均能在 \(2\) 个鸡蛋限制内找到答案 |
| 25 | \(\text{guess1}\) 的鸡蛋破碎次数分析:最多破碎 \(1\) 次(找到答案即结束) |
| 26 | \(\text{guess2}\) 的时间复杂度分析:\(O(\sqrt{n})\) 量级(由 \(w\) 的条件推导) |
| 27 | 具体数值计算:\(n=100\) 时,\(\text{guess1}\) 最多 \(100\) 次,\(\text{guess2}\) 最多 \(14\) 次 |
程序三:折半搜索(Meet-in-the-Middle)(第28-33题)
考察算法: 折半搜索(Meet-in-the-Middle),快速幂,尺取法(双指针),去重计数
代码思路:
- 将 \(n\) 个未知数分成两半(前 \(n/2\) 个和后 \(n/2\) 个)
- 分别DFS枚举两半的所有可能取值,计算对应的多项式值
- 对前半部分结果排序并去重(记录重复次数 \(\text{cntans1}\))
- 对后半部分从大到小枚举,用尺取法(双指针)在前半部分中查找和为 \(0\)(或目标值)的组合
- \(\text{mpow}\) 函数:快速幂计算 \(x^k\)
| 题号 | 考察知识点 |
|---|---|
| 28 | 尺取法的单调性依赖 — \(\text{ans2}\) 必须有序才能保证指针单调移动 |
| 29 | 快速幂算法验证:二分求幂,时间复杂度 \(O(\log k)\) |
| 30 | 去重操作的目的:统计前半部分每个值的出现次数,用于后续组合计数 |
| 31 | 具体代入计算:\(x^2 - y^2 + z^2 = 0\) 的整数解,即勾股数变形的计数 |
| 32 | 时间复杂度综合分析:DFS枚举 \(O\left(m^{n/2} \cdot \log P\right)\) + 排序 \(O\left(m^{n/2}\log m^{n/2}\right)\) + 尺取 \(O\left(m^{n/2}\right)\) |
| 33 | 代码功能推断:求满足 \(\displaystyle\sum_{i=1}^{n} k_i \cdot x_i^{p_i} = 0\) 且 \(1 \le x_i \le m\) 的整数解数量 |
三、完善程序题(共10题,分2段代码)
程序一:特殊最短路(第34-38题)
考察算法: 分层图最短路 / 状态扩展Dijkstra
代码思路:
- 在普通Dijkstra基础上增加一维状态 \(\text{used\_freebie}\)(\(0/1\) 表示是否已使用免费边)
- \(d[u][0]\):到达 \(u\) 且未使用免费边的最短路
- \(d[u][1]\):到达 \(u\) 且已使用免费边的最短路
- 状态转移:
- 不使用免费边:正常松弛 \(d[u][\text{used}] + w \rightarrow d[v][\text{used}]\)
- 使用免费边(仅当 \(\text{used}==0\)):\(d[u][0] + 0 \rightarrow d[v][1]\)
| 题号 | 空位 | 考察知识点 |
|---|---|---|
| 34 | __1__ |
初始状态:起点未使用免费边,填 \(0\) |
| 35 | __2__ |
Dijkstra的堆优化技巧:若当前距离不是最小值则跳过(\(\text{dist} > d[u][\text{used}]\)) |
| 36 | __3__ |
正常边松弛:更新 \(d[v][\text{used}]\),状态不变 |
| 37 | __4__ |
免费边转移:从 \(d[u][0]\) 转移,费用为 \(d[u][0] + 0\) |
| 38 | __5__ |
答案输出:取 \(\min(d[t][0], d[t][1])\),免费边可用可不用 |
程序二:最优测试/组合设计(第39-43题)
考察算法: 组合设计,二进制编码,全排列生成,信息论下界
代码思路:
- 核心思想:\(w\) 轮测试产生一个 \(w\) 位二进制串(\(1\) 表示退货,\(0\) 表示正常),要求不同故障生产线对应不同的二进制串
- 约束:二进制串中 \(1\) 的个数 \(\le k\)(最多 \(k\) 次退货)
- 使用 \(\text{count\_patterns}(w,k)\) 计算长度为 \(w\)、\(1\) 的个数 \(\le k\) 的码字总数,需 \(\ge n\)
- 生成方案:按 \(1\) 的个数从 \(0\) 到 \(k\),用 \(\text{prev\_permutation}\) 生成所有不同的 \(01\) 串
- 解码:将测试结果(\(\text{signature}\))与每条生产线的编码比对,找到匹配项
| 题号 | 空位 | 考察知识点 |
|---|---|---|
| 39 | __1__ |
信息论下界:码字总数 \(\text{count\_patterns}(w,k)\) 必须 \(\ge n\) 才能区分所有生产线 |
| 40 | __2__ |
\(\text{prev\_permutation}\) 的使用:初始状态为最大排列(前 \(\text{ones}\) 位为 \(1\)),需向前枚举 |
| 41 | __3__ |
矩阵转置:\(\text{plan}[i]\) 表示第 \(i\) 批次包含哪些生产线,由 \(\text{code}[j][i]\) 决定 |
| 42 | __4__ |
二进制解码:从 \(\text{signature}\) 中提取第 \(i\) 位,\((\text{signature} \gg i) \,\&\, 1\) |
| 43 | __5__ |
向量比较:\(\text{code}[j] == \text{sig\_bits}\) 判断第 \(j\) 条生产线的编码是否与测试结果完全匹配 |
单项选择题
第1题
有5个红色球和5个蓝色球,它们除了颜色之外完全相同。将这10个球排成一排,要求任意两个蓝色球都不能相邻,有多少种不同的排列方法?( )
A.25
B.30
C.6
D.120
【答案】:C
【解析】
要使任意两个蓝色球不相邻,可先将5个红色球排好,形成6个空隙(包括两端)。从这6个空隙中选5个放入蓝色球,每个空隙至多放一个。选法为组合数C(6,5)=6种。红球和蓝球各自颜色相同,无需再排列。故总方法数为6种。
第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
【解析】
ne[i] 表示模式串 P[0..i] 中相等前后缀的最长长度,枚举每个前缀进行计算即可:
ne[0] = 0 a
ne[1] = 0 a b
ne[2] = 1 a b a
ne[3] = 0 a b a c
ne[4] = 1 a b a c a
ne[5] = 2 a b a c a b
ne[6] = 3 a b a c a b a
第3题
对一个大小为16(下标0-15)的数组上构建线段树,查询区间[3, 11]时,最少需要访问多少个树结点(包括路径上的父结点和完全包含在查询区间内的结点)?( )
A.7
B.8
C.9
D.10
【答案】:B
【解析】
建树后模拟即可,会访问以下区间所代表的树结点:
[0,15],[0,7],[0,3],[2,3],[3,3],[4,7],[8,15],[8,11]。
第4题
将字符串"cat","car","cart","case","dog","do"插入一个空的Trie 树(前缀树)中。构建完成的 Trie树(包括根结点)共有多少个结点?( )
A.8
B.9
C.10
D.11
【答案】:D
【解析】
根节点一个,随后用具体的字符串前缀表示 Trie 树上的一个结点:c,d,ca,do,cat,car,cas, dog, cart,case。共计11个结点。
第5题
对于一个包含n个顶点和m条边的有向无环图(DAG),其拓扑排序的结果有多少种可能?( )
A.只有1种
B.最多n种
C.等于n-m种
D.以上都不对
【答案】:D
【解析】
考虑n个点n-1 条边的极端情况,点1向其他n-1个点连边。此时有(n-1)!种拓扑序列。
第6题
在一个大小为13的哈希表中,使用闭散列法的线性探查来解决冲突。哈希函数为H(key)=key mod 13。依次插入关键字18,26,35,9,68,74。插入74后,它最终被放置在哪个索引位置?( )
A.5
B.7
C.9
D.11
【答案】:D
【解析】
线性探测是指发生冲突后向后依次探测空余位置。以下数字存入哈希表的完整过程:
18 mod 13 = 5,存入5;
26 mod 13 = 0,存入0;
35 mod 13 = 9,存入9;
9 mod 13 = 9,存在冲突,向后探测至10存入;
68 mod 13 = 3,存入3;
7 mod 13 = 9,存在冲突,向后探测至10,存在冲突,向后探测至11存入。
闭散列法:DS做题有套路—哈希查找(闭散列,线性探查)_哔哩哔哩_bilibili
第7题
一个包含8个顶点的完全图(顶点的编号为1到8),任意两点之间的边权重等于两顶点编号的差的绝对值。例如,顶点3和7之间的边权重为 |7-3|=4。该图的最小生成树的总权重是多少?( )
A.7
B.8
C.9
D.10
【答案】:A
【解析】
边权最小可能是1,可以发现1-2-3-...-8这条路径上的边权都是1,且能够让所有点联通。那么生成树的总权重就是7。
第8题
如果一棵二叉搜索树的后序遍历序列是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
【解析】
二叉搜索树的特点是左子树上所有节点的值都小于该节点的值,右子树上所有节点的值都大于该节点的值。
按照左-右-根为后序遍历的顺序,可以确定6为根,2,5,4为左子树,8,12,10为右子树。将左右子树继续递归求解,可以知道2,5分别为4的左右儿子,8,12分别为10的左右儿子,4,10分别为6的左右儿子。再进行前序遍历(根左右),得到结果6,4,2,5,10,8,12
第9题
一个0-1背包问题,背包容量为20。现有5个物品,其重量和价值分别为7,5,4,3,6和15,12,9,7,13。装入背包的物品能获得的最大总价值是多少?( )
A.43
B.41
C.45
D.44
【答案】:D
【解析】
依次考虑丢弃1,2,3,4,5个物品时的最大价值。可以发现当丢弃第2个物品的时候,其余物品的重量为20,价值为44。
第10题
在一棵以结点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
【解析】
根的深度小于任意一个其他结点,因此任意结点与根结点1的LCA都是根结点1。
第11题
递归关系式 \(T(n)=2T(n/2)+O(n^2)\) 描述了某个分治算法的时间复杂度。请问该算法的时间复杂度是多少?( )
A.\(O(n)\)
B.\(O(nlogn)\)
C.\(O(n^2)\)
D.\(O(n^2logn)\)
【答案】:C
【解析】
展开递推式:\(T(n)=o(n^2+2*(n/2)^2 +4*(n/4)^2+...)=O(n^2(1+1/2+1/4+...))\)。显然 \(n^2\) 右边项内相加不超过 2,因此总复杂度是 \(O(n^2)\)
第12题
在一个初始为空的最小堆(min-heap)中,依次插入元素20,12,15,8,10,5。然后连续执行两次"删除最小值"(delete-min)操作。请问此时堆顶元素是什么?( )
A.10
B.12
C.15
D.20
【答案】:A
【解析】
模拟即可,插入元素后堆内的顺序为5,8,10,12,15,20。删除两次最小值5和8后变为了10,12,15,20。堆顶是10。
第13题
1到1000之间,不能被2、3、5中任意一个数整除的整数有多少个?( )
A.266
B.267
C.333
D.734
【答案】:A
【解析】
考虑容斥:答案 = 总数量 - 被2整除 - 被3整除 - 被5整除 + 被2*3整除 + 被3*5整除 + 被2*5整除 - 被2*3*5整除。
即1000 - 1000/2 - 1000/3 - 1000/5 + 1000/6 + 1000/15 + 1000/10 - 1000/30 = 266
第14题
斐波那契数列的定义为 F(0)=0,F(1)=1,F(n)=F(n-1)+F(n-2)。使用朴素递归方法计算F(n) 的时间复杂度是指数级的,而使用动态规划(或迭代)方法的时间复杂度是线性的。造成这种巨大差异的根本原因是?( )
A.递归函数调用栈开销过大
B.操作系统对递归深度有限制
C.朴素递归中存在大量的重叠子问题未被重复利用
D.动态规划使用了更少的数据存储空间
【答案】:C
【解析】
F(n)=F(n-1)+F(n-2)=F(n-2)+F(n-3)+F(n-3)+F(n-4)。可以发现递归的过程中重复计算了很多子问题,需要复用这些子问题答案来降低时间复杂度。
第15题
有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
【解析】
优先完成截止时间最早的任务,我们按照截止时间排序,那么在完成上一个任务时候我们就保证了已经留出来尽可能多的时间给未完成的任务。具体的完成顺序是A3,A1,A2,A5,A4,完成的时间段分别是[0,2],[2,5],[5,9],[9,10],[10,15]。
阅读程序
#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题
当输入的n=3的时候,程序输出的答案为3。( )
A.正确
B.错误
【答案】:A
【解析】
不出现相邻位置数字连续(递增1)的排列有:132,213,321
第17题
在dfs函数运行过程中,k的取值会满足1≤k≤n+1。( )
A.正确
B.错误
【答案】:A
【解析】
在调用dfs(1)时,k的初值为1,随后不断通过递归加一变成 n+1后,到达递归出口不再继续加一。
第18题
删除第19行的"flag[i]=false;",对答案不会产生影响。( )
A.正确
B.错误
【答案】:B
【解析】
当枚举第k个位置选择放数字i时,通过flag[i]=true进行标记,防止数字 i 在后面的位置被再一次放上。而从i枚举到i+1后,原本放在位置k的数字i,它的标记需要撤销,才能保证后续能够正确地判断数字是否放置过。
第19题
当输入的n=4的时候,程序输出的答案为。( )
A.11
B.12
C.24
D.9
【答案】:A
【解析】
枚举出所有24种排列,排除掉不合法的情况即可。
第20题
如果因为某些问题,导致程序运行第25行的 dfs 函数之前,数组p的初值并不全为0,则对程序的影响是( )。
A.输出的答案比原答案要小
B.无法确定输出的答案
C.程序可能陷入死循环
D.没有影响
【答案】:D
【解析】
p[ ] 数组的主要功能是用于在第15行去判断是否出现连续数字,而p[k-1]的值会在之前的dfs(k-1)中会被重新赋值,原本存在的值不会造成任何影响。
【答案】D
第21题
假如删去第14行的"if(flag[i]) continue;",输入3,得到的输出答案是( )。
A.27
B.3
C.16
D.12
【答案】:
【解析】
去掉后无需保证每个数字i是否在之前放置过,若不考虑连续数字的限制,可以直接枚举出所有3^3=27种情况,然后去除掉不合法情况即可。
#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;
}
第22题
当输入为"6 5 1"时,猜测次数为5;当输入"6 5 2"时,猜测次数为3。( )
A.正确
B.错误
【答案】:A
【解析】
guess1函数会从1开始逐层枚举并检验,当i=5时结束,猜测次数为5。 guess2函数则是先将6层楼分成了3块,每块分别有3,2,1层。首先猜测3没碎,然后猜测5碎了。然后使用第2个鸡蛋,从4开始继续猜测,没碎,5不用猜测了。总猜测次数是3次。
第23题
不管输入的n和k具体为多少,t=2时的猜测数总是小于等于t=1时的猜测数。( )
A.正确
B.错误
【答案】:B
【解析】
考虑k=1,n=2的边界情况。t=1时猜测次数为k=1;而t=2时会先猜测2再猜测1,共2次操作。
第24题
不管t=1或t=2,程序都一定会猜到正确结果。( )
A.正确
B.错误
【答案】:A
【解析】
通过程序的整体分析可以知道两个函数的具体功能,它们都不会让鸡蛋的破碎次数超过2次。
第25题
函数guess1在运行过程中,cnt broken的值最多为( )。
A.0
B.1
C.2
D.n
【解析】guess1函数是暴力枚举每层去检测,当i=k时,鸡蛋会发生唯――次破碎,同时也得到了答案,程序结束。
【答案】B
第26题
函数guess2在运行过程中,最多使用的猜测次数的量级为( )。
A.\(O(n)\)
B.\(O(n^2)\)
C.\(O(\sqrt n)\)
D.\(O(log n)\)
【答案】:C
【解析】
由w*(w-1)<n的条件可以知道,w的量级是\(O(\sqrt n)\),它的含义是将n层楼分成了w个块。第43行for循环枚举每个块检查的时候,鸡蛋在其中一个块破碎后会马上进入到这个块去检查每一层。已知每块中的层数也是w的级别,因此总共的量级是\(O(\sqrt n)\)
第27题
当输入的n=100的时候,代码中t=1和t=2分别需要的猜测次数最多分别为( )。
A.100,14
B.100,13
C.99,144
D.99,13
【答案】:A
【解析】
guess1的猜测次数与有关,k最大取到100。guess2 中会先将100层分为14,13,...,3,2,1等若干块。当k=14时,正好会将前14层楼各猜测一次。
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <vector>
#define ll long long
int n, m;
std::vector<int> k, p;
inline int mpow(int x, int k) {
int ans = 1;
for (; k; k = k >> 1, x = x * x) {
if (k & 1)
ans = ans * x;
}
return ans;
}
std::vector<int> ans1, ans2;
int cnt1, cnt2;
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= ; ++i) {
dfs(ans, cnt, l+1, r, v+k[l]*mpow(i, p[l]));
}
return;
}
std::vector<int> cntans1;
int main() {
scanf("%d%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题
删除第51行的"std::sort(ans2.begin(), ans2.end()); "后,代码输出的结果不会受到影响。( )
A.正确
B.错误
【答案】:B
【解析】
第54行使用的是尺取法,需要保证ans2[i]是从大到小枚举,否则las 没有单调性,无法从小到大查找 ans1。
第29题
假设计算过程中不发生溢出,函数mpow(x, k)的功能是求出 x^k 的取值。( )
A.正确
B.错误
【答案】:A
【解析】
mpow函数的功能是使用二分求快速幂。
第30题
代码中第39行到第50行的目的是为了将ans1数组进行"去重"操作。( )
A.正确
B.错误
【答案】:A
【解析】
ans2[i]对应的 ans1 可能不止一个,为了降低时间复杂度,只能预处理ans1 中所有相同的值。
第31题
当输入为"3 15 1 2 -1 2 1 2"时,输出结果为( )。
A.4
B.8
C.0
D.10
【答案】:B
【解析】
把输入代入进去,得到的式子是x²-y²+z²=0,其中1≤x,y,≤15,其中(x,z,y)就是勾股定理三元组,符合条件的只有(3,4,5),(4,3,5),(6,8,10),(8,6,10),(9,12,15),(12,9,15),(5,12,13),(12,5,13)共八组。
第32题
记程序结束前p数组元素的最大值为P,则该代码的时间复杂度是( )。
A.\(O(n)\)
B.\(O(m^nlogm^n)\)
C.\(O(m^{n/2}logm^{n/2})\)
D.\(O(m^{n/2}(logm^{n/2}+logP))\)
【答案】:D
【解析】
每次dfs会生成 \(m^{n/2}\) 个值,每次递归都要调用一次 mpow,一次快速幂的时间复杂度为 \(O(logP)\),但是 dfs 的时间复杂度主要看最后一层,因此 dfs 的总复杂度为\(O(m^{n/2}\times logP)\)。
之后使用 sort 对 ans1、ans2 排序,时间复杂度为\(O(m^{n/2}\times logm^{n/2})\)。
之后的去重、尺取法均为线性算法,时间复杂度为 \(O(m^{n/2})\)。
综上,因为不知道 P 和 m 谁更大,所以不能省略,整个程序的时间复杂度是\(O(m^{n/2}(logm^{n/2}+logP))\)。
第33题
本题所求出的是( )。
A.满足 \(a,b,c \in [1,m]\) 的整数方程 \(a^3+b^3=c^3\) 解的数量
B.满足 \(a,b,c \in [1,m]\) 的整数方程 \(a^2+b^2=c^2\) 解的数量
C.满足 \(xi\in [0,m]\) 的整数方程 \(\sum_{i=1}^nk_i\cdot x_i^{p_i}=0\) 的解的数量
D.满足 \(xi\in [1,m]\) 的整数方程 \(\sum_{i=1}^nk_i\cdot x_i^{p_i}=0\) 的解的数量
【答案】:D
【解析】
根据第 37 行 dfs 的系数是 n>>1,可以确定未知数个数一定是n个。根据第24行 i 从 1 到 m 枚举,可以确定未知数的取值范围是1到m。
完善程序
(特殊最短路)给定一个含 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, __1__});
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 > __2__) {
continue;
}
for (const auto &edge : adj[u]) {
int v = edge.to;
int w = edge.weight;
if (d[u][used] + w < __3__) {
__3__ = d[u][used] + w;
pq.push({__3__, v, used});
}
if (used == 0) {
if (__4__ < d[v][1]) {
d[v][1] = __4__;
pq.push({d[v][1], v, 1});
}
}
}
}
cout << __5__ << endl;
return 0;
}
第34题
1处应填( )
A.0
B.1
C.-1
D.false
【答案】:A
【解析】
从起点出发时,还没有机会免费,所以used=0。
第35题
2处应填( )
A.d[u][!used]
B.d[u][used]
C.d[t][used]
D.INF
【答案】:B
【解析】
堆优化的 Dijkstra 一定用最小值去更新其他点,如果 dist 不是最小值,说明 d[u][used] 已经了完成更新,直接跳过。
第36题
3处应填( )
A.d[v][1]
B.d[v][used]
C.d[u][used]
D.d[v][0]
【答案】:B
【解析】
不管是否使用过免费机会,当前边都可以正常转移,转移以后 used 不变。
第37题
4处应填( )
A.d[v][0]
B.d[v][1]
C.d[u][0]
D.d[u][1]
【答案】:C
【解析】
62行专门判断 used 是否为0,是为了使用免费的机会,所以d[v][1]来自于d[u][0]+0
第38题
5处应填( )
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。
(最优测试)工厂有n条生产线(编号0~n-1),已知其中恰有一条生产线存在缺陷。工厂打算通过客户反馈来接测试生产线,从而找到存在缺陷的生产线。每一轮测试为,从若干生产线的产品取样合成一个批次发给客户。若该批次中包含缺陷生产线的产品,客户将要求退货(结果记为1),否则正常收货(记为0)。受售后压力限制,在所有发货批次中,最多只能有k次退货(即结果为1的次数≤k)。工厂的目标是,设计最少的回接测试轮数w (发货总批次),保证根据客户收货或退货的反馈结果,唯一确定存在缺陷的生产线。以下程序实现了工厂的目标,包含两部分:i) 确定 w 的最小值,并设计最优测试方案;ii) 根据测试结果推断存在缺陷的生产线。该程序确定最小值的方法为:由于不同的生产线故障时,测试应当返回不同的结果,因此 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 (__1__) {
++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 (std::__2__);
}
vector<vector<int>> plan(w);
for (int i=0; i<w; ++i) {
for (int j=0; j<n; ++j) {
if (__3__) {
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 (__4__) {
sig_bits[i] = 1;
}
}
for (int j=0; j<n; ++j) {
if (__5__) return j;
}
}
int main() {
cin >> n >> k;
int ans = solve();
cout << ans << endl;
return 0;
}
第39题
1处应填( )
A.(1 << w) < n
B.count_patterns(w, k) < n
C.count_patterns(k ,w) < n
D.comb(w, k) < n
【答案】:B
【解析】
第33行的 while 循环目的是求的最小可行值,而题面提示了“该程序确定w最小值的方法为:由于不同的生产线故障时,测试应返回不同的结果,因此轮测试的可能结果数不应少于生产线数量。”,所以w对应的结果数需要>=n,而第18行的函数是求1的出现次数不超过k的01串数量,正是我们需要的code。
第40题
2处应填( )
A.next_permutation(bits.begin(), bits.end())
B.prev_permutation(bits.begin(), bits.end())
C.next_permutation(bits.begin(), bits.end()+ones)
D.prev_permutation(bits.begin(), bits.end()+ones)
【答案】:B
【解析】
algorithm 库中的permutation 代表的是对应区间的全排列,其中从小到大的数值被认为是最小的全排列,从大到小的数值被认为是最大的全排列。43行fill是填充函数,会把bits数组的前 ones个数改成1,而42行把bits初始化成0了,此时bits 明显是最大的全排列,因此只能向前。必须使用 prev permutation 函数。同理前 ones 个数都是1,只排这一部分没有意义。
这一段的实际目的是生成n个不一样的01串,其中1的个数从0个到最多k个。
第41题
3处应填( )
A.(j >> i) & 1
B.(i >> j) & 1
C.code[i][j] == 1
D.code[j][i] == 1
【答案】:D
【解析】
分析第59行的行为,把第j条生成线加入第i批货物,必须是code[j][i]==1。
第42题
4处应填( )
A.(signature >> i) & 1
B.(signature >> i) ^ 1
C.signatures | (1 << i)
D.(signature >> i ) | 1
【答案】:A
【解析】
按照题面“test_subset()函数为抽象测试接口,输入所有批次的方案并返回一个二进制编码;该编码表示为每批次的检测结果(最低位是第1批次、最高位是第w批次)”所以这里在求第i批次是否会退货,只有A选项能取出 signature 的第i位。
第43题
5处应填( )
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
【解析】
根据return j可以确认,这里是检查j是不是答案。按照题目做法,只要退货的状态和j的出现状态完全相同,就能说明j是有缺陷的产品线。退货状态是 sig_bits,j的出现状态是 code[j],由于两个都是 vector,可以直接用==判断相等(时间复杂度是O(size)的),选择B选项。
浙公网安备 33010602011771号