图 由哪两部分组成( )。
考点:图的组成(A1)。
(A1)考点:图 由顶点集合 和边集合 组成,记作 ——边是顶点对,描述谁和谁相连(图的基本概念为大纲未明列、初赛高频内容)。
解析:先有顶点,再有"顶点与顶点之间的边";图的全部信息就是"有哪些点、有哪些边",与点的位置、边的长短无关。
排除法:选"一个数组和若干个链表"的人混淆了图的逻辑结构与存储结构(那是邻接表实现);选"坐标和连线长度"的人被几何画法误导,图不关心位置与长度;选"一个起点和一个终点"的人把图和一条路径搞混了。
存图时执行 add(1, 2) 只在 号顶点的邻接表里加入了 。这样存出的图中" 到 有边, 到 未必有边",该图是( )。
考点:有向与无向(A2)。
(A2)考点:每条边有方向(从出发端指向到达端)的图是有向图;add(1, 2) 只存了 ,反方向未必有边。
解析:有向边是"单行道": 能走,不代表 能走;无向边才是"双行道",存的时候要正反各加一次。
排除法:选"无向图,因为边没有长度"的人把"方向"和"长度"混为一谈——判据是方向不是长度;选"完全图"的人,完全图说的是"任意两点都有边",与方向无关;选"连通图,因为 能走到 "的人,连通描述"能否互相到达",描述不了边的方向。
无向图 中,顶点 与顶点 、、 各有一条边相连,再无其他边与 相连。顶点 的度是( )。
考点:无向图的度(A3)。
(A3)考点:无向图中顶点的度=与它相连的边数。顶点 连着 、、 三条边,度是 。
解析:数度就是"数有几条边碰到这个点",一条边算一次,与边的另一端是谁无关。
排除法:选 的人少数了一条边;选 的人多数了一条;选 的人把每条边按"两个端点"数了两遍——那是握手定理里"度数总和"的口径,不是单个点的度。
有向图的边为 、、、。顶点 的入度和出度分别是( )。
考点:入度与出度(A4)。
(A4)考点:有向图中,入度=指向该点的边数,出度=从该点出发的边数。、 都指向 ,没有边从 出发,所以顶点 入度 、出度 。
解析:看箭头方向:箭头射进来记入度,箭头射出去记出度; 只出现在两条边的"终点"位置。
排除法:选"入度 、出度 "的人把箭头方向看反了;选"入度 、出度 "的人把入边又数了一遍出边;选"入度 、出度 "的人只数了一条入边。
一个无向图有 个顶点,各顶点的度依次为 、、、、、。该图的边数是( )。
考点:握手定理(A5)。
(A5)考点:无向图所有顶点度数之和=边数的 倍(每条边给两个端点各贡献 度)。度数和 ,边数 。(2024 年 CSP-J 单选真题考法)
解析:一条边 让 、 各加 度,所以总和一定是偶数且恰为 ;反过来 度数和 。
排除法:选 的人忘了除以 ,把度数和直接当边数;选 的人少数了一个 ;选 的人多加了一度。
一个有向图有 条边。所有顶点的入度之和、所有顶点的出度之和分别是( )。
考点:有向图入度和等于出度和(A6)。
(A6)考点:有向图中每条边恰好给起点贡献 出度、给终点贡献 入度,所以入度总和=出度总和=边数。 条边则两者都是 。(2025 年 CSP-J 单选真题考法)
解析:一条边"一出一入"成对出现,两条总和天然相等,都等于边数,与图是否连通无关。
排除法:选" 和 "的人把出度又乘了 ;选" 和 "的人把边数除以了 (那是无向图度数和的口径);选"取决于图是否连通"的人没抓住"每条边一一对应"的本质。
无向图中顶点 上画了一个自环,另外还有 条普通边连着 。顶点 的度是( )。
考点:自环与重边(A7)。
(A7)考点:无向图中自环给顶点贡献 度(边的两个端点都是它自己)。 条普通边贡献 度,加自环的 度,共 度。
解析:一条边贡献 度的规则对自环同样适用,只是两端重合——不要按"一条边 度"去数自环。
排除法:选 的人把自环只算 度;选 的人干脆没算自环;选 的人把自环算了 度。
个顶点的无向完全图(每两个顶点之间恰有一条边)共有多少条边( )。
考点:无向完全图边数(A8)。
(A8)考点: 个顶点的无向完全图边数 。:。
解析:每个顶点与其余 个点各连一条边, 个点共 个"端",每条边被数两遍,除以 得 ——这正是握手定理的应用。
排除法:选 的人忘了"每个点的边会被另一端再数一次";选 的人忘了除以 ;选 的人按 算成了"点对皆可双向"。
下列关于路径的说法正确的是( )。
考点:简单路径(A9)。
(A9)考点:简单路径要求路径上的顶点不重复(除首尾相接成环的情形另算)。判断标准落在"顶点"上,不是边。
解析:路径是把边首尾相接串起来走;若中途绕回走过的点,就产生了"绕圈",不再是简单路径。
排除法:选"没有重复的边"的人描述的是另一种更弱的限制,顶点仍可能重复(可以绕一个环);选"必须经过所有顶点"的人把简单路径和哈密顿路径混了;选"长度必须为偶数"的人,路径长度没有奇偶限制。
无向图的边为 、、。顶点序列 构成( )。
考点:回路(A10)。
(A10)考点:起点与终点相同的路径称为回路(环)。 用了 条边、首尾同为 ,是长度为 的回路。
解析:数回路长度=数经过的边数;序列里 出现两次(首、尾),边恰好三条。
排除法:选"长度为 的简单路径"的人数错了边数,且首尾重合已不是路径;选"不是回路,因为它用了 条边"的人以为回路只能有偶数条边—— 条边首尾相接照样成环;选"两个独立的连通分量"的人,三点三边明明连成一片。
个顶点的无向图,边为 、、。该图的连通分量个数是( )。
考点:连通分量计数(A11)。
(A11)考点:连通分量=图中"极大的连通块"。边 、 连成 , 连成 ,剩下的 、 各自是孤立点分量,共 个。
解析:数分量的口诀:先找"有边相连"的块,再数孤立点——每个孤立点自成一个分量,最容易漏。
排除法:选 的人漏数了孤立点;选 的人以为图整体连通;选 的人把 拆成了两块。
有 个顶点的无向图至少应该有多少条边,才能确保它是一个连通图( )。
考点:连通图至少 n 减 1 条边(A12)。
(A12)考点: 个顶点的无向图连通至少需要 条边(树就是取到下界的连通图)。 个点至少 条。(2020 年 CSP-J 单选真题考法)
解析:每加一条边最多把两个块并成一个,从 个孤立块并成 块至少要做 次"合并",所以至少 条边。
排除法:选 的人以为"点数=边数"才连通,其实 条摆成链即可;选 、 的人多估了——边再多也是"至少"之上,不改变下界。
一个 个顶点、 条边的无向连通图,至少要删去多少条边才能变成一棵树( )。
考点:删边成树(A13)。
(A13)考点: 个顶点的连通图要变成树(连通且恰 条边),删掉的边数 。、:。(2021 年 CSP-J 单选真题考法)
解析:树是"边最少的连通图"( 条);从 条删到 条,要删 条,且只删"环上多余的边"就能保持连通。
排除法:选 的人按 算,少减了 ;选 的人删到只剩 条边,图不再连通;选 的人把目标边数当成了删除数。
阅读下面的程序:
01int g[10][10] = {}; 02g[1][2] = 1; g[2][1] = 1; 03g[2][3] = 1; g[3][2] = 1; 04cout << g[1][3];
程序的输出是( )。
考点:矩阵加边代码(B1)。
(B1)考点:邻接矩阵用 表示"、 之间有直接边"。程序只赋值了 、 两组, 从未写过,数组初始化为全 ,输出 0。
解析:int g[10][10] = {} 把矩阵清零;判断"有没有直接边"只看一个格子, 能通过 走到 不影响 的值——连通≠相邻。
排除法:选 1 的人把"可达"当成了"相邻", 到 是两步的路径而非直接边;选 2 的人以为输出的是路径长度;选 -1 的人以为未赋值的元素是 ,其实静态数组 {} 初始化是 。
用邻接矩阵 存无向图。关于矩阵元素的关系,正确的是( )。
考点:无向矩阵对称性(B2)。
(B2)考点:无向图的邻接矩阵是对称矩阵:对任意 、 都有 ——因为无向边 同时给两个格子赋值。
解析:加边代码总是成对写 g[u][v] = g[v][u] = 1;,这正是"一条无向边、两个方向都能走"的体现。
排除法:选"只有对角线相等"的人没注意每对对称格子都相等;选"仅当图连通时才对称"的人,对称性来自"每条边成对赋值",与连通无关;选"取决于加边顺序"的人——只要每条边都按标准写法成对赋值,顺序不影响对称性。
用 int g[1000][1000] 存 个顶点的邻接矩阵,该数组占用的内存约为( )。
考点:矩阵空间 n 平方(B3)。
(B3)考点:邻接矩阵开 个 int( 字节), 时 字节,约 MB。
解析:空间与边数无关、只与点数有关——哪怕只有 条边也要开满一百万个格子,这是矩阵"以空间换查边速度"的代价。
排除法:选 KB 的人只算了一行( 字节);选 KB 的人把字节数少算了一位;选 MB 的人多乘了一个 。
用邻接矩阵存图,判断顶点 与 之间是否有直接边,所需时间是( )。
考点:矩阵查边复杂度(B4)。
(B4)考点:邻接矩阵判断 是否有边只需读 一个格子,时间 。
解析:行列号直接定位格子,一次内存访问出结果,这是邻接矩阵相对邻接表最大的优势。
排除法:选 的人按邻接表"扫邻居"的思维估;选 的人混淆了两种存储——扫完 的所有邻居是邻接表的代价;选 的人把"扫一整行"当成了必要操作,其实按列号直达。
由 个顶点构成的有向连通图,用邻接矩阵表示时,该矩阵中至少存在多少个非零元素( )。
考点:有向连通矩阵非零下限(B5)。
(B5)考点: 个顶点的有向连通图至少要 条边,每条边在矩阵里占一个非零格子(有向边只赋值一个方向),所以至少 个非零元素。(2022 年 CSP-J 单选真题考法)
解析:连通的下界是"树形"结构( 条边都能用上);这 条有向边互不重合时非零格子最少。
排除法:选 的人多算了一条;选 的人以为还需要额外冗余边;选 的人按满矩阵算——那是完全图才有的事,与"至少"不符。
用邻接矩阵存 个顶点(编号 )的无向图,矩阵如下(行、列都按 编号):
0 1 0 1 0 1 0 1 0 0 0 1 0 0 1 1 0 0 0 1 0 0 1 1 0
顶点 的度是( )。
考点:行和即度数(B6)。
(B6)考点:无向图邻接矩阵中,第 行元素之和=顶点 的度。第 行为 ,和为 ,即顶点 的度是 (与 、 相连)。
解析:一行里的每个 代表"一条与 相连的边",数 就是数度;由对称性列和也等于度。
排除法:选 的人只看到第一个 ;选 的人把对角线或别的行的 也数了进来;选 的人数的是全矩阵 的个数的一半之类,与单点度数无关。
用邻接矩阵存有向图,程序如下:
01int s = 0; 02for (int i = 1; i <= n; ++i) 03 s += g[i][v];
变量 s 最终是( )。
考点:列和即入度(B7)。
(B7)考点:有向图矩阵中 表示"有一条边从 指向 "。固定 对行求和=指向 的边数=入度。
解析:行号变化、列号固定,扫的正是"第 列";出度要反过来固定行扫一行。
排除法:选"出度"的人扫反了方向——出度是固定行、扫一行;选"度数"的人用了无向图口径,有向图要分入出;选"总边数"的人把所有格子都算上了。
无向图中顶点 有一个自环。用邻接矩阵存图后,用"第 行元素之和"计算顶点 的度,得到的结果会( )。
考点:自环的矩阵表示(B8)。
(B8)考点:自环存在对角线上:,只占一个格子,却给 贡献 度。用"行和"算度会少 。
解析:行和把自环只数了 次,而度的定义里自环算 ——所以带自环时行和=度数 ,要补回来。
排除法:选"多 "的人方向说反了;选"不变"的人忽略了对角线只占一格的事实;选"无法存"的人——对角线位置本来就是留给自环的,能存。
用邻接矩阵存边权均为正整数的带权无向图,无边的位置填 。若读出 ,可以断定( )。
考点:带权矩阵(B9)。
(B9)考点:带权矩阵用 存边权,无边位置填 (权为正的前提下)。读到 只能断定"没有直接边"。
解析: 无法与"权为 的边"区分,所以约定权为正时用 兼表无边;带负权时应改用"无穷大"表示无边。
排除法:选"有一条权为 的边"的人,题干已约定权均为正整数, 只能是无边的哨兵值;选" 到 不可达"的人混淆了"没有直接边"与"没有路径"—— 可能经其他点绕到 ;选"顶点 不存在"的人,矩阵行不会因无边而失效。
要存一个 、 的无向图(边数远小于 )。最合适的存储方式是( )。
考点:稀疏图选表不选矩阵(B10)。
(B10)考点: 时矩阵要 个格子(约 GB),内存不可行;边数 远小于 ,是典型稀疏图,用邻接表 空间即可。
解析:存储选型的第一问是"空间开不开得下":稠密小图用矩阵省事,大稀疏图必须上邻接表。
排除法:选"邻接矩阵,因为查边方便"的人没算空间账——查再快,开不出来都是空谈;选"二维 bool"的人只把内存除以 , 个 bool 依然天文数字;选"各存一份"的人内存直接翻倍,更不可行。
阅读建图代码:
01vector<int> g[100]; 02void add(int u, int v) { g[u].push_back(v); }
g[u] 存放的是( )。
考点:vector 邻接表结构(C1)。
(C1)考点:vector 邻接表给每个顶点挂一个动态数组,g[u] 存放 的所有出边邻居,按 push_back 的先后顺序排列。
解析:add(u, v) 就是"把 追加到 的邻居名单末尾"——名单只记邻居编号,不记路径、不记权值。
排除法:选" 到其他点的路径"的人混淆了"直接邻居"和"多跳路径",名单里只有一步可达的点;选"所有顶点的编号"的人——那是顶点表不是邻接表;选"坐标和权值"的人把图存储和题目数据搞混了。
用 void add(int u, int v) { g[u].push_back(v); } 存一条无向边 ,只执行了 add(1, 2)。为了正确表示无向边,还必须执行( )。
考点:无向图加两条边(C2)。
(C2)考点:无向边 用"只加单向"的 add 函数存时,必须调用两次:add(1, 2) 和 add(2, 1),两个方向的邻接表各存一份。
解析:无向边的本质是"两个方向都能走";只加 add(1, 2) 存出的是有向边,从 出发根本看不到 。
排除法:选 add(1, 1) 的人给顶点 挂了个自环式的错误标记,方向问题没解决;选 add(2, 2) 的人给顶点 挂了同样的错误标记;选"什么都不缺"的人把无向图当有向图存了,遍历会漏。
无向图有 条边(无自环、无重边),用 vector 邻接表按标准写法存图,所有 g[u] 里的元素总数是( )。
考点:表中元素总数 2m(C3)。
(C3)考点:无向图 条边按标准写法(每条正反各加一次)存邻接表,全部 g[u] 的元素总数是 。 时为 。
解析:一条边贡献两个元素(起点表里一个、终点表里一个),与加边顺序无关;有向图才是每条边一个元素。
排除法:选 的人忘了无向图每条边要存两次;选 的人按 算成了边两两组合;选"取决于加边顺序"的人——顺序只影响每个表内邻居的先后,不影响总数。
用邻接表把图遍历一遍:外层 for 枚举每个顶点,内层枚举其邻接表。总时间复杂度是( )( 为顶点数、 为边数)。
考点:遍历总复杂度 n 加 m(C4)。
(C4)考点:邻接表遍历:外层枚举 个顶点、内层把每条表元素看一遍,无向图元素共 个,总时间 。
解析:每个顶点被外层碰一次、每条边(的两个方向元素)被内层碰一次,加起来就是 ,常数不影响量级。
排除法:选 的人把"每个点要扫自己的邻居"误算成"每个点要扫所有边";选 的人沿用了矩阵的复杂度;选 的人凭空加了排序的对数因子。
邻接表建图后执行:
01for (int v : g[u]) 02 cout << v << " ";
依次输出的是( )。
考点:遍历邻居写法(C5)。
(C5)考点:for (int v : g[u]) 依次取出 的全部出边邻居,顺序=当年 push_back 的加边顺序(不是编号顺序)。
解析:vector 邻接表天然保持插入序——先加的边先被枚举,这也是同一张图用不同加边顺序会得到不同 DFS 序的原因。
排除法:选"按编号从小到大"的人把 vector 当有序容器了,除非手动排序,否则就是插入序;选"所有尚未访问的顶点"的人——循环里没有任何 vis 判断,取的全是邻居;选"祖先顶点"的人用了树的概念,图上没有天然的祖先。
无向图按标准写法加边 add(1,2)、add(1,3)、add(4,1)(每条都加正反两次)。此时 g[1].size() 是( )。
考点:size 即度数(C6)。
(C6)考点:无向图标准加边下,g[u].size() =顶点 的度。三条边 、、 各往 g[1] 里放一个元素,g[1].size() 是 。
解析:每条与 相连的边都让 的表长加 ,表长就是度数——比矩阵行和更直接。
排除法:选 的人漏了 这条边;选 的人把正反两次 push_back 都算进了 的表里——反向元素进的是对方的表;选 的人只数了一条边。
用邻接表存图,判断" 与 之间是否有直接边",需要扫一遍 g[u],时间是( )。
考点:表查边复杂度(C7)。
(C7)考点:邻接表判" 是否有边"要线性扫 g[u],时间为 ( 的邻居越多越慢)。
解析:表里没有"按编号直达"的下标机制,只能挨个看;最坏情况 与所有点相连时退化为 ,但按"度"描述最准确。
排除法:选 的人把矩阵的特性搬到了表上;选 的人以为表是有序可二分的;选 的人用的是最坏退化情形,不如按 刻画精确。
阅读链式前向星代码:
01int head[100], nxt[200], to[200], cnt = 0; 02void add(int u, int v) { 03 to[++cnt] = v; 04 nxt[cnt] = head[u]; 05 head[u] = cnt; 06}
数组 to[cnt] 存放的是( )。
考点:链式前向星结构(C8)。
(C8)考点:链式前向星用三个数组存边:to[cnt] 是第 cnt 条边的终点,nxt[cnt] 是同起点的下一条边号,head[u] 是 的第一条边号。
解析:它就是"数组模拟链表"版的邻接表——每条边记录"去哪"和"下一条在哪",顺着 nxt 链就能枚举完 的所有出边。
排除法:选"起点"的人——起点由 head[u] 的下标体现,不需要在边里存;选"权值"的人,这份代码根本没存权;选"上一次访问的顶点"的人把它当成遍历时的临时记录了,其实它是静态建图数据。
链式前向星加边函数:
01int head[100], nxt[200], to[200], cnt = 0; 02void add(int u, int v) { 03 to[++cnt] = v; 04 nxt[cnt] = head[u]; 05 head[u] = cnt; 06}
依次执行 add(1, 2)、add(1, 3)、add(1, 4),然后从 head[1] 开始沿 nxt 链枚举邻居,得到的顺序是( )。
考点:头插法的逆序(C9)。
(C9)考点:add 把新边插到链头(nxt[cnt] = head[u]; head[u] = cnt;),所以从 head[1] 沿 nxt 枚举的顺序是后加的先出:4 3 2。
解析:依次加 、、:加 后链是 ;加 后 的下一条是 ;加 后链头变 ——链变成了 。
排除法:选 2 3 4 的人以为按插入顺序出链,头插恰恰相反;选 2 4 3、4 2 3的人都排错了链的指向,只能得到完整逆序链。
链式前向星中,head[] 数组通常初始化为 而不是 ,原因是( )。
考点:head 初始化为负一(C10)。
(C10)考点:边号从 开始,head[u] = -1 表示" 没有出边"—— 会与合法边号冲突, 才是安全的哨兵值。
解析:遍历写 for (int i = head[u]; i != -1; i = nxt[i]);若不初始化,全局数组默认的 会被当成"第 条边"读出垃圾数据(见 I 组排查题)。
排除法:选" 会被自动跳过,不需要循环条件"的人——跳过靠的正是 i != -1 这个条件本身;选"表示内存未分配"的人把哨兵值和内存管理混了;选"只是习惯,用 效果相同"的人没意识到 是合法边号会撞车。
用边集数组 struct Edge { int u, v; } e[M]; 存图。它最适合的场景是( )。
考点:边集数组(C11)。
(C11)考点:边集数组把每条边存成一个结构体(起点、终点、可选权值),按条躺在数组里——适合"逐条处理所有边"(统计、排序、枚举),不适合按顶点查邻居。
解析:三种存储各有所长:矩阵按"点对"索引、邻接表按"起点"索引、边集按"条"索引——按什么单位访问,就选什么存储。
排除法:选"频繁询问两点是否相邻"的人,边集要扫全表才能回答,矩阵才是 ;选"快速枚举某点全部邻居"的人,那要扫全部 条边过滤,邻接表直接给;选"只能存有向图"的人——无向边存成"两条有向边"即可。
图有 个顶点,最多 条边,程序需要频繁判断"任意两点 、 之间是否有直接边"。最合适的存储是( )。
考点:三种存储选择(C12)。
(C12)考点: 时矩阵只要 万个格子(约 MB),完全开得下;又需要频繁查任意两点是否相邻(),邻接矩阵是最佳选择。
解析:选存储看两件事:空间是否可行( 几千矩阵都行)、访问模式是什么(频繁按点对查→矩阵;按点枚举邻居→表;按条处理边→边集)。
排除法:选邻接表的人每次查边都要扫 g[u],频繁查询会白白变慢;选链式前向星的人同样按邻居扫,且代码更绕;选边集数组的人每答一次"是否相邻"都要扫 条边。
阅读 DFS 程序:
01void dfs(int u) { 02 vis[u] = true; 03 cout << u << " "; 04 for (int v : g[u]) 05 if (!vis[v]) dfs(v); 06}
第一行 vis[u] = true; 的作用是( )。
考点:递归框架与标记作用(D1)。
(D1)考点:DFS 递归框架里 vis[u] = true 标记" 已访问",作用是防止沿环回到已访问的点造成无限递归与重复输出。
解析:图不一定是树,可能有环;没有标记, 这样的环会让递归永远转下去(D15 专门验证)。
排除法:选"记录路径长度"的人,路径长度没存在 vis 里;选"给输出排序"的人,输出顺序由遍历本身决定;选"没有作用,删掉结果不变"的人只在树上侥幸成立,有环必炸。
无向图有 个顶点、 条边:、、、。用 vector 邻接表按题目给出的顺序加边(每条边正反各加一次),从 号点开始 DFS。访问顺序是( )。
考点:小图 DFS 序追踪(D2)。
(D2)考点:按插入序建表后 g[1] = [2,3]、g[2] = [1,4]、g[3] = [1,4]、g[4] = [2,3]。dfs(1):访问 ,先走 ; 的邻居里 未访问,走 ; 的邻居里 未访问,走 ——访问序 1 2 4 3。
解析:DFS 一条路走到底再回头:, 的邻居全部已访问,逐层回溯结束。
排除法:选 1 2 3 4 的人按 BFS 层序走了;选 1 3 4 2 的人在 处先走了后加的邻居 (那是另一种合法 DFS 序,但与本题插入序不符);选 1 4 2 3 的人让 直接连到了 ——边 根本不存在。
同一张无向图( 点、边 、、、)分别用两种方式存:
两种方式都从 开始 DFS,得到的访问序分别是( )。
考点:遍历序取决于邻居枚举序(D3)。
(D3)考点:同一张图、同一 个起点,DFS 序仍可能不同:方式一插入序使 g[1] = [3,2],先走 得 1 3 4 2;方式二矩阵按编号升序枚举邻居,先走 得 1 2 4 3。
解析:DFS 序由"每个点枚举邻居的顺序"唯一决定——表按插入序、矩阵按编号序,两套顺序自然导出两个访问序。
排除法:选两个 1 2 4 3 的人没注意方式一的插入序把 排在了 前面;选两个 1 3 4 2 的人把矩阵也当成插入序了;选含 1 2 3 4 的人混入了 BFS 层序。
同一张连通无向图,从同一起点出发做 DFS,两次运行得到了不同的访问顺序。最可能的原因是( )。
考点:DFS 序不唯一的来源(D4)。
(D4)考点:连通图从固定起点 DFS,访问序不唯一——它由"每个顶点枚举邻居的顺序"决定:邻接表随加边顺序变,矩阵固定按编号升序。
解析:DFS 只规定"一条路走到底、回头换下一条"的骨架,不规定邻居先试谁;换一种存图或加边顺序,就可能得到另一个合法序。
排除法:选"结果本来随机"的人——不随机,给定枚举序后完全确定;选"起点不同"的人——题干已固定同一起点;选"顶点编号变化"的人——编号没变,变的是枚举顺序。
无向图有 个顶点,边为 、、、、。以 为起点做深度优先遍历(枚举邻居的顺序任意),、、、 四个点中,有可能作为最后一个被遍历到的点的个数是( )。
考点:可能最后遍历的点(D5)。
(D5)考点:枚举所有可能的邻居顺序,、、 都可能垫底(如 、、),而 永远不行—— 只与 相连, 要被访问必须先经过 ,所以 不可能是最后一个。共 个。(2021 年 CSP-J 单选真题考法)
解析:判"不可能最后"的抓手是割点式的必经性: 是通往 的唯一跳板,垫底者必须没有"只能经它到达"的邻居挂着。
排除法:选 的人漏了某个分支(三种角色不同的点都能垫底);选 的人没发现 是必经点;选 的人把不唯一性想得太保守。
要对一张非连通图的所有顶点做 DFS,主程序如下:
01int comp = 0; 02for (int u = 1; u <= n; ++u) 03 if (!vis[u]) { 04 comp++; 05 ____________; 06 }
空缺处应填( )。
考点:非连通图多次启动(D6)。
(D6)考点:非连通图一次 DFS 只能覆盖起点所在分量;主循环对每个未访问顶点补一次 dfs(u),才能遍历全图。
解析:if (!vis[u]) 保证每个分量只在"第一个碰到的点"处启动一次;comp 顺便数出了分量个数。
排除法:选 dfs(u + 1) 的人跳过了可能的相邻点且会越界;选"只 vis[u] = true 不遍历"的人,同分量其他点永远没被访问,分量统计全错;选 comp = 0 的人把计数器清掉了。
要统计一张图的连通分量个数,下列说法正确的是( )。
考点:分量数与遍历方式无关(D7)。
(D7)考点:连通分量是图本身的性质;DFS 和 BFS 都能把一个分量完整走遍,统计出的分量个数必然相同。
解析:数分量的算法骨架="外层扫点 + 对未访问点启动一次遍历"——用什么遍历填空都行,结果只取决于图的连通结构。
排除法:选"只能 DFS"或"只能 BFS"的人把手段当成了目的;选"都要先排序"的人,排序与连通性无关。
程序从顶点 开始 DFS,全局变量 cnt 在每个顶点被访问时加 。DFS 结束后判断整张图是否连通,条件是( )。
考点:DFS 判连通(D8)。
(D8)考点:从 出发 DFS 后若访问计数 cnt == n,说明 个点全部可达,图连通;否则 cnt 就是 所在分量的点数。
解析:一次 DFS 恰好覆盖起点所在连通分量的全部点——"访问到的点数"与"总点数"相等即连通。
排除法:选 cnt == 1 的人只访问到了起点(孤立点情形);选 cnt == n - 1 的人把"树的边数 "错记成了点数;选 cnt >= 2 的人,两个点的分量也满足,远不够判整图连通。
从顶点 出发做了一次 DFS。DFS 结束后 vis[v] 为 true,可以断定( )。
考点:DFS 判可达(D9)。
(D9)考点:dfs(u) 结束后 vis[v] 为真 ⟺ 存在从 到 的路径(可能经过很多中间点)。
解析:DFS 恰好把"从 出发能走到的所有点"标完——这正是可达性的定义。
排除法:选"有直接边"的人混淆了边与路径(可达可能要绕好几跳);选"最短路径就是 DFS 访问路径"的人——DFS 找到的路径常常绕远,最短要用 BFS;选"一步回到 "的人把有向当无向、把路径当边了。
用显式栈代替递归做 DFS:弹出栈顶 、访问它,然后把 的未访问邻居压栈。为了让访问顺序与"递归 DFS(邻居按 的顺序枚举)"一致,压栈时应( )。
考点:显式栈压栈顺序(D10)。
(D10)考点:栈"后进先出",想让邻居按 的顺序被处理,必须逆序()压栈,让最先该处理的 最后压、最先弹出。
解析:递归 DFS 与显式栈 DFS 要得到同一访问序,方向必须拧着来——栈会把你最后放的先拿出来。
排除法:选"正序压栈"的人访问序会整个反过来(先 后 后 );选"随便压"的人,顺序直接决定结果;选"只压一个邻居其余丢弃"的人,图的大部分点再也访问不到。
个顶点的链形无向图:(只有这 条边)。从 开始递归 DFS,递归最深时调用栈中同时有几个 dfs 函数帧( )。
考点:链形图递归深度(D11)。
(D11)考点:链 从 递归 DFS 一路不回头,最深时 dfs(1)→dfs(2)→…→dfs(5) 同时挂 层调用帧。
解析:递归深度=走过的路径长;链形图是"最深的坑", 个点的链深达 ——这也是 长链爆栈的原因(见 I5)。
排除法:选 的人少数了最外层那帧(或忘了起点自身);选 的人只算了某个局部;选 的人多数了一层。
用邻接表存图( 个顶点、 条边)做一次完整 DFS,时间复杂度是( )。
考点:邻接表 DFS 复杂度(D12)。
(D12)考点:邻接表 DFS:每个顶点进出一次、每条边(的两个方向元素)被扫一次,总时间 。
解析:复杂度来自"每点一次 + 每边一次",这是邻接表对稀疏图的核心优势。
排除法:选 的人沿用了矩阵的账;选 的人把"扫自己的邻居"误算成"扫所有边";选 的人多加了不存在的排序成本。
改用邻接矩阵存同一张图( 个顶点)做 DFS,每个顶点出边时要扫描矩阵的一整行。时间复杂度是( )。
考点:邻接矩阵 DFS 复杂度(D13)。
(D13)考点:矩阵 DFS 访问每个顶点时都要扫一整行( 个格子)找邻居, 个点共 ——与边数无关。
解析:矩阵把"找邻居"做成了全行扫描,哪怕一个邻居都没有也要扫完一行,这是它对稀疏图不友好的根本原因。
排除法:选 的人把表的账搬给了矩阵;选 的人跟边数较上了劲,矩阵的代价只看点数;选 的人漏算了每个点的整行扫描。
对 个顶点的连通无向图做 DFS,全部 个点都被访问。整个过程中"沿着它走到新顶点"的边(树边)共有多少条( )。
考点:DFS 生成树边数(D14)。
(D14)考点:DFS 过程中"每次沿它走到新顶点"的边(树边)恰有 条:除了起点,每个点都由唯一一条树边"领进门"。
解析: 个点被访问 发生 次"从已访问走到未访问";其余的边都是回到已访问点的回边(非树边)。
排除法:选 的人多数了起点;选 的人把所有边都算成了树边,环上的边是走不到新点的;选 的人毫无依据地加一。
有向图有 个顶点,边为 、、。把 DFS 代码中的 vis[u] = true; 一行删掉,从 开始递归 DFS,程序会( )。
考点:删去标记行的后果(D15)。
(D15)考点:有环图上删掉 vis[u] = true 后,递归沿着 无限深入,最终栈溢出(运行错误)。
解析:没有标记,环上的点可以无限次重复进入;递归每层都压栈,深度无限增长直到崩溃——vis 正是防这个的(对照 D1)。
排除法:选"输出 1 2 3 正常结束"的人以为递归会自己停;选"正好两轮停止"的人,没有任何机制让它停;选"编译不通过"的人——这是运行期的逻辑错误,编译期查不出。
阅读 BFS 程序:
01queue<int> q; 02q.push(s); vis[s] = true; 03while (!q.empty()) { 04 int u = q.front(); q.pop(); 05 for (int v : g[u]) 06 if (!vis[v]) { 07 vis[v] = true; // 甲 08 q.push(v); 09 } 10}
注释"甲"处的标记时机是( )。
考点:入队即标记(E1)。
(E1)考点:BFS 在入队的同时做 vis[v] = true——一个点第一次被发现就标记,保证同一顶点不会被第二次入队。
解析:队列的意义是"等待扩展的待办清单";入队即标记等于"登记待办的同时盖章",清单里永远不会有重复条目。
排除法:选"出队时才标记"的人会看到同一顶点被邻居们反复塞进队列(E2 演示后果);选"访问完所有邻居再标记"的人拖延盖章,中间随时可能重复入队;选"放哪都一样"的人——位置不同,队列规模和正确性都可能出问题。
把标准 BFS(入队时标记)改成"出队时才标记":把邻居 q.push(v) 时不标记,顶点出队之后才执行标记。无向图边为 、、,从 开始 BFS。顶点 会被入队几次( )。
考点:出队才标记的重复入队(E2)。
(E2)考点:出队才标记时, 先被 的扩展入队一次; 出队时 还没被标记(只是排着队),又被 的扩展再入队一次——共 次。
解析: 出队 → 、 入队(未标记); 出队并标记,扫到邻居 未标记 → 第二次入队。重复入队会成倍放大队列规模,大图上内存爆炸(见 I1)。
排除法:选 次的人以为队列自带去重;选 次的人多算了一轮( 出队后邻居都已标记,不会再入队);选 次的人—— 是 的邻居,第一轮必入队。
无向图有 个顶点、 条边:、、、。邻接表按题目给出的顺序加边,从 号点开始 BFS。访问顺序(出队顺序)是( )。
考点:小图 BFS 序追踪(E3)。
(E3)考点:bfs(1): 出队,、 依次入队; 出队, 入队; 出队( 已标记); 出队。访问序(=出队序)1 2 3 4。
解析:BFS 按层扩展:第 层 、第 层 、第 层 ;同层内按入队先后——这正是它与 DFS(1 2 4 3)在同一张图上的鲜明对照。
排除法:选 1 2 4 3 的人按 DFS 的"一条路走到底"走了;选 1 3 4 2 的人颠倒了 、 的入队顺序(插入序 在前);选 1 4 3 2 的人让 直达 ,边 不存在。
无向图有 个顶点、 条边:、、、。从 做 BFS,与起点距离为 (最少经过 条边)的顶点集合是( )。
考点:按层分组(E4)。
(E4)考点:距起点 恰好 条边的点:( 或 );、 距离是 。
解析:BFS 第 层=距离为 的所有点:第 层 、第 层 ,一层一层往外推。
排除法:选 的人交成了第 层;选 、 的人把一层一层的边界划错了,把距离 和距离 的点混在了一起。
BFS 只用一个 dist 数组、不另设 vis 数组:dist[s] = 0 表示起点,其余位置初始化为 。初始化为 (而不是 )的原因是( )。
考点:dist 初值为负一(E5)。
(E5)考点:单数组写法里 dist[s] = 0 已把"距离 "给了起点,其余位置必须用 表示"未访问",才能与合法距离值区分开。
解析:dist[v] == -1 同时兼任"没访问过"的判断——一个数组两用,这是 BFS 最省事的惯用写法。
排除法:选" 输出好看"的人与机制无关;选" 表示无穷大方便排序"的人——没有排序环节;选"都一样"的人,若初始化为 ,未访问点与起点距离混同,判重立即失效。
BFS 程序片段:
01int u = q.front(); q.pop(); 02for (int v : g[u]) 03 if (dist[v] == -1) { 04 dist[v] = dist[u] + 1; 05 q.push(v); 06 }
新发现的邻居 的距离是在什么时刻、由谁推出来的( )。
考点:dist 由队首加一(E6)。
(E6)考点:邻居 的距离在被发现的那一刻由队首 推出:dist[v] = dist[u] + 1—— 正是刚刚出队、正在扩展的点。
解析: 第一次被碰到只能从 来,所以 的最短距离= 的距离加一步;这发生在入队时,不是 自己出队时才算。
排除法:选" 出队时自加 "的人把计算时机推迟了一轮, 出队时它的距离早该定好了;选"整层处理完统一加"的人引入了不存在的"层计数器";选"由入队个数决定"的人——距离与队列长度无关。
无向图有 个顶点,边为 、、、、。从 开始 BFS,dist[5] 的值是( )。
考点:dist 数组数值(E7)。
(E7)考点:边 、、、、:第 层 、第 层 、第 层 ,dist[5] 是 。
解析: 的邻居是 和 : 两步; 三步,取最短得 ——第一次到达即是答案。
排除法:选 的人以为 与起点有直接边——没有;选 的人沿长边 走了;选 的人把 当成了起点。
无权图(或所有边权相同)中,求从 到 的最少边数路径,应该用( )。
考点:无权最短路用 BFS(E8)。
(E8)考点:无权图(所有边代价相同)的最少边数路径用 BFS:按层向外扩展,第一次到达 时经过的层数就是最短距离。
解析:队列保证"近的先走"——第 层的点都是在 步内可达时才入队,第一次碰到 必然最短。
排除法:选"DFS 一条路走到底"的人,DFS 碰到的第一条路常常绕远;选"贪心选最短的边"的人,逐边贪心在图上会走进死胡同;选"每次走编号最小的邻居"的人,编号与距离毫无关系。
"用 BFS 求出的步数一定是最短步数"成立的前提是( )。
考点:BFS 最短路的前提(E9)。
(E9)考点:BFS 求最短路成立的前提是所有边权相等(含无权);边权不等时按层数就不是按代价,必须换用专门的最短路算法。
解析:BFS 的"层"=步数;只有每步代价相同,步数最少才等价于总代价最少。
排除法:选"必须是树"的人,任何等权图都行;选"必须是有向图"的人——方向与该性质无关;选"必须没有环"的人——有环时按层扩展照样第一次到达即最短。
BFS 求得 dist[t] = 3( 到 的最少边数)。这条最短路径一共经过多少个顶点( )。
考点:最短路经过点数(E10)。
(E10)考点:dist[t] = 3 表示最少走 条边,路径上的顶点数=边数 (含 与 两端)。
解析:一条 条边的路径穿起 个点——"经过几个点"与"走几条边"差一,这是高频粗心点。
排除法:选 的人把边数当成了点数;选 的人多加了两端各一次;选"无法确定"的人——点数恰为边数加一,是确定的。
BFS 时在入队处记录 pre[v] = u( 是从 第一次发现并入队的)。找到 后沿 pre 链从 走回 ,得到的是倒序的路径,所以输出前要( )。
考点:prev 数组回溯方向(E11)。
(E11)考点:pre[v] 记录" 是从谁第一次被发现"的;从 沿 pre 链走到 $s` 得到的是从终点到起点的倒序路径,输出前要把序列整个反转。
解析:pre 指向的是"来路"(靠近起点的方向),所以回溯天然逆序;反转后才是 的正序路径。
排除法:选"再做一次 DFS"的人——路径已经记录在 pre 里,反转即可;选"把 pre 数组排序"的人——排序会破坏前驱链;选"pre 链本来就是正序"的人方向恰好弄反。
多源 BFS 把所有起点先全部入队并标记(dist 为 ),再开始普通扩展。这样求出的 dist[v] 是( )。
考点:多源 BFS(E12)。
(E12)考点:多源 BFS 把所有起点一起入队(dist 全为 )再统一扩展,每个点被最近的源先碰到,dist[v] = 到最近源的距离。
解析:相当于所有源"同时点火、匀速外扩",谁先到达谁记录距离——一次 BFS 干完"对每个源各跑一遍"的活。
排除法:选"到第一个入队的源"的人——各源同层扩展,不存在哪个源独占;选"到所有源距离的平均值"的人,BFS 不做平均;选"不能求距离"的人,多源恰是 BFS 的经典用法。
从 做 BFS 后,dist 数组中的最大值为 。这个 的含义是( )。
考点:单源最远点(E13)。
(E13)考点:dist 最大值 的含义是:离 最远的点距 为 条边(单源意义下的"偏心")。
解析:dist 只登记了"到 "的距离;全图任意两点的最远距离(直径)可能出现在与 无关的两点之间。
排除法:选"图的直径"的人混淆了"离 最远"与"全图最远",直径需要对每个点都跑一遍才能确认;选"边数最多的分量的边数"的人,距离和边数是两个口径;选"顶点数减 "的人把"链长上限"当成了普适结论。
关于 DFS 和 BFS 的对比,正确的是( )。
考点:DFS 与 BFS 对比(E14)。
(E14)考点:DFS 靠栈(递归调用栈或显式栈)实现、一条路走到底再回头;BFS 靠队列实现、一层一层向外扩。
解析:容器决定了形状:栈让遍历"纵向深入"(适合判可达、找任意路径、回溯枚举),队列让遍历"横向铺开"(适合按层、无权最短)。
排除法:选"DFS 靠队列、BFS 靠栈"的人正好说反;选"都靠递归"的人——BFS 是纯迭代写法;选"BFS 空间一定更小"的人——BFS 存整层,层宽时反而更大(链形图上 DFS 深而窄、BFS 层小;星形图相反)。
无向图边为 、、、,邻接表按给出顺序加边( 的邻居先 后 )。从 出发分别做 DFS 和 BFS,两个访问序列的第二个访问的顶点分别是( )。
考点:同图两序对照(E15)。
(E15)考点:插入序使 g[1] = [2,3]( 在前)。DFS:访问 后钻进第一个未访问邻居 ;BFS: 出队后 第一个入队、也第二个出队——两序的第二个访问点都是 。
解析:无论深搜广搜,"第一个被处理的邻居"都是枚举顺序里的第一个;两序的分歧从第三个点开始(DFS 钻到 ,BFS 仍在本层走 )。
排除法:选"DFS 是 、BFS 是 "的人忽略了 DFS 同样先试 ;选"DFS 是 、BFS 是 "的人颠倒了 BFS 的入队顺序;选"都是 "的人两边都记反了。
把一张 行 列的迷宫看成图:能走的格子是顶点。那么"图中的边"对应的是( )。
考点:网格即图(F1)。
(F1)考点:把网格图看成一模一样的图:能走的格子=顶点,上下左右相邻的两个能走格子之间的通路=边( 方向);对角格子不相邻。
解析:网格题不用显式建 g 数组——方向数组 dx/dy 动态算邻居,但"点与边"的图模型没有变,DFS/BFS/泛洪照用。
排除法:选"四周的围墙"的人——围墙是边界不是边;选"每一行的所有格子"的人——那把一整行连成了链,跳过了"相邻"条件;选"任意两个能走的格子"的人——不相邻的格子之间没有边。
网格 DFS 常用方向数组:
01int dx[4] = {-1, 1, 0, 0}; 02int dy[4] = {0, 0, -1, 1};
取 i = 0 时,(r + dx[0], c + dy[0]) 是哪个格子( )。
考点:四方向数组(F2)。
(F2)考点:dx[0] = -1, dy[0] = 0 组合表示行号减一、列号不变—— 正上方的一格。
解析:行向下增长,所以 是向上;dx、dy 按下标配对, 上、 下、 左、 右。
排除法:选"下方(行号加 )"的人把行增长方向弄反——那是 dx[1] = 1 的方向;选"左边(列号减 )"的人忘了左由 dy[2] = -1 配 dx = 0;选"右边(列号加 )"的人同样配错了方向下标。
网格泛洪程序:
01int nr = r + dx[i], nc = c + dy[i]; 02if (____________ && grid[nr][nc] == old) 03 flood(nr, nc);
空缺处应填(设网格 行 列,下标从 开始)( )。
考点:边界检查的短路顺序(F3)。
(F3)考点:&& 从左到右短路求值,判界必须写在最前:0 <= nr && nr < R && 0 <= nc && nc < C,先确认下标合法,再访问 grid[nr][nc]。
解析:条件顺序在含数组访问的表达式里就是安全问题——越界下标一旦被求值就是未定义行为(2022 年完善程序真题 is_valid 的写法正是判界在前)。
排除法:选 grid[nr][nc] != 0 的人拿格子内容当边界判据,越界访问已经发生;选 nr < R && nc < C 的人漏掉了负方向(行、列都可能减成负数);选 || 连接的人——或逻辑几乎恒真,全部格子都会通过。
方向连通中,处于网格内部的格子 的邻居个数是( )。
考点:八方向(F4)。
(F4)考点: 方向连通把斜对角也算邻居,内部格的邻居=周围一圈 格。
解析: 邻域去掉中心自己,剩下 个方向:上下左右 个正方向+ 个斜角。
排除法:选 的人只数了正方向;选 的人漏了两个斜角;选 的人把格子自己也算进去了。
统计网格中目标格连通块个数的程序框架:
01int cnt = 0; 02for (int r = 0; r < R; ++r) 03 for (int c = 0; c < C; ++c) 04 if (grid[r][c] == 1 && !vis[r][c]) { 05 flood(r, c); 06 cnt++; 07 }
cnt++ 放在 flood 之后的含义是( )。
考点:连通块计数结构(F5)。
(F5)考点:cnt++ 紧跟在 flood 之后:每遇到一个未被之前泛洪覆盖的目标格,就意味着踩进了一个新连通块——启动一次泛洪把它整块"吃掉",计数加一。
解析:块内第一个被外层扫到的格子触发泛洪,块内其余格子全被标记;外层再扫到它们时 vis 已真,不再触发——保证每块只数一次。
排除法:选"每个目标格加 "的人数出的是目标格总数;选"每行加 "的人与行无关(一块可能横跨多行);选"flood 内每扩展一格加 "的人把块大小当成了块数。
方向连通下,下面网格(1 是目标格)中最大连通块的大小是( )。
1 1 0 0 0 1 0 1 1 0 0 1 1 1 1 1
考点:最大连通块(F6)。
(F6)考点:逐块泛洪统计:左上块 大小 ;右下块 沿最后一行并成一片,大小 ——最大 。
解析:关键是最后一行四个 连成横线,把右列的竖块与左下角的 全并进了一块;分块数错通常就是漏了这种"拐弯连接"。
排除法:选 的人只数了左上块;选 、 的人把右下大块断开了——第三行的 列并不断开最后一行的横向连接。
把网格中与起点同色连通的区域全部染成新色的泛洪程序:
01void fill(int r, int c, char old, char nw) { 02 queue<pair<int,int>> q; 03 q.push({r, c}); grid[r][c] = nw; 04 while (!q.empty()) { 05 auto t = q.front(); q.pop(); 06 for (int i = 0; i < 4; ++i) { 07 int nr = t.first + dx[i], nc = t.second + dy[i]; 08 if (inborder(nr, nc) && grid[nr][nc] == ______) 09 { grid[nr][nc] = nw; q.push({nr, nc}); } 10 } 11 } 12}
空缺处应填( )。
考点:泛洪染色判旧色(F7)。
(F7)考点:泛洪要把与起点同色连通的区域整体换色,扩展条件必须判 grid[nr][nc] == old——只有旧色格子才属于这个区域。(2022 年 CSP-J 完善程序真题的填空考法)
解析:判新色会把"已经染好的"当成候选,判固定字符、判 都与"同色连通"的语义无关;old 是唯一正确的依据。
排除法:选 nw 的人判成了新色,起点的邻居若已染过就永远不再入队,泛洪当场中断;选 '#' 的人把某个具体字符当成了通用判据;选 0 的人用数字判字符网格,类型都对不上。
泛洪染色程序的扩展片段:
01int nr = t.first + dx[i], nc = t.second + dy[i]; 02if (inborder(nr, nc) && grid[nr][nc] == old) { 03 grid[nr][nc] = nw; 04 q.push({nr, nc}); 05}
其中 grid[nr][nc] = nw; 写在 q.push({nr, nc}); 之前而不是之后,作用是( )。
考点:入队前先改色(F8)。
(F8)考点:grid[nr][nc] = nw 放在 q.push 之前:格子入队的同一刻就被改成新色,别的邻居再看它已不满足 == old,同一格不会被第二次发现、第二次入队。
解析:这与"入队即标记"是同一机制(E1/E2),只是把 vis 数组换成了格子颜色本身。
排除法:选"让队列里坐标更小"的人——改色与坐标数值无关;选"其他写法必然死循环"的人——配上 vis 数组同样安全,只是多开内存;选"交换两行结果不变"的人——先入队后改色的窗口期里,同一格可能被多个邻居重复入队。
用"直接把格子改成新色"代替 vis 数组来防止重复访问。这种写法的前提是( )。
考点:原地改色与 visited(F9)。
(F9)考点:用"改色"代替 vis 的前提:后续判断只依赖旧色、原图信息不再需要——改色破坏原图没有副作用。
解析:若后面还要读原始地图(比如按原色统计),就不能原地改,必须另开 vis 或复制一份地图。
排除法:选"任何网格题都能这样写"的人忽略了"需要保留原图"的场景;选"必须先复制一份"的人——复制只是替代方案之一,不是前提;选"只有 方向才允许"的人——方向数与能否改色无关。
把 行 列网格的格子编号成一维(行优先):id = r * C + c。在 列的网格中,第 行第 列(下标从 起)的格子编号是( )。
考点:格子编号公式(F10)。
(F10)考点:行优先编号 id = r * C + c( 是列数):、、 时 id 是 。
解析:前两行共 个格子,第 行从 号起,第 列再偏移 得 ——把二维坐标压成一维下标的标准公式。
排除法:选 的人按 算成了 ;选 的人用加法 之类拼凑;选 的人用列主序 算反了行列地位。
# 表示岛屿、. 表示海水, 方向连通。下面地图中共有岛屿(连通块)几个( )。
# . # # . . # . # . . # # . # #
考点:岛屿计数(F11)。
(F11)考点:逐个 # 泛洪:、、、——共 座岛。
解析:容易看漏的连接:右上块靠 这个"拐点"把 连起来;右下块靠最后一行的 横向连接;左下 竖向相连。
排除法:选 的人常把 并进别的块或漏成孤立点;选 、 的人把同一块拆开了——注意 与 相邻, 与 相邻,它们是同一座岛。
对 行 列的网格做一次完整的多连通块泛洪统计(每个格子至多被入队、访问一次),时间复杂度是( )。
考点:泛洪复杂度(F12)。
(F12)考点:每个格子至多被改色、入队一次,总时间 ——与格子数同阶。
解析:判重机制(改色/标记)保证任何格子第二次被发现时已不满足条件,不存在重复扩展;总工作量就是"扫一遍地图"的量级。
排除法:选 的人以为格点之间两两都要比较;选 的人把方向枚举当成了指数爆炸;选 的人多加了不存在的排序因子。
的网格为 S . . S .(S 是源点,. 是空格)。所有源点同时入队(距离 )开始 BFS,则从左到右 个格子的距离依次是( )。
考点:多源泛洪最近距离(F13)。
(F13)考点:多源 BFS 里每个格子归"最先碰到它的源"管:第 、 格是源(距离 );第 格邻左源、第 格直接邻右源、第 格邻右源——距离依次是 0 1 1 0 1。
解析:最容易看走眼的是第 格:它左隔第 格离左源 步,但右边紧挨着第 格这个源,最近距离取 。多源问题里"离哪个源近"要逐源比较,不能顺着开头一个源一路数到底。
排除法:选 0 1 2 0 1 的人只跟左源数到底,漏了第 格右邻就是源;选 0 1 2 1 0 的人还把两端的源点位置也弄错了;选 0 0 1 0 1 的人把第 格也当成了源。
回溯法中反复出现"恢复现场"这个词。它指的是( )。
考点:恢复现场的含义(G1)。
(G1)考点:恢复现场=递归返回之后,撤销本层在递归前做过的修改(如 vis[i] = false、swap 换回、数组改回),让上层能从"没做过这个选择"的状态继续试别的分支。(回溯为大纲未明列、初赛真题实考内容)
解析:回溯的核心节奏是"改 → 递 → 撤":进去前改,出来后撤;撤销只针对本层的修改,不动别的层的状态。
排除法:选"每层递归前清空整个数组"的人破坏了其他分支正在使用的数据;选"把 ans 重置为 "的人——ans 只会被更大的值更新,从不清零;选"vis 永久保留"的人恰好说反,那是不回溯的普通 DFS。
生成 全排列的程序:
01void perm(int k) { // 已选了 k 个数 02 if (k == n) { output(); return; } 03 for (int i = 1; i <= n; ++i) 04 if (!vis[i]) { 05 vis[i] = true; a[k] = i; 06 perm(k + 1); 07 ____________; 08 } 09}
空缺处应填( )。
考点:全排列 vis 版补全(G2)。
(G2)考点:空缺处填 vis[i] = false;——递归返回后撤销本层的标记,让"数字 "在其他分支里还能被选。
解析:这行是"改 → 递 → 撤"三步的第三步;漏了它,深层占用的数字永远不释放,后面的分支全部空转(G10 演示后果)。
排除法:选 output(); 的人把输出塞进了循环里,每层乱打印;选"只 a[k] = 0 还原数组"的人没 undo 标记,vis 仍锁着数字;选 return; 的人让本层试完第一个数字就退出,排列只剩一条。
生成 全排列的程序(vis 标记数组、a 存当前排列):
01void perm(int k) { 02 if (k == n) { output(); return; } 03 for (int i = 1; i <= n; ++i) 04 if (!vis[i]) { vis[i] = true; a[k] = i; perm(k + 1); vis[i] = false; } 05}
当 、for 循环按 的顺序尝试时,程序第二个输出的排列是( )。
考点:全排列输出序(G3)。
(G3)考点:按 的顺序尝试,输出按字典序:第 个 1 2 3,接着选 开头的分支继续试 、——第 个输出 1 3 2。
解析:第一层锁定 ;第二层先试 、第三层只剩 ,输出 1 2 3;回溯后第二层试 、第三层选 ,输出 1 3 2——vis 版天然字典序。
排除法:选 1 2 3 的人数错了序号,它是第一个;选 2 1 3 的人以为第一层还没走完 就换了开头;选 3 2 1 的人跳到了最后一个。
另一种全排列写法:
01void perm(int k) { 02 if (k == n) { output(); return; } 03 for (int i = k; i <= n; ++i) { 04 swap(a[k], a[i]); 05 perm(k + 1); 06 swap(a[k], a[i]); 07 } 08}
紧随递归调用的第二次交换,作用是( )。
考点:swap 版的第二次交换(G4)。
(G4)考点:第二次 swap(a[k], a[i]) 是恢复现场:把数组换回递归前的样子,保证本层循环继续试下一个 时起点状态正确。
解析:swap 版不靠 vis 靠"换回来"——不换回的话,数组已被深层搅乱,后续分支在错误状态上枚举,方案或重复或丢失。
排除法:选"把数组排成字典序"的人——swap 版输出不保证字典序;选"让输出多打印一遍"的人它与输出无关;选"没有作用可以删除"的人删掉后立刻产出错误排列。
回溯的每一层由三步组成。正确的执行顺序是( )。
考点:修改递归撤销的顺序(G5)。
(G5)考点:回溯每一层的固定节奏:做出修改 → 递归 → 撤销修改——先改才能把选择传给下层;下层返回后立刻撤销,本层换下一个选择重试。
解析:顺序一错意义全失:撤销放在递归前等于"没做选择就递归";不撤销则后续分支带着残迹出发。
排除法:选"撤销 → 递归 → 修改"的人递归时什么都没发生;选"递归 → 做出修改 → 撤销修改"的人修改和撤销都发生在递归之后,下层什么都收不到;选"做出修改 → 撤销修改 → 递归"的人先撤再递,等于递归仍在原始状态上空转。
枚举 个元素所有子集的回溯:每个元素有"选"和"不选"两个分支,到第 层输出。 时输出总共执行多少次( )。
考点:子集决策树规模(G6)。
(G6)考点:每个元素两个分支(选/不选), 个元素决策到底,叶子数 ; 时 output 执行 次。
解析:决策树每层分叉一次、共 层;每个叶子对应一个子集—— 片叶子恰是 元素子集总数(含空集)。
排除法:选 的人按排列数 算了;选 的人只数了每层的一种走法;选 的人漏了空集对应的叶子。
枚举组合(从 中选 个数)的回溯:
01void comb(int start, int k) { 02 if (k == m) { output(); return; } 03 for (int i = start; i <= n; ++i) { 04 c[k] = i; 05 comb(i + 1, k + 1); 06 } 07}
参数 start(下一层从比刚选的数更大的编号开始试)的本质作用是( )。
考点:start 参数去重(G7)。
(G7)考点:start(下一层只试比刚选的数更大的编号)的本质是去重:保证组合里的数递增,同一组合不会以不同顺序重复出现。
解析:没有 start, 会以 和 两种顺序各出一次;有了它,每个组合只有"升序"这一种生成方式。
排除法:选"只是提速的剪枝,去掉后集合一样"的人——去掉后集合会变大(同组合重复出现),这不是优化是正确性;选"限制元素个数"的人,个数由 k == m 控制;选"为了字典序"的人,字典序是副产品不是本质。
的全排列回溯树:根节点是"还没选任何数",每个节点表示一种部分排列,叶子是完整排列。整棵树(含根与叶)的节点总数是( )。
考点:排列树节点数(G8)。
(G8)考点: 的排列树:根 个+第 层 个+第 层 个+叶子 个,共 个节点。
解析:每深一层,剩余可选数少一个:;叶子才是完整排列,中间节点是"部分排列"。
排除法:选 的人只数了叶子(完整排列);选 的人漏了根节点( 之类的糊涂账);选 的人把第二层当成了叶子。
用回溯生成长度为 的合法括号序列( 对括号),规则:可以放左括号当已放左括号数小于 ;可以放右括号当已放右括号数小于已放左括号数。能生成的序列个数是( )。
考点:合法括号生成(G9)。
(G9)考点:约束回溯——放左括号的额度最多 个;放右括号必须"已放右括号数 < 已放左括号数"。 对括号能生成 (()) 和 ()() 共 个。
解析:序列 (()):左左右右合法;()():交替合法;而 )( 开头右括号无左可配、())( 后期左括号超额,均被约束剪掉。
排除法:选 的人按"每对两种摆法"粗乘了;选 的人用 把非法序列也数进去了(如 )()();选 的人漏掉了一个合法解。
把第 80 题全排列程序中空缺的那一行忘写(即只标记、从不撤销)。 时程序总共输出几个排列( )。
考点:忘记恢复现场的后果(G10)。
(G10)考点:只标记不撤销:第一分支 1 2 3 输出后逐层返回,但 、 的标记永不释放;回到第一层继续试 、 时全被 vis 拦下——总共只输出 个排列。
解析:深层"借走"的数字没还,上层再也选不到它们;输出骤减为单条路径,这正是"忘记恢复现场"的标志性症状。
排除法:选 的人以为程序仍能遍历全排列;选 的人以为只影响一部分分支;选"无限多个"的人——没有增枝的来源,输出只会更少不会无限。
阅读程序片段(dfs 内的循环体):
01for (int i = 1; i < n; ++i) { 02 // 甲:把 d 数组中第 i-1 段与第 i 段合并成一段 03 dfs(n - 1, sum + s); // 乙 04 // 丙:把 d 数组恢复成合并前的样子 05}
这段程序在做的事情是( )。
考点:改递恢模式识别(G11)。
(G11)考点:for 循环里"甲改数组 → 乙递归 → 丙恢复数组"是标准回溯骨架,枚举的对象是"这一步合并哪两个相邻段"的所有选择,递归尝试每一种合并顺序。(2020 年 CSP-J 阅读程序真题的骨架,回溯为大纲未明列、初赛实考内容)
解析:每层合并一次使段数减一(),递归到只剩一段结算总代价;返回后恢复数组,让同一层能试"合并别的相邻对"。
排除法:选"求最短路"的人,这里没有边权与松弛,只有枚举合并顺序;选"给数组排序"的人,没有任何比较交换的排序结构;选"每层恰两个分支"的人,分支数是 个(相邻对的个数),随层数递减。
从若干正整数中选一些数使和恰好等于 。搜索时若当前已选数之和已经大于 ,直接 return 不再往下选。这种剪枝剪掉的是( )。
考点:可行性剪枝(H1)。
(H1)考点:可行性剪枝:一旦约束条件已不可能满足(已选和超过 ,再加正整数只会更大),立即 return 剪掉整棵子树。(剪枝为大纲未明列、初赛真题实考内容)
解析:它剪的是"永远到不了解"的分支;与时间限制无关、与最优劣无关——判断依据只有"约束还能不能满足"。
排除法:选"不是最优解的分支"的人说的是最优性剪枝(H2 的内容);选"与之前方案重复"的人说的是等效冗余排除;选"超过时间限制才触发"的人,剪枝依据是约束不是时钟。
搜索求"选物品的最大总价值"时维护当前最优答案 ans。若当前价值加上剩余物品价值总和仍然不超过 ans,就剪掉该分支。这里的"剩余总和"用来( )。
考点:最优性剪枝(H2)。
(H2)考点:"剩余物品价值总和"是这条分支的乐观上界(把还没选的全选上最多能到多少);上界都不超过当前 ans,这条分支再搜也白搜,剪掉。
解析:最优性剪枝的钥匙是构造一个"最好情况下"的估计值:上界 ≤ 已有答案 → 无希望,立刻回溯。
排除法:选"最悲观能差到多少"的人,悲观估计(下界)剪不了"追不上最优"的分支;选"计算已用时间"的人那是时限控制;选"检查剩余物品合法性"的人那是可行性剪枝的职责。
拼凑目标值时先把候选数排序再搜索(例如从大到小试)。关于这一步,正确的是( )。
考点:搜索顺序剪枝(H3)。
(H3)考点:答案与枚举顺序无关,但顺序决定哪个分支先被探索:让"更容易成功(或更早暴露失败)"的候选先上,剪枝条件更早触发、整棵搜索树大幅变小。
解析:排序改变的是搜索树的形状(先走哪枝),不改变解集——这就是"顺序不当不错、但慢"的原因。
排除法:选"不排序答案会错"的人,枚举顺序从不影响解集;选"为了字典序输出"的人,排序依据是"希望大小"不是字典序;选"能减少状态总数"的人——状态空间没变,变的只是访问次序与剪枝时机。
枚举排列时候选数里有两个相等的数(如两个 )。当前位已经试过第一个 并回溯后,遇到第二个 应该直接跳过,原因是( )。
考点:等效冗余排除(H4)。
(H4)考点:两个数值相等的候选数产生的分支完全等效:第一个 试过的所有后续排列,第二个 会原样再来一遍——跳过它是排除等效冗余,不是剪枝加速的巧合。
解析:重复元素是排列去重的经典坑:不跳过则每个排列按"用了哪个 "成倍重复输出。
排除法:选"相加会溢出"的人与溢出毫无关系;选"相同数不能同现一个排列"的人,两个 完全可以同时出现在排列里;选"下标大会越界"的人,跳过与否都不越界。
搜索求最大和,累加的数可能全为负数。把答案变量初始化为 ans = 0 会( )。
考点:答案变量初始化(H5)。
(H5)考点:求最大值时 ans 初值必须足够小(极小值或第一个可行解):值可能全为负时,初值 会让所有负解都过不了 sum > ans 的更新条件——漏掉全负解。
解析:更新条件是严格大于,初值若高过真正的最优,最优永远进不来;全正数据下用 碰巧安全,全负数据立刻翻车。
排除法:选"没有任何问题"的人只在全正数据上成立;选"会死循环"的人,循环由枚举驱动,与初值无关;选"把最大和算成最小"的人,结果是不会更新,不是取反。
阅读程序(题意为: 个数对排成一行,每步选相邻两段合并成一段,代价与两段有关;程序枚举所有合并顺序求最大总代价):
01void dfs(int n, int sum) { 02 if (n == 1) { ans = max(sum, ans); return; } 03 for (int i = 1; i < n; ++i) { 04 int a = d[i-1][0], b = d[i-1][1]; 05 int x = d[i][0], y = d[i][1]; 06 // 合并:两段变成一段 (a+x, b+y),其余前移 07 int s = a + x + abs(b - y); 08 dfs(n - 1, sum + s); 09 // 恢复 d 数组 10 } 11}
输入 ,两个数对为 和 。程序输出 ans 是( )。
考点:合并段回溯追踪(H6)。
(H6)考点: 时 for 循环只有 一轮:、、、,,递归到 结算,ans 是 。(2020 年 CSP-J 阅读程序真题骨架的小数据化,回溯为大纲未明列、初赛实考)
解析:只有相邻两段时"合并顺序"唯一,总代价就一项; 是两段第二分量之差的绝对值——别把 与 的加减看串。
排除法:选 的人把绝对值当成了 之外的 之类的错配;选 的人把 算成 ;选 的人少数了 。
某同学写的 BFS 在大图上队列膨胀到几十万个元素、内存暴涨。最可能的原因是( )。
考点:BFS 队列爆炸诊断(I1)。
(I1)考点:BFS 队列膨胀到几十万元素的头号原因:出队时才标记——顶点还排着队时未标记,被每个邻居各发现一次、重复入队,队列按指数级翻倍。
解析:修复只需把标记挪到入队时刻(E1 的标准写法);这属于"标记时机"错误,与递归、全局数组、连通性都无关。
排除法:选"用了递归写 BFS"的人——BFS 本就是队列迭代,不存在递归写法的问题形态;选"队列开成全局数组"的人,全局与否不影响入队次数;选"图是连通的"的人——连通图上标准 BFS 队列从不超过 个元素。
存无向边 时只执行了 add(1, 2),忘了 add(2, 1)。从 号点开始 DFS(图只有这一条边),能访问到的顶点个数是( )。
考点:漏加反向边的现象(I2)。
(I2)考点:只 add(1, 2) 时 g[2] 是空的——从 出发 DFS,一步也迈不出去,能访问到的只有 自己, 个顶点。
解析:无向边必须正反各存一次;漏了反向边,逆方向的遍历、度数统计全部失真(度数少算、可达误判)。
排除法:选 的人以为边还在双向起作用;选 的人,起点自身总会被访问;选"不确定"的人——g[2] 为空是确定的,访问数必为 。
链式前向星约定:边从 开始编号,head[u] 存 的第一条边号、无出边时为 ,遍历写 for (int i = head[u]; i != -1; i = nxt[i])(顶点编号从 开始)。某同学建图后从 号点遍历,程序访问了不存在的 号顶点并反复输出 。最可能的 bug 是( )。
考点:前向星忘初始化(I3)。
(I3)考点:head 未初始化为 (全局数组默认 ),遍历条件 i != -1 把 当成了合法边号,读出 to[0]、nxt[0] 里的 ——访问"顶点 "并在 号边上打转。
解析:哨兵值必须与初始化配套:约定 表无边,就必须在 main 里把 head 填成 (常用 memset 或循环赋值)。
排除法:选"to 开小了"的人,开小会越界崩溃或写坏别的数据,不会恰好访问顶点 ;选"编号应从 开始"的人与编号起点无关;选"cnt 忘清零"的人——全局 cnt 本就从 开始。
网格代码写成:
01if (grid[nr][nc] == '.' && 0 <= nr && nr < R && 0 <= nc && nc < C)
当 nr 或 nc 越界时,这个条件的问题是( )。
考点:先取格后判界(I4)。
(I4)考点:&& 自左向右短路求值:判界条件放在数组访问之后,越界的 nr/nc 会先被拿去取 grid[nr][nc]——数组越界访问已发生,程序未定义行为。
解析:正确顺序是"先界内、再取格"(F3 的条件);交换两半看似等价,实则在越界的那次求值上根本走不到判界。
排除法:选"顺序无所谓"的人只在永不越界时侥幸成立;选"只影响速度"的人——越界访问是正确性事故;选"只漏掉部分格子"的人,这是读野内存不是漏读。
在 个顶点的链形图()上从端点递归 DFS,程序崩溃。原因与对策是( )。
考点:链形爆栈的对策(I5)。
(I5)考点: 长链上递归 DFS 深度达 层,超过默认栈空间而崩溃;对策是改 BFS(队列迭代)或显式栈的迭代 DFS——遍历结果不变,栈深度由系统递归转为可控的堆/数组。
解析:递归深度=图上最长路径深;换成迭代写法后"栈"在堆上,容量按需分配,不再受系统栈限制。
排除法:选"数组开大一倍"的人——崩的是调用栈不是邻接数组;选"改用邻接矩阵"的人——存储换了递归照深;选"初始化 vis 后深度减半"的人,标记只防重复访问,不降低链形图的天然深度。
有 个顶点的无向图至少应该有( )条边才能确保是一个连通图。
1 #include <algorithm> 2 #include <iostream> 3 using namespace std; 4 5 int n; 6 int d[50][2]; 7 int ans; 8 9 void dfs(int n, int sum) { 10 if (n == 1) { 11 ans = max(sum, ans); 12 return; 13 } 14 for (int i = 1; i < n; ++i) { 15 int a = d[i - 1][0], b = d[i - 1][1]; 16 int x = d[i][0], y = d[i][1]; 17 d[i - 1][0] = a + x; 18 d[i - 1][1] = b + y; 19 for (int j = i; j < n - 1; ++j) 20 d[j][0] = d[j + 1][0], d[j][1] = d[j + 1][1]; 21 int s = a + x + abs(b - y); 22 dfs(n - 1, sum + s); 23 for (int j = n - 1; j > i; --j) 24 d[j][0] = d[j - 1][0], d[j][1] = d[j - 1][1]; 25 d[i - 1][0] = a, d[i - 1][1] = b; 26 d[i][0] = x, d[i][1] = y; 27 } 28 } 29 30 int main() { 31 cin >> n; 32 for (int i = 0; i < n; ++i) 33 cin >> d[i][0]; 34 for (int i = 0; i < n; ++i) 35 cin >> d[i][1]; 36 ans = 0; 37 dfs(n, 0); 38 cout << ans << endl; 39 return 0; 40 }
假设输入的 是不超过 的正整数,d[i][0]、d[i][1] 都是不超过 的正整数,完成下面的判断题和单选题。
若输入 为 ,此程序可能会死循环或发生运行错误。( )
若输入 为 ,接下来的输入全为 ,则输出为 。( )
输出的数一定不小于输入的 d[i][0] 和 d[i][1] 的任意一个。( )
若输入的 为 ,接下来的输入是 个 和 个 ,则输出为( )。
若输入的 为 ,接下来的输入是 个 和 个 ,则输出为( )。
若输入的 为 ,接下来的输入是 到 ,以及 到 ,则输出为( )。
对于有 个顶点、 条边的无向连通图 (),需要删掉( )条边才能使其成为一棵树。
以 a 为起点,对右边的无向图进行深度优先遍历,则 b、c、d、e 四个点中有可能作为最后一个遍历到的点的个数为( )。
考虑由 N 个顶点构成的有向连通图,采用邻接矩阵的数据结构表示时,该矩阵中至少存在( )个非零元素。
试补全程序。
1 #include <bits/stdc++.h> 2 using namespace std; 3 4 const int ROWS = 8; 5 const int COLS = 8; 6 7 struct Point { 8 int r, c; 9 Point(int r, int c) : r(r), c(c) {} 10 }; 11 12 bool is_valid(char image[ROWS][COLS], Point pt, 13 int prev_color, int new_color) { 14 int r = pt.r; 15 int c = pt.c; 16 return (0 <= r && r < ROWS && 0 <= c && c < COLS && 17 ① && image[r][c] != new_color); 18 } 19 20 void flood_fill(char image[ROWS][COLS], Point cur, int new_color) { 21 queue<Point> queue; 22 queue.push(cur); 23 24 int prev_color = image[cur.r][cur.c]; 25 ②; 26 27 while (!queue.empty()) { 28 Point pt = queue.front(); 29 queue.pop(); 30 31 Point points[4] = {③, Point(pt.r - 1, pt.c), 32 Point(pt.r, pt.c + 1), Point(pt.r, pt.c - 1)}; 33 for (auto p : points) { 34 if (is_valid(image, p, prev_color, new_color)) { 35 ④; 36 ⑤; 37 } 38 } 39 } 40 } 41 42 int main() { 43 char image[ROWS][COLS] = {{'g', 'g', 'g', 'g', 'g', 'g', 'g', 'g'}, 44 {'g', 'g', 'g', 'g', 'g', 'g', 'r', 'r'}, 45 {'g', 'r', 'r', 'g', 'g', 'r', 'g', 'g'}, 46 {'g', 'b', 'b', 'b', 'b', 'r', 'g', 'r'}, 47 {'g', 'g', 'g', 'b', 'b', 'r', 'g', 'r'}, 48 {'g', 'g', 'g', 'b', 'b', 'b', 'b', 'r'}, 49 {'g', 'g', 'g', 'g', 'g', 'b', 'g', 'g'}, 50 {'g', 'g', 'g', 'g', 'g', 'b', 'b', 'g'}}; 51 52 Point cur(4, 4); 53 char new_color = 'y'; 54 55 flood_fill(image, cur, new_color); 56 57 for (int r = 0; r < ROWS; r++) { 58 for (int c = 0; c < COLS; c++) { 59 cout << image[r][c] << " "; 60 } 61 cout << endl; 62 } 63 // 输出: 64 // g g g g g g g g 65 // g g g g g g r r 66 // g r r g g r g g 67 // g y y y y r g r 68 // g g g y y r g r 69 // g g g y y y y r 70 // g g g g g y g g 71 // g g g g g y y g 72 73 return 0; 74 }
①处应填( )
②处应填( )
③处应填( )
④处应填( )
⑤处应填( )
在无向图中,所有顶点的度数之和等于( )。
在一个有向图中,所有顶点的入度之和等于所有顶点的出度之和,这个总和等于?( )