在 Linux 中,显示当前工作目录的路径,应使用哪个命令?
🧭 逐项辨析
pwd(print working directory)= 显示当前目录的完整路径。cd 切换目录、ls 列目录内容、echo 输出文本。
试卷:CSP 2024 提高级(Senior)第一轮 · 单项选择 + 阅读程序 + 完善程序
用法:先独立作答,再点「显示解析」核对分步推理;右上角可切换 浅色/深色 配色。
CSP-S 提高级第一轮,难度较高。本卷可归为 8 大板块,阅读/完善程序是拉开分差的重点。
Q1 Linux 命令、Q3 递归导致栈溢出。
Q2 无序找最大 O(n)、Q10 哈希最坏 O(n)、Q11 完全二叉树结点数。
Q5 队列 FIFO、Q8 二分前提有序。
Q7 欧拉图性质、Q15 最小割计数。
Q4 排列、Q6 递推、Q12 完全图中 4 元环、Q13 数位和、Q14 相邻交换次数。
Q9 乘法逆元(扩展欧几里得)。
位运算 + 快排(带深度)、子序列价值 DP / 折半、二叉树哈希(中序合并 + 筛法)。
二分答案求第 K 小的和、分层图 Dijkstra 求严格次短路。
在 Linux 中,显示当前工作目录的路径,应使用哪个命令?
pwd(print working directory)= 显示当前目录的完整路径。cd 切换目录、ls 列目录内容、echo 输出文本。
长度为 n、元素互不相同且无序的数组,找最大元素的时间复杂度是?
数组无序,没有任何捷径,必须把 n 个元素都扫一遍才能确定最大值,所以是 O(n)。(若已排序则 O(1) 取末位,但本题无序。)
以下哪个函数调用会造成栈溢出?
baz 无条件调用自身,没有递归出口,每层还开一个大数组 a[1000],调用栈不断加深直至溢出崩溃。其余三个都是普通有限调用。
10 名选手,前三名颁金、银、铜牌(不并列、每人最多一枚),共多少种颁奖方式?
金银铜有顺序之分,是排列:P(10,3) = 10×9×8 = 720。
(若奖牌无区别,则是组合 C(10,3)=120,那是 A;本题三牌不同,用排列。)
最适合实现先进先出(FIFO)的数据结构是?
队列从队尾进、队头出,先进先出(FIFO)。栈是后进先出(LIFO)。
f(1)=1,n≥2 时 f(n)=f(n−1)+f(⌊n/2⌋),求 f(4)。
| n | f(n)=f(n−1)+f(⌊n/2⌋) | 值 |
|---|---|---|
| 1 | — | 1 |
| 2 | f(1)+f(1)=1+1 | 2 |
| 3 | f(2)+f(1)=2+1 | 3 |
| 4 | f(3)+f(2)=3+2 | 5 |
注意 ⌊4/2⌋=2、⌊3/2⌋=1。
n 个顶点的无向图是欧拉图,下列描述中哪项不一定正确?
无向欧拉图 ⟺ 连通 且 所有顶点度数为偶数 ⟺ 存在欧拉回路。所以 A、B、C 都一定成立。
边数奇偶没有限制:例如三角形 C₃ 是欧拉图,边数 3(奇);正方形 C₄ 也是欧拉图,边数 4(偶)。所以 D「边数是奇数」不一定。
二分查找过程中,以下哪个条件必须满足?
二分查找靠「与中间元素比较即可排除一半」,前提是数组有序(单调)。长度不必是 2 的幂,元素也不必是整数(浮点、字符串只要可比较即可)。
计算 n 在模 m 意义下的乘法逆元,下列哪种算法最为适合(通用)?
逆元存在的条件是 gcd(n,m)=1。扩展欧几里得求 n·x + m·y = 1 的 x 即逆元,对任意互质的模 m 都适用,O(log m)。
快速幂法(费马小定理 nm−2)只在 m 为质数时才成立;暴力 O(m) 太慢;线性筛法用于一次性预处理 1~n 的逆元。最通用的是 B。
哈希表用开放地址法解决冲突,装载因子 α,最坏情况下查找一个元素的时间复杂度为?
最坏情况下所有 n 个元素挤成一长串探测链(聚集),查找要走完整条链 → O(n)。
选项 C 的 O(1/(1−α)) 是平均查找长度,不是最坏。题问最坏,故选 D。
一棵 h 层的完全二叉树,最多包含多少个结点?
第 k 层最多 2k−1 个结点,h 层全满时总数 = 20+21+…+2h−1 = 2h − 1。
10 个顶点的完全图,有多少个长度为 4 的环?
① 从 10 个点中选 4 个:C(10,4) = 210。
② 4 个点能组成的不同 4 元环数:4 个点的圆排列 = (4−1)!/2 = 3 个。
210 × 3 = 630。
f(n) 为 n 各位数字之和,求使 f(f(x))=10 的最小自然数 x。
要 f(f(x))=10:先让 y=f(x) 满足 f(y)=10,最小的这种 y 是 19(1+9=10)。
再求 f(x)=19 的最小 x:数位和为 19 的最小数 —— 两位最多 9+9=18 不够,需三位,首位尽量小 → 1,9,9 → 199(1+9+9=19)。
验证:f(199)=19,f(19)=10 ✓。小于 199 的数数位和最大才 18,达不到 19。
长度 n 的 01 串含 k 个 1,每次交换相邻两字符。最坏情况下把 k 个 1 全移到最右边所需的交换次数是?
相邻交换次数 = 需要消除的「1 在 0 左边」的逆序对数。最坏情况是 k 个 1 全挤在最左、n−k 个 0 全在右。
此时每个 1 都要越过全部 (n−k) 个 0,共 k×(n−k) 次相邻交换。
下图是 7 个顶点的有向图。删除最少的边使 1 到 7 没有可行路径,问这样的「最少删边集合」共有多少种?
边集:1→2, 1→3, 1→4, 2→4, 2→5, 3→4, 4→6, 6→5, 5→7, 6→7。
最小割 = 最大边不相交路径数。点 7 只有两条入边(5→7、6→7),故最多 2 条边不相交路径(如 1→2→5→7 与 1→4→6→7)→ 最少删 2 条边。
枚举所有「删 2 条边即断开」的集合,恰有 4 种:
| # | 删除的两条边 | 为何断开 |
|---|---|---|
| 1 | {4→6, 1→2} | 4→6 断掉除 1→2→5→7 外所有路;再断 1→2→5→7 的起点 |
| 2 | {4→6, 2→5} | 同理,2→5 断掉 1→2→5→7 |
| 3 | {4→6, 5→7} | 5→7 断掉 1→2→5→7 |
| 4 | {5→7, 6→7} | 删掉 7 的全部入边,7 孤立 |
📄 阅读程序(一)· 位运算 + 带深度限制的快速排序
logic 是某种位运算;generate 用它造数组;recursion 是带「深度」参数的快速排序。
int logic(int x, int y){ return (x & y) ^ ((x ^ y) | (~x & y)); }
void generate(int a, int b, int *c){
for (int i = 0; i < b; i++) c[i] = logic(a, i) % (b + 1);
}
void recursion(int depth, int *arr, int size){ // 快排,depth 限制递归层数
if (depth <= 0 || size <= 1) return;
int pivot = arr[0]; int i = 0, j = size - 1;
while (i <= j){
while (arr[i] < pivot) i++;
while (arr[j] > pivot) j--;
if (i <= j){ swap(arr[i], arr[j]); i++; j--; }
}
recursion(depth-1, arr, j+1);
recursion(depth-1, arr+i, size-i);
}
// main: 读 a b d; generate(a,b,c); recursion(d,c,b); 输出 c[0..b-1]
当 1000 ≥ d ≥ b 时,输出的序列是有序的。
recursion 是快速排序,depth 限制递归层数。快排递归深度最坏为 size(这里 = b)。当 d ≥ b 时,深度限制不会提前截断,能完全排序,所以输出有序。
当输入 5 5 1 时,输出为 1 1 5 5 5。
5 1 1 5 5)generate:c[i]=(5|i)%6 → c = [5,5,1,1,5]。
recursion(d=1, …) 只允许一层划分(子调用 depth=0 立即返回,不再排序)。一趟以 pivot=5 划分后数组变成 5 1 1 5 5,并未完全排好。
所以输出是 5 1 1 5 5,不是 1 1 5 5 5。
假设数组 c 长度无限制,该程序所实现的算法的时间复杂度是 O(b)。
generate 是 O(b),但 recursion 是快速排序,平均 O(b log b)、最坏 O(b²),绝不是 O(b)。所以整体不是 O(b)。
函数 logic(int x, int y) 的功能是?
对单个 bit (x,y),逐一代入 (x&y)^((x^y)|(~x&y)):
| x | y | 结果 | x|y |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 0 | 1 | 1 | 1 |
| 1 | 0 | 1 | 1 |
| 1 | 1 | 1 | 1 |
四行都与 x|y 一致 → logic 就是按位或。
当输入为 10 100 100 时,输出的第 100 个数是?
c[i]=(10|i)%101,i=0..99。d≥b 所以输出已排序,第 100 个(末位)= 这 100 个数的最大值。
逐个比较 (10|i)%101 的最大值为 95(在 i=95 等处,10|95=95,95%101=95)。
x % (b+1) 让值落在 0~b,本题最大有效值 95。📄 阅读程序(二)· 子序列「数值之和」(DP 与折半暴力)
s 是长 n 的 01 串。solve2() 暴力枚举所有「长度 ≤ m 的子序列」,把每个子序列按二进制读成数值并求和。solve() 用 dp[mask] 统计「数值等于 mask 的子序列个数」再求 Σ mask·dp[mask],得到同样的和(mod 998244353)。
const int P = 998244353;
int dp[1<<M];
int solve(){ // dp 法:O(n·2^m)
dp[0] = 1;
for (int i = 0; i < n; ++i)
for (int j = (1<<(m-1))-1; j >= 0; --j){
int k = (j<<1) | (s[i]-'0');
if (j != 0 || s[i]=='1') dp[k] = (dp[k] + dp[j]) % P; // 避免前导 0
}
int ans = 0;
for (int i = 0; i < (1<<m); ++i) ans = (ans + 1ll*i*dp[i]) % P;
return ans;
}
int solve2(){ // 暴力枚举子集:O(2^n·n),仅 n≤20 用
int ans = 0;
for (int i = 0; i < (1<<n); ++i){
int cnt=0, num=0;
for (int j=0;j<n;++j) if (i&(1<<j)){ num=num*2+(s[j]-'0'); cnt++; }
if (cnt <= m) (ans += num) %= P;
}
return ans;
}
// main: 读 n m s; 若 n≤20 先输出 solve2(),再输出 solve()
假设 dp 长度无限制,solve() 的时间复杂度是 O(n×2m)。
外层 i 跑 n 次,内层 j 从 2m−1−1 到 0 跑约 2m−1 次。乘起来是 O(n·2m−1) = O(n·2m)。最后求和 O(2m) 更小。
输入 11 2 10000000001 时,程序输出两个数 32 和 23。
n=11, m=2, s="10000000001"。n≤20 所以先输出 solve2()=32,再输出 solve()=23。两值不同是正常的——它们统计口径有别(见 17-选3)。
在 n ≤ 10 时,solve() 的返回值始终小于 410。
子序列数值最多约 2m,子序列个数最多 2n,和的量级约 2n·2m ≤ 2n+m。在 n≤10 的合理范围内远小于 410=220。实际枚举所有输入,最大值仅 58025 < 1048576。
当 n=10 且 m=10 时,有多少种输入(01 串)使两行结果完全一致?
m=10≥n,约束 cnt≤m 永远成立,solve2 = 所有子序列数值之和。solve 用 dp 但只保留 m 位、且避免前导 0 的计数方式与 solve2 在含前导 0 的子序列上略有差异。枚举 1024 个串,恰有 11 个使两者相等(如全 0 串及若干特殊结构)。
当 n ≤ 6 时,solve() 的最大可能返回值为?
枚举 n≤6 的所有输入,最大返回值出现在 n=6, m=6, s=111111,此时所有非空子序列都是全 1 的二进制数,求和 = 665。
若 n=8, m=8,solve 与 solve2 返回值的最大可能差值为?
solve2 会把含前导 0 的子序列也按数值(去掉前导 0 后的值)计入;solve 的 dp 对前导 0 有特殊处理(j!=0 || s[i]=='1'),两者在以 0 开头的子序列上不同。枚举 n=8,m=8 全部 256 个串,最大差值 2059,出现在 01111111。
📄 阅读程序(三)· 二叉树哈希(中序合并 + 埃氏筛)
把下标 1..n 看成二叉树(i 的左右孩子是 2i、2i+1)。p[i] 标记 i 是否为素数,H 是双模哈希。h[i]=h[2i]+h[i]+h[2i+1](左、根、右)自底向上合并,输出 h[1].h1 和不同哈希的个数。
struct H { int h1,h2,l; ...
H operator+(const H& b) const { // 拼接两段哈希
H r; r.l=l+b.l;
r.h1=(1ll*h1*p1[b.l]+b.h1)%P1;
r.h2=(1ll*h2*p2[b.l]+b.h2)%P2; return r; }
} h[maxn];
void init(){ /* 埃氏筛求素数 p[] + 预处理幂 p1[],p2[] */ }
int solve(){
for (int i=n;i;--i){
h[i]=H(p[i]);
if (2*i+1<=n) h[i]=h[2*i]+h[i]+h[2*i+1]; // 左 + 根 + 右 = 中序
else if (2*i<=n) h[i]=h[2*i]+h[i];
}
cout<<h[1].h1<<endl; // 第一行
sort(h+1,h+n+1);
return unique(h+1,h+n+1)-(h+1); // 第二行:不同哈希个数
}
若能自动把 maxn 改为 n+1,所实现算法的时间复杂度是 O(n log n)。
埃氏筛 O(n log log n);自底向上算 h[] 共 n 个结点,每个合并 O(1)(用预处理幂)→ O(n);最后 sort 是 O(n log n)。三者取最大 = O(n log n)。
时间开销的瓶颈是 init() 函数。
init() 的埃氏筛是 O(n log log n),而 solve() 里的 sort 是 O(n log n),后者更大。所以真正的瓶颈在 solve 的排序,不是 init。
若修改常数 B1 或 K1 的值,程序可能会输出不同的结果。
第一行输出的是 h[1].h1,它直接由哈希基数 B1、偏移 K1 算出。改变它们 → h1 的值改变 → 第一行输出不同。所以「可能输出不同结果」成立。
solve() 中 h[] 的合并顺序可以看作是?
h[i] = h[2i] + h[i] + h[2i+1] = 左子树 + 根 + 右子树。「左—根—右」正是中序遍历的顺序。
输入 10,输出的第一行是?
n=10 时,B1=2、K1=0、P1=998244353,模 1 取 b(素数为 1、非素数为 0),按中序合并算 h[1].h1。各结点 1..10 的素性序列经中序拼接、用基数 2 的哈希计算,结果 = 83。
(B1=2、K1=0 时 h1 恰好是「中序素性序列」看作二进制数的值,所以是个不大的数。)
输入 16,输出的第二行是?
第二行统计 h[1..n] 中本质不同的哈希值个数(排序后去重)。n=16 时,16 个子树里有些结构相同(哈希相同),去重后剩 10 个不同值。
🧩 完善程序(一)· 求两序列两两之和的第 K 小
单调不降序列 A、B 各取一数相加得 N² 个和,求第 K 小。做法:二分答案 sum,用 get_rank(sum)=「和 ≤ sum 的对数」判断;get_rank 内对每个 a[i] 用 upper_bound 统计 b 中 ≤ sum−a[i] 的个数。补全 ①~⑤。
int* upper_bound(int *a, int *an, int ai){ // 返回第一个 > ai 的位置
int l = 0, r = __①__;
while (l < r){ int mid=(l+r)>>1;
if (__②__) r = mid; else l = mid + 1; }
return __③__;
}
long long get_rank(int sum){
long long rank = 0;
for (int i=0;i<n;++i) rank += upper_bound(b, b+n, sum-a[i]) - b;
return rank; // 和 ≤ sum 的对数
}
int solve(){ // 二分最小的 sum 使 rank(sum) ≥ k
int l = 0, r = __④__;
while (l < r){ int mid = ((long long)l+r)>>1;
if (__⑤__) l = mid + 1; else r = mid; }
return l;
}
① 处应填?
upper_bound 在 [0, 长度] 上二分下标。数组长度 = 末指针 − 首指针 = an - a。取 an-a-1 会漏掉最后一个位置。
② 处应填?
upper_bound 要返回第一个 > ai 的位置。当 a[mid] > ai 时答案在左半(含 mid),r=mid;否则 l=mid+1。所以条件是 a[mid] > ai。
(若写 >= 就成了 lower_bound,统计会少算等于 ai 的元素,导致「≤ sum」的计数出错。)
③ 处应填?
循环结束时 l 就是「第一个 > ai 的下标」,对应指针 a+l。get_rank 用 (a+l)-b 得到「b 中 ≤ sum−a[i] 的个数」。
a+l,与外部「指针相减得计数」配套。④ 处(二分答案的右端)应填?
A、B 单调不降,最大的和是 a[n-1]+b[n-1](末元素之和)。二分答案区间设为 [0, a[n-1]+b[n-1]] 既正确又紧凑。
B 越界(下标 n);C、D 用 maxn 虽然也是上界但偏大且不贴切。
⑤ 处应填?
get_rank(sum) = 和 ≤ sum 的对数,随 sum 单调不减。第 K 小的和 = 最小的 sum 使 get_rank(sum) ≥ k。
当 get_rank(mid) < k 说明 mid 太小,答案在右侧 → l=mid+1;否则 r=mid。所以条件填 get_rank(mid) < k。
l=mid+1 / r=mid 收敛到下确界。🧩 完善程序(二)· 严格次短路(分层图 Dijkstra)
把每个点拆成两层:下标 v(0~n−1)存最短路,下标 n+v 存严格次短路。用 Dijkstra 同时维护。upd(a,b,d,q) 尝试用 d 更新 dis[b],更新成功时若 b 是最短层(b<n),把被顶替的旧最短路下放到次短层。补全 ①~⑤。
const int inf = 522133279; // = 0x1f1f1f1f bool upd(int a,int b,int d, pq& q){ if (d >= dis[b]) return false; if (b < n) __①__; // 旧最短路下放到次短层 q.push(__②__); // 大根堆存 -d 模拟小根堆 dis[b]=d; pre[b]=a; return true; } void solve(){ pq q; q.push(make_pair(0,s)); memset(dis, __③__, sizeof(dis)); // 填 inf memset(pre,-1,sizeof(pre)); dis2=dis+n; pre2=pre+n; dis[s]=0; while(!q.empty()){ int aa=q.top().second; q.pop(); if (vis[aa]) continue; vis[aa]=true; int a=aa%n; for (int e=head[a]; e; e=nxt[e]){ int b=to[e], c=w[e]; if (aa < n){ if (!upd(a,b,dis[a]+c,q)) __④__; // 最短层更新失败 → 尝试次短层 } else upd(n+a, n+b, dis2[a]+c, q); } } } void out(int a){ // 递归打印次短路方案 if (a != s){ if (a<n) out(pre[a]); else out(__⑤__); } printf("%d%c", a%n+1, " \n"[a==n+t]); }
① 处应填?
此刻要把 dis[b] 改成更小的 d(找到了更短的路)。原来的 dis[b](旧最短路,前驱 pre[b])就成了 b 的次短路候选,应推进次短层:upd(pre[b], n+b, dis[b], q)。
注意要用旧值 dis[b] 和旧前驱 pre[b](B 用了新值 d、新前驱 a,是错的)。
② 处应填?
priority_queue 默认大根堆。要让「距离小的先出队」,就 push -d(取负),并按 first 排序、second 存节点:make_pair(-d, b)。
取出时 q.top().second 是节点,距离用其绝对值。
③ 处(memset 填充 inf)应填?
memset 把每个字节设成同一值。要让每个 int 等于 inf = 522133279 = 0x1f1f1f1f,就用 0x1f(四个字节都填 0x1f)。
对照:0x3f → 0x3f3f3f3f ≈ 1.06×10⁹(常用),但本题 inf 恰好是 0x1f1f1f1f,故填 0x1f。
memset(a,0x3f,...) 让每个 int ≈ 1.06×10⁹(常用 INF)。本题 inf=0x1f1f1f1f 对应字节 0x1f。memset 只能按字节填,不能填任意整数。④ 处(最短层更新失败后)应填?
当前在最短层(aa<n),用 a 的最短距离 dis[a]+c 去更新 b 的最短路失败(说明这条路不比已知最短更短)。那它可能是 b 的次短路,于是用同样的 dis[a]+c 更新次短层 n+b:upd(a, n+b, dis[a]+c, q)。
B、C 用了 dis2[a](次短层距离)不对,这里 a 还在最短层。
⑤ 处(次短层节点回溯前驱)应填?
pre2 = pre+n,所以次短层节点(a≥n,真实点是 a%n)的前驱是 pre2[a%n] = pre[n + a%n] = pre[a]。out 递归到它即可正确回溯次短路方案。
C 写 pre2[a] 会下标越界;B 取的是最短层前驱,错层。