图论

强连通分量

  • dfn[x]dfn[x]dfn[x] 为结点 xxx 搜索的时间次序。
  • low[x]low[x]low[x] 为 uuu 或 uuu 的子树(经过最多一条后向边或栈中横叉边)能够回溯到的最早的栈中结点的编号。
  • 由定义可以得出:

low[x]=min⁡low[x] = \minlow[x]=min
{\{{
dfn[x]dfn[x]dfn[x]
low[y],(x,y)为树枝边,x为y的父结点low[y], (x, y) 为树枝边,x 为 y 的父结点low[y],(x,y)为树枝边,x为y的父结点
dfn[y],(x,y)为后向边或指向栈中结点的横叉边dfn[y], (x, y) 为后向边或指向栈中结点的横叉边dfn[y],(x,y)为后向边或指向栈中结点的横叉边
}\}}

inline void Tarjan(int x)
{dfn[x] = low[x] = ++tis;stk[++top] = x;ins[x] = true; int y;for (arc *e = adj[x]; e; e = e->nxt)if (!dfn[y = e->to]){Tarjan(y);CkMin(low[x], low[y]);}else if (ins[y])CkMin(low[x], dfn[y]);if (dfn[x] == low[x]){++C;do{y = stk[top--];ins[y] = false;col[y] = C;}while (y != x);}
}

  • 无向图中 low[x]low[x]low[x] 为 xxx 或 xxx 的子树经过最多一条后向边能够追溯到的树中结点的次序号。
  • 根据定义,有:

low[x]=min⁡low[x] = \minlow[x]=min
{\{{
dfn[x]dfn[x]dfn[x]
dfn[y],(x,y)为后向边dfn[y], (x,y) 为后向边dfn[y],(x,y)为后向边
low[y],(x,y)为树枝边low[y], (x,y) 为树枝边low[y],(x,y)为树枝边
}\}}

  • 桥 (x,y)(x, y)(x,y) 的判断条件: (x,y)(x,y)(x,y) 为树枝边且 dfn[x]<low[y]dfn[x] < low[y]dfn[x]<low[y]。
  • 将桥标记后用并查集确定边双连通分量。
  • 结论 对于一个有桥的连通图,求加最少数量的边,使其变为边双连通图。用 Tarjan\text{Tarjan}Tarjan 求出边双,将边双缩为一点,则原图变为一颗树。记这棵无根树中叶子结点的个数为 leafleafleaf,则所加最少边数为 ⌊leaf+12⌋\lfloor\frac{leaf + 1}{2}\rfloor⌊2leaf+1​⌋。

证明 可归纳证明,每次优先选择路径上有至少两个支链的叶子结点合并。

inline void Tarjan(int x)
{dfn[x] = low[x] = ++tis;int y;for (int e = adj[x]; e; e = nxt[e]){if (e == (up[x] ^ 1)) //树枝边的反向边要注意判断continue;if (!dfn[y = to[e]]){up[y] = e;Tarjan(y);CkMin(low[x], low[y]);if (dfn[x] < low[y])bridge[e] = bridge[e ^ 1] = true;}else CkMin(low[x], dfn[y]);}
}

割点

  • low[x]low[x]low[x] 定义同上。
  • xxx 为割点的判断条件:
  1. xxx 为树根,且 xxx 有多于一个的子树。
  2. xxx 不为树根,(x,y)(x,y)(x,y) 为树枝边且 dfn[x]≤low[y]dfn[x] \le low[y]dfn[x]≤low[y]。
  • 在求割点的过程中就能顺便求出点双连通分量。
  • 在搜索图时,每找到一条树枝边或后向边(注意实现时后向边的反向边不应加入栈中),就把这条边加入栈中。若某点 xxx 满足 (x,y)(x,y)(x,y) 为树枝边且 dfn[x]≤low[y]dfn[x] \le low[y]dfn[x]≤low[y] ,把边从栈顶一个个取出,直到遇到了边 (x,y)(x, y)(x,y),取出的这些边与其相连的点,组成一个点双连通分量。
  • 与求割点不同,求点双时并不需要判断树根,方便将所有点双取出。

典例 POJ2942

证明 若某点在偶环上,则一定存在一个偶环与已知的奇环有公共边,则可将偶环和已知的奇环合并得到一个新的奇环。

  • 用二分图染色判断每个点双中是否存在奇环即可。
  • 完整代码:
#include <cstdio>
#include <iostream>template <class T>
inline void read(T &res)
{char ch;while (ch = getchar(), !isdigit(ch));res = ch ^ 48;while (ch = getchar(), isdigit(ch))res = res * 10 + ch - 48;
}template <class T>
inline void CkMin(T &x, T y) {x > y ? x = y : 0;}const int N = 1e3 + 5;
const int M = 2e6 + 5;int fa[N], col[N], dfn[N], low[N], stkx[M], stky[M];
int tis, n, m, top;
bool edge[N][N], inc[N], ans[N];struct arc
{   int to;arc *nxt;
}p[M], *adj[N], *T = p;inline void linkArc(int x, int y)
{(++T)->nxt = adj[x]; adj[x] = T; T->to = y;(++T)->nxt = adj[y]; adj[y] = T; T->to = x;
}inline bool dfsColoring(int x)
{for (arc *e = adj[x]; e; e = e->nxt){int y = e->to;if (col[y] == -1){col[y] = col[x] ^ 1;if (!dfsColoring(y))return false;}else if (col[y] == col[x])return false;}return true;
}inline void solvePBC(int x, int y)
{T = p;for (int i = 1; i <= n; ++i)adj[i] = NULL;int u, v;do{u = stkx[top], v = stky[top];linkArc(u, v);inc[u] = inc[v] = true;--top;}while (x != u || y != v);for (int i = 1; i <= n; ++i)col[i] = -1;col[x] = 0;if (!dfsColoring(x)){for (int i = 1; i <= n; ++i)if (inc[i])ans[i] = true;}for (int i = 1; i <= n; ++i)inc[i] = false;
}inline void Tarjan(int x)
{dfn[x] = low[x] = ++tis;for (int y = 1; y <= n; ++y){if (y == fa[x] || !edge[x][y])continue;if (dfn[y] < dfn[x]) // 包含条件 !dfn[y]{++top;stkx[top] = x;stky[top] = y;}if (!dfn[y]){fa[y] = x;       Tarjan(y);CkMin(low[x], low[y]);if (dfn[x] <= low[y])solvePBC(x, y);}elseCkMin(low[x], dfn[y]);}
}int main()
{while (1){read(n); read(m);if (!n && !m)break ;for (int i = 1; i <= n; ++i)ans[i] = false;for (int i = 1; i <= n; ++i)for (int j = 1; j <= n; ++j)edge[i][j] = i == j ? false : true;for (int i = 1, x, y; i <= m; ++i){read(x); read(y);edge[x][y] = edge[y][x] = false;}tis = top = 0;for (int i = 1; i <= n; ++i)dfn[i] = low[i] = fa[i] = 0;for (int i = 1; i <= n; ++i)if (!dfn[i])Tarjan(i);int fans = 0;for (int i = 1; i <= n; ++i)fans += !ans[i];printf("%d\n", fans);} return 0;
}

圆方树

  • 对每个点双连通分量建一个新点,每个点向其所属的点双连边,形成树结构。
  • 具体应用待补充。

2-SAT

  • 2-SAT\text{2-SAT}2-SAT 问题指的是解下列形式的布尔方程:
    (a∨b)∧(c∨d)∧(e∨f)∧…(a\vee b)\wedge(c\vee d)\wedge(e\vee f)\wedge\dots(a∨b)∧(c∨d)∧(e∨f)∧…
  • 其中 a,b,c,…a,b,c,\dotsa,b,c,… 称为文字,是一个布尔变量或其否定。
  • 利用 ⇒\Rightarrow⇒(蕴含) 将每个子句 (a∨b)(a\vee b)(a∨b) 写成等价形式 (¬a⇒b∧¬b⇒a)(\neg a \Rightarrow b \wedge \neg b \Rightarrow a)(¬a⇒b∧¬b⇒a),对每个布尔变量 xxx 构造两个顶点 xxx 和 ¬x\neg x¬x,以 ⇒\Rightarrow⇒ 关系为边建立有向图。
  • 若存在 xxx 和 ¬x\neg x¬x 在同一强连通分量内,则无解。
  • 对强连通分量缩点后的图求拓扑序,则若 xxx 所在的强连通分量的拓扑序在 ¬x\neg x¬x 所在的强连通分量的拓扑序之后,则 xxx 为真,否则 ¬x\neg x¬x 为真。
  • 强连通分量的编号即为逆拓扑序。
  • 常见的等价形式转换:
    • (x=1)⇔(¬x⇒x),(x=0)⇔(x⇒¬x)(x = 1) \Leftrightarrow (\neg x \Rightarrow x), (x = 0) \Leftrightarrow (x \Rightarrow \neg x)(x=1)⇔(¬x⇒x),(x=0)⇔(x⇒¬x)
    • ¬(a∨b)⇔(a⇒¬b∧b⇒¬a)\neg(a\vee b) \Leftrightarrow (a \Rightarrow \neg b \wedge b \Rightarrow \neg a)¬(a∨b)⇔(a⇒¬b∧b⇒¬a)
    • kkk 个点中至多选一个,令这 kkk 个点分别为 a1,a2,…aka_1, a_2, \dots a_ka1​,a2​,…ak​,新建 2k2k2k 个点,preipre_iprei​ 表示 [1,i][1, i][1,i] 均不选,sufisuf_isufi​ 表示 [i,k][i, k][i,k] 均不选,作如下连边:
      • ai⇒prei−1,ai⇒sufi+1a_i \Rightarrow pre_{i-1}, a_i \Rightarrow suf_{i + 1}ai​⇒prei−1​,ai​⇒sufi+1​
      • prei⇒prei−1,prei⇒¬aipre_i \Rightarrow pre_{i - 1}, pre_i \Rightarrow \neg a_{i}prei​⇒prei−1​,prei​⇒¬ai​
      • sufi⇒sufi+1,sufi⇒¬aisuf_i \Rightarrow suf_{i + 1}, suf_i \Rightarrow \neg a_{i}sufi​⇒sufi+1​,sufi​⇒¬ai​
  • 上述算法只适用于判断可行性并给出一种可行方案。

欧拉回路

  • 设图 G=(V,E)G = (V,E)G=(V,E)。
  • 欧拉回路/路径 图 GGG 中经过每条边一次并且仅一次的回路/路径。
  • 欧拉图 存在欧拉回路的图。
  • 半欧拉图 存在欧拉路径但不存在欧拉回路的图。
  • 基图 忽略有向图所有边的方向,得到的无向图。
  • 定理1 无向图 GGG 为欧拉图,当且仅当 GGG 为连通图且所有顶点的度为偶数。
  • 定理2 无向图 GGG 为半欧拉图,当且仅当 GGG 为连通图且除了两个顶点的度为奇数之外,
    其它所有顶点的度为偶数。
  • 定理3 有向图 GGG 为欧拉图,当且仅当 GGG 的基图连通,且所有顶点的入度等于出度。
  • 定理4 有向图 GGG 为半欧拉图,当且仅当 GGG 的基图连通,且存在顶点 xxx 的入度比出度
    大 1、yyy 的入度比出度小 1,其它所有顶点的入度等于出度。
  • 求欧拉图 GGG 的欧拉回路:
inline void findCircuit(int x)
{for (int &e = adj[x]; e; e = nxt[e])if (!vis[e]){int c = e;vis[c] = true; if (type & 1) // type = 1 为无向图,type = 0 为有向图vis[c ^ 1] = true;findCircuit(to[c]);stk[++top] = c;}
}
  • 若题目要求用简单环覆盖图中所有边,则先求出欧拉回路,将欧拉回路上的边依次入栈,一旦入栈过程中发现有重点,则不断弹栈至重点处,则弹栈取出的所有边组成一个简单环,最终即可得到一个简单环的覆盖方案。

Prüfer 序列

  • 对树建立 Prüfer 序列

    • 每次选择一个编号最小的叶子结点删除,在序列中记录它连接的那个结点。
    • 重复 n−2n - 2n−2 次直至剩下两个结点,算法结束。
    • 线性实现上述过程只需用指针 ppp 记录当前编号最小的叶子结点,若删点后产生的新的叶子结点比 ppp 小则继续删除这个叶子结点不产生新的叶子结点或产生的叶子结点比 ppp 大。
  • Prüfer 序列的性质
    • 构造完 Prüfer 序列原树剩下的两个结点之一一定是编号最大的结点 nnn。
    • 每个结点在序列中出现的次数是其度数减一,没有出现的就是叶子结点。
  • 用 Prüfer 序列重建树
    • 由 Prüfer 序列的性质还原出每个点的度数。
    • 依次枚举 Prüfer 序列上的点,选择一个度数为 1 且编号最小的结点与之连接,同时将两者的度数减一。
    • 重复 n−2n - 2n−2 次后只剩下两个度数为 1 的点,将它们建立连接,算法结束。
    • 线性实现上述过程同样是用指针 ppp 记录度数为 1 且编号最小的结点,具体做法类似。
inline void TreeToPrufer()
{for (int i = 1, x; i < n; ++i){read(fa[i]);++deg[fa[i]];}// 这里的 fa[i] 指以 n 为根时结点 i 的父结点int p = 1, x = 0;for (int i = 1; i <= n - 2; ++i)if (x && x < p){ans[i] = fa[x]; x = !--deg[fa[x]] ? fa[x] : 0;}else{while (deg[p])++p;ans[i] = fa[p];x = !--deg[fa[p]] ? fa[p] : 0;++p;}
}inline void PruferToTree()
{for (int i = 1; i <= n - 2; ++i){read(ans[i]);++deg[ans[i]];}int p = 1, x = 0;for (int i = 1; i <= n - 2; ++i)if (x && x < p){fa[x] = ans[i];x = !--deg[ans[i]] ? ans[i] : 0;}else{while (deg[p])++p;fa[p] = ans[i];x = !--deg[ans[i]] ? ans[i] : 0;++p;}for (int i = 1; i < n; ++i)if (!fa[i]){fa[i] = n;break ;}
}
  • Cayley 公式 完全图 KnK_nKn​ 有 nn−2n^{n-2}nn−2 棵生成树。

证明 由构造和还原过程可知,任意一个长度为 n−2n - 2n−2、值域为 [1,n][1,n][1,n] 的整数序列都可以通过 Prüfer 序列双射对应一个生成树。

  • 结论1 nnn 个结点有标号有根树的数量为 nn−1n^{n - 1}nn−1。
  • 结论2 nnn 个结点的度数依次为 d1,d2,…,dnd_1,d_2,\dots,d_nd1​,d2​,…,dn​ 的无根树的数量为 (n−2)!∏i=1n(di−1)!\frac{(n - 2)!}{\prod\limits_{i = 1}^{n}(d_i - 1)!}i=1∏n​(di​−1)!(n−2)!​。
  • 结论3 把 nnn 个点划分为 kkk 个连通块,已知第 iii 个连通块的内部连边情况和大小 aia_iai​,包含所有连通块的生成树数量为 nk−2∏i=1kain^{k - 2}\prod\limits_{i = 1}^{k}a_ink−2i=1∏k​ai​。

证明 待补充。

Boruvka 算法

  • Boruvka\text{Boruvka}Boruvka 算法是一种古老的求解最小生成树的算法。
  • 初始时视 nnn 个点为 nnn 个连通块,每次遍历所有点和边,连接一个连通块中和其它连通块相连的最小的一条边,直到合并成一个连通块。
  • 每次连通块个数至少减少一半,可用并查集实现,时间复杂度 O((n+m)log⁡n)\mathcal O((n + m)\log n)O((n+m)logn)。

树哈希

  • 通过将树结构映射到一个便于存储的哈希值来判断一些树是否同构。
  • 常用的哈希方法有以下两种,设 fxf_xfx​ 为以 xxx 为根的子树的哈希值,则
    fx=(sizex∑fson(x,i)Bi−1)modP(1)f_x = \left(size_x \sum f_{son(x,i)} B^{i - 1}\right)\mod P \tag{1}\\ fx​=(sizex​∑fson(x,i)​Bi−1)modP(1)fx=(1+∑fyprimesizey)modP(2)f_x = \left(1 + \sum \limits f_y prime_{size_y}\right) \mod P \tag{2}\\ fx​=(1+∑fy​primesizey​​)modP(2)
  • 其中 B,PB,PB,P 为选定的质数,son(x,i)son(x, i)son(x,i) 表示按照 fff 排序后 xxx 的第 iii 个子结点,primeiprime_iprimei​ 表示第 iii 个素数。
  • 第二种方法的冲突概率更低,但需要预处理素数。
  • 通过换根 DP\text{DP}DP 即可得到以任意结点为根整棵树的哈希值,以下为第二种方法的代码。
inline void dfs1(int x)
{f[x] = sze[x] = 1;for (int y : e[x]){dfs1(y);f[x] = (1ll * f[y] * pri[sze[y]] + f[x]) % mod;sze[x] += sze[y];}
}inline void dfs2(int x)
{for (int y : e[x]){int tmp = f[x];add(tmp, g[x]);dec(tmp, 1ll * f[y] * pri[sze[y]] % mod);g[y] = 1ll * tmp * pri[_n - sze[y]] % mod;dfs2(y); }add(f[x], g[x]);
}
  • 另一种更简单的方法是直接以树的重心为根 DP\text{DP}DP,因为树的重心不会超过两个,两棵树同构当且仅当重心数目相同且对应的哈希值相同。
  • 上述所有方法在判断两棵树同构之前都应确保两棵树的结点数相同。

无向图三元环计数

  • 先给所有的边定向,若两端点度数不同,则由度数较小的点向度数较大的连边,否则由编号较小的向编号较大的连边,具体统计过程见代码。
  • 考虑图中的一条边 u→vu\to vu→v,设 vvv 在新图中的出度为 outvout_voutv​,总复杂度即 ∑outv\sum out_v∑outv​。
    • 若 vvv 在原图中的度数小于等于 m\sqrt mm​,则显然有 outv≤mout_v \le \sqrt moutv​≤m​。
    • 若 vvv 在原图中的度数大于 m\sqrt mm​,在新图中它只能向度数大于 m\sqrt mm​ 的点连边,原图中这样的点不会超过 2m2\sqrt m2m​ 个,所以有 outv≤2mout_v \le 2\sqrt moutv​≤2m​。
  • 综上,该算法的时间复杂度为 O(mm)\mathcal O(m\sqrt m)O(mm​),同时也意味着答案的规模也为 O(mm)\mathcal O(m \sqrt m)O(mm​)。
inline bool cmp(const int &x, const int &y)
{return deg[x] < deg[y] || deg[x] == deg[y] && x < y;
}inline int countCycle()
{int res = 0;for (int i = 1; i <= m; ++i)++deg[px[i]], ++deg[py[i]];for (int i = 1; i <= m; ++i){if (!cmp(px[i], py[i]))std::swap(px[i], py[i]); e[px[i]].push_back(py[i]);}for (int x = 1, y; x <= n; ++x){for (int y : e[x])vis[y] = x;for (int y : e[x])for (int z : e[y])res += vis[z] == x;}return res;
}

LCA

DFS 序 + ST 表

  • 预处理时间复杂度 O(nlog⁡n)\mathcal O(n \log n)O(nlogn),空间复杂度 O(nlog⁡n)\mathcal O(n \log n)O(nlogn),单次询问时间复杂度 O(1)\mathcal O(1)O(1)。
  • 设结点 xxx 的 DFS\text{DFS}DFS 序编号为 dfn[x]dfn[x]dfn[x],则 x,y(dfn[x]<dfn[y])x,y(dfn[x] < dfn[y])x,y(dfn[x]<dfn[y]) 的 LCA\text{LCA}LCA 为 [dfn[x]+1,dfn[y]][dfn[x] + 1, dfn[y]][dfn[x]+1,dfn[y]] 上深度最小的结点的父亲。
inline void dfs(int x)
{dfn[x] = ++tis;dep[x] = dep[fa[x]] + 1;f[0][dfn[x]] = fa[x];for (arc *e = adj[x]; e; e = e->nxt){int y = e->to;if (y == fa[x])continue ;fa[y] = x;dfs(y);}
}inline int queryLCA(int x, int y)
{if (x == y)return x;x = dfn[x], y = dfn[y];if (x > y) std::swap(x, y);++x;int k = Log[y - x + 1];return depMin(f[k][x], f[k][y - (1 << k) + 1]);
}inline void init()
{Log[0] = -1;for (int i = 1; i <= n; ++i)Log[i] = Log[i >> 1] + 1;dfs(rt);for (int j = 1; j <= Log[n]; ++j)for (int i = 1; i + (1 << j) - 1 <= n; ++i)f[j][i] = depMin(f[j - 1][i], f[j - 1][i + (1 << j - 1)]);
}

Tarjan

  • 离线,时间复杂度和空间复杂度均为线性。
  • 这里因为使用了 vectorpair 实测常数较大。
inline int ufs_find(int x)
{if (fa[x] != x)   return fa[x] = ufs_find(fa[x]);return x;
}inline void Tarjan(int x)
{   fa[x] = x;vis[x] = true;  for (arc *e = adj[x]; e; e = e->nxt){int y = e->to;if (vis[y])continue ;Tarjan(y);fa[y] = x;}for (pir e : query[x])if (vis[e.first])ans[e.second] = ufs_find(e.first);
}

网络流

  • 设源点为 sss,汇点为 ttt,每条边 eee 的流量上限为 c(e)c(e)c(e),流量为 f(e)f(e)f(e)。
  • 指对于某一顶点集合 P⊂VP \subset VP⊂V,从 PPP 出发指向 PPP 外部的那些原图中的边的集合,记作割 (P,V/P)(P, V /\ P)(P,V/ P)。这些边的容量被称为割的容量。若 s∈P,t∈V/Ps\in P, t\in V /\ Ps∈P,t∈V/ P,则称此时的割为 s−ts-ts−t 割。
  • 对于任意的 s−ts-ts−t 流 FFF 和任意的 s−ts-ts−t 割 (P,V/P)(P,V/\ P)(P,V/ P) 割,由每个点的流量平衡条件得:
    F的流量=P出边总流量−P入边总流量≤割的容量F 的流量 = P出边总流量 - P 入边总流量 \le 割的容量 F的流量=P出边总流量−P入边总流量≤割的容量
  • 对于在残量网络中不断增广得到的流 FFF,设其对应的残量网络中从 sss 出发可到达的顶点集为 SSS,则对于 SSS 指向 V/SV/\ SV/ S 的边 eee 有 f(e)=c(e)f(e) = c(e)f(e)=c(e),而对 V/SV/\ SV/ S 指向 SSS 的边有 f(e)=0f(e) = 0f(e)=0,则:
    F的流量=S出边总流量−S入边总流量=S出边总流量=割的容量F 的流量 = S 出边总流量 - S入边总流量 = S出边总流量 = 割的容量 F的流量=S出边总流量−S入边总流量=S出边总流量=割的容量
  • 因而 FFF 为最大流,同时 (S,V/S)(S,V/\ S)(S,V/ S) 为最小割,即最大流等于最小割

Dinic 算法

  • 主要思想即每次寻找最短的增广路,构造分层图,并沿着它多路增广。
  • 每次多路增广完成后最短增广路长度至少增加 1,构造分层图次数为 O(n)\mathcal O(n)O(n),在同一分层图中,每条增广路都会被至少一条边限制流量(我们称之为瓶颈),显然任意两条增广路的瓶颈均不相同,因而增广路总数为 O(m)\mathcal O(m)O(m),加上当前弧优化,我们就能避免对无用边多次检查,寻找单条增广路的时间复杂度为 O(n)\mathcal O(n)O(n),总时间复杂度 O(n2m)\mathcal O(n^2m)O(n2m)。
const int N = 1e4 + 5;
const int M = 2e5 + 5;
int nxt[M], to[M], cap[M], adj[N], que[N], cur[N], lev[N];
int n, m, src, des, qr, T = 1;inline void linkArc(int x, int y, int w)
{nxt[++T] = adj[x]; adj[x] = T; to[T] = y; cap[T] = w;nxt[++T] = adj[y]; adj[y] = T; to[T] = x; cap[T] = 0;
}inline bool Bfs()
{for (int x = 1; x <= n; ++x)    cur[x] = adj[x], lev[x] = -1;// 初始化具体的范围视建图而定,这里点的范围为 [1,n]que[qr = 1] = src;lev[src] = 0;for (int i = 1; i <= qr; ++i){int x = que[i], y;for (int e = adj[x]; e; e = nxt[e])if (cap[e] > 0 && lev[y = to[e]] == -1){lev[y] = lev[x] + 1;que[++qr] = y;if (y == des)return true;}}return false;
} inline ll Dinic(int x, ll flow)
{if (x == des)return flow;int y, delta; ll res = 0;  for (int &e = cur[x]; e; e = nxt[e]) if (cap[e] > 0 && lev[y = to[e]] > lev[x]){delta = Dinic(y, Min(flow - res, (ll)cap[e]));if (delta){cap[e] -= delta;cap[e ^ 1] += delta;res += delta;if (res == flow)break ; //此时 break 保证下次 cur[x] 仍有机会增广 }} if (res != flow)lev[x] = -1;return res;
}inline ll maxFlow()
{ll res = 0;while (Bfs())res += Dinic(src, Maxn);return res;
}
  • 单位网络 在该网络中,所有边的流量均为 1,除源汇点以外的所有点,都满足入边或者出边最多只有一条。
  • 结论 对于包含二分图最大匹配在内的单位网络,Dinic\text{Dinic}Dinic 算法求解最大流的时间复杂度为 O(mn)\mathcal O(m\sqrt n)O(mn​)。

证明 对于单位网络,每条边最多被考虑一次,一轮增广的时间复杂度为 O(m)\mathcal O(m)O(m)。

假设我们已经完成了前 n\sqrt nn​ 轮增广,还需找到 ddd 条增广路才能找到最大流,每条增广路的长度至少为 n\sqrt nn​。这些增广路不会在源点和汇点以外的点相交,因而至少经过了 dnd\sqrt ndn​ 个点,d≤nd \le \sqrt nd≤n​,则至多还需增广 n\sqrt nn​ 轮,总时间复杂度 O(mn)\mathcal O(m\sqrt n)O(mn​)。

经典模型

  • 二者选其一的最小割 有 nnn 个物品和两个集合 A,BA,BA,B,若第 iii 个物品没有放入 AAA 集合花费 aia_iai​,没有放入 BBB 集合花费 bib_ibi​,还有若干个限制条件,若 uiu_iui​ 和 viv_ivi​ 不在一个集合则花费 wiw_iwi​。

    • 源点 sss 向第 iii 个点连一条容量为 aia_iai​ 的边,第 iii 个点向汇点 ttt 连一条容量为 bib_ibi​ 的边,在 uiu_iui​ 和 viv_ivi​ 之间连容量为 wiw_iwi​ 的双向边,最小割即最小花费。
  • 最大权闭合子图 给定一张有向图,每个点都有一个权值(可为负),选择一个权值和最大的子图,使得子图中每个点的后继都在子图中。
    • 若点权为正,则 sss 向该点连一条容量为点权的边。
    • 若点权为负,则该点向 ttt 连一条容量为点权的相反数的边。
    • 原图上所有边的容量设为 +∞+\infty+∞。
    • 则答案为正点权之和减去最小割。
  • 分糖果问题 nnn 个糖果 mmm 个小孩,小孩 iii 对糖果 jjj 有偏爱度 ai,j=1/2a_{i,j} = 1/2ai,j​=1/2,设 ci,j=0/1c_{i,j} = 0/1ci,j​=0/1 表示小孩 iii 是否分得了糖果 jjj,小孩 iii 觉得高兴当且仅当 ∑j=1nci,jai,j≥bi\sum\limits_{j = 1}^{n} c_{i,j} a_{i,j}\ge b_ij=1∑n​ci,j​ai,j​≥bi​,判断是否存在方案使所有小孩都高兴。
    • 偏爱度 ai,j=1/2a_{i,j} = 1/2ai,j​=1/2 不好建图,转换思路先分配所有 ai,j=2a_{i,j} = 2ai,j​=2 的糖果。
    • sss 向所有糖果连一条容量为 1 的边,小孩 iii 向 ttt 连一条容量为 ⌊bi2⌋\lfloor \frac{b_i}{2} \rfloor⌊2bi​​⌋ 的边。
    • 对于所有满足 ai,j=2a_{i,j} = 2ai,j​=2 的边,令糖果 jjj 向小孩 iii 连一条容量为 1 的边。
    • 求得最大流 ansansans,则存在方案当且仅当 ans+n≥∑j=1mbians + n\ge \sum \limits_{j = 1}^{m}b_ians+n≥j=1∑m​bi​。
  • 动态流问题 宽为 www 的河上有 nnn 块石头,第 iii 块坐标 (xi,yi)(x_i,y_i)(xi​,yi​),同一时刻最大承受人数为 cic_ici​,现有 mmm 个游客想要渡河,每人每次最远跳 ddd 米 ,单次耗时 1 秒,求全部渡河的最少时间( n,m≤50n,m \le 50n,m≤50)。
    • 答案取值范围较小可暴力枚举,将每一时刻的石头都视作一点,在石头上跳跃可视作从第 ttt 时刻的石头 iii 跳向第 t+1t + 1t+1 时刻的石头 jjj,每次将时刻加一,建出新的点和边后跑最大流,直至总流量大于等于 mmm。

费用流

  • 在最大流的前提下使该网络总花费最小。

SSP 算法

  • 每次寻找单位费用最小的增广路进行增广,直至图中不存在增广路为止。

  • 设流量为 iii 的时候最小费用为 fif_ifi​,假设初始网络上没有负圈,f0=0f_0 = 0f0​=0。

  • 假设用 SSP\text{SSP}SSP 算法求出的 fif_ifi​ 是最小费用,我们在 fif_ifi​ 的基础上,找到一条最短的增广路,从而求出 fi+1f_{i + 1}fi+1​,此时 fi+1−fif_{i + 1} - f_ifi+1​−fi​ 就是这条最短增广路的长度。

  • 假设存在更小的 fi+1f_{i + 1}fi+1​,设其为 fi+1′f_{i+1}'fi+1′​,则 fi+1′−fif'_{i + 1} - f_ifi+1′​−fi​ 一定对应一个经过至少一个负圈的增广路。若残量网络中存在至少一个负圈,则可在不增加 sss 流出的流量的情况下使费用减小,与 fif_ifi​ 是最小费用矛盾。

  • 综上,SSP\text{SSP}SSP 算法可以正确求出无负圈网络的最小费用最大流,设最大流为 FFF,总时间复杂度 O(Fnm)\mathcal O(Fnm)O(Fnm)。

const int N = 5e3 + 5;
const int M = 1e5 + 5;
int nxt[M], to[M], cap[M], que[M], cst[M], adj[N], dis[N];
bool vis[N]; int n, m, src, des, ans, T = 1, qr;inline void linkArc(int x, int y, int w, int z)
{nxt[++T] = adj[x]; adj[x] = T; to[T] = y; cap[T] = w; cst[T] = z;nxt[++T] = adj[y]; adj[y] = T; to[T] = x; cap[T] = 0; cst[T] = -z;
}inline bool SPFA()
{for (int x = 1; x <= n; ++x)dis[x] = Maxn, vis[x] = false;dis[que[qr = 1] = src] = 0;for (int i = 1, x, y; i <= qr; ++i){vis[x = que[i]] = false;for (int e = adj[x]; e; e = nxt[e])if (cap[e] > 0 && dis[y = to[e]] > dis[x] + cst[e]){dis[y] = dis[x] + cst[e];if (!vis[y])vis[que[++qr] = y] = true;}}return dis[des] < Maxn;
}inline int Dinic(int x, int flow)
{if (x == des){ans += flow * dis[des];return flow;}vis[x] = true;int y, delta, res = 0;for (int e = adj[x]; e; e = nxt[e])if (!vis[y = to[e]] && cap[e] > 0 && dis[y] == dis[x] + cst[e])// vis 数组防止 dfs 在总费用为 0 的环上死循环 {delta = Dinic(y, Min(flow - res, cap[e]));if (delta){cap[e] -= delta;cap[e ^ 1] += delta;res += delta;if (res == flow)break ;   } }return res;
}inline int MCMF()
{ans = 0;int res = 0;while (SPFA())res += Dinic(src, Maxn);return res;
}

经典模型

  • 餐巾计划问题 一家餐厅在接下来的 TTT 天内需用餐巾,第 iii 天需要 rir_iri​ 块干净餐巾,餐厅可任意购买单价为 ppp 的干净餐巾,或者将脏餐巾送往快洗部(单块餐巾需洗 mmm 天,花费 fff)或慢洗部(单块餐巾需洗 nnn 天,花费 sss),求这 TTT 天内的最小花费。

    • 主要的难点在于需将干净餐巾和脏餐巾区分开,第 iii 天分设点 i,i′i,i'i,i′,流向这两点的流量分别表示第 iii 天的干净餐巾和脏餐巾。
    • sss 向 iii 连一条容量为 +∞+\infty+∞ 、费用为 ppp 的边,iii 向 ttt 连一条容量为 rir_iri​、费用为 0 的边。
    • sss 向 i′i'i′ 连一条容量为 rir_iri​、费用为 0 的边,i′i'i′ 向 i+mi + mi+m 连一条容量为 +∞+\infty+∞,费用为 fff 的边,向 i+ni + ni+n 连一条容量为 +∞+\infty+∞,费用为 sss 的边,向 (i+1)′(i + 1)'(i+1)′ 连一条容量为 +∞+\infty+∞,费用为 000 的边。
    • 显然该建图能使到 ttt 的边满流,则最小费用即为所求。

二分图最大匹配

  • 记图 G=(V,E)G = (V,E)G=(V,E)。

    • 匹配 GGG 中两两没有公共端点的边集合 M⊆EM \subseteq EM⊆E。
    • 边覆盖 GGG 中的任意顶点都至少是 FFF 中某条边的边集合 F⊆EF \subseteq EF⊆E。
    • 独立集 GGG 中两两互不相连的顶点集 S⊆VS \subseteq VS⊆V。
    • 点覆盖 GGG 中任意边都有至少一个端点属于 PPP 的顶点集合 P⊆VP \subseteq VP⊆V。
  • 结论1 对于不存在孤立点的图,∣最大匹配∣+∣最小边覆盖∣=∣V∣|最大匹配|+|最小边覆盖| = |V|∣最大匹配∣+∣最小边覆盖∣=∣V∣。

证明 设最大匹配数为 xxx,则其覆盖的点数为 2x2x2x,剩余点两两之间没有边。则最少需要增加 ∣V∣−2x|V| - 2x∣V∣−2x 条边才能将所有点覆盖,则最小边覆盖数为 x+∣V∣−2x=∣V∣−xx + |V| - 2x = |V| - xx+∣V∣−2x=∣V∣−x。

  • 结论2 ∣最大独立集∣+∣最小点覆盖∣=∣V∣|最大独立集| + |最小点覆盖| = |V|∣最大独立集∣+∣最小点覆盖∣=∣V∣。

证明 只需证明独立集和点覆盖一一对应且互为关于 VVV 的补集即可。取一个点覆盖关于 VVV 的补集,若其不是一个独立集,则存在两点有公共边,不难发现这与点覆盖的定义矛盾。

  • 结论3二分图中,∣最大匹配∣=∣最小点覆盖∣|最大匹配|=|最小点覆盖|∣最大匹配∣=∣最小点覆盖∣。

证明 设最大匹配数为 xxx,即让匹配中的每条边都和其一个端点关联。xxx 个点是足够的,否则若存在一条边未被覆盖,加入这条边能得到一个更大的匹配。xxx 个点是必需的,因为匹配的 xxx 条边两两无公共点。

  • 结论4有向无环图中,∣最小点不相交路径覆盖∣+∣最大匹配∣=∣V∣|最小点不相交路径覆盖|+|最大匹配|=|V|∣最小点不相交路径覆盖∣+∣最大匹配∣=∣V∣。这里的最大匹配指的是,将原图每个点拆成入点和出点两点,对于有向边 x→yx \to yx→y,将 xxx 的入点向 yyy 的出点连边,在该二分图上求得的最大匹配。

    • 若需求 ∣最小点可相交路径覆盖∣|最小点可相交路径覆盖|∣最小点可相交路径覆盖∣,用 Floyd\text{Floyd}Floyd 算法求出原图的传递闭包,在传递闭包上建图即可。

证明 只需证明二分图中匹配与原图中的路径覆盖一一对应且总和为 ∣V∣|V|∣V∣。对于每个没有匹配边的出点,我们都能构造一条路径,若其对应的入点存在匹配边,则将该匹配边加入该路径同时继续考虑该匹配边的出点直至其对应的入点不存在匹配边。得到的所有路径恰能覆盖所有点,且没有匹配边的出点和入点恰好能两两配对分别构成所有路径的起点和终点。

匈牙利算法

  • 从每个点出发尝试找到一条增广路,时间复杂度 O(nm)\mathcal O(nm)O(nm)。
inline bool Hungary(int x)
{int y;for (arc *e = adj[x]; e; e = e->nxt)if (!mateR[y = e->to])return mateR[y] = x, true;for (arc *e = adj[x]; e; e = e->nxt){if (vis[y = e->to] == tis)continue ;vis[y] = tis;if (Hungary(mateR[y]))return mateR[y] = x, true;}return false;
}inline int maxMatch()
{int cnt = 0;for (int i = 1; i <= n; ++i){++tis;if (Hungary(i))++cnt;} return cnt;
}

二分图最大权匹配

Kuhn-Munkres算法

  • 详细流程待补充。

    • 参考 OI wiki 百度百科 洛谷题解
  • 若二分图左右部的点数不相同,需将点数补至相同,并将所有不存在的边的权设为 0。
  • 若题目对是否是完备匹配有要求,需将不存在的边设为 −∞-\infty−∞(具体值需保证一旦选这种边就比所有不选这种边的方案更劣),此时也能处理原图边权有负数的情况。
typedef long long ll;
const int N = 405;
const int Maxn = 2e9;
int que[N], w[N][N], slacky[N];
int labx[N], laby[N], matex[N], matey[N], pre[N];
bool visx[N], visy[N];
int nl, nr, qr, n, m; ll ans;inline bool Augment(int y)
{if (matey[y]){que[++qr] = matey[y];visx[matey[y]] = visy[y] = true;return false;}else {while (y){int x = pre[y];matey[y] = x;std::swap(matex[x], y);}return true;}
}inline void bfsHungary(int src)
{for (int i = 1; i <= n; ++i){       pre[i] = 0;visx[i] = visy[i] = false;slacky[i] = Maxn;}visx[que[qr = 1] = src] = true;while (1){for (int i = 1, x; i <= qr; ++i){x = que[i];for (int y = 1; y <= n; ++y)  if (!visy[y]){int delta = labx[x] + laby[y] - w[x][y];if (delta > slacky[y])continue ;pre[y] = x;if (delta > 0)slacky[y] = delta;else if (Augment(y))return ;}}int nxt, delta = Maxn;for (int y = 1; y <= n; ++y)if (!visy[y] && slacky[y] < delta)delta = slacky[y], nxt = y;for (int i = 1; i <= n; ++i){if (visx[i])labx[i] -= delta;if (visy[i])laby[i] += delta;else slacky[i] -= delta; }qr = 0;if (Augment(nxt))return ;}
}int main()
{read(nl); read(nr); read(m);n = Max(nl, nr);int x, y, z;while (m--){read(x);read(y);read(z);CkMax(w[x][y], z);CkMax(labx[x], z);}for (int i = 1; i <= n; ++i)bfsHungary(i);for (int i = 1; i <= n; ++i) ans += w[i][matex[i]];put(ans), putchar('\n');for (int i = 1; i <= nl; ++i)put(w[i][matex[i]] ? matex[i] : 0), putchar(' ');
}

Algorithm Review 5 图论相关推荐

  1. ARTS3(Algorithm, Review , Tip/Techni, Share)

    Algorithm:每周至少做一个leetcode的算法题: Review:阅读并点评至少一篇英文技术文章: Tip/Techni:学习至少一个技术技巧: Share:分享一篇有观点和思考的技术文章. ...

  2. Algorithm Review 7 数学相关

    博弈论 待补充. Nim Game 给定 nnn 堆石子,第 iii 堆石子有 aia_iai​ 个,两名玩家轮流行动,每次可以任选一堆,取走任意多个石子,但不能不取,取走最后一个石子者获胜. 结论1 ...

  3. Algorithm Review 3 数论

    数论 若 a≡b(modm)a \equiv b (\mod m)a≡b(modm),则 (a,m)=(b,m)(a, m) = (b, m)(a,m)=(b,m). a≡b(modmi)(1≤i≤n ...

  4. Prim Algorithm(普利姆算法)

    Prim算法介绍 普里姆算法(Prim's algorithm),图论中的一种算法,可在加权连通图里搜索最小生成树.意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点,且其所有边的权 ...

  5. 【转载】网络流和最小费用流

    这段时间复习了下网络流模型,感觉比以前的理解有了长足进展,虽然我知道这东西难就难在建模上,而它的算法本身其实难度不大,但我还是决定说一些我的理解,毕竟理解了本质的东西运用起来才会更灵活. 最大流的求解 ...

  6. 网络最大流和最小费用流

    from: http://richardxx.yo2.cn/articles/网络最大流和最小费用流.html 这段时间复习了下网络流模型,感觉比以前的理解有了长足进展,虽然我知道这东西难就难在建模上 ...

  7. 《左耳听风》-ARTS-打卡记录-第二十五周

    <左耳听风>-ARTS-打卡记录-第25周 坚持不懈是一句正确的废话.前段时间,我在我的读者群中发起了一个名为 ARTS 的活动.每人每周写一个 ARTS:Algorithm 是一道算法题 ...

  8. Prim算法简易教程(~简单易懂,附最详细注释代码)

    文章目录 1 最小生成树(Minimum Spanning Tree,MST) 2 Prim算法 2.1 简介 2.2 具体步骤 2.3 算法示例图 2.4 算法实现 2.5 算法分析 2.6 测试 ...

  9. DCC2020:VVC帧间预测中的几何划分

    本文来自DCC2020论文<Advanced Geometric-based Inter Prediction for Versatile Video Coding> 几何划分相较于三角划 ...

  10. H.266/VVC帧间预测技术学习:几何划分模式(Geometric partitioning mode, GPM)

    几何划分模式 (Geometric partitioning mode ,GPM)原理 针对图像中运动物体的边界部分,VVC采用了几何划分模式进行帧间预测.如下图所示,GPM模式在运动物体的边界处进行 ...

最新文章

  1. php更新数据库时间戳,关于Thinkphp5 里面数据库自动更新与创建时间的问题
  2. 电子词典系统vc++_电子词典系统
  3. windows下mysql中文乱码_windows下mysql中文乱码, 配置解决方法
  4. Faster RCNN minibatch.py解读
  5. JavaScript-严格检查模式
  6. liunx php的项目地址,在 Linux 配置 PHP 项目
  7. 嵌入式nosql php,NoSQL 嵌入式数据库NeDB示例
  8. 前端小白到技术专家,可能么?
  9. linux学习之路之使用脚本来复制二进制程序和所需的库文件
  10. python是什么 自学-自学Python会有什么困难?老男孩自学python编程
  11. linux下重启tomcat,日志查看
  12. 哪个网站可以免费学计算机办公软件,有没有可以免费学习办公软件的网站?
  13. Flash 实验 飞机爆炸
  14. OAS ( Open Adoption Software ) 类公司的崛起
  15. 创新之道,亚马逊创新之旅背后的故事
  16. 使用drawio画地图
  17. 基于matlab的频率特性测试仪,基于虚拟仪器的网络频率特性测试仪
  18. 智慧高速公路车路协同系统框架及要求第二部分
  19. 开源MPEG-4编码器 XviD简介
  20. 使用pdfFactory为PDF文件设定查看选项

热门文章

  1. 南航计算机院招聘,2018南航计算机专硕我考上啦!
  2. 常用经典SQL语句大全完整版
  3. android 连接魅族手机,android studio连接魅族真机
  4. n2android,刚上车,中国移动N2开箱评测,好给力的说!
  5. 退役前的做题记录3.0
  6. 正反馈理论与管理学——马太效应、比…
  7. Neo4j下载安装及使用
  8. Python反编译apk,获取各类信息
  9. android超频闪退,显卡超频后玩游戏闪退 | 手游网游页游攻略大全
  10. TOPSIS优劣解距离法