进阶图论学习笔记
零、杂项
-
P2323 [HNOI2006] 公路修建问题:这里在分析贪心问题时使用了先取满限制条件再试着替换优化的“倒推法”,觉得很妙。
-
P2491 [SDOI2011] 消防:蓝书上原题,可以利用树的直径的性质,优化极简的 \(O(n)\)。(感觉我可以做一个树的直径与树上贪心的专题。。。)
二、2-SAT
-
P6378 [PA2010] Riddle:通过建立新点,据传递性,优化了建边数量。
-
P3825 [NOI2017] 游戏:搜索 + 2-SAT
三、最小树形图
1. 朱刘算法
算法的核心思想:尝试使用 DAG 的方式贪心。如果贪出环了,就使用 反悔贪心 的方法把环 缩为一个点,迭代执行直到图上不存在环。
时间复杂度:\(O(nm)\)
算法流程:
事实证明如果还不能独立写出这个算法,只能说步骤总结得不够详细!
-
输入,按照 边集数组 的方式存储图。
-
开始迭代:
-
初始化。
-
为每一个点找到最小入边。注意根不应有最小入边,还应注意此处的边是否在同一
scc
内(即自环)。 -
将每一条新入边加入答案。如果此时发现有除了根以外的点没有入边,则无解。
-
找环(相当于在 内向基环树 上找环):
-
每次从一个点开始沿着边走,标记走到的点,直到找到被标记过的点或者走到根。
-
如果当前被标记过的点为从不同点出发标记的,跳过。
-
否则说明我们找到了一个新环。建立一个新的
scc
,再走一遍环,将所有点加入。
-
-
如果这一步没有找到任何环,说明贪心成功,可以终止迭代输出答案了。
-
否则我们还需要缩点。先对所有不在环内的点也建立
scc
。 然后对于每一条边,将其边权修改为与环内边权之差。
-
(总之就是当心一下根的特判,其余细节其实不多。)
例题:
2. Tarjan
以后我实在闲着没事再来学这个。。。
四、网络流
五、二分图
下面内容全部都是从蓝书抄的
1. 定义
一张无向图的 \(n\) 个节点可以分成 \(A, B\) 两个非空集合,且同一集合内的点之间都没有边相连,那么称其为二分图,\(A, B\) 分别成为二分图的左部和右部。
2. 二分图判定
用 DFS 染色法。将第一个点染为黑色,并将其相邻的点染为白色;往下递归,将每个点相邻的点染为相反的颜色,如果矛盾说明不是二分图。
- P1525 [NOIP2010 提高组] 关押罪犯:二分 + 二分图判定
3. 二分图最大匹配
-
算法
-
匈牙利算法:核心在寻找匹配边、非匹配边交错出现的“增广路”。时间复杂度 \(O(nm)\)。
点击查看代码
#include<bits/stdc++.h> using namespace std; const int MAXN = 505, MAXM = 5e4+5; int n, m, e, head[MAXN], mth[MAXN]; bool vst[MAXN]; //head 仅用来标记左部点:右部点不需要进行尝试,只需要找到 match 的对象即可 //vst, mth 仅用来标记右部点:对于任意左部点,只要对应的右部点没有 match,则一定没访问 struct node{ int to, nxt; } edge[MAXM]; inline void Add_edge(int i, int from, int to){ edge[i].to = to; edge[i].nxt = head[from]; head[from] = i; return; } inline bool DFS(int x){ for(int i = head[x]; i; i = edge[i].nxt){ int to = edge[i].to; if(vst[to]) continue; vst[to] = true; if(!mth[to] or DFS(mth[to])) {mth[to] = x; return true;} } return false; } int main(){ scanf("%d%d%d", &n, &m, &e); for(int i = 1; i <= e; i++){ int ui, vi; scanf("%d%d", &ui, &vi); Add_edge(i, ui, vi); } int ans = 0; for(int i = 1; i <= n; i++){ memset(vst, false, sizeof(vst)); if(DFS(i)) ++ans; } cout<<ans; return 0; }
-
网络最大流:见 此,复杂度可以做到 \(O(m \sqrt{n})\),但好像专门卡匈牙利放这个的题目并不多。
-
-
建模要素
-
0 要素:节点可分为独立的两个集合,每个集合内部有 0 条边。
-
1 要素:每个节点只能和 1 条匹配边相连。
-
-
题目
4. 二分图最大多重匹配
两种主要解决方案:
-
拆点。这也是图论问题中的常见转化方法。复杂度 \(O(nwm)\)(\(w\) 系最多拆为几个点)。
-
网络最大流。复杂度 \(O(n^2m)\)。
-
导弹防御塔:二分答案 + 拆点 + 二分图最大匹配。
5. 二分图带权匹配
即权值和最大/小的一组二分图最大匹配。【注意:一定得先为最大匹配。】
两种主要解决方案:
-
KM 算法。只能用于最大匹配为 完备匹配 时。时间复杂度 \(O(n^2m)\),用 BFS 优化后可以达到 \(O(nm)\)。
这个算法的关键科技在“顶标”的设计:每个节点都有一个“顶标”,并且对于一条边 \((i, j)\),我们始终维护其满足 \(v(i)+v(j) \ge w(i, j)\)。主要框架还是和匈牙利差不多。至于优化就是把匈牙利改成 BFS 写法即可。
点击查看代码
int n, m, head[MAXN], mth[MAXN]; ll la[MAXN], lb[MAXN], d[MAXN];//顶标 和 辅助数组d bool va[MAXN], vb[MAXN];//标记是否在失配树内 inline bool DFS(int x){ va[x] = true; for(int i = head[x]; i; i = edge[i].nxt){ int to = edge[i].to; if(vb[to]) continue; if(la[x]+lb[to] != edge[i].wi){ d[to] = min(d[to], la[x]+lb[to]-edge[i].wi); //to 可能在 DFS 后面被加到交错树中,因此需要先用数组存一下 continue; } vb[to] = true; if(!mth[to] or DFS(mth[to])) {mth[to] = x; return true;} } return false; } //main 函数 memset(la, 0xcfcf, sizeof(la)); for(int i = 1; i <= n; i++) for(int j = head[i]; j; j = edge[j].nxt) la[i] = max(la[i], edge[j].wi); for(int i = 1; i <= n; i++){ while(true){//当左部点找到增广路之前 memset(va, false, sizeof(va)); memset(vb, false, sizeof(vb)); memset(d, 0x3f3f, sizeof(d)); if(DFS(i)) break; ll dlt = INF; for(int j = 1; j <= n; j++) if(!vb[j]) dlt = min(dlt, d[j]); for(int j = 1; j <= n; j++){ if(va[j]) la[j] -= dlt; if(vb[j]) lb[j] += dlt; } } }
-
费用流。复杂度为 \(O(fnm)\),由于此处有 \(f \le n\),复杂度为 \(O(n^2m)\)。
题目:
-
Ants:这道题用到了一个神奇的结论——互不相交时的线段总长度和最小,即四边形两边之和小于对角线之和。
6. 二分图最小点覆盖
-
结论
二分图最小点覆盖大小 = 二分图最大匹配大小
-
构造方法
执行完匈牙利算法后,对所有左部非匹配点再跑一次 DFS,标记访问到的点。此时,左部未被标记的点、右部被标记的点形成的就是二分图最小点覆盖。
-
建模要素
- 2 要素:节点可分为独立的 2 个集合,每条边至少在 2 个中选一个。
-
题目
-
Machine Schedule:以机器 A/B 为两个集合,以任务为边。
-
P6062 [USACO05JAN] Muddy Fields G:以板子横/竖为两个集合,以格子为边。
-
7. 二分图最大独立集
-
结论
在任意无向图中,有:最大独立集大小 = 总点数 - 最小点覆盖大小。
故有:二分图最大独立集大小 = 总点数 - 二分图最大匹配大小。
-
建模要素
- 2 要素:节点可分为独立的 2 个部分,每条边最多在 2 个中选一个。
-
题目
-
P3355 骑士共存问题:以黑/白格子为两个集合,以一次跳跃为边。(可以发现,骑士每一次跳跃会变化一次格子颜色。)
-
P5030 长脖子鹿放置:可以发现此时不能以格子颜色进行划分了。单独对行进行分析,发现每次跳跃都会改变一次行数的奇偶性。故以行奇/偶作为两个集合,以一次跳跃作为边。
-
8. DAG 最小路径点覆盖
-
路径无重复
将 DAG 的每个点拆为“出点”和“入点”,从每个出点连边指向入点。可以发现,这是一个二分图。
可以发现,除了路径起点、终点,每个点对应的入点都有一个入度,每个点对应的出点都有一个出度。这是一组二分图匹配。
若要使得路径尽量少,就是使得终点尽量少,即使得二分图非匹配的出点尽量少。故用 总点数 - 二分图最大匹配数 即可。
-
路径有重复
通过 floyd 跑传递闭包,在可达的节点之间连边,即可转化为无重复路径的情况。
-
答案为最小路径点覆盖的数量。证明如下:
...
蓝书上也给出了本题方案该如何构造:
设答案集合为 \(E\)。初始先将所有终点加入 \(E\),然后试着调整。可以发现如果两个点之间有直接的连边就说明二者绝对可达。故此,求出 \(E\) 的可达集合 \(next(E)\),将 \(E\) 与 \(next(E)\) 的交中的所有节点向其起点方向移动,直到其不在 \(next(E)\) 中。
但我感觉有点问题。。。因为你向上挪动的过程中,会造成 \(next(E)\) 的扩大啊?
举个例子:
感觉蓝书上的构造方法有点问题啊……
设想这么一个图:1 2 1 3 1 4 3 5 4 6 5 2 6 2
然后令选出的最小点覆盖为 (1, 2),(3, 5),(4, 6)
接着节点 2 上跳,跳到了 1
那么请问怎么有解???