T1
有 5 个红色球和 5 个蓝色球,它们除了颜色之外完全相同。将这 10 个球排成一排,要求任意两个蓝色球都不能相邻,有多少种不同的排列方法? ( )
- A. 25
- B. 30
- C. 6
- D. 120
选 C.
排列组合:不相邻问题
- 先排 \(5\) 个红球,形成 \(6\) 个空位(含两端):\(\texttt{\_ R \_ R \_ R \_ R \_ R \_}\)。
- 从 \(6\) 个空位中选 \(5\) 个插入蓝球,方法数为组合数 \(C_6^5 = 6\)。
- 红球和蓝球内部无区别,无需额外排列。
T2
在 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\}\)
next
数组定义为“最长公共前后缀长度”,计算如下:
- \(i=0\)(\(\texttt{a}\)):\(\text{next}[0] = 0\)(单个字符无前后缀)。
- \(i=1\)(\(\texttt{b}\)):前缀\(\texttt{a}\)与后缀\(\texttt{b}\)无公共,\(\text{next}[1]=0\)。
- \(i=2\)(\(\texttt{a}\)):前缀\(\texttt{a}\)与后缀\(\texttt{a}\)匹配,\(\text{next}[2]=1\)。
- \(i=3\)(\(\texttt{c}\)):前缀\(\texttt{ab}\)与后缀\(\texttt{ba}\)无公共,\(\text{next}[3]=0\)。
- \(i=4\)(\(\texttt{a}\)):前缀\(\texttt{a}\)与后缀\(\texttt{a}\)匹配,\(\text{next}[4]=1\)。
- \(i=5\)(\(\texttt{b}\)):前缀\(\texttt{ab}\)与后缀\(\texttt{ab}\)匹配,\(\text{next}[5]=2\)。
- \(i=6\)(\(\texttt{a}\)):前缀\(\texttt{aba}\)与后缀\(\texttt{aba}\)匹配,\(\text{next}[6]=3\)。
T3
对一个大小为 \(16\)(下标 \(0 \sim 15\))的数组上构建满线段树。查询区间 \([3, 11]\) 时,最少需要访问多少个树结点(包括路径上的父结点和完全包含在查询区间内的结点)?( )
- A. 7
- B. 8
- C. 9
- D. 10
线段树结构:根结点覆盖\(\texttt{0-15}\),左子树\(\texttt{0-7}\),右子树\(\texttt{8-15}\),依此类推。
- 根结点:\(\texttt{[0,15]}\)(覆盖整个区间,需分解为左右子树)。
- 左子树\(\texttt{[0,7]}\):与\(\texttt{[3,11]}\)重叠部分为\(\texttt{[3,7]}\),继续分解:
- 左子结点\(\texttt{[0,3]}\):与\(\texttt{[3,7]}\)重叠部分为\(\texttt{[3,3]}\),继续分解:
- 左子结点\(\texttt{[0,1]}\):与\(\texttt{[3,3]}\)无重叠,忽略。
- 右子结点\(\texttt{[2,3]}\):与\(\texttt{[3,3]}\)重叠,分解为\(\texttt{[2,2]}\)(无重叠)和\(\texttt{[3,3]}\)(完全包含,计入叶子结点)。
- 左子结点\(\texttt{[0,3]}\):与\(\texttt{[3,7]}\)重叠部分为\(\texttt{[3,3]}\),继续分解:
- 右子结点\(\texttt{[4,7]}\):完全包含于\(\texttt{[3,7]}\),直接计入(无需分解)。
- 右子树\(\texttt{[8,15]}\):与\(\texttt{[3,11]}\)重叠部分为\(\texttt{[8,11]}\),继续分解:
- 左子结点\(\texttt{[8,11]}\):完全包含于\(\texttt{[3,11]}\),直接计入(无需分解)。
- 右子结点\(\texttt{[12,15]}\):无重叠,忽略。
- 路径父结点:\(\texttt{[0,15]}\)、\(\texttt{[0,7]}\)、\(\texttt{[8,15]}\)、\(\texttt{[0,3]}\)、\(\texttt{[2,3]}\)(共\(5\)个)。
- 完全包含结点:\(\texttt{[4,7]}\)、\(\texttt{[3,3]}\)、\(\texttt{[8,11]}\)(共\(3\)个)。
- 总结点数:\(5+3=8\)。
T4
将字符串 "cat", "car", "cart", "case", "dog", "do" 插入一个空的 Trie 树(前缀树)中。构建完成的 Trie 树(包括根结点)共有多少个结点?( )
- A. 8
- B. 9
- C. 10
- D. 11
Trie 树共享前缀,结点结构如下:
- 根结点→\(\texttt{c}\)→\(\texttt{a}\)→\(\texttt{t}\)(\(\texttt{cat}\));
- \(\texttt{c}\)→\(\texttt{a}\)→\(\texttt{r}\)(\(\texttt{car}\))→\(\texttt{t}\)(\(\texttt{cart}\));
- \(\texttt{c}\)→\(\texttt{a}\)→\(\texttt{s}\)(\(\texttt{cas}\))→\(\texttt{e}\)(\(\texttt{case}\));
- 根结点→\(\texttt{d}\)→\(\texttt{o}\)(\(\texttt{do}\))→\(\texttt{g}\)(\(\texttt{dog}\))。
- 总结点:根 + \(\texttt{c,a,t,r,s,e,d,o,g}\) → 共\(11\)个。
T5
对于一个包含 n 个顶点和 m 条边的有向无环图(DAG),其拓扑排序的结果有多少种可能? ( )
- A. 只有 1 种
- B. 最多 n 种
- C. 等于 n-m 种
- D. 以上都不对
拓扑排序数量取决于 DAG 结构:线性链只有 \(1\) 种,独立节点有 \(n!\) 种,故 A、B、C 均错误。
T6
在一个大小为 13 的哈希表中,使用闭散列法的线性探查来解决冲突。哈希函数为H(key)=key mod 13。依次插入关键字 18, 26, 35, 9, 68, 74。插入 74 后,它最终被放置在哪个索引位置?( )
- A. 5
- B. 7
- C. 9
- D. 11
线性探查插入过程。插入顺序及位置:
- \(\texttt{18} \rightarrow 5\),\(\texttt{26} \rightarrow 0\),\(\texttt{35} \rightarrow 9\),\(\texttt{9} \rightarrow 10\)(冲突后探查),\(\texttt{68} \rightarrow 3\)
- \(\texttt{74} \bmod 13 = 9\),冲突后线性探查(循环):\(9 \rightarrow 10 \rightarrow 11\)
T7
一个包含 8 个顶点的完全图(顶点的编号为 1 到 8),任意两点之间的边权重等于两顶点编号的差的绝对值。例如,顶点 3 和 7 之间的边权重为 ∣7 − 3∣ = 4。该图的最小生成树的总权重是多少? ( )
- A. 7
- B. 8
- C. 9
- D. 10
MST(最小生成树)需连接所有顶点且权重最小,选择相邻顶点边(权重1):\(\texttt{1-2}\)(1)、\(\texttt{2-3}\)(1)、\(\texttt{3-4}\)(1)、\(\texttt{4-5}\)(1)、\(\texttt{5-6}\)(1)、\(\texttt{6-7}\)(1)、\(\texttt{7-8}\)(1),共 \(7\) 条边,总权重 \(7\)。
T8
如果一棵二叉搜索树的后序遍历序列是 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
二叉搜索树前序遍历。
二叉搜索树保证左子树所有结点值均不大于根结点,右子树所有结点值均不小于根结点,因此其中序遍历序列就是所有结点值排序后形成的序列。
- 后序序列最后一个元素为根(\(\texttt{6}\)),左子树(\(\texttt{2,5,4}\)),右子树(\(\texttt{8,12,10}\))。
- 左子树根\(\texttt{4}\)(左\(\texttt{2}\),右\(\texttt{5}\)),右子树根\(\texttt{10}\)(左\(\texttt{8}\),右\(\texttt{12}\))。
- 前序遍历:根→左→右→\(\texttt{6}\)→\(\texttt{4}\)→\(\texttt{2}\)→\(\texttt{5}\)→\(\texttt{10}\)→\(\texttt{8}\)→\(\texttt{12}\)。
T9
一个 0-1 背包问题,背包容量为 20。现有 5 个物品,其重量和价值分别为 7,5,4,3,6和 15,12,9,7,13。装入背包的物品能获得的最大总价值是多少?( )
- A. 43
- B. 41
- C. 45
- D. 44
最优组合:\(\texttt{7+4+3+6=20}\)(重量),价值 \(\texttt{15+9+7+13=44}\)。
T10
在一棵以结点 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
根节点为 \(1\),LCA(12,1) 必为 \(1\)(根是所有节点的祖先),故D错误。
T11
递归关系式 T(n) = 2T(n/2) + O(n²) 描述了某个分治算法的时间复杂度。请问该算法的时间复杂度是多少?( )
- A. \(O(n)\)
- B. \(O(n \log n)\)
- C. \(O(n^2)\)
- D. \(O(n^2 \log n)\)
做法一:主定理:$ a=2,b=2,f(n)=n^2, n^{\log_b a}=n\(。\)f(n)$ 更优,故复杂度为 \(O(n^2)\)。
做法二:
故 \(T(n) = O(n^2)\).
T12
在一个初始为空的最小堆(min-heap)中,依次插入元素 20, 12, 15, 8, 10, 5。然后连续执行两次“删除最小值”(delete-min)操作。请问此时堆顶元素是什么?( )
- A. 10
- B. 12
- C. 15
- D. 20
模拟即可。依次插入元素过程中堆的内部为:
- \([20]\)
- \([12,20]\)
- \([12,20,15]\)
- \([8,12,15,20]\)
- \([8,10,15,20,12]\)
- \([5,10,8,20,12,15]\)
删除一次堆顶,变为:
- \([8,10,15,20,12]\)
再删除一次堆顶,变为:
- \([10,12,15,20]\)
最终堆顶为 \(10\)。
T13
1 到 1000 之间,不能被 2、3、5 中任意一个数整除的整数有多少个?( )
- A. 266
- B. 267
- C. 333
- D. 734
容斥原理
-
计算不能被 2、3、5 整除的数,即总数减去能被 2、3、5 中至少一个数整除的数
-
计算能被 2、3、5 整除的数(集合A、B、C)
- $ |A|(被2整除):\left\lfloor \frac{1000}{2} \right\rfloor = 500 $
- $ |B|(被3整除):\left\lfloor \frac{1000}{3} \right\rfloor = 333 $
- $ |C|(被5整除):\left\lfloor \frac{1000}{5} \right\rfloor = 200 $
-
计算两两交集(重复部分)
- $ |A∩B|(被2和3整除,即被6整除):\left\lfloor \frac{1000}{6} \right\rfloor = 166 $
- $ |A∩C|(被2和5整除,即被10整除):\left\lfloor \frac{1000}{10} \right\rfloor = 100 $
- $ |B∩C|(被3和5整除,即被15整除):\left\lfloor \frac{1000}{15} \right\rfloor = 66 $
-
计算三者交集(重复减去的部分)
- $ |A∩B∩C|(被2、3、5整除,即被30整除):\left\lfloor \frac{1000}{30} \right\rfloor = 33 $
-
能被 2、3、5 中至少一个数整除的数的个数为:
- $ ∣A∪B∪C∣=∣A∣+∣B∣+∣C∣−∣A∩B∣−∣A∩C∣−∣B∩C∣+∣A∩B∩C∣ =743 $
-
不能被 2、3、5 中任意一个数整除的数的个数为:
- $ 1000 −∣A∪B∪C∣=1000−734=266 $
T14
斐波那契数列的定义为 F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2)。使用朴素递归方法计算 F(n)的时间复杂度是指数级的。而使用动态规划(或迭代)方法的时间复杂度是线性的。造成这种巨大差异的根本原因是?( )
- A. 递归函数调用栈开销过大
- B. 操作系统对递归深度有限制
- C. 朴素递归中存在大量的重叠子问题未被重复利用
- D. 动态规划使用了更少的数据存储空间
朴素递归重复计算 \(F(n-2)\) 等子问题,动态规划通过存储子问题结果避免重复。
T15
有 5 个独立的、不可抢占的任务 A1, A2, A3, A4, A5 需要在一台机器上执行(从时间0 开始执行),每个任务都有对应的处理时长和截止时刻,按顺序分别为 3,4,2,5,1 和5,10,3,15,11。如果某一个任务超时,相应的惩罚等于其处理时长。为了最小化总惩罚,应该优先执行哪个任务?( )
- A. 处理时间最短的任务 A5
- B. 截止时间最早的任务 A3
- C. 处理时间最长的任务 A4
- D. 任意一个任务都可以
截止时间最早优先(EDF)可最小化超时:\(\texttt{A3}\)(截止\(3\),时长\(2\))→\(\texttt{A1}\)(截止\(5\),时长\(3\))→\(\texttt{A2}\)(截止\(10\),时长\(4\))→\(\texttt{A5}\)(截止\(11\),时长\(1\))→\(\texttt{A4}\)(截止 \(15\),时长 \(5\)),总惩罚 \(0\)。
T16 程序阅读第一题
#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;
}
该程序通过深度优先搜索(DFS)计算 \(1\) 到 \(n\) 的全排列中,满足“任意相邻两个数,后一个数不是前一个数加 \(1\)”的排列总数。核心逻辑是递归枚举所有排列,通过剪枝条件排除不符合要求的情况。
全局变量:
flag[27]
:标记数字是否已使用(避免重复选数)。p[27]
:存储当前排列(p[k]
表示第 \(k\) 个位置的数字)。ans
:计数符合条件的排列数。
DFS函数:
- 递归参数
k
:当前排列的位置(从 \(1\) 到 \(n\))。 - 终止条件:
k == n+1
,表示生成一个有效排列,ans++
。 - 剪枝条件:
if (k > 1 && i == p[k-1] + 1) continue;
(若当前数是前一个数加 \(1\),则跳过)。 - 回溯操作:
flag[i] = false
(恢复现场,允许数字 \(i\) 被重新选择)。
T16-1
当输入的n=3的时候,程序输出的答案为3。
正确。
\(n=3\) 时,需排除“后一个数是前一个数加 \(1\)”的排列。\(1 \sim 3\) 的全排列共 \(6\) 种,符合条件的有 \(3\) 种:
- \([1,3,2], [2,1,3], [3,2,1]\),故输出 \(3\),正确。
T16-2
在dfs函数运行过程中,\(k\) 的取值会满足 \(1≤k≤n+1\)。
正确。
初始调用 dfs(1)
,递归终止条件为 k == n+1
,故 \(k\) 的取值范围是 \(1 \rightarrow 2 \rightarrow \dots \rightarrow n+1\),即 \(1 \le k \le n+1\)。
T16-3
删除第19行的
flag[i]=false;
,对答案不会产生影响。
错误。
flag[i]=false
是回溯的关键步骤,用于释放当前数字 i
,允许其在后续递归中被重新选择。若删除,数字 i
被选中后将永久标记为“已使用”,导致大部分排列无法生成,答案会显著偏小。
T16-4
当输入的 \(n=4\) 的时候, 程序输出的答案为()。
- A. 11
- B. 12
- C. 24
- D. 9
选 A.
\(n=4\) 时,符合条件的排列需满足所有相邻位置均不出现“后数 \(=\) 前数 \(+1\)”。通过枚举或递归计算,共有 \(11\) 种有效排列。
T16-5
如果因为某些问题,导致程序运行第25行的dfs函数之前,数组p的初值并不全为0,则对程序的影响是()。
- A. 输出的答案比原答案要小
- B. 无法确定输出的答案
- C. 程序可能陷入死循环
- D. 没有影响
选 D.
p[k]
在递归中会被显式赋值(p[k] = i
),覆盖初始值。- 仅
p[k-1]
(前一个位置的数字)会影响剪枝条件,但p[k-1]
在递归中已被正确设置为前一步选择的数字,与初始值无关。
T16-6
假如删去第 14 行的
if(flag[i]) continue;
,输入 \(3\),得到的输出答案是( )
- A. 27
- B. 3
- C. 16
- D. 12
选 C.
删除该条件后,程序允许重复选数(即生成“可重复数字的序列”),需满足“相邻数字后数 \(\neq\) 前数 \(+ 1\)”。
- 第 \(1\) 位:\(3\) 种选择(\(1,2,3\))。
- 第 \(2\) 位:若前数为 \(1\) 或 \(2\),各有 \(2\) 种选择;若前数为 \(3\) ,有 \(3\) 种选择(共 \(2+2+3=7\) 种)。
- 第 \(3\) 位:根据前两位的组合,总序列数为 \(16\)。
T17 程序阅读第二题
#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;
}
本题套入一个题目背景可以更方便地理解代码:
有两个完全相同的鸡蛋,存在一个 \([1, n]\) 范围内的最小高度 \(k\),使得这两个鸡蛋在从小于该高度 \(k\) 的位置放开后落到地面上不会破碎,但一旦高度大于等于 \(h\) 则鸡蛋会破碎。
你可以进行任意次试验(
check
函数功能),每次试验可以任意选择一个 \([1, n]\) 范围内的高度,将手头上拥有的任意一个鸡蛋从该高度放开,让其落到地面上,然后观察其破碎情况。如果鸡蛋没有碎,那么接下来你可以重复使用该鸡蛋进行试验;但如果鸡蛋碎了,那就只能使用下一个鸡蛋。你只有两个鸡蛋可以用,如果两个鸡蛋都破了就没法再进行试验了。请尝试找到会让这两个鸡蛋破碎的最小高度 \(k\) (
guess1
函数功能),并在找到最小高度的同时求出最少的试验次数(guess2
函数功能)。
然后观察本题的几个重要变量及函数:
check()
函数用于检测某次试验是否打破了某个鸡蛋,并统计试验次数,记录在 cnt_check
内;如果本次试验的高度超过会导致鸡蛋破碎的高度 \(k\),则 cnt_broken++
,表示鸡蛋碎了一个。如果破碎次数达到 \(2\) 个则无法继续执行 check()
函数。
guess1()
函数比较简单,就是用最朴素的方法从最小高度 \(1\) 开始慢慢试到 \(n\),只要鸡蛋发生了破碎,就可以直接找到这个最小高度 \(h\)。这种方法没法保证试验的次数最少,但在找到答案时只会破碎一个鸡蛋。
guess2()
函数较为复杂,它会把区间 \([1, n]\) 划分为多段,使得不论答案 \(h\) 落在哪一段内,最坏情况下的试验次数是固定最小的。
- 以 \(n = 15\) 举例,我们尝试将其划分为 \([1, 5], [6, 9], [10,12], [13, 14], [15, 15]\) 这样的五段,可以发现这五段区间的长度分别为 \(5, 4, 3, 2, 1\)。
- 我们可以按顺序先拿第一个鸡蛋试一遍每段区间的右端点,也就是 \(5, 9, 12, 14, 15\)。假如鸡蛋在我们试 \(12\) 这个高度时碎了,那就说明会让鸡蛋破碎的高度一定在以 \(12\) 作为右端点的 \([10, 12]\) 这段区间内。接下来因为我们只剩一个鸡蛋了,所以只能像
guess1()
函数那样从小往大试一遍每种高度,以求出最终答案。 - 为什么这样划分区间?还是举目前的这个例子:
- 如果我们最终的答案落在区间 \([1, 5]\) 内,那么在确定区间之前第一个鸡蛋只需要试验 \(1\) 次就可以,接下来只需要把第二个鸡蛋按顺序试一遍 \(1 \sim 4\) 内的所有整数,最坏情况下试验 \(1+4\) 次即可确定答案。
- 如果我们最终的答案落在区间 \([6, 9]\) 内,那么在确定区间之前第一个鸡蛋需要试验 \(2\) 次(也就是 \(5, 9\) 这两个右端点),接下来第二个鸡蛋需要按顺序试一遍 \(6 \sim 8\) 内的所有整数,最坏情况下试验 \(2 + 3\) 次即可确定答案。
- 如果我们最终的答案落在区间 \([10, 12]\) 内,发现最坏情况下需要试验 \(3+2\) 次。
- 也就是说,在确定区间之前的试验次数越多,我们这种划分方法可以保证确定区间后按顺序尝试的次数会随之变少,最终使得最坏情况下的试验次数基本保持不变。在当前例子里就是 \(1+4=2+3=3+2=\dots=5\) 次。
- 最后发现每段区间的长度呈现出一种等差数列的形式,从 \(1\) 到 \(w\) 的等差数列求和公式为 \(\dfrac{w(w+1)}2\),那么我们只需要找到满足 \(\dfrac{w(w+1)}2 \ge n\) 的最小正整数 \(w\),这便是最大区间的长度,该方式在最坏情况下的尝试次数即 \(w\) 次。
- 再观察 \(w\) 与 \(n\) 两者之间的关系,可以得出
guess2()
时间复杂度约为 \(O(\sqrt n)\)。
T17-1
当输入为
6 5 1
时,猜测次数为 \(5\);当输入为6 5 2
时,猜测次数为 \(3\)。
正确。
当 \(n=6, k=5\) 时执行 guess1()
,按顺序从 \(1\) 开始尝试到 \(k = 5\) 过,check()
调用次数为 \(5\)。
当 \(n=6, k=5\) 时执行 guess2()
,首先找到满足 \(\dfrac{w(w+1)}2 \ge n\) 的最小 \(w = 4\),接下来把 \([1,6]\) 这段区间分为 \([1, 4], [5, 6]\) 两段。第一次尝试 \(4\),鸡蛋没破;第二次尝试 \(6\),鸡蛋破了,确定答案在区间 \([5, 6]\) 内。
根据代码,ti
变量表示当前这段区间的长度,从 w
开始每次 \(-1\);nh
变量表示当前这段区间的右端点,从 w
开始每次加 ti
。当检查当前右端点 nh
发现鸡蛋破碎后,执行内部循环 j = nh-ti+1 ~ nh-1
。
因为此时 ti = 2
,nh = 6
,所以循环等价于 j = 5 ~ 5
,尝试高度 \(5\) 发现 \(5 \ge k = 5\),鸡蛋破碎,于是直接确定答案为 \(5\),check()
函数总共调用 \(2+1 = 3\) 次。
T17-2
不管输入的 \(n\) 和 \(k\) 具体为多少,\(t=2\) 时的猜测数总是小于等于 \(t=1\) 时的猜测数。
错误。
假设此时 \(n\) 很大,\(k=1\)。
guess1()
试验一次即可得到答案。
但 guess2()
要先试验第一段区间的最大值,鸡蛋破碎后再从 \(1\) 开始一个个试验,所以在这个例子里总共要试验 \(2\) 次。
T17-3
不管 \(t=1\) 或 \(t=2\),程序都一定会猜到正确结果。
正确。
两份程序都没什么问题,区别只在于最坏情况下的时间复杂度。
T17-4
函数
guess1
在运行过程中,cnt_broken
的值最多为( )。
- A. \(0\)
- B. \(1\)
- C. \(2\)
- D. \(n\)
选 B.
guess1()
函数从小猜到大,在第一个鸡蛋破碎后就可以直接得到答案,所以破碎次数恒为 \(1\)。
T17-5
函数
guess2
在运行过程中,最多使用的猜测次数的量级为( )。
- A. \(O(n)\)
- B. \(O(n^2)\)
- C. \(O(\sqrt n)\)
- D. \(O(\log n)\)
选 C.
猜测次数的最大值 \(w\) 为满足 \(\dfrac{w(w+1)}2 \ge n\) 的最小值,量级为 \(O(\sqrt n)\)。
T17-6
当输入的 \(n = 100\) 时,代码中 \(t=1\) 和 \(t=2\) 分别需要的猜测次数最多分别为( )。
- A. \(100, 14\)
- B. \(100, 13\)
- C. \(99, 14\)
- D. \(99, 13\)
选 A.
guess1()
最坏情况下从 \(1\) 猜到 \(n=100\) 过,共 \(100\) 次。
guess2()
最坏情况下恰好猜 \(w\) 次,其中 \(w\) 为满足 \(\dfrac{w(w+1)}2 \ge n\) 的最小值,在 \(n=100\) 时 \(w = 14\)。
T18 程序阅读第三题
#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 = 1; i <= m; ++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;
}
本题比较考验代码阅读及理解能力。
首先观察 mpow(x, k)
函数,不难看出是借助快速幂算法求解 \(x^k\)。
然后观察 dfs()
函数,重点在于 \(l, r, v\) 这三个参数。
- 当 \(l \le r\) 时,下一次
dfs
的区间变为了 \([l+1, r]\),且参数 \(v\) 的变化涉及到的下标只和 \(l\) 有关,说明我们每次搜索只需要关注下标 \(l\) 的位置对答案的影响即可,按顺序处理区间内从左往右的每个位置。- 对于 \(l\) 下标位置对应的两个数值 \(k_l, p_l\),搜索过程多了一层循环 \(i = 1 \sim m\),然后将 \(\large k_l \times i^{p_l}\) 加入了参数 \(v\) 内。
- 当 \(l \gt r\) 时,
++cnt
的同时将当前 \(v\) 参数加入ans
数组内。
综上可以得出结论:dfs()
函数的任务是对于区间 \([l, r]\) 内的每个位置 \(i\),求出 \(\large k_i \times x_i ^ {p_i}\) 的总和,即 \(\large \sum \limits _{i=l}^r k_i \times x_i^{p_i}\),其中每个 \(x_i \in [1, m]\),然后将得到的每种总和加入数组 ans
内。
最后观察 main()
函数,在输入完成后调用了两次 dfs()
函数,分别对区间 \([1, \dfrac n 2]\) 和 \([\dfrac n 2 + 1 , n]\) 进行处理,将答案存储在 ans1
和 ans2
两个数组内。
对于 ans1
数组,排序后从前往后看一遍每个数,同时开了一个 cntans1
数组。当新数字与 ans1[newcnt1-1
相同时,仅让 cntans1[newcnt1 - 1]
自增 \(1\);而当新数字与其不同时,会将这个新数字移动到 ans1[newcnt1]
的位置上之后再让变量 newcnt1
自增,同时 cntans1
新增一个数字 \(1\)。
- 先单独考虑
ans1
数组,当新数字与前面的数字相同时没有任何改变,而当新数字与前面的数字不同时,则往前移动到newcnt1
的位置,明显这是在完成去重的操作,同时newcnt1
变量表示的是当前已经找到了多少种不同的数。 - 再考虑
cntans1
数组,当新数字与前面的数字相同时最后一个位置自增,而当不同时则新增一个数字 \(1\),明显统计的是每种数字在原数组中重复的次数(去重的同时统计重复次数)。
最后对于 ans2
数组,排序后倒着从后往前看一遍每个数字 ans2[i]
(自大往小),同时借助双指针算法,让 las
变量在 ans1
数组内从 \(0\) 开始不断向后移动。根据条件,这里查找的就是满足 ans1[las] + ans2[i] == 0
的数值对数量,在满足条件同时将 cntans1[las]
位置的值计入答案,即在原本的 ans1
数组内有多少个数字加上 ans2[i]
后为 \(0\)。
总结,本题原本的功能是为了对于 \([1, n]\) 内的每个位置,都借助搜索找出一个数字 \(x_i \in [1, m]\),然后对于每个位置 \(i\) 求出 \(\large k_i \times x_i ^ {p_i}\) 的总和,判断有多少种方案能够使得总和为 \(0\)。
但如果按照这个方法直接处理整个数组,搜索的时间复杂度可能会达到 \(O(m^n)\)。所以本题将原区间 \([1, n]\) 分为 \([1, \dfrac n 2]\) 和 \([\dfrac n 2 + 1 , n]\) 这两段分别进行搜索,每段的时间复杂度均为 \(O(m^{\frac n 2})\),再借助双指针对剩下的答案进行求解。
T18-1
删除第 \(51\) 行的
std::sort(ans2.begin(), ans2.end());
后,代码输出的结果不会受到影响。
错误。
如果数组不进行排序,下面的双指针过程就无法保证 ans1[las] + ans2[i]
的值随着 \(i\) 变小而变小,可能会导致部分合法的答案因为双指针已经略过对应位置而无法统计到答案内。
T18-2
假设计算过程中不发生溢出,函数
mpow(x, k)
的功能是求出 \(x^k\)。
正确。
T18-3
代码中第 \(39\) 行到第 \(50\) 行的目的是为了将
ans1
数组进行“去重”操作。
正确。
这段在对 ans1
进行去重的同时也统计了每种数字出现的次数。
T18-4
当输入为 “
3 15 1 2 -1 2 1 2
” 时,输出结果为( )。
- A. \(4\)
- B. \(8\)
- C. \(0\)
- D. \(10\)
选 B.
本题建议理解完程序含义,做完 T18-6 后再回来做。
当 \(n=3, k = 15\) 时,此时需要借助深搜确定三个下标位置对应的 \(x_1, x_2, x_3\),其中 \(x_i \in [1, 15]\),并使得:
将该数据代入,即:
发现形如勾股定理。答案即边长在 \(15\) 以内的直角三角形数量,共 \(4\) 种:
3 4 5
6 8 10
9 12 15
5 12 13
但因为 \(x_1\) 与 \(x_3\) 能调换,故结果共 \(4 \times 2 = 8\) 种。
T18-5
记程序结束前 \(p\) 数组元素的最大值为 \(P\),则该代码的时间复杂度是( )。
- A. \(O(n)\)
- B. \(O(m^n\log m^n)\)
- C. \(O(m^{\frac n 2}\log m^{\frac n 2})\)
- D. \(O(m^{\frac n 2}(\log m^{\frac n 2} + \log P))\)
选 D.
首先因为搜索折半了,两次搜索每次都只会搜 \(\frac n 2\) 个下标位置,每个下标位置的循环都是 \(O(m)\) 的,且每次搜到下一层时调用快速幂的复杂度为 \(O(\log P)\),故搜索复杂度为 \(O(m^{\frac n 2} \log P)\)。
每一遍搜索能得到的答案数量级(即 cnt
变量值)为 \(m^{\frac n 2}\),main
函数内排序的时间复杂度即 \(O(m^{\frac n 2}) \log m^{\frac n 2}\)。
综上,总时间复杂度为 \(O(m^{\frac n 2}(\log m^{\frac n 2} + \log P))\)。
T18-6
本题所求出的是( )。
- 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.满足 \(x_i \in [0, m]\) 的整数方程 \(\sum_{i=1}^n k_i\cdot x_i^{p_i} = 0\) 的解的数量
- D.满足 \(x_i \in [1, m]\) 的整数方程 \(\sum_{i=1}^n k_i\cdot x_i^{p_i} = 0\) 的解的数量
选 D.
详见上面的题意分析,注意 \(x_i\) 取值的循环是从 \(1\) 开始的。
T19 程序填空第一题(特殊最短路)
#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 usedbool 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({ ③ , v , used});}if(used == 0){if( ④ < d[v][1]){d[v][1] = ④ ;pq.push({d[v][1], v, 1});}}}}cout << ⑤ << endl;return 0;
}
给定一个含 \(N\) 个点、\(M\) 条边的带权无向图,边权非负。起点为 \(S\),终点为 \(T\)。对于一条 \(S\) 到 \(T\) 的路径,可以在整条路径中,至多选择一条边作为“免费边”:当第一次经过这条被选中的边时,费用视为 \(0\);如果之后再次经过该边,则仍按其原始权重计费。点和边均允许重复经过。求从 \(S\) 到 \(T\) 的最小总费用。
一道图论最短路的经典变型,做法有很多,比如建立分层图等。
本题采用的方法是直接把“是否已使用过一条免费边”存储到描述最短路的状态内部,用 d[i][j=0/1]
来表示从起点 \(S\) 出发走到点 \(i\),且状态为 \(j\)(\(j=0\) 表示还没使用免费边,\(j=1\) 表示已经使用过一条免费边)时的最短路长度。
然后借助 Dijkstra 的堆优化方法求解最短路。
T19-1
选 A. \(0\)
观察题意可知这个位置要填的是起点的 used_freebie
这个属性,一开始还没用过免费边,所以置 \(0\) 即可。
T19-2
选 B. d[u][used]
这里是 Dijkstra 堆优化的常见优化方法,当当前堆内取出的结构体与该点对应的实际最短路长度值不同时,说明在此之前该点的答案已经被其它点所松弛过,且答案更优秀,所以此时就不需要再重新松弛该点了。
当前点即取出的 u
,免费边使用的状态即 used
。
T19-3
选 B. d[v][used]
这一次借助 u
点松弛 v
点时如果不考虑使用免费边(或是已经使用过免费边的话),那么免费边的使用情况直接继承自 used
变量的值即可。
这里考虑的就是从 u
出发经过权值为 w
的边走到点 v
,故选择松弛的状态即 d[v][used]
。
T19-4
选 C. d[u][0]
在此之前已经判断了 used == 0
,说明这里就是在考虑经过一次免费边,也就是此时把从 u
到 v
的这条边当作免费边使用。
使用免费边前的状态就是站在 u
点上,即 d[u][0]
;使用免费边后的状态就是站在 v
点上,即 d[v][1]
。
所以这一步就是借助 d[u][0]
的答案来更新 d[v][1]
的答案。
T19-5
选 C. min(d[t][0], d[t][1])
虽然本题已保证边权非负,如果要找最短路长度,肯定是选择某条边成为免费边是最优秀的,答案看上去 d[t][1]
很对。
但是如果遇到一些特殊情况,即 \(S=T\) 且所有边的边权均为正整数时,此时如果取 d[t][1]
作为答案,肯定是要走到其它点上之后再走回当前点,这样就要经过两条边,即使选择某条便为免费边,也会导致边权成为一个正整数。但很明显这种情况下答案就是 \(0\),所以还是得在使用与不使用两种情况下选择最小值输出。
T20 程序填空第二题(最优测试)
#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 ( std:: ② );}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;
}
本题背景比较偏向于应用工程。
就是说目前有 \(n\) 条生产线分别编号为 \(0 \sim n-1\),其中只有一条生产线有问题,需要找出有问题的生产线是哪条。判断的方法是每次选择一部分生产线的产品将其合并起来发送给客户,如果产品被退货了就说明选择的这部分产品包含了那条有问题的生产线。
但现在最多只能够接受 \(k\) 次退货,在此限制下需要设计出一套测试方案,每次测试你可以给定一部分生产线,text_subset
函数会辅助检查这部分生产线中是否存在有问题的生产线,回答 \(0/1\) 表示 没有/有。
现在你的首要目的是确定测试的最少次数 \(w\),使得在 \(w\) 次测试以内,你的这套方案能通过每一种测试结果来唯一确定有问题的生产线是哪条。
- 这一步,程序的做法是先借助循环从小到大枚举 \(w\),其中 \(w\) 就是测试次数,然后求出在 \(w\) 次测试以内,且退货次数不超过 \(k\) 次的前提下,能够唯一确定问题的生产线数量最多有多少。
- 如果我们用 \(0\) 表示没有退货,用 \(1\) 表示有退货,那就可以设计出一个长度为 \(w\) 的 \(0/1\) 字符串,只要保证字符串内 \(1\) 的个数不超过 \(k\),就能够用能构造出的每一种字符串来唯一确定一条有问题的生产线。
- 问题就变成了长度为 \(w\) 且 \(1\) 的个数 \(\le k\) 的 \(0/1\) 字符串有多少种,可以借助组合数进行求解,枚举 \(t = 0 \sim \min(w, k)\) 表示 \(1\) 的个数,长度为 \(w\) 且拥有 \(t\) 个 \(1\) 的字符串共 \(C_w^k\) 种,即总方案数为 \(\sum\limits_{t=0}^{\min(w, k)} C_w^t\)。这也就是
count_patterns(w, k)
函数所求解的内容。
在找到最小长度 \(w\) 后,我们只需要任意构造出 \(n\) 个长度为 \(w\) 且 \(1\) 的个数不超过 \(k\) 的字符串,作为唯一确定每条有问题的生产线的方案即可。这里我们用一个二维数组 code[i][j]
来表示找到的每种字符串,\(0 \le i \lt n, 0 \le j \lt w\)。
- 上面对于
code[i][j]
的解释是如果code[i][j] == 0
则说明“如果 \(i\) 是有问题的生产线,那么第 \(j\) 次测试的结果是不会退货”,如果code[i][j] == 1
则说明“如果 \(i\) 是有问题的生产线,那么第 \(j\) 次测试的结果是会退货” - 当然这样也就说明可以用
code[i][j]
的值来表示第 \(j\) 次测试是否包含了第 \(i\) 条生产线,如果为 \(1\) 则包含。
回到代码的“第 2 步:生成测试方案”,接下来的 for 循环枚举了要构造的字符串中 \(1\) 的个数为 ones
,然后构造了一个长度为 \(w\) 的 \(0/1\) 数组 bits
,将其前 ones
个位置置为 \(1\),其余位置均置 \(0\)。接下来根据寻找全排列的函数,把每一种恰好包含 ones
个 \(1\) 的字符串全找出来,存储在 code
这个二维数组内。
然后构建 plan
二维数组,这里就是要传递到接口 test_subset
的参数,表示每次测试我们要把哪些生产线放在一次测试。根据上面对 code
数组的描述,code[i][j]
表示的就是第 \(j\) 次测试中是否包含生产线 \(i\),所以这里可以通过 code
数组目前的数值来直接构造 plan
数组。
来到“第 4 步:结果解码”,因为 test_subset
接口返回的是一个数值,我们需要将其转为二进制后处理成 \(0/1\) 数组,所以这里就是在转码。
最后一个循环,就是根据测试的结果 sig_bits
数组,回到上面的 code
数组一一比对,检查哪个生产线出问题时结果与当前的 sig_bits
数组完全对应。找到对应的状态后即可直接返回生产线编号,完成本题。
T20-1
选 B. count_patterns(w, k) < n
(可以用一个奇怪的想法排除 A/D,如果这里不使用 count_pattern
函数的话,这个函数写着就没用了。)
根据上面的解释,这里是在求长度为 \(w\) 且 \(1\) 的个数不超过 \(k\) 的 \(0/1\) 串数量,然后判断总数是否达到所要求的 \(n\) 种。如果达到了,则此时的 \(w\) 就是能唯一确定每条生产线是否有问题的最少测试次数。
T20-2
选 B. prev_permutation(bits.begin(), bits.end())
注意这里 bits
数组的初始值是前面 ones
个 \(1\),后面 w - ones
个 \(0\),这是字典序最大的情况。
为了找到所有由 ones
个 \(1\) 组成的长度为 w
的字符串,这里要使用的是 prev_permutation
往前找字典序较小的所有排列才可以。排除 A/C。
对于 D,只对前 ones
个 \(1\) 找所有排列和没找一样,排除。
T20-3
选 D. code[j][i] == 1
根据上文(第 2 步),code
数组第一维表示每条生产线,最大值与 \(n\) 有关;第二维表示每种测试方案,最大值与 \(w\) 有关。
回到该空位,i
和 j
两层循环分别与 测试方案 和 生产线 有关,最大值分别与 w
和 n
有关,所以应当按 code[j][i]
的顺序使用。
至于为什么要用 code
数组,因为 code[j][i]
表示的就是第 \(i\) 次测试是否包含第 \(j\) 条生产线,与条件内的 plan[i].push_back(j)
对应。
T20-4
选 A. (signature >> i) & 1
二进制转为普通 \(0/1\) 数组,按顺序取出每一位上是 \(0\) 还是 \(1\) 即可。
判断二进制第 \(i\) 位上是否为 \(1\),可以采取右移的方法将右边无关的数先去除,再判断移动后的最低位,即 (signature >> i) & 1
。
当然也可以采取左移的方法,将 \(1\) 往左移动到想要判断的那一位上,再借助按位与进行判断,即 signature & (1 << i)
。
T20-5
选 B. code[j] == sig_bits
这里就是根据测试的结果 sig_bits
数组,回到上面的 code
数组一一比对,检查哪个生产线出问题时结果与当前的 sig_bits
数组完全对应。直接比较 code[j]
整个数组是否和 sig_bits
完全相等即可。