椰程信奥·互动课件 专题15·搜索2:剪掉不看的,圈层找最短
10:00 1 / 18
🧭 椰程信奥 · CSP复赛专题系列

专题15 · 搜索2

上一讲我们学会了 DFS「一条路走到黑」。这一讲解决两个更实际的问题:
怎么让它别走冤枉路(剪枝),以及怎么保证第一次找到的就是最优解(BFS)。
三道题:八皇后、抓住那头牛、小猫爬山。

五种剪枝策略 BFS 模板与「首个解即最优」 八皇后 · 抓牛 · 小猫爬山
地图

从"能搜出来"到"搜得快、搜得对"

上节回顾:DFS 的三个动作

① 从当前状态出发试每一种可能;
② 走到走不动为止(一条路走到黑);
③ 退回来换一条路(回溯)。
代价:搜索树规模随层数指数级膨胀。

本节两块内容

块一 · 剪枝:在搜索树上"提前砍掉不可能的分支",让指数变小。
块二 · BFS:改用队列一层一层扩散,天生保证"第一次到就是最短"。

⚠️ 先判断题目要什么,再选武器: 要方案数 / 可达性 / 所有解 → DFS;要最短、最快、最少步数 → BFS。 选错了,剪枝再好也救不回来。

① 八皇后

典型 DFS + 冲突判断,答案 92。

② 抓住那头牛

典型 BFS 最短路,首个解即最优。

③ 小猫爬山

剪枝策略的综合应用。

核心

剪枝:在搜索树上"提前说不"

剪枝 = 明知道走下去没用,就提前回头根① 可行性剪枝:已经不可能合法② 最优性剪枝:已经不比当前答案好③ 记忆化:这个状态算过了④ 改变搜索顺序:先试更可能的⑤ 数学剪枝:用公式直接排除一批红色虚线圈 = 被剪掉的分支。剪得越早,省下的子树越大。

先看数字有多可怕:一层 3 个分支、深 15 层,就是 3¹⁵ ≈ 1400 万个节点; 深 20 层是 35 亿。剪枝不是"优化一点点",是决定能不能在 1 秒内跑完。

① 优化搜索顺序

先搜分支少的节点(如小猫爬山按重量从大到小放)。 越早找到一个好的可行解,后面的最优性剪枝越有效。

② 排除等效冗余

几种选择本质一样时只搜一种。例:小猫爬山里"放进 1 号车"和"放进 2 号车", 若两车剩余载重相同,是等效的 → 只试一个。

③ 可行性剪枝

往下走之前先检查这个状态合不合法:迷宫越界、重量超限、坐标出界…… 不合法就立刻返回,不再继续递归。

④ 最优性剪枝

已知一个可行解代价为 best,若当前累计代价已经 ≥ best, 再往下只会更差 → 整棵子树砍掉。这是最立竿见影的一条。

⑤ 记忆化

把算过的状态存下来,下次直接查表(上一讲已讲)。注意:它只适用于 状态可重复出现且结果与路径无关的题,八皇后、小猫爬山这类"路径相关"的题不适用。

⚠️ 剪枝的前提是"砍掉的确实没有用"。 拿不准时先对拍:写一个不剪枝的暴力版,小数据下比对答案,确认剪枝没剪错再交。
3D🎲 剪枝 = 整棵子树直接砍掉🖱 拖拽旋转 · 双击复位
为什么用 3D:剪枝的价值在于「省掉了多少」。立体搜索树上被剪掉的枝整片变灰,省下的规模是可数的。
动画①

同一棵树:剪枝到底省了多少

节点里的数字是代价,路径代价 = 从根到叶子的累加。目标是找最小。 已知当前最优解时,若累计代价已经 ≥ 最优,这棵子树就不必再看。

点「走一步」开始。先切到「不剪枝」看它有多笨。
核心

一层一层扩散,所以第一次到就是最短

为什么队列能保证最短

队列是先进先出:先处理距离 0 的所有点 → 再处理距离 1 的 → 再距离 2 的…… 也就是说所有节点按"距离"从小到大出队。
因此某个节点第一次被访问时,走的肯定是最短路。

与 DFS 的结构差异

DFS 用栈(递归),是一条路走到底再回头;
BFS 用队列,是所有路齐头并进。
DFS 找"有没有",BFS 找"最少几步"。

BFS 四步框架(背下来)

① 初始状态入队,并标记已访问(入队时就要标记,不是出队时)
② 队列非空时,取出队首 x
③ 若 x 是目标 → 答案就是 dist[x],直接结束
④ 枚举 x 能到达的每个 y:没越界 且 没访问过 → 标记、dist[y]=dist[x]+1、入队
⚠️ 最高频的错误:出队时才判重。 那样同一个点会被多个前驱重复入队,队列炸掉、复杂度退化。 正确做法是"入队即标记"。
动画②

盯着队列看:它怎么一层层长大

从 1 号点出发。每步做一件事:取队首 → 把它的邻居入队。
看什么:队列里始终保持着"这一层的点都在下一层的点前面", 这就是 BFS 正确的充要条件。
代码

抓牛的 BFS:每一行都有理由

#include <iostream>
#include <queue>
using namespace std;

const int MAXP = 200000;
int dist[200005];                       // ① dist 兼当 visited:-1 = 没走过

int main(){
    int n, k; cin >> n >> k;
    for (int i = 0; i <= MAXP; i++) dist[i] = -1;   // ② 初始化别漏

    queue<int> q;
    dist[n] = 0; q.push(n);             // ③ 起点:入队 + 标记(同时做!)

    while (!q.empty()){
        int x = q.front(); q.pop();
        if (x == k){ cout << dist[x] << '\n'; return 0; }  // ④ 首次到达即最短
        int nx[3] = { x - 1, x + 1, x * 2 };
        for (int i = 0; i < 3; i++){
            int y = nx[i];
            if (y < 0 || y > MAXP) continue;       // ⑤ 可行性剪枝(越界)
            if (dist[y] != -1) continue;            // ⑥ 判重:走过的不回头
            dist[y] = dist[x] + 1;                  // ⑦ 入队即标记
            q.push(y);
        }
    }
    return 0;
}

为什么上界是 200000

K ≤ 100000,走到 200000 以上必然绕远, 不可能比"先走到 K 附近"更优。

为什么能直接 return

BFS 按层扩散,第一次弹出 K 时 dist 已经是最小值。后面再有也只是更长的路。

数组开多大

MAXP = 200000,数组开 200005。 宁可稍大,不要刚好——x*2 很容易撞边界。

例题1

八皇后:把"不能互相攻击"翻译成三个数组

第一步:降维(最关键)

N 个皇后放 N 行 → 每行恰好一个。
于是不用"在 N² 个格子里选 N 个",只需按行递归,每行决定放在哪一列。
搜索规模从 C(64,8) 降到 8⁸ 量级,再靠冲突判断砍掉绝大多数。

第二步:冲突怎么判

不同行(天然满足);
同列 → col[c];
主对角线(↘) → 同一条线上 行 − 列 恒定;
副对角线(↙) → 同一条线上 行 + 列 恒定。

③ 一个必踩的下标坑

行 − 列 会出负数(如第 1 行第 8 列 → −7),直接当数组下标就越界。 统一加一个偏移量:d1[row - c + n],副对角线 d2[row + c] 最大 2n,数组开 2n+5。

int a = row - c + n;   // 主对角线:偏移 +n 防负数
int b = row + c;       // 副对角线:范围 2 ~ 2n
if (col[c] || d1[a] || d2[b]) continue;   // 任一被占 → 换一列
⚠️ 标记后一定要回溯:col[c] = d1[a] = d2[b] = 0; 忘了恢复,后面的行会误判成"冲突",解数会变少(常见错误答案:0 或 1)。
动画③

看它怎么试错、怎么回头

N =
点「下一步」:从上到下逐行试放,冲突会红闪并换列。
盯住三个标记数组:每放一个皇后, col/d1/d2 各点亮一位;回溯时三个同时熄灭——漏灭一个就全盘皆错。
代码

完整实现 + 为什么是 92

int n, col[20], d1[40], d2[40];
int total = 0, printed = 0;
vector<int> path;

void dfs(int row){
    if (row > n){                        // ① 最后一行也放好了 → 一个解
        total++;
        if (printed < 3){                // 只打印前 3 个解
            for (int i = 0; i < n; i++){ if (i) cout << ' '; cout << path[i]; }
            cout << '\n'; printed++;
        }
        return;
    }
    for (int c = 1; c <= n; c++){        // ② 枚举列
        int a = row - c + n, b = row + c;
        if (col[c] || d1[a] || d2[b]) continue;      // ③ 冲突 → 试下一列
        col[c] = d1[a] = d2[b] = 1;      // ④ 现场标记
        path.push_back(c);
        dfs(row + 1);
        path.pop_back();
        col[c] = d1[a] = d2[b] = 0;      // ⑤ ★ 回溯恢复,漏了就 WA
    }
}

N=8 → 92

这是全解计数,不是"找到一个就停"。 所以不能在找到一个解后 return。

要不要剪枝

八皇后本身就是靠冲突判断做可行性剪枝。 想要更快可以上位运算优化(用 int 的位表示占用情况)。

常见 WA

① 忘了回溯;② 对角线下标没 +n 偏移; ③ 数组开小(d2 要 2n);④ 找到一个解就 return。

例题2

把"移动方式"翻译成"图的边"

建模

状态 = 农夫所在的坐标 x(一个整数);
边 = 三种移动:x−1、x+1、x×2,每条边长都是 1;
起点 = N,终点 = K;
求 = 最短路长度。
这就是标准的无权图最短路 → 直接 BFS。

样例 5 → 17

5 → 10(×2)→ 9(−1)→ 18(×2)→ 17(−1),共 4 步。
注意它先冲过头再往回退:×2 增长很快,往回退一点反而更短。 这也说明贪心(一直往 K 靠近)是错的,必须 BFS。

⚠️ 为什么不能 DFS:DFS 会一条路走到底(比如一直 ×2 冲到 20 万), 找到的路径不是最短,而且深度极大容易爆栈。求"最少步数"一律先想 BFS。
动画④

看距离是怎么一圈圈铺开的

农夫在 N,牛在 K。每步把队首能到的三个位置入队。
动画⑤

剪枝策略的综合应用

N 只小猫要坐缆车下山,每辆缆车最大载重 W,求最少需要几辆。 三种剪枝同时上场:按重量从大到小放(优化顺序)、放不下就换车(可行性)、 车数已经 ≥ 已知最优(最优性)。

先按重量从大到小排好序,再一只只放。

为什么从大到小

重猫选择少、更难安排,先处理难的能更早暴露矛盾、 更快剪枝;反过来先放轻猫,最后重猫无处可去,要试到很深才发现。

最优性剪枝

当前已用 c 辆车,已知最优 ans。 若 c >= ans,再怎么放也不会更好 → 整枝砍掉。

等效冗余

多辆车剩余载重相同时,放进哪辆都一样 → 只试一辆, 避免重复搜索等价方案。

动画⑥

同一张图,两种走法

切到 BFS,看队列怎么一层层铺开;切回 DFS,看它钻到多深才回头。

DFS 适用

方案数、全排列、可达性、连通块、所有解。 用递归,注意回溯恢复现场和栈深度。

BFS 适用

最短步数、最快、最近、最少次数。 用队列,注意入队即判重和状态上界。

辨析

四个高频失分点

① 判重时机

❌ 出队时判重 → 同一个点被多个前驱重复入队,队列爆炸。
✅ 入队那一刻就标记 dist[y]=dist[x]+1。

② 回溯漏恢复

❌ 八皇后只标记不清零 → 解数变少甚至为 0。
✅ 标记与恢复成对出现,写了 =1 就找对应的 =0。

③ 对角线下标负数

❌ d1[row-c] 直接越界(或未定义行为)。
✅ 统一偏移:row-c+n;数组开到 2n+5。

④ 剪枝剪错了

❌ 最优性剪枝写成 cum > best 却在找到解前 best 没初始化 → 剪光。
✅ best 初始为无穷大;且只在确认代价单调不减时才敢这么剪。

知识树

搜索2 ─┬─ 剪枝(优化顺序 / 等效冗余 / 可行性 / 最优性 / 记忆化)
     └─ BFS(队列 / 入队即判重 / 首次到达即最优 / 适用"最少"类问题)

拿分

一道题 100 分是分档给的——不会正解也要先拿保底

复赛一道题的 100 分由十几到二十几个测试点组成, 前面几档专门为「没想出正解的人」准备。四道题各拿 30~40 分, 加起来往往就是一等奖和二等奖的分界。

第 1 档 特判 5~10 分 约 2 分钟 第 2 档 暴力 20~30 分 约 8 分钟 第 3 档 特殊性质 40~60 分 约 15 分钟 第 4 档 正解 100 分 约 25 分钟

① 特判档:先别想算法,看数据范围表

题目给的数据范围表就是出题人给你的送分清单。看到「n = 1」「只有一组数据」这类一行, 就先写个 if 直接输出答案。2 分钟换 5~10 分,全场最划算。

② 暴力档:按题意最直白地写一遍

多重循环、DFS 全枚举、朴素 O(n³)……不要优化,只要保证最小那档全对。 它既是保底分,又是后面对拍的标尺——没有它你无法证明正解是对的。

③ 特殊性质档:题目里那句「若……」

常见的送分性质:数据已经有序、所有值完全相同、规模小到可以 O(n²)、 只出现一种类型。题目写出来就是让你拿的,专门写一份即可。

④ 正解档:思路定了,就赢了八成

考场上的时间几乎都花在想思路上,不是写代码。写完先跑样例, 再用第 ② 档的暴力版造小数据对拍——这是唯一能证明你思路对的办法。

考场纪律:每档设时间上限,到点还没调通就 立刻提交当前版本,保住已有分数,再往上冲。最常见的翻车是"正解写了 50 分钟没过,连暴力分都没交上去"。
拿分

本讲 2 道题,各自的四档怎么走

八皇后 · QUEEN

第1档 · 2 分钟
n == 1 → 1

第2档 · 8 分钟
枚举所有排列再判对角线,n ≤ 8 稳过

第3档 · 10 分钟
只做「不同行不同列」(不判对角线),先拿部分分

第4档 · 15 分钟
逐行 DFS + 列 / 主对角线 / 副对角线三个标记数组

抓住那头牛 · COW

第1档 · 2 分钟
N ≥ K → 只能往回走,答案 = N − K

第2档 · 8 分钟
DFS 枚举所有路径取最短,深度 ≤ 15 稳过

第3档 · 10 分钟
只做 ±1(不做 ×2),先拿部分分

第4档 · 15 分钟
BFS 三方向扩展 + vis 数组,上界 0..100000

50 分钟时间盒(一题的节奏):
0–5 分钟 读题 + 圈出数据范围表里最小的那几档 → 5–15 分钟 写完第 1、2 档并先交一次(保住 20~30 分)→ 15–40 分钟 冲第 3、4 档 → 40–50 分钟 造小数据对拍、检查 freopen 与文件名。
课堂上别让学生"从头想正解"。先要求每个人 15 分钟内交出第 1、2 档, 再放开冲正解。这个顺序一换,平均分通常能涨一大截——因为它杜绝了"想了 40 分钟,交上去 0 分"。
检测

四道题,做完就知道会不会了

① BFS 为什么"第一次到达就是最短"?

② 八皇后回溯时忘了清零标记,会怎样?

③ 抓住那头牛,农夫在 5、牛在 17,最少几步?

④ 小猫爬山为什么要按重量从大到小放?

椰程锦囊:检测③可以让学生先猜再验证——多数人会猜"一直往 17 靠近", 实际最优是先 ×2 冲到 10 再绕。用这个反差讲清"贪心不成立,BFS 才保证最优"。

目录