wp_图论与搜索
图论与搜索
DFS 深度优先搜索
用途:枚举、路径搜索、连通性。
常见坑:注意回溯还原(如标记数组清零),加剪枝防超时。
F. Parabola Independence 寻找最常路
- 核心模型:寻找最长路,数学几何性质
- 修正逻辑 (Patch):把图分为上下两部分,按照规则建立DAG,通过dfs(好久没写好生疏)找到上下对于这个点而言最长路径,夹一下出结果
- 难点:数学几何性质
- 关键代码:
1 | struct line |
流沙(dfs找子树)
- 核心模型:,贪心,继承,dp
- 思维误区 (Bug):
- 修正逻辑 (Patch):
- 关键代码:
1 | void solve() |
分治->Generate01String
- 核心模型:括号匹配,单调性分治递归搜索树
- 思维误区 (Bug):第一时间没有观察到其树状递归单调性
- 修正逻辑 (Patch):注意递归顺序最大的出发点把-1和n看作一个虚空大括号,每加上一个小括号子树进去就要加一次
- 关键代码:
1 | void solve() |
小猫爬山
1 | int n, w, ans = 20; |
复杂度:O(2^n) 最坏,剪枝后更优。
E2. Interactive Graph (Hard Version)
- 核心模型: 字典序 + 计数:核心的跳跃量 c[u] 本质是在字典序排列里做区间跳跃,跟数位DP的思想非常像
- 思维误区 (Bug):
- 修正逻辑 (Patch):
- 关键代码:
1 | void solve() |
静海拾光
-
核心模型:跟E2几乎一样的东西,注意到字典序构成的树dfs即可
-
关键代码:
1 | void solve() |
树上问题
D2.Tree coloring树上染色
- 核心模型:树上染色
- 做法:事实上这就是一道大模拟。题目要求不是父子关系(好求),不是同一层。我们就直接把每一次的染色都分层不同的颜色。
- 错误点:容易想贪心:我想要在同一层里,每个同学都有自己**“绝对不能碰的禁忌色(亲爹色)”**。
纯贪心的逻辑是:“轮到我了,桌子上只要有我能用的,我挑个最小的拿走,不管别人死活。”
这就必然会导致一个惨案:前面的人为了图省事,顺手拿走了一个极其普通的颜色,结果把后面的人逼上了绝路,导致原本够用的蜡笔,硬生生不够用了! - 关键代码:
1 | void solve() |
流沙树上dp
- 核心模型:树上dp(涉及图论概念:子树:包含自己还有自己的所有儿子)。树上贪心 (赛时一发过只是放在这参考一下树上的遍历
- 关键代码:
1 | void solve() |
国士知遇豫让心树上dp
- 核心模型:给定一棵树,每个节点可以赋予一个(或者)。问你有多少种赋值方案,使得赋值完毕后从根节点到达每一个叶子节点的唯一简单路径所经过的点构成的括号串是一个合法括号串
- 修正逻辑: 注意到合法括号串的trick:把左括号变成-1,右括号变成1,最后相加等于0.那么我们定义
dp[i][j]为在点i的前缀和为j的方案数,考虑转移。难点:$$dp[u][j] = \prod_{v \in son(u)} dp[v][j]$$ 用乘法的原因是:你必须同时满足: 子树往下所有的叶子路径必须合法(假设满足状态 的有 种填法)并且 子树往下所有的叶子路径必须合法(假设满足状态 的有 种填法), - 既然这两件事必须同时发生且互不干扰,那么在状态 下, 节点往下生长的总合法方案数自然就是 。
- 关键代码:
1 |
|
K. 星云桥 III(图上dfs)
- 核心模型:给定一张 n 个点 m 条边的无重边无自环的连通无向图。这张图的所有简单环是否都满足其大小恰好等于5。若这张图没有简单环,也视为满足条件。
- 解法:把这题转化成在一颗树上找环,假如环的大小不等于5就错。wtf->树的深度。树上边差分来统计每条树边被“非树边(返祖边)”覆盖的次数。这也是在不写 Tarjan 算法的情况下,离线寻找桥(割边)或判定仙人掌图(Cactus Graph)的经典解法。一个图跑完树上边差分,发现所有的树边 cnt <= 1(每条边最多被一个非树边覆盖,即最多在一个环里),并且非树边本身也没有重边,那么你就可以在赛场上果断判定:这是一个仙人掌图。有环套环直接判否就好
- 关键代码:
1 | void solve() |
BFS
P1434 [SHOI2002] 滑雪
- 链接: 题目链接
- 算法类型: 拓扑dp
- 此为标准代码请认真研读
- AC 代码:
1 | vi dr = {1, 0, -1, 0}; |
- 思路:
-
- 建图(这里用反图更方便)
- 2.\ 计算入度
- 3.\ 入度为0的点入队
- 4.\BFS:
- 5.\ 答案 = max(dp[])
-
1 | //BFS |
BFS例题:P1443 马的遍历
- 题号: P1443
- 链接: [题目链接]https://www.luogu.com.cn/problem/P1443
- 算法类型: BFS
- AC 代码:
1 |
|
- 注意事项:
- 记得BFS不要漏了
bool visit数组 - 记得BFS状态转移(?)方程
mp[hsh][ljl] = mp[h][w] + 1; - 最短路问题 不是“另一种算法”,它就是 DP 在无权图上的高效实现
- 记得BFS不要漏了
- 改进思路:
- 考虑严格 cnt == k 的情况,调整 check 函数。
P1825 [USACO11OPEN] Corn Maze S
- 题号: P1825
- 链接: 题目链接
- 算法类型: BFS
- 记录原因:
- AC了,终点看引参数进数组!不要天天被神秘UB卡住脑子
- 这道题无非是标准BFS上面加了个规则函数:这道题放这里就是教你如何写有规则的搜索
- AC 代码:
1 | void goincsm(const vector<vector<int>> &mp, int &xx, int &yy) |
- 注意事项:
- 取地址代表可更改
int &xx:适用于那些总是要更改的量 const vector<vector<int>> &mp既安全又高校的传图方式
- 取地址代表可更改
P2895 [USACO08FEB] Meteor Shower S
- 题号: P2895
- 链接: 题目链接
- 算法类型: BFS
- 错误原因:
- 边界检查边界检查边界检查
- 时间更新逻辑
- vis数组限制逻辑(依旧时间更新逻辑)
- AC 代码:
1 | vi dx = {-1, 0, 1, 0}; |
- 注意事项:
- 注意:这道题没有规定地图大小,对于流星影响(限制点)最大可到301*301.所以我们的搜索应该开到303(最保险),包括continue地搜索限制
- 注意time更新逻辑:lily只能在ti之前到达这个点,所以对于每个影响的点需要参考的是time+1;
- 注意bfs每层地更新逻辑:每次入队都是time(i)时间点可到地所有点
- 结构体lambda搜索技巧:
1 | 根据ti寻找bar中所需要地项,返回迭代器 |
P1162 填涂颜色 提供深搜广搜两种做法
- 题号: P1162
- 链接: 题目链接
- 算法类型: 搜索板子
- 错误原因:
- 注意从四周寻找‘0’切入
- AC 代码广搜版:
1 | vector<vector<int>> mp(35, vi(35)); |
- AC 代码深搜版:
1 | void dfs(int a, int b, vector<vector<int>> &mp) |
- 注意事项:
- 注意从四周引入点
- 注意入队时机
- 改进思路:
- 学习两种搜索方式
无向图分组
用途:求连通分量数。
常见坑:双向边需存两次,初始化标记数组。
1 | vector<vector<int>> a(2e5 + 10); |
复杂度:O(n+m),空间 O(n+m)。
图论:常见做法:反向建边
P4017 最大食物链计数(拓扑排序)
- 题目概述 给出一张有向无环图,求出最长路径的数量(最长路径定义:入度为0的点到初读为0的点),n是节点数量,m是路径数量
- 数据范围:n:2e3,m:1e5
- 初始思路 ;记忆化加DFS
- 正解思路:DFS+DP
- AC代码
1 | int mod = 80112002; |
- AC代码:拓扑排序
1 | void solve() |
- 拓扑排序的目标是将所有节点排序,使得排在前面的节点不能依赖于排在后面的节点。
- 作用:
- 确定任务执行顺序
- DAG 上的动态规划
- 检测环路:如果拓扑排序无法将所有节点都加入到最终的序列中(
- **“顺序”、“依赖”、“先决条件”,或者需要在一个有向图中进行基于依赖的计算(如 DP)时
差分约束
倍杀测量者
- 核心模型:
- 思维误区 (Bug):
- 修正逻辑 (Patch):建立D[0]为基础点,逻辑是当维护|
x[v] == x[u] + w|u -> v, w和v -> u, -w就这样加进来两条边。超级源点只是保证每个点至少入队一次 - 关键代码:
1 |
|
图论:dijkstra
简单 Dijkstra 模板题
P4779 【模板】单源最短路径(标准版)
- 题号: P4799
- 链接: 题目链接
- 算法类型: 图论模板
- AC 代码:
1 | void solve() |
- 注意事项:
- 注意Dijkstra算法用最小堆优化可以时间复杂度最低
- Dijkstra算法只能处理非负权路径问题
- 为什么不用队列?:贪心最快,如果是菊花图复杂度会退化到nm
- 含负权路用什么算法?用队列
- 思路:
- 认真研读并学习114514次
水群
- 题号: D
- 链接: 题目链接
- 算法类型: Disjkstr最短路
- 错误原因:
- 看成DP了!!有向无环有向无环有向无环!
- 迪克算法还在追我
- 最短路问题
- AC 代码:
1 | void solve() |
- 思路:
- 没什么好说的,迪克算法模板,我都懒得贴出代码,详情请见[简单 Dijkstra 模板题]<#p4779-模板单源最短路径标准版>
代号N
精简题干: 定义一棵树:共n个节点:一个度数为3的节点,若干个度数为2的节点,3个度数为1的节点.给出n-1条边,给出两个端点以及边权,可以操作k次将某条边边权变成0.求根节点到叶节点的度数最大值的最小值.
- AC 代码:
1 | struct edge |
- 思路剖析:
- 非常简单的大数据和的最大值的最小值.解法一:
sort之后逐个pop_back()(注意vector的pop是最后一位,所以要从小到大排序).解法二:维护最大根堆priority_queue<int>,n次操作复杂度nlogn. - 事实上解法一是优解,但是为了学习这个有意思的容器这道题用的做法是最大根堆
- 这道题的难点是读懂题…然后是建带权无向图…依旧模板.
- 记得带权无向图两个端点都pushback一次
- 然后就是用bfs带着上一个节点和下一个节点广度优先探索(这个还能用dfs解法,下文附上,作为图论路径转移学习,请严肃学习114514次)
- 非常简单的大数据和的最大值的最小值.解法一:
大部分图论用BFS做,在面临剪枝需求/需要回溯/所有方案的时候用DFS做
-
细节注意:
- 二维拓展数组记得这样开
vector<vector<edge>> mp(n + 1);不会爆空间 - Cpp17不支持结构体取地址,就这么写
auto [now, par] = pos.front(); - 记得k次操作不一定用玩一定要提前出循环
if (pq[idx].empty()) break;以免UB
- 二维拓展数组记得这样开
-
新结构
priority_queue: 自动维护堆的“插入+弹出最大/最小”工具,“贪心/最短路/Top-K/滑动窗口最大值” 都能靠它快速实现- 注意优先队列没办法删除除了堆顶以外的元素,所以注意
if (d > dist[u]) continue;
冲向黄金城
- 解法: 题目要求求出每个点是否可达->到达每个点的代价越小最后能够遍历到的点的数量越多->dijkstra-> 普通dij是靠距离作为key(代价)来排序,我们这里的代价一个是“到达的时间”一个是“距离”->哪种状态能给我未来留下最大的操作空间?->在这道题里,“未来的操作空间”是由剩下的车票数量决定的,所以“到达的车票序号”自然就成了至高无上的第一关键字
- 优化: 这里为了快速找到下一次用到的车票和时间需要做一个RMQ问题来确定。我们用dij’均摊了m的复杂度就需要想办法优化k的复杂度了->静态的就用st表
- 关键代码:
1 | struct STTable |
floyd变体
括号路径
- 核心模型:看到数据范围和问法想到离线floyd,从floyd的初始化想,我门可以做一个类似区间dp的东西。考虑合法的括号子序列,松弛操作有两种,一种是往两边扩张,一种是和别的合法括号并排。dij是nmlogn的,数据范围支持我们一项一项转移。核心思路类似于floyd的第一维dp变形
- 关键代码:
1 | struct kuo |
最小生成树
MC0573潜入相府中
- 核心模型:三分+kruskal
- 思维误区 (Bug): n,m,L ( 1≤n,m≤10^6 1≤L≤10^9 ) 如果每次都暴力sort跑克鲁斯卡尔,复杂度是 O(调用次数 * m log m)。爆了。我们家一个小小的优化跑三路归并
- 关键代码:
1 |
|
分层图
逃出生天
- 核心模型:一句话概括题意/数学本质 (如: 中位数贪心 / 差分约束)
- 思维误区 (Bug):记录第一直觉为什么错了 (如: 以为是DP其实是贪心 / 读错题)
- 修正逻辑 (Patch):下次看到什么特征,要修正为正确思路
- 关键代码:
1 | vi dx = {1, 0, -1, 0, 0}; |
G_Exploration
- 核心模型:分层图dp
- 思维误区 (Bug):一开始看见权值/2地往下递减错误的想用bfs。bfs本质上就是一点一点枚举路径,复杂度是指数级别的
- 修正逻辑 (Patch):ce[v][k] 是指:从v出发走k步最大地值。题目是要求/边权,我们可以反向思考成从某点出发*边权。可以证明出单调性
- 发现重复子问题 → 想到DP
- 发现步数上界只有30 → 把步数作为DP的维度
- 发现正向状态太多 → 翻转问题方向
- 关键代码:
1 | void solve() |
trick:容斥与路径数
-
情景:已知条件:
- 从起点到 A 的路径数
- 从起点到 B 的路径数
- A 在 B 的左上方(A 可以在 B 之前被经过)
-
想求:
- 从起点到 B、且途中没有经过 A 的路径数
-
解法:到B且不经过A = 到B的所有路径 - 到B且经过A的路径 = 到A的路径 × 从A到B的路径
- 子问题1:求0,0到点A的路径数:相当于向下走Ax步向右走Ay步-> nCr(Ax+Ay,Ax);
- 子问题1推广到从 (prex, prey) 走到 (x, y):->nCr(abs(x - prex) + abs(y - prey), abs(x - prex))
- 因为是要算很多个点递推关系所以dp方程变为:
dp[j] = (((dp[j] - (dp[k] * nCr(abs(x - prex) + abs(y - prey), abs(x - prex)) % MOD)) + MOD) % MOD) % MOD;
G. Path Summing Problem
-
核心模型:见解法一容斥dp
-
题解:如题意容易想到贡献法:用贡献法转化为“每种数字对结果的贡献”
-
正着想可以想到容斥,反着想可以想到枚举不经过某个点v的路径数量
-
然后我们发现容斥的解法只能靠sz方的枚举点,用全是1可以卡掉.然后想到普通dp->意思是逐步算出不经过某个数字的点的路径数量,但是会被全是1不同的数字卡掉
-
所以想到根号分治
-
然后开始枚举,因为当sz(也就是某个数字arr[i]出现的次数)<B的时候容斥最好,大于的时候普通dp最好
-
关键代码:
1 | void solve() |
bitset优化bfs
B.《金牌题》
- 核心模型:
Alice 和 Bob 正在一张有向连通图上探索。两人具有不同的起点。
对于两人来说,每秒同时进行以下的操作:
如果当前所在的顶点有一条或多条出边,他们会任意地沿着一条出边移动到相邻的顶点;否则,如果他们当前所在的顶点没有出边,他们会停留在该顶点。
现在 Wensy 想知道,在某一时刻,他们二人是否有可能相遇呢?
- 思维误区 (Bug): 第一直觉想知道:对于每个点每个人可能到得时间,复杂度会爆炸。然后第二想法是对于这一个时间每个人在的点有没有交集。考虑状态空间n^2,只能容忍再多根号n得运算,如果遇到超级稠密图有可能
- 修正逻辑 (Patch):设 是 Alice 在第 步可能到达的所有顶点的集合。设 是 Bob 在第 步可能到达的所有顶点的集合。两人在第 步能相遇的充分必要条件是:集合 和集合 有交集。
- 在时间维度上这道题卡bitset
- 卡常教学:
A.set(x)->A[x]=1if (A.test(i))->if (vis[i] == 1)if ((A & B).any())->bool meet = false;for (int i = 1; i <= n; i++) {if (A_vis[i] == 1 && B_vis[i] == 1) {meet = true;break;}}if (meet) ...next_A |= mp[i];->状态转移:转移到下一个点,意思是A下一步可能会在哪
- 关键代码:
1 |
|
- 均摊下的可过做法(不需要优化卡常)
1 |
|
tarjan
图的遍历(tarjan算法)
-
题目描述:给出一张有向图(不保证无环),节点编号1到n,求每个节点能到达的最大编号
-
错误解法:我一开始想到的:dfs加记忆化,从入度为0的点开始dfs到出度为0的点,每个点的答案在确认的时候和自己还有其他的路径比较一下;
- 错解中的逻辑问题
- 只D入度为0的点,但是当图中出现环就会漏掉
- 为了剪枝设计了vis数组来标记有没有被访问过,但是没有重置:这个题目不保证无环,所以必须重置,甚至说这个vis的存在就没啥必要
- 在有环图中,当 DFS 访问到一个正在递归栈中的点时,说明遇到了环。正确的 DFS 应该使用三态
- 在ans中要先初始化ans[i]=i,因为每个点都可以到达自己。以免出现没有连通的点
- 错解中的逻辑问题
vis在以下情况不需要重置:当你的图是有向无环图 (DAG).或者你对每个点的计算结果是确定的、最终的、且不会随起点变化时,你可以设置 vis 后不再变回
当他作为强连通分量 (SCC) 标记也可以不重置
比如说在这道题里面,当你反向建图让他从最大数字的点开始DFS的时候vis就可以选择不重置:因为N在这条路径上是最大的点,后面的状态可以通过继承前面的状态来确定答案
-
正解一:反向建图:原理:贪心保证剪枝成功(一旦一个点的答案 被确定,它就是正确的最大值,且永远不需要重新计算。)
- 问题: 为什么 可以永不重置?
- 思考: 当我们从 开始 DFS 时,如果 还没有 ,我们就设 。在此之前,所有的 都没有在 中到达 (否则 早就被标记了)。因此,没有比 更大的点是 可达的。
-
正解二:tarjan算法:求“缩点”操作的高效算法
- 缩点然后顺序dfs+dp。求完强连通分量可以保证图片是DAG
-
检查自己方案合理性的思考路径:
- 1.\ 图的特性:有向?无环?连通性?(有没有孤立的点)边权?(负权边?)
- 2.\ 做法检验:有没有环?依赖顺序是什么?状态是否能持久?(影响剪枝)(在 DAG 上,从拓扑序逆序(即从终点开始)计算是可靠的。原图 DFS 依赖于子问题的答案,必须确保子问题先被计算。)
-
AC正解1(反向图思路)
1 | void dfs(int a, int f, const vector<vi> &mp, vi &ans) |
- AC正解2(Tarjan思路)
1 |








