5 个红球、5 个蓝球(同色无区别)排成一排,要求任意两个蓝球都不相邻,有多少种排法?
🧭 插空法
先排 5 个相同红球:_R_R_R_R_R_,形成 6 个空位(含两端)。
把 5 个相同蓝球放进这 6 个空位,每空至多 1 个:C(6,5) = 6。
试卷:CSP 2025 提高级(Senior)第一轮 · 单项选择 + 阅读程序 + 完善程序
用法:先独立作答,再点「显示解析」核对分步推理;右上角可切换 浅色/深色 配色。
CSP-S 提高级第一轮,难度与覆盖面都高于入门级。本卷可归为 8 大板块。
Q1 不相邻排列(插空法)、Q13 容斥原理。
Q2 KMP next 数组、Q4 Trie 前缀树结点数。
Q3 线段树查询、Q6 哈希线性探查、Q8 BST 遍历、Q12 堆。
Q5 拓扑排序、Q7 最小生成树、Q10 LCA 性质。
Q9 0-1 背包、Q14 重叠子问题、Q15 贪心调度。
Q11 主定理、Q26/32 递归与折半搜索量级。
DFS 计数排列、两枚鸡蛋(双蛋)最优策略、折半搜索 meet-in-the-middle。
分层 Dijkstra(免费边)、组合编码群体测试。
5 个红球、5 个蓝球(同色无区别)排成一排,要求任意两个蓝球都不相邻,有多少种排法?
先排 5 个相同红球:_R_R_R_R_R_,形成 6 个空位(含两端)。
把 5 个相同蓝球放进这 6 个空位,每空至多 1 个:C(6,5) = 6。
模式串 P = abacaba,其 next 数组(最长公共前后缀长度)是?
| 前缀 | a | ab | aba | abac | abaca | abacab | abacaba |
|---|---|---|---|---|---|---|---|
| 最长前后缀 | 0 | 0 | 1 (a) | 0 | 1 (a) | 2 (ab) | 3 (aba) |
该 next 数组(KMP 失配函数,长度 n+1=8,含首位哨兵 0)即 {0,0,0,1,0,1,2,3},与 A 一致。
大小为 16(下标 0~15)的满线段树,查询区间 [3, 11] 时,最少需访问多少个结点(含路径上父结点与完全包含的结点)?
从根 [0,15] 递归,遇「完全包含」就停止。访问到的 8 个结点:
| 访问结点 | 状态 |
|---|---|
| [0,15] | 部分重叠,递归 |
| [0,7] | 部分重叠,递归 |
| [0,3] | 部分重叠,递归 |
| [2,3] | 部分重叠,递归 |
| [3,3] | 完全包含 ✓ |
| [4,7] | 完全包含 ✓ |
| [8,15] | 部分重叠,递归 |
| [8,11] | 完全包含 ✓ |
共 8 个结点。
将 cat, car, cart, case, dog, do 插入空 Trie,构建完成(含根结点)共多少个结点?
(root) ├─ c ─ a ─ t // cat │ ├ r ─ t // car, cart │ └ s ─ e // case └─ d ─ o ─ g // do, dog
逐个数结点:root, c, a, t, r, t, s, e, d, o, g = 11 个。(do 与 dog 共享 d-o,cat/car/cart/case 共享 c-a。)
含 n 个结点、m 条边的有向无环图(DAG),其拓扑排序结果有多少种可能?
链状 DAG(1→2→…→n)只有 1 种拓扑序;而完全没有边时有 n! 种。同样的 n、m 也可能对应不同的方案数,所以 A、B、C 给出的固定公式都不对。
大小 13 的哈希表,线性探查,H(key)=key mod 13。依次插入 18, 26, 35, 9, 68, 74。插入 74 后它落在哪个索引?
| key | key%13 | 探查落点 |
|---|---|---|
| 18 | 5 | 5 |
| 26 | 0 | 0 |
| 35 | 9 | 9 |
| 9 | 9 | 9占→10 |
| 68 | 3 | 3 |
| 74 | 9 | 9占→10占→11 |
74 mod 13 = 9,9 和 10 已被占(35、9),落到 11。
8 个顶点(编号 1~8)的完全图,边权 = 两端编号差的绝对值。最小生成树总权重是多少?
相邻编号之间边权 = 1(最小可能)。取 1-2, 2-3, …, 7-8 共 7 条边,把 8 个点连成一条链,总权 = 7×1 = 7。
生成树必须有 n−1=7 条边,每条 ≥1,所以 7 是下界,可达 → 最优。
二叉搜索树的后序遍历为 2, 5, 4, 8, 12, 10, 6,其前序遍历是?
后序最后一个是根 = 6。比 6 小的 {2,5,4} 是左子树,比 6 大的 {8,12,10} 是右子树。
左子树后序 2,5,4 → 根 4,左 {2},右 {5};右子树后序 8,12,10 → 根 10,左 {8},右 {12}。
前序 = 根 → 左 → 右 = 6, (4,2,5), (10,8,12) = 6,4,2,5,10,8,12。
背包容量 20,5 个物品重量 {7,5,4,3,6}、价值 {15,12,9,7,13}。最大总价值是多少?
全取重 7+5+4+3+6=25 超重。最优选 {7, 4, 3, 6}:重量 7+4+3+6 = 20(刚好),价值 15+9+7+13 = 44。
等价于在全取(价值 56)基础上去掉重 5 价值 12 的物品:56−12 = 44,是去掉单个物品里最划算的。
f[w]=max(f[w], f[w−wᵢ]+vᵢ)(逆序枚举容量)。物品少时也可直接枚举 2⁵ 个子集。以结点 1 为根的树中,LCA(12, 18) = 4。下列哪个 LCA 组合是不可能出现的?
LCA(12,18)=4 说明 4 是 12 和 18 的祖先。于是:
| 选项 | 分析 | 是否可能 |
|---|---|---|
| A | 4 是 12 的祖先 → LCA(12,4)=4 | 可能 |
| B | 4 是 18 的祖先 → LCA(18,4)=4 | 可能 |
| C | 三者 LCA 仍是 4 | 可能 |
| D | 1 是根,任何点与根的 LCA 都是根 1,不可能是 4 | 不可能 ✘ |
递归式 T(n) = 2T(n/2) + O(n2) 描述某分治算法,其时间复杂度是?
分界 nlog_b a = nlog₂2 = n1 = n。比较 f(n)=n2 与 n:f 多项式地更大(情形三),故 T(n) = Θ(f(n)) = O(n²)。
直觉:每层合并代价 n²,且这一项随递归层数迅速衰减,顶层 n² 主导。
空最小堆依次插入 20, 12, 15, 8, 10, 5,再连续两次 delete-min,堆顶元素是?
最小堆每次 delete-min 弹出的是当前最小值。元素集合 {5,8,10,12,15,20},两次弹出最小的 5 和 8,剩下最小是 10,即新堆顶。
1 到 1000 之间,不能被 2、3、5 中任意一个整除的整数有多少个?
|2|=500, |3|=333, |5|=200;|6|=166, |10|=100, |15|=66;|30|=33。
能被 2/3/5 至少一个整除 = 500+333+200 − 166−100−66 + 33 = 734。
不能被整除 = 1000 − 734 = 266。
朴素递归求斐波那契是指数级、而 DP/迭代是线性,造成巨大差异的根本原因是?
朴素递归里 F(n) 会反复展开同一个子问题(如 F(n−2) 被算多次),调用次数随 n 指数增长。DP/记忆化让每个子问题只算一次,降到 O(n)。
5 个不可抢占任务,处理时长 {3,4,2,5,1}、截止时刻 {5,10,3,15,11},超时惩罚=处理时长。为最小化总惩罚,应优先执行哪个?
按截止时间排序:A3(3), A1(5), A2(10), A5(11), A4(15)。依次执行的完成时刻:
| 任务 | A3 | A1 | A2 | A5 | A4 |
|---|---|---|---|---|---|
| 完成时刻 | 2 | 5 | 9 | 10 | 15 |
| 截止 | 3 | 5 | 10 | 11 | 15 |
| 是否超时 | 否 | 否 | 否 | 否 | 否 |
全部按时完成,总惩罚 0。所以应先做截止最早的 A3。若先做 A4(耗时 5),A3 就赶不上截止 3 了。
📄 阅读程序(一)· DFS 计数特殊排列
判断题正确填 A、错误填 B。统计 1~n 的排列 p 中,不存在「相邻位置后一个 = 前一个 +1」的排列数。
bool flag[27];
int n, p[27], ans = 0;
void dfs(int k) {
if (k == n + 1) { ++ans; return; } // 填满 n 个位置,计数
for (int i = 1; i <= n; ++i) {
if (flag[i]) continue; // 第14行:i 已用过
if (k > 1 && i == p[k - 1] + 1) continue; // 不能等于上一个 +1
p[k] = i; flag[i] = true;
dfs(k + 1);
flag[i] = false; // 第19行:回溯
}
}
int main() { scanf("%d", &n); dfs(1); printf("%d\n", ans); } // 第25行 dfs
当输入 n=3 时,程序输出 3。
| 排列 | 是否有 p[k]=p[k−1]+1 | 合法? |
|---|---|---|
| 123 | 2=1+1 | ✘ |
| 132 | 无 | ✓ |
| 213 | 无 | ✓ |
| 231 | 3=2+1 | ✘ |
| 312 | 2=1+1 | ✘ |
| 321 | 无 | ✓ |
合法 3 个 → 输出 3。
flag 标记已用元素,剪枝条件过滤非法排列。dfs 运行过程中,k 的取值满足 1 ≤ k ≤ n+1。
从 dfs(1) 开始,每层 dfs(k+1);当 k=n+1 时立即 return,不再递归更深。所以 k 取遍 1..n+1,不会超出。
k==n+1 保证深度上界;下界由初始调用 dfs(1) 决定。删除第 19 行的 flag[i]=false;,对答案不会产生影响。
flag[i]=false 在递归返回后释放元素 i,让后续分支能重新使用它。删掉它,元素一旦用过就永远占用,无法再生成其他排列,答案会大幅减少 → 题述「不影响」错误。
当输入 n=4 时,程序输出为?
用容斥估算:设事件 Aᵢ 为「数对 (i,i+1) 相邻且按此顺序」。把 (i,i+1) 粘成一块,4 个元素含 1 块时排列数 = 3! = 6,共 3 个这样的相邻对(12,23,34);两块合并… 容斥得 24 − 3·6 + (含两对) − … = 11。直接运行也得 11。
合法排列举例:1324, 1432, 2143, 2413, 2431, 3214, 3241, 4132, 4213, 4321, 3142 等共 11 个。
若运行第 25 行 dfs 前数组 p 的初值并不全为 0,对程序的影响是?
剪枝用到 p[k-1],而 p[k−1] 一定是在父层调用 dfs(k) 之前刚被赋值的(p[k]=i 在递归之前)。因此每次读 p[k−1] 时它都是本次搜索写入的新值,与初值无关。
所以 p 的初值是否为 0 完全不影响结果 → 没有影响。
删去第 14 行的 if(flag[i]) continue;,输入 3,输出是?
删掉 flag 检查后,每位可任取 1~3(可重复),只剩约束「p[k] ≠ p[k−1]+1」。统计长度 3 的序列:
| 上一个值 | 下一个可选 | 种数 |
|---|---|---|
| 1 | 1,3(不能 2) | 2 |
| 2 | 1,2(不能 3) | 2 |
| 3 | 1,2,3 | 3 |
按首位分类求和:首=1 → 2+3=5;首=2 → 2+2=4;首=3 → 2+2+3=7。合计 5+4+7 = 16。
📄 阅读程序(二)· 两枚鸡蛋找临界楼层
k 为临界值(h≥k 会摔碎),最多 2 枚蛋(cnt_broken≤2)。guess1 线性逐层试;guess2 用「三角数」步长的最优策略。「猜测次数」= 调用 check 的次数。
int cnt_broken = 0, cnt_check = 0, n, k;
bool check(int h) { // 试探楼层 h
++cnt_check;
if (cnt_broken == 2) return false; // 没蛋了
if (h >= k) { ++cnt_broken; return true; }
else return false;
}
void guess1(int n){ for(int i=1;i<=n;++i) if(check(i)){ assert_ans(i); return; } }
void guess2(int n){ // 先求最小 w 使 w(w+1)/2 ≥ n
int w; for(w=1; w*(w+1)/2 < n; ++w);
for(int ti=w, nh=w; ; --ti, nh+=ti, nh=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;
}
}
} // 主函数:t=1 调 guess1,否则 guess2
输入 "6 5 1" 时猜测次数为 5;输入 "6 5 2" 时猜测次数为 3。
t=1(guess1 线性):check(1..5),到 5 时 5≥5 命中 → 5 次。
t=2(guess2):w 满足 w(w+1)/2≥6 → w=3。首试 nh=3(<5 没碎),再 nh=3+2=5 试(5≥5 碎),第二枚从 4 线性补查到 5 → 共 check(3),check(5),check(4) ... 实测 3 次命中 → 3 次。
不管 n、k 为多少,t=2 的猜测数总是 ≤ t=1 的猜测数。
取 k=1:guess1 第一次 check(1) 就命中,仅 1 次;而 guess2 要先跳到 nh=w(>1),check(w) 命中后还要线性回查到 1,至少 2 次。此时 guess2 > guess1。
所以「t=2 总是 ≤ t=1」不成立。guess2 优的是最坏情况,并非每个输入都更少。
不管 t=1 或 t=2,程序都一定能猜到正确结果。
guess1 线性扫描必然在 i=k 处命中。guess2 的三角步长设计保证两枚蛋预算内能定位(首跳找到碎的区间,第二枚在区间内线性确定),对 1≤k≤n 全部正确。
函数 guess1 运行过程中,cnt_broken 的值最多为?
guess1 从 1 往上试,前面 i<k 都不碎;第一次遇到 i=k 时 h≥k,cnt_broken 由 0 增到 1,并立即 return。所以全程 cnt_broken 最多 = 1。
函数 guess2 运行中,最多使用的猜测次数量级为?
w 是最小满足 w(w+1)/2 ≥ n 的整数,约等于 √(2n)。最坏情况第一枚跳 w 次左右、第二枚再线性 w 次左右,总量级 O(√n)。
例如 n=100 时 w=14(14×15/2=105≥100),最坏约 14 次。
当 n=100 时,t=1 与 t=2 分别需要的「最多」猜测次数是?
t=1:当 k=100 时要 check(1..100),最多 100 次。
t=2:w=14(14×15/2=105≥100,13×14/2=91<100),最坏 14 次。
📄 阅读程序(三)· 折半搜索(meet-in-the-middle)
统计满足 Σ kᵢ·xᵢpᵢ = 0(xᵢ ∈ [1,m])的解数。把 n 个变量分两半,各自枚举所有取值之和,再配对求和为 0 的组合数。
int mpow(int x,int k){ int a=1; for(;k;k>>=1,x=x*x) if(k&1) a=a*x; return a; } // 快速幂 xᵏ
void dfs(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[l]*mpow(i,p[l])); // 枚举 x_l = i
}
// 主:dfs 前半得 ans1、后半得 ans2;ans1 排序去重并记重数 cntans1;
std::sort(ans2.begin(), ans2.end()); // 第51行
// 双指针:对每个 ans2 找 ans1 中使和为 0 的项,累加重数 → ans
删除第 51 行 std::sort(ans2…) 后,输出结果不会受影响。
末尾用单调双指针(las 只增)在已排序的 ans1 中为每个 ans2 找配对。若 ans2 未排序,遍历顺序错乱,单调指针会漏配,结果出错。
用例 3 15 1 2 -1 2 1 2:原输出 8,去掉 ans2 排序后变成 2,明显不同。
假设不溢出,mpow(x,k) 的功能是求 xk。
for(;k;k>>=1,x=x*x) if(k&1) a=a*x;:按 k 的二进制逐位处理,遇到为 1 的位就把当前 x 的幂乘进答案,x 每轮自乘。最终 a = xk,复杂度 O(log k)。
代码中(排序后那段循环)的目的是为了将 ans1 数组进行「去重」操作。
排序后扫描:相同值合并、并在 cntans1 里累加它出现的次数;不同值则新建一项。结果 ans1 变为「不同值列表」,cntans1 是对应的重数。这正是带计数的去重。
当输入为 3 15 / 1 2 / -1 2 / 1 2 时,输出结果为?(即求 x₁²−x₂²+x₃²=0、x∈[1,15] 的解数)
枚举有序三元组 (x₁,x₃,x₂),满足斜边 ≤ 15 的勾股数:
| 斜边 x₂ | 直角边 (x₁,x₃) 有序对 |
|---|---|
| 5 | (3,4),(4,3) |
| 10 | (6,8),(8,6) |
| 13 | (5,12),(12,5) |
| 15 | (9,12),(12,9) |
共 2+2+2+2 = 8 个有序解。
记 p 数组元素最大值为 P,则代码的时间复杂度是?
每半枚举 mn/2 种取值(不是 mn,这是折半的关键)。生成每个值时要做快速幂 O(log P);最后对 mn/2 个数排序 O(mn/2 log mn/2)。两部分相加即 D。
本题所求出的是?
dfs 中 for(int i=1;i<=m;++i),每个变量 xᵢ 取 1~m(不含 0)。整个程序统计使 Σ kᵢ·xᵢpᵢ = 0 的取值组合数。A、B 只是它的特例,C 的范围 [0,m] 不符。
i=1 表示 xᵢ∈[1,m]。该程序是「广义子集和=0 计数」的折半搜索模板。🧩 完善程序(一)· 带一条「免费边」的最短路
无向图非负边权,整条路径中至多选一条边首次经过时费用记 0。用分层 Dijkstra:状态 (结点 u, 是否已用免费 0/1),d[u][0/1]。补全 ①~⑤。
d[s][0] = 0;
pq.push({0, s, __①__}); // 起点,免费机会未用
while (!pq.empty()) {
auto [dist, u, used] = pq.top(); pq.pop();
if (dist > __②__) continue; // 过期状态跳过
for (auto &e : adj[u]) {
int v = e.to, w = e.weight;
if (d[u][used] + w < __③__) { // 正常走这条边
__③__ = d[u][used] + w;
pq.push({__③__, v, used});
}
if (used == 0) { // 把这条边当免费边
if (__④__ < d[v][1]) {
d[v][1] = d[u][used];
pq.push({d[v][1], v, 1});
}
}
}
}
cout << __⑤__ << endl;
① 处应填?
状态第三维 used_freebie:0 表示「免费机会还没用」,1 表示「已用」。起点自然是 0,与 d[s][0]=0 对应。
② 处应填(过期状态判断)?
从堆里取出 (dist,u,used) 后,如果 dist > d[u][used],说明这是已被更优值取代的旧记录,直接跳过。所以填 d[u][used]。
③ 处应填(正常走边、保持 used 不变的松弛目标)?
正常付费走 u→v,免费状态不变,松弛的是同一层 d[v][used]:若 d[u][used]+w 更小就更新它并入队(层仍为 used)。
④ 处应填(用免费边到 v 的新代价)?
此处 used==0。把 u→v 当免费边,则到 v(已用免费,层 1)的代价 = d[u][0] + 0 = d[u][0]。条件判断它是否优于现有 d[v][1],故 ④ = d[u][0](即 d[u][used])。
⑤ 处应填(最终答案)?
题目说「至多」选一条免费边,所以到达 t 时可能用了(层 1)也可能没用(层 0),取两者较小值 min(d[t][0], d[t][1])。
🧩 完善程序(二)· 组合编码的群体测试
n 条生产线恰有 1 条有缺陷,每轮把若干产品混批发货,含缺陷则退货(1)否则收货(0),退货总数 ≤ k。求最少轮数 w 并给每条线分配一个「≤k 个 1」的不同码字以唯一定位。补全 ①~⑤。
// count_patterns(w,k): 长度 w、1 的个数 ≤ k 的码字总数 int solve(int n, int k){ int w = 1; while (__①__) ++w; // 取最小 w 使码字数 ≥ n cout << w << endl; 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); // [1..1,0..0] do { for (int b=0;b<w;++b) code[idx][b]=bits[b]; if (++idx >= n) break; } while (__②__); // 枚举该 ones 下所有排列 } 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); // 第 i 批含第 j 条线? int signature = test_subset(plan); vector<int> sig_bits(w, 0); for (int i=0;i<w;++i) if (__④__) sig_bits[i]=1; // 取出第 i 位 for (int j=0;j<n;++j) if (__⑤__) return j; // 码字匹配的即缺陷线 }
① 处应填(while 继续条件)?
每条线要一个不同码字,且 1 的个数 ≤ k。可用码字数 = count_patterns(w,k)。当它 < n(不够分)时继续增大 w;够了就停。所以条件填 count_patterns(w,k) < n。
不能用 (1<<w)(那是不限 1 的个数的总数)或 comb(w,k)(只数恰好 k 个 1)。
② 处应填(枚举固定 1-个数的所有排列)?
fill 把 1 都堆在前面:[1,1,…,1,0,…,0],这是字典序最大的排列。next_permutation 会立刻返回 false(只生成 1 个);必须用 prev_permutation 在整个范围上依次生成更小的排列,遍历全部 C(w,ones) 种。
③ 处应填(第 i 批是否包含第 j 条线)?
code 定义为 code(n, vector<int>(w)),即 code[线][批次]。第 j 条线在第 i 批次的位是 code[j][i];为 1 说明该线产品进了第 i 批,故 plan[i] 加入 j。所以填 code[j][i]==1(C 把下标写反了)。
④ 处应填(取出 signature 的第 i 位)?
signature 是二进制编码(最低位是第 1 批结果)。取第 i 位:右移 i 位再 & 1,即 (signature >> i) & 1。^1 是取反,| 是置位,都不对。
(x>>i)&1、置位 x|(1<<i)、清位 x&~(1<<i)。⑤ 处应填(找码字与测试结果一致的生产线)?
缺陷线 j 的码字 code[j] 应与测试得到的 sig_bits 完全一致。vector 的 == 会逐元素比较,直接写 code[j] == sig_bits 即可。
A(顺序无关的排列判断)会误配;C 的 plan[j] 是批次列表、维度都不对;D 只比了某一位 i(且 i 已出循环)。