CSP-S 2026 初赛试题解析(第一部分:选择题(8-15))精讲



第 8 题:拓扑排序------四个小朋友怎样排队?

考点

有向无环图、拓扑排序

题目给出一个有向无环图:

  • 顶点:{1,2,3,4}

  • 边:{(1,2),(1,3)}

  • 顶点 4 与任何顶点都不相邻。

问:这个图有多少种不同的拓扑序?

  • A. 12

  • B. 8

  • C. 4

  • D. 6


第一步:把图画出来

边 (1, 2) 表示 1 指向 2,边 (1, 3) 表示 1 指向 3。


第二步:理解什么叫拓扑序

拓扑序就是给图中的顶点排队,并且满足所有箭头的要求。

例如:

1, 2, 3, 4

是合法的,因为 1 在 2、3 前面。

但:

2, 1, 3, 4

不合法,因为 1 指向 2,1 必须排在 2 前面。

因此,我们只需要记住:

  • 1 必须在 2 前面。

  • 1 必须在 3 前面。

  • 2 和 3 之间没有先后限制。

  • 4 可以排在任何位置。


第三步:先决定 1 的位置

因为 1 必须排在 2、3 前面,所以 1 不可能排在最后,也不可能排在 2 或 3 后面。

我们分情况讨论。

情况一:1 排在第 1 位。

剩下的 2、3、4 可以任意排列:

3! = 6

种。

情况二:1 排在第 2 位。

那么 2 和 3 都必须排在 1 后面。

第 1 位只能由 4 占据,因此只剩下 2、3、4 的排列方式受到限制,具体只有:

4, 1, 2, 3

4, 1, 3, 2

共 2 种。

所以总数为:

6+2 = 8

正确答案:B(8)。


小结

拓扑排序不是要求所有点都按编号排列,而是要求所有箭头的先后关系都成立。

遇到这种题,先找出"谁必须在谁前面",再分类计数,通常比直接枚举全部排列更清晰。



第 9 题:分治算法------把大任务拆成两个小任务

考点

分治算法、时间复杂度

题目给出:

T(n) = T(n/3) + T(2n/3) + Θ(n)

并且:

T(1) = O(1)

问:T(n) 的渐进时间复杂度是什么?

  • A. Θ(nlog⁡n)

  • B. Θ(n^2)

  • C. Θ(n^{1.5})

  • D. Θ(n)


第一步:看懂题目要求

想象你要完成一个大任务,规模是 n。

你把它分成两份:

  • 第一份规模是 n / 3。

  • 第二份规模是 2n / 3。

除了完成这两个小任务,你还要花费 Θ(n) 的时间来分配任务、整理结果等。

因此:

T(n)=T(n/3)+T(2n/3)+Θ(n)

这里的 Θ(n) 可以理解为:额外工作量与 n 成正比。


第二步:画出分治的任务树


一点特别重要:

第一层的工作量大约是 n,

第二层所有子任务的规模总和仍然约为 n。

继续分下去,每一层的总工作量都可以理解为约 n。

第三步:一共有多少层?

每次沿着最大的子任务继续走,规模会变成原来的 2/3。

经过 k层后,规模大约为:

当它下降到 1 左右时,递归就结束了。

因此:

≈1

整理可得:

换成渐进复杂度的写法,就是:

k=Θ(log⁡n)


第四步:总工作量是多少?

每一层的总工作量约为 n,而层数约为 log⁡n。

所以:

T(n)=Θ(n)×Θ(log⁡n)

最终得到:

正确答案:A。


小结

分治题可以先问两个问题:

  1. 每一层的总工作量是多少?

  2. 一共需要多少层?

这道题每层约为 n,一共约有 logn层,所以得到 nlog⁡n。

注意:不能只看"分成了两个子问题",还必须看子问题的规模以及每层的总工作量。



第 10 题:树的直径与重心------树上最远的两座城市在哪里?

考点

树、直径、重心

题目给出一棵有 9 个结点的无根树,边如下:

(1,2),(1,3),(2,4),(2,5),(3,6),(6,7),(7,8),(5,9)

问:树的直径(按边数计算)和重心分别是什么?

  • A. 直径 6,重心为结点 3

  • B. 直径 7,重心为结点 2

  • C. 直径 8,重心为结点 1

  • D. 直径 7,重心为结点 1


第一步:把树画出来

根据题目中的边,可以画出下面的结构:


第二步:什么是树的直径?

树的直径就是:任意选择两个结点,它们之间的距离可能达到的最大值。

这里的距离按经过的边数计算,不是经过的结点数。

观察这棵树,可以发现最长的一条路径是:

9→5→2→1→3→6→7→8

我们数一数:

路径 经过的边数
9 → 5 1
5 → 2 2
2 → 1 3
1 → 3 4
3 → 6 5
6 → 7 6
7 → 8 7

所以,树的直径为 7。

注意:这条路径一共有 8 个结点,但只有 7 条边。题目明确要求按边数计算,因此答案不是 8。


第三步:什么是树的重心?

树的重心和几何中心不是一回事。

想象你要把一棵树形结构从某个结点切开。切掉这个结点后,树会分成若干个连通部分。

如果选择一个结点切掉后,剩下的所有部分中,最大的那一部分尽可能小,那么这个结点就是树的重心。

这棵树一共有 9 个结点。

我们分别检查结点 1 和结点 2。

如果删除结点 1:

  • 左侧部分:结点 2、4、5、9,共 4 个。

  • 右侧部分:结点 3、6、7、8,共 4 个。

最大部分有 4 个结点。


如果删除结点 2:

  • 结点 4 单独成为一部分,有 1 个结点。

  • 结点 5、9 成为一部分,有 2 个结点。

  • 另一侧的结点 1、3、6、7、8,共 5 个。

最大部分有 5 个结点。


删除结点 1 后,最大的部分只有 4 个结点;删除结点 2 后,最大的部分有 5 个结点。

因此,结点 1 更符合重心的定义。

正确答案:D(直径 7,重心为结点 1)。


小结

  • 树的直径:找最远的两个结点之间的最长路径。

  • 树的重心:删除后,最大连通部分尽可能小的结点。

这两个概念名字相似,但定义完全不同,不要混淆。



第 11 题:缩点后的有向无环图------最少需要添加几条边?

考点

强连通分量、缩点、有向无环图

题目说:一张有向图缩点后得到一个有向无环图(DAG),其中:

  • 一共有 6 个顶点。

  • 入度为 0 的顶点有 3 个。

  • 出度为 0 的顶点有 4 个。

问:为了使原图变成强连通图,至少需要添加多少条有向边?

  • A. 7

  • B. 6

  • C. 4

  • D. 3


第一步:认识入度和出度

想象每个结点是一座城市,箭头是单向道路。

  • 入度:有多少条箭头指向这个城市。

  • 出度:有多少条箭头从这个城市指向外面。

入度为 0 的结点,没有其他结点能直接沿箭头进入它。

出度为 0 的结点,没有箭头从它通向其他结点。

在有向无环图中,入度为 0 的点称为源点,出度为 0 的点称为汇点。


第二步:什么叫强连通?

强连通的意思是:

对于任意两个结点 u 和 v,都能从 u沿有向边到达 v,也能从 v 到达 u。

简单说,每个城市都必须能通过单向道路到达其他所有城市。


第三步:记住缩点后的经典结论

对于一个已经缩点、且包含至少两个顶点的有向无环图,最少需要添加的边数为:

其中:

  • S:入度为 0 的顶点数。

  • T:出度为 0 的顶点数。

为什么?

每个源点都需要获得进入它的路径,每个汇点都需要获得离开它的路径。通过合理连接这些源点和汇点,可以把整个图串成一个能够互相到达的环。

这条结论是有向图算法中的经典定理。


第四步:代入题目数字

题目给出:

S=3,T=4

因此:

max⁡(3,4) = 4

正确答案:C(4)。


小结

遇到这种题,如果题目明确说的是"缩点后得到的 DAG",就要想到强连通分量的经典结论。

需要注意:如果缩点后只剩一个顶点,图已经强连通,不需要添加边;本题有 6 个顶点,不属于这种特殊情况。



第 12 题:6 个结点能组成多少种不同的二叉树?

考点

二叉树形态、卡特兰数

题目问:含有 6 个结点的不同形态二叉树有多少棵?

结点不带标号,并且区分左右子树。

  • A. 42

  • B. 429

  • C. 132

  • D. 720


第一步:什么叫不同形态?

我们先看只有 3 个结点的二叉树。

当结点数量增加时,可能的形态数量增长得非常快。

这道题有一个专门的数学工具:卡特兰数(Catalan number)。


第二步:为什么二叉树数量和卡特兰数有关?

假设一棵二叉树有 n个结点。

根结点占用 1 个结点,剩下 n−1 个结点要分给左子树和右子树。

如果左子树有 i 个结点,那么右子树就有:

n−1−i

个结点。

因此,可以得到递推式:

其中:

空树也算一种形态,这是递推公式的起点。

通项公式:


第三步:逐步计算

前几个卡特兰数是:

结点数 n 不同形态的数量
0 1
1 1
2 2
3 5
4 14
5 42
6 132

因此,6 个结点对应:

正确答案:C(132)。


第四步:怎样帮助记忆?

卡特兰数不只用在二叉树上,还出现在合法括号序列、某些入栈出栈方案计数等问题中。

如果考试直接问 6 个结点的不同形态二叉树数量,可以通过递推计算,或者记住前几个常用结果。

但要特别注意:题目说"不带标号,区分左右子树",这是形态计数,不是给 6 个结点分别编号后的二叉树数量。



第 13 题:字符串的前缀与后缀------找出首尾相同的小片段

考点

字符串、真前缀、真后缀

给定字符串:

复制代码
string S = "ababaabab";

问:所有既是真前缀又是真后缀的非空子串,其长度之和是多少?

  • A. 4

  • B. 6

  • C. 7

  • D. 5


第一步:什么是前缀?

字符串的前缀,就是从开头开始截取的一段。

例如:

复制代码
S = ababaabab

它的前缀包括:

复制代码
a
ab
aba
abab
ababa
...

什么叫真前缀?

不能是字符串本身。

因此,整个 ababaabab 不算真前缀。


第二步:什么是后缀?

后缀就是从字符串末尾开始截取的一段。

例如:

复制代码
S = ababaabab

它的后缀包括:

复制代码
b
ab
bab
abab
aabab
...

同样,真后缀也不能是整个字符串。


第三步:找出既是前缀又是后缀的片段

我们从较短的长度开始检查。

长度 前缀 后缀 是否相同
1 a b 否
2 ab ab 是
3 aba bab 否
4 abab abab 是
5 ababa aabab 否
6 ababaab baabab 否
7 ababaab? --- 重新按字符串位置判断

我们只需要正确找到所有相同的非空真前缀和真后缀。这里的字符串长度为 9:

复制代码
下标: 0 1 2 3 4 5 6 7 8
字符: a b a b a a b a b

前缀 ab 与后缀 ab 相同,长度为 2。

前缀 abab 与后缀 abab 相同,长度为 4。

其他长度的前缀与后缀都不相同。


第四步:求长度之和

2+4=6

正确答案:B(6)。


小结

这道题最稳妥的方法是:

  1. 明确字符串长度。

  2. 从长度 1 开始,分别取前缀和后缀。

  3. 比较两段字符串是否相同。

  4. 排除整个字符串本身。

  5. 把符合条件的长度相加。

注意:本题只要求真前缀和真后缀,不是要求所有相同子串,也不是要求最长公共子串。



第 14 题:归并排序统计逆序对------相等的数字算不算?

考点

归并排序、逆序对、比较运算符

题目给出归并排序统计逆序对的核心代码:

复制代码
if (a[i] <= a[j]) {
    tmp[k++] = a[i++];
}
else {
    tmp[k++] = a[j++];
    ans += mid - i + 1;
}

如果把判断条件中的:

复制代码
a[i] <= a[j]

改成:

复制代码
a[i] < a[j]

那么 ans 统计出的结果会变成什么?

  • A. 完全不变

  • B. 变为原来的两倍

  • C. 变为满足 i<j 且 ai ≥ aj 的数对个数

  • D. 变为原来的一半


第一步:什么是逆序对?

想象有一排数字:

复制代码
3  1  2

如果一个数字在前面,另一个数字在后面,但前面的数字更大,就形成一个逆序对。

例如:

  • (3, 1) 是逆序对,因为 3>1 。

  • (3, 2) 是逆序对,因为 3>2 。

  • (1, 2) 不是逆序对,因为 1<2 。

所以,这组数字一共有 2 个逆序对。

通常,逆序对的定义要求:

i < j, ai > aj

注意,这里是严格大于号 >,不是大于等于号 >=。


第二步:归并排序为什么能够统计逆序对?

归并排序会把数组分成左右两部分,并让左右两部分分别有序。

假设左边是:

复制代码
2  4  6

右边是:

复制代码
1  3  5

如果当前左边数字是 4,右边数字是 3,那么右边的 3 比左边的 4 小。

因为左边已经排好序,所以 4 后面的 6 也一定比 3 大。

因此,右边的 3 会与左边的 4、6 形成逆序对,一共 2 个。

代码:

复制代码
ans += mid - i + 1;

就是一次性统计左边从 i 到 mid 的所有数字与当前右边数字构成的逆序对数量。


第三步:原来的 <= 有什么作用?

原代码是:

复制代码
if (a[i] <= a[j]) {
    tmp[k++] = a[i++];
}
else {
    tmp[k++] = a[j++];
    ans += mid - i + 1;
}

如果两个数字相等,例如:

复制代码
左边:2
右边:2

那么:

复制代码
2 <= 2

结果为真。

程序进入 if 分支,取出左边的数字,不会增加 ans。

这很合理,因为相等的数字不构成通常定义下的逆序对。


第四步:改成 < 后会发生什么?

修改后:

复制代码
if (a[i] < a[j]) {
    tmp[k++] = a[i++];
}
else {
    tmp[k++] = a[j++];
    ans += mid - i + 1;
}

现在,如果左右两边的数字相等:

复制代码
2 < 2

结果为假!

程序会进入 else 分支,并执行:

复制代码
ans += mid - i + 1;

这意味着:相等的数字也会被计入统计结果。

因此,修改后的程序统计的不是严格逆序对,而是满足以下条件的数对:

i < j, ai ≥ aj

其中既包括前面的数字大于后面的数字,也包括两者相等的情况。

正确答案:C。


第五步:用一个例子验证

数组:

复制代码
2  2

原来的逆序对数量为 0,因为两个数字相等。

但修改后的程序会将这对相等的数字也计入统计。

比较条件 两个数字相等时 是否计入
a[i] <= a[j] 进入 if 否
a[i] < a[j] 进入 else 是

小结

这是一个非常经典的程序阅读陷阱。

< 和 <= 只差一个等号,但当程序统计的是数量时,这个等号可能直接改变答案。

做题时一定要问:边界相等时,程序到底会进入哪个分支?



第 15 题:快速幂

考点

快速幂、二进制拆分、取模

题目给出下面的函数:

复制代码
long long power(long long a, long long b, long long p) {
    long long r = 1 % p;

    while (b) {
        if (b & 1)
            r = r * a % p;

        a = a * a % p;
        b >>= 1;
    }

    return r;
}

执行:

复制代码
power(2, 100, 1000)

返回值是多少?

  • A. 576

  • B. 376

  • C. 976

  • D. 176


第一步:题目到底要求什么?

我们要求:

也就是计算 除以 1000 的余数。

如果从 2 开始,连续乘 100 次,数字会变得非常大,普通整数无法保存这么大的完整结果。

快速幂的思路是:

把乘方拆成平方,再利用指数的二进制表示决定什么时候乘进答案。


第二步:理解代码中的三个关键操作

1. b & 1:检查指数是不是奇数
复制代码
if (b & 1)

如果 b 是奇数,那么 b & 1 为 1;如果 b 是偶数,则为 0。

例如:

复制代码
5 的二进制是 101,末位是 1 → 奇数
6 的二进制是 110,末位是 0 → 偶数

所以,只有指数为奇数时,才执行:

复制代码
r = r * a % p;

2. a = a * a % p:不断平方
复制代码
a = a * a % p;

每次循环,都把当前的底数平方,并取模。

这样就能依次得到:

指数每次翻倍,不需要一个一个乘。


3. b >>= 1:指数除以 2
复制代码
b >>= 1;

这是右移一位,相当于对非负整数进行整除 2。

例如:

复制代码
100 → 50 → 25 → 12 → 6 → 3 → 1 → 0

当 b 变成 0,循环就结束。


第三步:手算关键结果

为了避免数字太大,我们始终只保留除以 1000 的余数。

首先:

所以:

继续平方:

只保留后三位:

再平方:

因此:


第四步:把 拆开

注意:

100=80+20

所以:

我们已经得到:

因此:

= 176 * 576 = 101376

所以,最终余数为:

正确答案:B(376)。


第五步:为什么快速幂这么重要?

假设指数从 100 变成 1,000,000,000。

如果使用普通循环,每次乘一次底数,就需要进行大量运算。

快速幂每次将指数除以 2,因此只需要大约:

O(log⁡b)

次循环。

指数很大时,这种优化就非常重要。


本题记忆口诀

指数除以二,底数不断平方;奇数乘进答案,每一步都取模。


第 8---15 题答案汇总

题号 正确选项 核心考点
8 B 拓扑排序与合法排列计数
9 A 分治递推式、时间复杂度
10 D 树的直径与重心
11 C DAG 缩点、强连通图
12 C 二叉树形态、卡特兰数
13 B 字符串真前缀与真后缀
14 C 归并排序统计逆序对
15 B 快速幂、取模

给小学生的学习建议

这 8 道题的难度差异比较大。要分三层学习:

  1. 先理解题目: 第 8 题、第 10 题、第 13 题,可以先通过画图、列举和比较来建立直觉。

  2. 再理解算法: 第 9 题、第 14 题、第 15 题,重点看清递推关系、分支条件和循环过程。

  3. 最后记住经典结论: 第 11 题的强连通图加边公式、第 12 题的卡特兰数,适合在理解后整理成知识卡片。

提高级初赛的学习目标,不只是知道答案,更重要的是看到题目时,能够识别它考查的知识,再一步一步推导出结果。这样遇到新的题目和代码变形时,学生才能真正做到举一反三。


相关推荐
_wyt0011 小时前
洛谷 P10112 [GESP202312 八级] 奖品分配 题解
c++·数学·gesp·八级
朝朝辞暮i2 小时前
C++ 第 33 课:事件循环——spin() 到底在干什么
开发语言·c++·算法
殷色玫瑰2 小时前
AVL树:从平衡因子到四种旋转的完整实现
c语言·数据结构·c++·算法·visualstudio
进化中的刀盾兽2 小时前
从零实现 C++ AI 大模型接入 SDK(九):LLMManager 统一模型管理与请求路由
c++
会周易的程序员2 小时前
STVM OSAL 层架构设计文档
c++·物联网·嵌入式·虚拟机·iot·软plc·iec61131
.YM.Z3 小时前
C++——【红黑树】原理详解:定义、性质、插入变色与旋转实现
开发语言·c++
无名猿3 小时前
constexpr 能力扩张:从 C++11 到 C++20 的编译期计算
c++·性能优化·现代c++·编译期
007张三丰3 小时前
C/C++ 内存管理详解:从内存分布到 new/delete 底层原理
java·c语言·c++·内存管理
三月微暖寻春笋4 小时前
【和春笋一起学C++】(七十二)类模板
开发语言·c++·实例·类模板·使用类模板·数组模板