二分图 & 网络流
二分图
可以考虑黑白染色(即一条边两边端点颜色不同)和没有奇环转化成二分图的模型。
-
匹配:选择若干条边使得每个点周围至多只有一条边被选。
-
最大匹配:最多的边。
-
增广路用于将匹配边替换为一些当前可能更优的非匹配边,增广路中匹配边与非匹配边交替出现。
-
匈牙利算法: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;
}
- 点覆盖:选若干点使得所有边都有被选择的点覆盖。
- 独立集:原图中点集的一个子集,使得子集中所有点没有边相连。(无向图最大团等于其补图的最大独立集)
- 最小点覆盖等价于最大匹配(前者所需点数 === 后者所需边数),最大独立集等价于 nnn 减最小点覆盖。
模型:最小路径覆盖
基本模型:给出一张有向图,要用若干条不相交的简单路径覆盖所有点,求最少路径数。
- 做法:考虑拆点构建二分图。将所有点拆成两个点(记为 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′) 上有流量(即残余流量为 000)那么我们合并 x,yx,yx,y 即 x,yx,yx,y 在同一条路径上。最后找每条路径的起点 O(n2)O(n^2)O(n2) 打印即可。
变式:路径可以相交。做法:做传递闭包(即对于两个点 u,vu,vu,v,若 uuu 能走到 vvv 则新图中有边 u→vu\to vu→v),然后按照不相交的做法做即可。
Muddy Fields
给定一个 R×CR\times CR×C 的矩阵,每个点是草地和泥地中的一个。现在需要放若干条木板使得所有泥地被覆盖。每条木板长度任意宽度为 111,只能平行于长宽放置且覆盖区域必须全是泥地。求满足条件所需要的最少的木板数量。
一个经典套路:网格图中考虑行列连边。先考虑平行于行的木板,列是同理的;我们将每个点用一条尽可能长的木板,然后对于每条木板,我们都建一个点;那么对于点 (i,j)(i,j)(i,j),至少需要行列上两条木板的其中一条进行覆盖。那我们考虑把这两条木板连边,那么就得到了一张二分图,做一遍最小点覆盖即最大匹配即可。
模型:二分图博弈
基本模型:一个棋子在一个矩阵棋盘上,先手后手每次上下左右移动这颗棋子,不能移动到重复的格子上,谁先走不动谁输。
- 结论:若所有最大匹配都包含起点,先手必胜;否则后手必胜。
- 判断方法;先求一遍最大匹配,然后去掉棋子起点再求一遍最大匹配。第二遍看答案是否相同即可。
网络流
一张有向图,每条边有一个容量表示经过这条边的流量的上限。一般有一个源点和汇点(出度 / 入度为 000)。需要满足三条性质:容量限制、斜对称性(设 (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 较为主流。主要思路:不断寻找增广路来提升流量,用反悔贪心做(即可以撤销之前的方案)。
模型:最大流求二分图最大匹配
建立超级源点 SSS 和超级汇点 TTT,SSS 向二分图左边的所有点连一条容量为 111 的有向边,原图所有边附加上 111 的容量,二分图右边的所有点向 TTT 连一条容量为 111 的有向边,最后求一遍最大流即可。
EK
不断使用 bfs 寻找增广路进行增广。每当我们找到一条增广路,我们就求出当前每条边的剩余流量的最小值,则最大流可以获得这个最小值,然后每条边的剩余流量都减去这个最小值;最后考虑反悔,令每条边的反向边的剩余流量加上这个最小值,初始这些边的剩余流量为 000。理论复杂度 O(nm2)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(n2m)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;
}
}
最大流问题的解题思路
- 对于问题 PPP 考虑建流网络 GGG。
- 证明可行解与可行流能互相转化、一一对应。
- 最大可行解就转化成最大流问题,最小割类似。
定理:最大流=最小割。于是可以考虑转化模型从而更好地构建网络流。可以通过将点拆成入点和出点将删点转化为删边。
网络流建模:总是考虑构造出一些限制,在边上用 +∞+\infty+∞ 或者 ∞2\infty^2∞2 在最小割中进行约束。
模型:最大权值闭合图
基本模型:给定一张有向图,点有点权;求一张子图,使得子图中点权和最大,且子图中的点所有出边指向的点都在该子图中。
做法:建立超级源点 SSS 和汇点 TTT,若节点 uuu 有正点权,则建一条 S→uS\to uS→u,边权为点权;否则 u→Tu\to Tu→T 建一条边,边权为点权相反数。将原图上所有边权改为 ∞\infty∞,跑网络最大流,将所有正点权的权值求和后减去最大流(最小割)即为答案。
在网络流中的一个割中,每一种方案都对应原图中一种合法方案,表现为一种割把网络流分成两部分,其中包含 SSS 那部分没有连向 TTT 中的点。由于原图中的边权都是 ∞\infty∞,所以最小割去除的边一定与 SSS 或 TTT 中的一个点相连。不选择一个正点权的点,表现为断开了与 SSS 相连的那条边;选择了一个负点权的点,表现为断开了与 TTT 相连的那条边。
最大获利
给定 nnn 个点,选择第 iii 个点需要 PiP_iPi 的代价;给出共 mmm 个点对,对于第 iii 个点对 (Ai,Bi)(A_i,B_i)(Ai,Bi),如果 Ai,BiA_i,B_iAi,Bi 同时选择那么可以获得 CiC_iCi 的收益,求收益减代价的最大值。
考虑转化成上述模型,将每个用户群建一个点,权值为 CiC_iCi;每个基站建一个点,权值为 −Pi-P_i−Pi;然后用户向基站连边,按照上述模型做法做即可。
Task Assignment to Two Employees
有两个人和若干个任务,第 iii 个人完成第 jjj 个任务,可以先获得 vi,j×piv_{i,j}\times p_ivi,j×pi 的利润,其中 pip_ipi 初始为 p0p_0p0;然后使 p0←p0×si,jp_0\gets p_0\times s_{i,j}p0←p0×si,j。每个任务必须且只能由两人中的一人完成。可以任意选择完成任务的顺序和完成的人,求最大利润。
对于两个任务 (v1,s1),(v2,s2)(v_1,s_1),(v_2,s_2)(v1,s1),(v2,s2),如果把它们交给同一个人做且任务相邻,那么贡献即为 max(v1×s2,v2×s1)\max(v_1\times s_2,v_2\times s_1)max(v1×s2,v2×s1),即考虑先后顺序。 考虑转化为最小割,将每个任务都看作一个点,按照以下方法进行建边:
- 对于不同的两点 i,ji,ji,j,连接容量均为 max(s1,iv1,j,s1,jv1,i)+max(s2,iv2,j,s2,jv2,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) 的双向边(反边照常建),这里将每条边的贡献翻倍以便于建边;
- 对于每个点 iii 记录一个顶标 h1/2.ih_{1/2.i}h1/2.i 表示按照上述规则建边时与第 1/21/21/2 个人的相关的边权之和。
- 令 S,TS,TS,T 分别为超级源点和超级汇点,最后对于每个任务 iii,连接 S→iS\to iS→i 容量为 2p0v1,i+h1,i2p_0v_{1,i}+h_{1,i}2p0v1,i+h1,i,连接 i→Ti\to Ti→T 容量为 2p0v2,i+h2,i2p_0v_{2,i}+h_{2,i}2p0v2,i+h2,i。
最后最小割的方案中,保留下来的边即为一组合法最优解。由于贡献翻了一倍进行计算,所以最终答案需要除以二。
Sum of Abs
给定一张 nnn 个点 mmm 条边的无向图,第 iii 个点有两个点权 AiA_iAi 和 BiB_iBi;删去第 iii 个点和与之相连的边的代价为 AiA_iAi。定义一个极大连通块的权值为连通块中点的 BBB 权值和的绝对值。最大化删除若干点后,所有极大连通块权值和减去总代价。
考虑每个点对答案的贡献。对于第 iii 个点,显然如果它被删除则有 −ai-a_i−ai 的贡献。如果它最终被归入权值和为正的连通块中则有 bib_ibi 的贡献;反之则有 −bi-b_i−bi 的贡献。不考虑是否合法,理想的最优答案显然是将所有 bib_ibi 为正的点丢到同一个连通块(下文中称为”正连通块“)中,所有 bib_ibi 为负的点丢到另一个连通块(下文中称为”负连通块“)中。
现在考虑使答案变得合法所需要的代价。对于第 iii 个点,如果它被删去则有 ai+∣bi∣a_i+|b_i|ai+∣bi∣ 的代价,前者为删除代价,后者为从合法答案中去除的部分;如果 bib_ibi 为正却被归入了负连通块中,那么代价为 bi×2b_i\times 2bi×2,分别为正连通块中缺少的部分和负连通块中 bi>0b_i>0bi>0 导致减少的部分;如果 bib_ibi 为负却被归入了正连通块中,那么同理代价为 (−bi)×2(-b_i)\times 2(−bi)×2。而对于同一个连通块中的点,它们最终的贡献取正取负显然应该是相同的。
于是我们考虑构建网络流中的最小割。建立超级源点 SSS 和超级汇点 TTT,令最终 bib_ibi 取正的点有连边 S→iS\to iS→i,反之有连边 i→Ti\to Ti→T。套路地,对于点 uuu,我们将其拆成入点 uuu 和出点 u′u'u′,则删除 uuu 就可以表示为删除 u→u′u\to u'u→u′ 这条边,故该边边权即为代价 au+∣bu∣a_u+|b_u|au+∣bu∣。对于原图中的边 (u,v)(u,v)(u,v),我们连两条容量无穷大的边 u′→v,v′→uu'\to v,v'\to uu′→v,v′→u 以防通过删边不删点获得错误的答案。
最后按照上述的代价进行建边即可。最终答案即为理想答案减去最小割。
Special Edges
给定一张 nnn 个点 mmm 条边的网络流,源点和汇点分别为 1,n1,n1,n。qqq 次询问每次更改编号为 111 到 kkk 的边的容量(初始容量均为 000),对于每次询问求出此时的最大流。
1≤k≤101\le k\le 101≤k≤10,1≤容量≤251\le \text{容量}\le 251≤容量≤25。
考虑在不知道 kkk 条边的边权时的答案,我们设包含且仅包含这 kkk 条边的边集为 KKK。将最大流转化为最小割,那么我们就可以计算 KKK 的任意子集在最小割中时至少的答案。由于 k≤10k\le 10k≤10 故考虑状压,对于 KKK 中被割的边,我们将容量设为 000,反之设为 ∞\infty∞,然后跑最大流求解即可。最终对于每轮询问,我们仍然枚举 kkk 条边是否被割去,然后拿着新的容量填进当前 KKK 中被割了的边的边权,把所有答案取 min\minmin 即为所求。
但显然复杂度爆炸,考虑优化。注意到我们在枚举 kkk 条边割与不割的状态时,可以通过上一个相邻状态的答案推出新一轮的答案。具体地,我们先在最开始算出 KKK 均割去的答案(即容量全 000);在枚举状态时设 KKK 中当前割去的边集为 AAA,我们找到一个边集 A′A'A′,满足 A′A'A′ 的答案已经得到且 A′A'A′ 是 AAA 的子集,那么我们在边集为 A′A'A′ 时得出的残量网络中,将 AAA 中少割去的边的容量设为 ∞\infty∞,跑出来的最大流即为从 A′A'A′ 到 AAA 最大流答案增加的流量。这样每次计算量就减少了很多。
但复杂度仍然偏高。注意到边的容量最大值只有 252525,我们考虑令 ∞=25\infty=25∞=25,此时跑 EK 算法的复杂度要优于 Dinic。于是我们只在计算 KKK 全部割去的答案时使用 Dinic,后续均使用 EK。这样复杂度就基本正确了。
上下界网络流
https://www.luogu.com.cn/article/28oo8d8l
费用流
在最大流的基础上给每条边附加了一个单位费用,代价为单位费用乘这条边的流量。在流量最大的情况下需要费用最大/小。
做法:将 EK 算法中的 bfs 替换为 spfa,原图中反边的单位费用为正边的相反数(仍然是考虑反悔),spfa 以单位费用为边权跑最短/长路,最后单轮产生的费用即为这一轮产生的流量乘从 SSS 到 TTT 的最短路。大部分和最大流代码类似。
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};
}
}

779

被折叠的 条评论
为什么被折叠?



