二分图 & 网络流
二分图
可以考虑黑白染色(即一条边两边端点颜色不同)和没有奇环转化成二分图的模型。
-
匹配:选择若干条边使得每个点周围至多只有一条边被选。
-
最大匹配:最多的边。
-
增广路用于将匹配边替换为一些当前可能更优的非匹配边,增广路中匹配边与非匹配边交替出现。
-
匈牙利算法:bfs/dfs,从每个点出发找增广路,找不到了就得到了极大匹配,即最大匹配。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1005;
const int maxm = 5e4 + 5;
int L,R,m;
struct Edge {
int to,nxt;
} e[maxm << 1];
int head[maxn], cnt;
void addEdge(int u,int v) {
e[++ cnt] = Edge{v,head[u]};
head[u] = cnt;
}
int n, f[maxn]; bool vis[maxn];
int dfs(int u) {
for (int i = head[u];i;i = e[i].nxt) {
int v = e[i].to;
if (vis[v]) continue;
vis[v] = 1;
if (!f[v] || dfs(f[v]))
return f[v] = u, 1;
}
return 0;
}
int main() {
scanf("%d%d%d",&L,&R,&m), n = L + R;
for (int i = 1, u, v;i <= m;i ++) {
scanf("%d%d",&u,&v);
addEdge(u,v + L), addEdge(v + L,u);
}
int ans = 0;
for (int i = 1;i <= L;i ++) {
for (int j = 1;j <= n;j ++)
vis[j] = 0;
ans += dfs(i);
}
printf("%d",ans);
return 0;
}
- 点覆盖:选若干点使得所有边都有被选择的点覆盖。
- 独立集:原图中点集的一个子集,使得子集中所有点没有边相连。(无向图最大团等于其补图的最大独立集)
- 最小点覆盖等价于最大匹配(前者所需点数 = = = 后者所需边数),最大独立集等价于 n n n 减最小点覆盖。
模型:最小路径覆盖
基本模型:给出一张有向图,要用若干条不相交的简单路径覆盖所有点,求最少路径数。
- 做法:考虑拆点构建二分图。将所有点拆成两个点(记为 x , x ′ x,x' x,x′),对于原图中 ( x , y ) (x,y) (x,y) 的边,在新图中连一条 ( x , y ′ ) (x,y') (x,y′) 的边。
- 定理:最小路径覆盖 = n − =n~- =n − 二分图的最大匹配。
- 对于输出方案,用网络流跑二分图最大匹配,根据残量网络使用并查集进行维护。具体地,从左边点出发向外走,对于每条经过的形如 ( x , y ′ ) (x,y') (x,y′) 的边,如果 ( x , y ′ ) (x,y') (x,y′) 上有流量(即残余流量为 0 0 0)那么我们合并 x , y x,y x,y 即 x , y x,y x,y 在同一条路径上。最后找每条路径的起点 O ( n 2 ) O(n^2) O(n2) 打印即可。
变式:路径可以相交。做法:做传递闭包(即对于两个点 u , v u,v u,v,若 u u u 能走到 v v v 则新图中有边 u → v u\to v u→v),然后按照不相交的做法做即可。
Muddy Fields
给定一个 R × C R\times C R×C 的矩阵,每个点是草地和泥地中的一个。现在需要放若干条木板使得所有泥地被覆盖。每条木板长度任意宽度为 1 1 1,只能平行于长宽放置且覆盖区域必须全是泥地。求满足条件所需要的最少的木板数量。
一个经典套路:网格图中考虑行列连边。先考虑平行于行的木板,列是同理的;我们将每个点用一条尽可能长的木板,然后对于每条木板,我们都建一个点;那么对于点 ( i , j ) (i,j) (i,j),至少需要行列上两条木板的其中一条进行覆盖。那我们考虑把这两条木板连边,那么就得到了一张二分图,做一遍最小点覆盖即最大匹配即可。
模型:二分图博弈
基本模型:一个棋子在一个矩阵棋盘上,先手后手每次上下左右移动这颗棋子,不能移动到重复的格子上,谁先走不动谁输。
- 结论:若所有最大匹配都包含起点,先手必胜;否则后手必胜。
- 判断方法;先求一遍最大匹配,然后去掉棋子起点再求一遍最大匹配。第二遍看答案是否相同即可。
网络流
一张有向图,每条边有一个容量表示经过这条边的流量的上限。一般有一个源点和汇点(出度 / 入度为 0 0 0)。需要满足三条性质:容量限制、斜对称性(设 ( u , v ) (u,v) (u,v) 的流量为 f ( u , v ) f(u,v) f(u,v),那么有 f ( u , v ) = − f ( v , u ) f(u,v)=-f(v,u) f(u,v)=−f(v,u))、流守恒性(从源点流出的流量与流入汇点的流量相等)。
两个经典的等价问题模型:
- 最大流
- 最小割
算法:EK 和 Dinic 较为主流。主要思路:不断寻找增广路来提升流量,用反悔贪心做(即可以撤销之前的方案)。
模型:最大流求二分图最大匹配
建立超级源点 S S S 和超级汇点 T T T, S S S 向二分图左边的所有点连一条容量为 1 1 1 的有向边,原图所有边附加上 1 1 1 的容量,二分图右边的所有点向 T T T 连一条容量为 1 1 1 的有向边,最后求一遍最大流即可。
EK
不断使用 bfs 寻找增广路进行增广。每当我们找到一条增广路,我们就求出当前每条边的剩余流量的最小值,则最大流可以获得这个最小值,然后每条边的剩余流量都减去这个最小值;最后考虑反悔,令每条边的反向边的剩余流量加上这个最小值,初始这些边的剩余流量为 0 0 0。理论复杂度 O ( n m 2 ) O(nm^2) O(nm2)。
namespace EK {
int pre[maxn], id[maxn]; Queue q;
int bfs() {
memset(pre,-1,sizeof(pre));
q.clear(); q.push(S);
while (!q.empty()) {
int u = q.front(); q.pop();
for (int i = head[u], v, w;i;i = e[i].nxt) {
v = e[i].to, w = e[i].val;
if (pre[v] != -1 || w == 0) continue;
pre[v] = u, id[v] = i; q.push(v);
if (pre[T] != -1) return 1;
}
} return pre[T] != -1;
}
int EK() {
int ans = 0;
while (bfs()) {
int tmp = inf;
for (int now = T;now != S;now = pre[now])
tmp = min(tmp,e[id[now]].val);
for (int now = T;now != S;now = pre[now])
e[id[now]].val -= tmp, e[id[now] ^ 1].val += tmp;
ans += tmp;
} return ans;
}
}
Dinic
定义残量网络表示求解最大流过程中,剩余流量为正的所有边组成的子图。用 bfs 求出分层图后 dfs 多路增广并像 EK 算法实时更新剩余流量和当前增广得到的流量,使用当前弧优化后时间复杂度 O ( n 2 m ) O(n^2m) O(n2m)。
namespace Dinic {
int dep[maxn]; Queue q;
int cur[maxn];
bool bfs() {
for (int i = S;i <= T;i ++)
cur[i] = head[i], dep[i] = inf;
dep[S] = 0; q.clear(), q.push(S);
while (!q.empty()) {
int u = q.front(); q.pop();
for (int i = head[u], v;i;i = e[i].nxt) {
if (dep[v = e[i].to] < inf || e[i].val == 0) continue;
dep[v] = dep[u] + 1; q.push(v);
if (v == T) return 1;
}
} return dep[T] < inf;
}
int dfs(int u,int lim) {
if (u == T || lim == 0) return lim; // 这句不能忘!
int flow = 0;
for (int i = cur[u], v, tmp;i;i = e[i].nxt) {
cur[u] = i; // 当前弧优化
if (dep[v = e[i].to] == dep[u] + 1 && (tmp = dfs(v,min(lim,e[i].val)))) {
flow += tmp, lim -= tmp, e[i].val -= tmp, e[i ^ 1].val += tmp;
if (lim == 0) break;
}
} return flow;
}
int Dinic() {
int ans = 0;
while (bfs()) ans += dfs(S,inf);
return ans;
}
}
最大流问题的解题思路
- 对于问题 P P P 考虑建流网络 G G G。
- 证明可行解与可行流能互相转化、一一对应。
- 最大可行解就转化成最大流问题,最小割类似。
定理:最大流=最小割。于是可以考虑转化模型从而更好地构建网络流。可以通过将点拆成入点和出点将删点转化为删边。
网络流建模:总是考虑构造出一些限制,在边上用 + ∞ +\infty +∞ 或者 ∞ 2 \infty^2 ∞2 在最小割中进行约束。
模型:最大权值闭合图
基本模型:给定一张有向图,点有点权;求一张子图,使得子图中点权和最大,且子图中的点所有出边指向的点都在该子图中。
做法:建立超级源点 S S S 和汇点 T T T,若节点 u u u 有正点权,则建一条 S → u S\to u S→u,边权为点权;否则 u → T u\to T u→T 建一条边,边权为点权相反数。将原图上所有边权改为 ∞ \infty ∞,跑网络最大流,将所有正点权的权值求和后减去最大流(最小割)即为答案。
在网络流中的一个割中,每一种方案都对应原图中一种合法方案,表现为一种割把网络流分成两部分,其中包含 S S S 那部分没有连向 T T T 中的点。由于原图中的边权都是 ∞ \infty ∞,所以最小割去除的边一定与 S S S 或 T T T 中的一个点相连。不选择一个正点权的点,表现为断开了与 S S S 相连的那条边;选择了一个负点权的点,表现为断开了与 T T T 相连的那条边。
最大获利
给定 n n n 个点,选择第 i i i 个点需要 P i P_i Pi 的代价;给出共 m m m 个点对,对于第 i i i 个点对 ( A i , B i ) (A_i,B_i) (Ai,Bi),如果 A i , B i A_i,B_i Ai,Bi 同时选择那么可以获得 C i C_i Ci 的收益,求收益减代价的最大值。
考虑转化成上述模型,将每个用户群建一个点,权值为 C i C_i Ci;每个基站建一个点,权值为 − P i -P_i −Pi;然后用户向基站连边,按照上述模型做法做即可。
Task Assignment to Two Employees
有两个人和若干个任务,第 i i i 个人完成第 j j j 个任务,可以先获得 v i , j × p i v_{i,j}\times p_i vi,j×pi 的利润,其中 p i p_i pi 初始为 p 0 p_0 p0;然后使 p 0 ← p 0 × s i , j p_0\gets p_0\times s_{i,j} p0←p0×si,j。每个任务必须且只能由两人中的一人完成。可以任意选择完成任务的顺序和完成的人,求最大利润。
对于两个任务 ( v 1 , s 1 ) , ( v 2 , s 2 ) (v_1,s_1),(v_2,s_2) (v1,s1),(v2,s2),如果把它们交给同一个人做且任务相邻,那么贡献即为 max ( v 1 × s 2 , v 2 × s 1 ) \max(v_1\times s_2,v_2\times s_1) max(v1×s2,v2×s1),即考虑先后顺序。 考虑转化为最小割,将每个任务都看作一个点,按照以下方法进行建边:
- 对于不同的两点 i , j i,j i,j,连接容量均为 max ( s 1 , i v 1 , j , s 1 , j v 1 , i ) + max ( s 2 , i v 2 , j , s 2 , j v 2 , i ) \max(s_{1,i}v_{1,j},s_{1,j}v_{1,i})+\max(s_{2,i}v_{2,j},s_{2,j}v_{2,i}) max(s1,iv1,j,s1,jv1,i)+max(s2,iv2,j,s2,jv2,i) 的双向边(反边照常建),这里将每条边的贡献翻倍以便于建边;
- 对于每个点 i i i 记录一个顶标 h 1 / 2. i h_{1/2.i} h1/2.i 表示按照上述规则建边时与第 1 / 2 1/2 1/2 个人的相关的边权之和。
- 令 S , T S,T S,T 分别为超级源点和超级汇点,最后对于每个任务 i i i,连接 S → i S\to i S→i 容量为 2 p 0 v 1 , i + h 1 , i 2p_0v_{1,i}+h_{1,i} 2p0v1,i+h1,i,连接 i → T i\to T i→T 容量为 2 p 0 v 2 , i + h 2 , i 2p_0v_{2,i}+h_{2,i} 2p0v2,i+h2,i。
最后最小割的方案中,保留下来的边即为一组合法最优解。由于贡献翻了一倍进行计算,所以最终答案需要除以二。
Sum of Abs
给定一张 n n n 个点 m m m 条边的无向图,第 i i i 个点有两个点权 A i A_i Ai 和 B i B_i Bi;删去第 i i i 个点和与之相连的边的代价为 A i A_i Ai。定义一个极大连通块的权值为连通块中点的 B B B 权值和的绝对值。最大化删除若干点后,所有极大连通块权值和减去总代价。
考虑每个点对答案的贡献。对于第 i i i 个点,显然如果它被删除则有 − a i -a_i −ai 的贡献。如果它最终被归入权值和为正的连通块中则有 b i b_i bi 的贡献;反之则有 − b i -b_i −bi 的贡献。不考虑是否合法,理想的最优答案显然是将所有 b i b_i bi 为正的点丢到同一个连通块(下文中称为”正连通块“)中,所有 b i b_i bi 为负的点丢到另一个连通块(下文中称为”负连通块“)中。
现在考虑使答案变得合法所需要的代价。对于第 i i i 个点,如果它被删去则有 a i + ∣ b i ∣ a_i+|b_i| ai+∣bi∣ 的代价,前者为删除代价,后者为从合法答案中去除的部分;如果 b i b_i bi 为正却被归入了负连通块中,那么代价为 b i × 2 b_i\times 2 bi×2,分别为正连通块中缺少的部分和负连通块中 b i > 0 b_i>0 bi>0 导致减少的部分;如果 b i b_i bi 为负却被归入了正连通块中,那么同理代价为 ( − b i ) × 2 (-b_i)\times 2 (−bi)×2。而对于同一个连通块中的点,它们最终的贡献取正取负显然应该是相同的。
于是我们考虑构建网络流中的最小割。建立超级源点 S S S 和超级汇点 T T T,令最终 b i b_i bi 取正的点有连边 S → i S\to i S→i,反之有连边 i → T i\to T i→T。套路地,对于点 u u u,我们将其拆成入点 u u u 和出点 u ′ u' u′,则删除 u u u 就可以表示为删除 u → u ′ u\to u' u→u′ 这条边,故该边边权即为代价 a u + ∣ b u ∣ a_u+|b_u| au+∣bu∣。对于原图中的边 ( u , v ) (u,v) (u,v),我们连两条容量无穷大的边 u ′ → v , v ′ → u u'\to v,v'\to u u′→v,v′→u 以防通过删边不删点获得错误的答案。
最后按照上述的代价进行建边即可。最终答案即为理想答案减去最小割。
Special Edges
给定一张 n n n 个点 m m m 条边的网络流,源点和汇点分别为 1 , n 1,n 1,n。 q q q 次询问每次更改编号为 1 1 1 到 k k k 的边的容量(初始容量均为 0 0 0),对于每次询问求出此时的最大流。
1 ≤ k ≤ 10 1\le k\le 10 1≤k≤10, 1 ≤ 容量 ≤ 25 1\le \text{容量}\le 25 1≤容量≤25。
考虑在不知道 k k k 条边的边权时的答案,我们设包含且仅包含这 k k k 条边的边集为 K K K。将最大流转化为最小割,那么我们就可以计算 K K K 的任意子集在最小割中时至少的答案。由于 k ≤ 10 k\le 10 k≤10 故考虑状压,对于 K K K 中被割的边,我们将容量设为 0 0 0,反之设为 ∞ \infty ∞,然后跑最大流求解即可。最终对于每轮询问,我们仍然枚举 k k k 条边是否被割去,然后拿着新的容量填进当前 K K K 中被割了的边的边权,把所有答案取 min \min min 即为所求。
但显然复杂度爆炸,考虑优化。注意到我们在枚举 k k k 条边割与不割的状态时,可以通过上一个相邻状态的答案推出新一轮的答案。具体地,我们先在最开始算出 K K K 均割去的答案(即容量全 0 0 0);在枚举状态时设 K K K 中当前割去的边集为 A A A,我们找到一个边集 A ′ A' A′,满足 A ′ A' A′ 的答案已经得到且 A ′ A' A′ 是 A A A 的子集,那么我们在边集为 A ′ A' A′ 时得出的残量网络中,将 A A A 中少割去的边的容量设为 ∞ \infty ∞,跑出来的最大流即为从 A ′ A' A′ 到 A A A 最大流答案增加的流量。这样每次计算量就减少了很多。
但复杂度仍然偏高。注意到边的容量最大值只有 25 25 25,我们考虑令 ∞ = 25 \infty=25 ∞=25,此时跑 EK 算法的复杂度要优于 Dinic。于是我们只在计算 K K K 全部割去的答案时使用 Dinic,后续均使用 EK。这样复杂度就基本正确了。
上下界网络流
https://www.luogu.com.cn/article/28oo8d8l
费用流
在最大流的基础上给每条边附加了一个单位费用,代价为单位费用乘这条边的流量。在流量最大的情况下需要费用最大/小。
做法:将 EK 算法中的 bfs 替换为 spfa,原图中反边的单位费用为正边的相反数(仍然是考虑反悔),spfa 以单位费用为边权跑最短/长路,最后单轮产生的费用即为这一轮产生的流量乘从 S S S 到 T T T 的最短路。大部分和最大流代码类似。
namespace EK {
int dis[maxn], pre[maxn], flow[maxn], id[maxn];
bool vis[maxn];
queue<int> q;
pair<int,int> spfa() {
memset(dis, 0x3f, sizeof(dis));
memset(vis, 0, sizeof(vis));
memset(flow, 0x3f, sizeof(flow));
vis[S] = 1, dis[S] = 0; q.push(S);
while (!q.empty()) {
int u = q.front(); q.pop(); vis[u] = 0;
for (int i = head[u], v, w, c;i;i = e[i].nxt) {
if ((w = e[i].val) == 0 || dis[v = e[i].to] <= dis[u] + (c = e[i].cost))
continue;
dis[v] = dis[u] + c, flow[v] = min(flow[u], w), pre[v] = u, id[v] = i;
if (!vis[v]) { vis[v] = 1; q.push(v); }
}
}
if (dis[T] >= inf) return {-1,114514};
int lim = flow[T];
for (int i = T;i != S;i = pre[i])
lim, e[id[i]].val -= lim, e[id[i] ^ 1].val += lim;
return {lim, dis[T] * lim};
}
pair<int,int> EK() {
int mxflow = 0, ans = 0;
while (1) {
auto tmp = spfa();
if (tmp.first == -1) break;
ans += tmp.second, mxflow += tmp.first;
} return {mxflow, ans};
}
}