NOIP复习——图论模板
最大流
\(\texttt{dinic}\) 算法,时间复杂度 \(O(n^2m)\),处理二分图匹配问题的复杂度是 \(O(m\sqrt{n})\)。
dinic算法
// 最大流,dinic算法
// 最大流,dinic算法
namespace MF {
int n, m, S, T, dep[N];
int head[N], cur[N], nex[M << 1], e[M << 1], tot = 1;
i64 w[M << 1];
void add(int x, int y, i64 z) {
nex[++tot] = head[x];
head[x] = tot;
e[tot] = y;
w[tot] = z;
}
void link(int x, int y, i64 z) {
add(x, y, z);
add(y, x, 0);
}
bool bfs() {
fill(dep, dep + n + 1, 0); // 注意此处的范围!!!
queue<int> q;
q.push(S);
dep[S] = 1;
while (!q.empty()) {
int x = q.front();
q.pop();
for (int i = head[x]; i; i = nex[i]) {
int y = e[i];
if (w[i] && !dep[y]) { // 忽略 w == 0 的边
dep[y] = dep[x] + 1;
q.push(y);
}
}
}
return dep[T] != 0;
}
i64 dfs(int x, i64 flow) {
if (x == T) {
return flow;
}
i64 res = 0;
for (int &i = cur[x]; i; i = nex[i]) {
int y = e[i];
if (dep[y] == dep[x] + 1) {
i64 tem = dfs(y, min(flow, w[i]));
w[i] -= tem;
w[i ^ 1] += tem;
flow -= tem;
res += tem;
if (!flow) {
break;
}
}
}
return res;
}
i64 Flow() {
i64 maxf = 0;
while (bfs()) {
for(int i = 0; i <= n; ++i) {
cur[i] = head[i]; // 注意此处的范围!!!
}
maxf += dfs(S, INF);
}
return maxf;
}
}
最小费用最大流
与最大流一样,使用 \(\texttt{dinic}\) 算法,将 BFS 找增广路替换为使用 SPFA,最小化代价。
dinic算法+spfa
// 最小费用最大流,dinic算法,其中bfs找增广路改为spfa
// 最小费用最大流,dinic算法,其中bfs找增广路改为spfa
namespace MCMF {
int n, m, S, T, vis[N];
i64 dis[N];
int head[N], cur[N], nex[M << 1], e[M << 1], tot = 1;
i64 w[M << 1], c[M << 1];
void add(int x, int y, i64 val, i64 cos) {
nex[++tot] = head[x];
head[x] = tot;
e[tot] = y;
w[tot] = val;
c[tot] = cos;
}
void link(int x, int y, i64 val, i64 cos) {
add(x, y, val, cos);
add(y, x, 0, -cos);
}
bool spfa() {
for(int i = 0; i <= n; ++i) { // 注意此处范围
dis[i] = INF;
vis[i] = 0;
}
dis[S] = 0;
vis[S] = 1;
queue<int> q;
q.push(S);
while(!q.empty()) {
int x = q.front();
q.pop();
vis[x] = 0;
for(int i = head[x]; i; i = nex[i]) {
int y = e[i];
if(w[i] && dis[y] > dis[x] + c[i]) {
dis[y] = dis[x] + c[i];
if(!vis[y]) {
vis[y] = 1;
q.push(y);
}
}
}
}
return dis[T] != INF;
}
i64 dfs(int x, i64 flow) {
if(x == T) {
return flow;
}
vis[x] = 1; // 对于y,dis[y] = dis[x] + c[i]的点x可能有很多,而y只需要被遍历到1次
i64 res = 0;
for(int &i = cur[x]; i; i = nex[i]) {
int y = e[i];
if(!vis[y] && w[i] && dis[y] == dis[x] + c[i]) { // 注意!vis[y]
i64 tem = dfs(y, min(flow, w[i]));
w[i] -= tem;
w[i ^ 1] += tem;
flow -= tem;
res += tem;
if(!flow) {
break;
}
}
}
vis[x] = 0;
return res;
}
pair<i64, i64> Flow() {
i64 maxf = 0, minc = 0;
while(spfa()) {
for(int i = 0; i <= n; ++i) { // 注意此处范围
cur[i] = head[i];
}
i64 f = dfs(S, INF);
maxf += f;
minc += f * dis[T];
}
return {maxf, minc};
}
}
tarjan算法
关于 \(\texttt{tarjan}\) 算法,一定要分有向图和无向图来讨论。
有向图的写法:更新 \(low[x]\) 的时候需要保证 \(y\) 在栈中。
有向图
void tarjan(int x) {
dfn[x] = low[x] = ++dfntot;
stk[++top] = x;
instk[x] = 1;
for(int i = head[x]; i; i = nex[i]) {
int y = e[i];
if(!dfn[y]) {
tarjan(y);
low[x] = min(low[x], low[y]);
} else if(instk[y]) { // y 在栈中
low[x] = min(low[x], dfn[y]);
}
}
}
无向图的写法:需要注意重边,比如求边双的时候,是不能进行去重的,需要处理一下。另外更新 \(low[x]\) 的时候不需要判断 \(y\) 是否在栈中。
无向图
void tarjan(int x, int from) { // from 表示 fa[x]->x 这条边的编号,边从 2 开始编号
dfn[x] = low[x] = ++dfntot;
stk[++top] = x;
for(int i = head[x]; i; i = nex[i]) {
int y = e[i];
if(!dfn[y]) {
tarjan(y, i);
low[x] = min(low[x], low[y]);
} else if(i != (from ^ 1)) { // 处理重边
low[x] = min(low[x], dfn[y]);
}
}
}
为什么更新 \(low[x]\) 的时候不需要判断 \(y\) 是否在栈中?
结论:在 \(\texttt{tarjan}\) 算法更新 \(low[x]\) 的时候,对于 \(dfn[y]\not=0\) 的 \(y\),\(y\) 一定在栈中。
证明:考虑反证,假设 \(y\) 不在栈中,由于 \(dfn[y]\not=0\) 且 \(y,x\) 联通,\(y\) 应该会扩展到 \(x\),这说明 \(y\) 是 \(x\) 在树上的祖先。而此时 \(x\) 还没有处理完,则 \(y\) 一定在栈中,与假设矛盾。我们也可以验证一下这个结论,具体见 这个提交记录,注意第 \(27\) 行的
assert
,发现结论正确。
割点和割边(无向图)
割点:如果把一个点删除后,图中连通块的个数增加了,那么这个点就是割点。
割边:如果把一条边删除后,图中连通块的个数增加了,那么这条边就是割边。
割点的判断:对整个图跑 \(\texttt{tarjan}\) 算法,假设当前搜索树的根是 \(rt\)。对于 \(rt\) 本身,它是割点等价于它有 \(\ge 2\) 个儿子;而对于 \(x(x\not=rt)\),它是割点等价于它存在一个儿子 \(y\) 使得 \(low[y]\ge dfn[x]\)。
割点——部分代码
void tarjan(int x, int fa) { // fa == 0 说明 x 是当前搜索树的根
dfn[x] = low[x] = ++cnt;
int son = 0;
for(int i = head[x]; i; i = nex[i]) {
int y = e[i];
if(!dfn[y]) {
tarjan(y, x);
low[x] = min(low[x], low[y]);
++son;
if((!fa && son >= 2) || (fa && low[y] >= dfn[x])) {
tag[x] = 1;
}
} else if(y != fa) {
low[x] = min(low[x], dfn[y]);
}
}
}
割边的判断:原理与判断割点类似,只需将 low[y] >= num[x]
改为 low[y]>num[x]
即可。并且不需要特判根节点的情况。
割边——部分代码
void tarjan(int x, int fa) {
dfn[x] = low[x] = ++cnt;
for(int i = head[x]; i; i = nex[i]) {
int y = e[i];
if(!dfn[y]) {
tarjan(y, x);
low[x] = min(low[x], low[y]);
if(low[y] > dfn[x]) {
tag[i] = tag[i ^ 1] = 1;
}
} else if(y != fa) {
low[x] = min(low[x], dfn[y]);
}
}
}
点双和边双(无向图)
点双连通分量:点双连通子图指一个没有割点的子图,点双连通分量是一个无向图中的极大点双连通子图。
边双连通分量:边双连通子图指一个没有割边的子图,边双连通分量是一个无向图中的极大边双连通子图。
求点双:注意到点双之间至多有一个公共点,并且这个公共点一定是割点。在求割点的过程中,用栈保存搜索树上的点,如果 \(x\) 的儿子 \(y\) 满足 \(low[y]\ge dfn[x]\),那么 \(x\) 及 \(y\) 子树内的点属于同一个点双,不断退栈将 \(y\) 子树内的点计入同一个点双即可。注意只需要将 \(y\) 的子树退栈,并且不需要退 \(x\),因为 \(x\) 可以属于多个点双。
点双——部分代码
void tarjan(int x) {
dfn[x] = low[x] = ++dfntot;
if(!deg[x]) { // 特判单独的点
vec[++cnt].push_back(x);
}
stk[++top] = x;
instk[x] = 1;
for(int i = head[x]; i; i = nex[i]) {
int y = e[i];
if(!dfn[y]) {
tarjan(y);
low[x] = min(low[x], low[y]);
if(low[y] >= dfn[x]) {
++cnt; // 点双个数
int tem;
do {
tem = stk[top--];
instk[tem] = 0;
vec[cnt].push_back(tem); // 统计第cnt个点双中的点
} while(tem != y); // 将y的子树退栈
vec[cnt].push_back(x); // x 不退栈
}
} else if(instk[y]) {
low[x] = min(low[x], dfn[y]);
}
}
}
求边双:如果 \(dfn[x]=low[x]\),那么 \(x-fa[x]\) 是割边,不断退栈到 \(x\),这些点在同一个边双中。
另一种方法:既然边双中没有割边,那么删掉所有割边,剩下的每个连通块都是边双。
边双——部分代码
void dfs(int x, int from) {
dfn[x] = low[x] = ++dfntot;
stk[++top] = x;
for(int i = head[x]; i; i = nex[i]) {
int y = e[i];
if(!dfn[y]) {
dfs(y, i);
low[x] = min(low[x], low[y]);
} else if(i != (from ^ 1)) {
low[x] = min(low[x], dfn[y]);
}
}
if(dfn[x] == low[x]) {
++cnt;
int y;
do {
y = stk[top--];
vec[cnt].push_back(y);
} while(y != x);
}
}
强连通分量(有向图)
部分代码
void tarjan(int x) {
dfn[x] = low[x] = ++dfntot;
stk[++top] = x, instk[x] = 1;
for(int i = head[x]; i; i = nex[i]) {
int y = e[i];
if(!dfn[y]) {
tarjan(y);
low[x] = min(low[x], low[y]);
} else if(instk[y]) {
low[x] = min(low[x], dfn[y]);
}
}
if(dfn[x] == low[x]) {
++cnt;
int y;
do {
y = stk[top--];
instk[y] = 0;
bel[y] = cnt; // y 属于第 cnt 个强连通分量
} while(y != x);
}
}
点分治
一般用于树上路径统计。
每次选择当前子树的重心作为根,统计经过根的贡献,然后递归分治根的所有儿子。
由于每次的根都是重心,所以每次递归会让子树大小减半,设计算经过单点的复杂度是 \(O(T)\),则总复杂度是 \(O(T\log{n})\)。
【模板】点分治:边带权,判断树上是否有长度为 \(k\) 的路径。
洛谷P4178 Tree:边带权,计算树上长度 \(\le k\) 的路径数量。
【模板】点分治
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5, M = 1e7 + 5, inf = 1.1e9;
int n, m, q[105], maxK;
vector<pair<int, int>> adj[N];
int siz[N], maxsiz[N], root, tot, vis[N];
void getRoot(int x, int fa) {
siz[x] = 1;
maxsiz[x] = 0;
for(auto [y, w] : adj[x]) {
if(vis[y] || y == fa) {
continue;
}
getRoot(y, x);
siz[x] += siz[y];
maxsiz[x] = max(maxsiz[x], siz[y]);
}
maxsiz[x] = max(maxsiz[x], tot - siz[x]);
if(maxsiz[x] < maxsiz[root]) {
root = x;
}
}
int dis[N], path[N], pathtot;
void getDis(int x, int fa) {
if(dis[x] <= maxK) {
path[++pathtot] = dis[x];
}
for(auto [y, w] : adj[x]) {
if(vis[y] || y == fa) {
continue;
}
dis[y] = dis[x] + w;
getDis(y, x);
}
}
int exist[M], ans[105];
void calc(int x) {
vector<int> used;
for(auto [y, w] : adj[x]) {
if(vis[y]) {
continue;
}
dis[y] = w;
pathtot = 0;
getDis(y, x);
for(int i = 1; i <= pathtot; ++i) {
for(int j = 1; j <= m; ++j) {
if(q[j] >= path[i]) {
ans[j] |= exist[q[j] - path[i]];
}
}
}
for(int i = 1; i <= pathtot; ++i) {
exist[path[i]] = 1;
used.push_back(path[i]);
}
}
for(auto c : used) {
exist[c] = 0;
}
}
void solve(int x) {
vis[x] = exist[0] = 1;
calc(x);
for(auto [y, w] : adj[x]) {
if(vis[y]) {
continue;
}
root = 0;
tot = siz[y];
getRoot(y, x);
solve(root);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for(int i = 1, u, v, w; i < n; ++i) {
cin >> u >> v >> w;
adj[u].push_back({v, w});
adj[v].push_back({u, w});
}
for(int i = 1; i <= m; ++i) {
cin >> q[i];
maxK = max(maxK, q[i]);
}
maxsiz[0] = inf;
tot = n;
getRoot(1, 1);
solve(root);
for(int i = 1; i <= m; ++i) {
cout << (ans[i] ? "AYE" : "NAY") << "\n";
}
return 0;
}
2-SAT 问题
给定若干变量 \(x_1,x_2,\dots,x_n\),每个变量有两个取值 \(true\) 和 \(false\),还有一些限制条件,如 \(\neg x_i\or x_j\)。
判断是否有解,有解指存在一种给所有变量赋值的方法,使得所有限制条件都被满足。
如果有解,需要构造一组解。
这就是经典的 \(2-SAT\) 问题,我们将每个变量 \(x_i\) 拆为两个点 \(x_i\) 和 \(\neg x_i\),分别表示 \(x_i=true/false\) 的状态。然后再根据限制条件建立有向边,如 \(\neg x_i\or x_j\) 可以转化为 \(x_i\rightarrow x_j\),\(\neg x_j\rightarrow \neg x_i\) 两条边。
然后用 \(\texttt{tarjan}\) 进行缩点,根据 强连通分量的定义,可以发现当 \(x_i\) 和 \(\neg x_i\) 在同一个强连通分量中时,如果取 \(x_i=true\) 则会推出矛盾,而取 \(x_i=false\) 仍然会推出矛盾,因此无解。
否则所有的强连通分量会构成一个 \(DAG\),此时 \(x_i\) 与 \(\neg x_i\) 的拓扑序谁大就取谁,就可以构造出一组解。这是因为如果取拓扑序小的,则有可能沿有向边走到拓扑序大的,就推出矛盾了;而取拓扑序大的一定不会走到拓扑序小的,一定合法。
技巧 + 大坑:\(\texttt{tarjan}\) 缩点之后,强连通分量的标号就是反拓扑序(标号为 \(k\) 的强连通分量会比 \(k+1\) 更早退栈),所以不用再写一个拓扑排序。并且也不要写拓扑排序,在图有多个拓扑起点的时候,直接令所有的起点拓扑序为 \(1\) 在一些题中是错误的!比如 [POI2001] 和平委员会 的样例。
模板题代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e6 + 5;
int n, m, x[N], nx[N], ans[N];
int head[N], nex[N], e[N], tot = 1;
void add(int x, int y) {
nex[++tot] = head[x];
head[x] = tot;
e[tot] = y;
}
int dfn[N], low[N], dfntot, stk[N], instk[N], top, bel[N], cnt, in[N], topn[N];
vector<int> adj[N];
void tarjan(int x) {
dfn[x] = low[x] = ++dfntot;
stk[++top] = x;
instk[x] = 1;
for(int i = head[x]; i; i = nex[i]) {
int y = e[i];
if(!dfn[y]) {
tarjan(y);
low[x] = min(low[x], low[y]);
} else if(instk[y]) {
low[x] = min(low[x], dfn[y]);
}
}
if(dfn[x] == low[x]) {
int y;
++cnt;
do {
y = stk[top--];
instk[y] = 0;
bel[y] = cnt;
} while(y != x);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; ++i) {
x[i] = i;
nx[i] = i + n;
}
for(int ii = 1; ii <= m; ++ii) {
int i, a, j, b;
cin >> i >> a >> j >> b;
// x[i] = a or y[j] = b
if(a && b) {
add(nx[i], x[j]);
add(nx[j], x[i]);
} else if(a && !b) {
add(nx[i], nx[j]);
add(x[j], x[i]);
} else if(!a && b) {
add(x[i], x[j]);
add(nx[j], nx[i]);
} else { // !a && !b
add(x[i], nx[j]);
add(x[j], nx[i]);
}
}
// 注意图中有 2n 个点
for(int i = 1; i <= 2 * n; ++i) {
if(!dfn[i]) {
tarjan(i);
}
}
for(int i = 1; i <= n; ++i) {
if(bel[x[i]] == bel[nx[i]]) {
cout << "IMPOSSIBLE\n";
return 0;
}
}
cout << "POSSIBLE\n";
for(int x = 1; x <= 2 * n; ++x) {
for(int i = head[x]; i; i = nex[i]) {
int y = e[i];
if(bel[x] != bel[y]) {
adj[bel[x]].push_back(bel[y]);
++in[bel[y]];
}
}
}
queue<int> q;
for(int i = 1; i <= cnt; ++i) {
if(!in[i]) {
q.push(i);
topn[i] = 1; // 拓扑序
}
}
while(!q.empty()) {
int x = q.front();
q.pop();
for(auto y : adj[x]) {
if(--in[y] == 0) {
topn[y] = topn[x] + 1;
q.push(y);
}
}
}
for(int i = 1; i <= n; ++i) {
cout << (topn[bel[x[i]]] > topn[bel[nx[i]]]) << " \n"[i == n];
}
return 0;
}