网络流笔记
网络流笔记
1. 最大流
流网络中流量最大的可行流。
1.1. EK 算法
每次利用 BFS 找到一条增广路。
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 5e4, inf = 1e18;
int n, m, s, t, u, v, head[N], cyf[N], tot = 1, pre[N];
bool vis[N], vg[N];
struct edge
{
int to, nxt, flow;
} e[N << 1];
void addedge(int u, int v, int w)
{
e[++tot] = {v, head[u], w}, head[u] = tot;
}
bool bfs(int s, int t)
{
queue<int> q;
memset(vis, 0, sizeof vis);
q.push(s), vis[s] = 1, cyf[s] = inf;
while(!q.empty())
{
int u = q.front(); q.pop();
for(int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if(!vis[v] && e[i].flow)
{
cyf[v] = min(cyf[u], e[i].flow);
q.push(v), vis[v] = 1, pre[v] = i;
if(v == t) return 1;
}
}
}
return 0;
}
int EK(int s, int t)
{
int maxflow = 0, cur;
while(bfs(s, t))
{
cur = t, maxflow += cyf[t];
vector<int> g;
while(cur != s)
{
int pr = pre[cur];
e[pr].flow -= cyf[t];
e[pr ^ 1].flow += cyf[t];
if(cur != t) g.push_back(cur > n ? cur - n : cur);
cur = e[pr ^ 1].to;
}
}
return maxflow;
}
1.2. Dinic 算法
对于 EK 算法,我们每次执行完 BFS 寻找增广路径之后,都要重新进行 BFS 寻找新的增广路径,但
是我们在 BFS 之后进行 DFS,就可以实现多路同时增广。
当前弧优化:如果某一时刻我们已经知道边 \((u, v)\) 已经增广到极限,边 \((u, v)\) 已无剩余容量或 \(v\) 的后侧已增广至阻塞),则 \(u\) 的流量没有必要再尝试流向出边 \((u, v)\). 据此,对于每个结点 \(u\),我们维护 \(u\) 的出边表中第一条还有必要尝试的出边。习惯上,我们称维护的这个指针为当前弧。
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e3 + 5;
int n, m, u, v, w, head[N], cur[N], dep[N], s, t, tot = 1;
struct edge
{
int to, nxt, flow;
} e[N << 2];
void addedge(int u, int v, int w)
{
e[++tot] = {v, head[u], w}, head[u] = tot;
}
bool bfs(int s, int t)
{
queue<int> q;
memset(dep, 0, sizeof dep);
memcpy(cur, head, sizeof cur);
q.push(s), dep[s] = 1;
while(!q.empty())
{
int u = q.front(); q.pop();
for(int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if(!dep[v] && e[i].flow)
dep[v] = dep[u] + 1, q.push(v);
}
}
return (bool) dep[t];
}
int dfs(int u, int t, int lim)
{
if(u == t) return lim;
int flow = 0;
for(int i = cur[u]; i && flow < lim; i = e[i].nxt)
{
int v = e[i].to;
cur[u] = i;
if(dep[v] == dep[u] + 1 && e[i].flow)
{
int w = dfs(v, t, min(lim - flow, e[i].flow));
e[i].flow -= w;
e[i ^ 1].flow += w;
flow += w;
}
}
return flow;
}
int dinic(int s, int t)
{
int maxflow = 0;
while(bfs(s, t))
{
int flow = dfs(s, t, 1 << 30);
maxflow += flow;
}
return maxflow;
}
2. 最小割
最小割=最大流。
常常用最小割表示损失。
3. 拆点
作用:
- 限制点出现个数:P2764 最小路径覆盖问题
- 点本身有不同的性质:
P4662 [BalticOI 2008] 黑手党
将一个节点 \(X\) 拆成 \(X_1\) 和 \(X_2\),从而将 \(X\) 的点权转移到边 \((X_1, X_2)\) 上。
4. 最大权闭合子图
4.1. 定义
闭合图:对于一个有向图 \(G\),存在点集合 \(V\),任取点 \(u\) 属于 \(V\),\(u\) 的出边的另一个点也属于 \(V\),则为闭合图。即随便一个起点,对应的终点就是没有出度的点。
最大权闭合子图:当每个点有一个权值 \(w\)(有正有负),点权和最大的闭合图为最大权闭合子图。
选择 \(x\) 获得 \(v_x\) 的价值,选择 \(y\) 获得 \(v_y\) 的价值,\(x, y\) 都选有 \(d_{x, y}\) 的奖励。
通常使用最小割解决。
4.2. 建图
- 增加源 \(S\) 汇 \(T\).
- 将原题中的边和点都看成事件,构造性地,将边转化为点事件,原图便转化为一个二分图。
- \(S\) 连接原图的正权点,容量为相应点权。
- 原图的负权点连接汇 \(t\),容量为相应点权的相反数。在此处,由于要求的是最小割即损失,“奖励”就变为了负权。
- 原图边的容量设为 \(inf\).
4.3. 答案
求出最大收益就是让损失最小。
把所有收益加起来,再减去最小割的损失,就是最大净收益。
\(ans = sum - maxflow\).
4.4. EX 最大权闭合子图
选择 \(x\) 获得 \(v_x\) 的价值,选择 \(y\) 获得 \(v_y\) 的价值,选 \(x\) 不选 \(y\) 有 \(d_{x, y}\) 的惩罚。
建图方法:
- 增加源 \(S\) 汇 \(T\).
- \(S\) 连接原图的正权点,容量为相应点权。
- 原图的负权点连接汇 \(t\),容量为相应点权的相反数。
- 原图边的容量设为 \(d_{x, y}\).
5. 网格/地图型网络流
对于 \(n \times m\) 的网格,通常要写 \(id(i, j) = (i - 1) \times m + j\) 转化坐标.
大致是 \(S \to X_1 \to X_2 \to Y_1 \to Y_2 \to \cdots \to T\).
\(X_1 \to X_2\) 的流量可以存节点 \(X\) 的某种性质。
6. 费用流
在最大流的基础上,每条边多了一个单位费用 \(c\),表示流过一单位的流需要花费 \(c\).
问流最大时,费用最小是多少。
建边:\((u, v, flow, c), (v, u, 0, -c)\).
考虑在 EK 算法中动态维护源点到 \(u\) 的最短路 \(dis_u\).
我们有 \(dis_v = \min(dis_v, dis_u + w(u, v))\).
可以使用 SPFA 解决(因为有负权边,所以不能用 Dijkstra)。
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 5e3 + 5, M = 2e5 + 5, inf = 1e18;
int n, m, s, t, u, v, w, c, head[N], cur[N], maxflow, mincost, tot = 1, cyf[N], pre[N], dis[N];
bool vis[N];
struct edge
{
int to, nxt, flow, cost;
} e[M << 1];
void addedge(int u, int v, int w, int c)
{
e[++tot] = {v, head[u], w, c}, head[u] = tot;
}
bool spfa(int s, int t)
{
queue<int> q;
memset(dis, 0x3f, sizeof dis);
memset(vis, 0, sizeof vis);
q.push(s), dis[s] = 0, vis[s] = 1, cyf[s] = inf;
while(!q.empty())
{
int u = q.front(); q.pop();
vis[u] = 0;
for(int i = head[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if(dis[v] > dis[u] + e[i].cost && e[i].flow)
{
dis[v] = dis[u] + e[i].cost, pre[v] = i;
cyf[v] = min(cyf[u], e[i].flow);
if(!vis[v]) vis[v] = 1, q.push(v);
}
}
}
return (dis[t] < inf);
}
void EK(int s, int t)
{
while(spfa(s, t))
{
int cur = t;
maxflow += cyf[t];
mincost += cyf[t] * dis[t];
while(cur != s)
{
int pr = pre[cur];
e[pr].flow -= cyf[t];
e[pr ^ 1].flow += cyf[t];
cur = e[pr ^ 1].to;
}
}
return;
}
signed main()
{
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> m >> s >> t;
for(int i = 1; i <= m; i++)
{
cin >> u >> v >> w >> c;
addedge(u, v, w, c);
addedge(v, u, 0, -c);
}
EK(s, t);
cout << maxflow << ' ' << mincost << '\n';
return 0;
}
7. 无源汇上下界可行流
模型:一个网络,求出一个流,使得每条边的流量必须在 \([L_i, R_i]\) 内,每个点必须满足 \(\text{总流入量}=\text{总流出量}\)(流量守恒)。
- 令每条边流量为流量下限,建出这个图的残量网络,每条边的 \(\text{流量}=\text{流量上限}-\text{流量下限}\);
- 求出原图不满足流量守恒的附加流,附加流与原图合并后流量守恒。设 \(deg_i\) 表示原图(而不是残量网络)中 \(\text{流入流量}-\text{流出流量}\),对于附加流来说:\(deg_i=\text{流出流量}-\text{流入流量}\).
- 对于不平衡的流量,我们建一个全新的源点 \(S'\) 和全新的汇点 \(T'\).
- 对于 \(deg_i > 0\),从 \(S'\) 到 \(i\) 建一条容量为 \(|deg_i|\) 的边;
对于 \(deg_i < 0\),从 \(i\) 到 \(T'\) 建一条容量为 \(|deg_i|\) 的边。
8. 有源汇上下界可行流
从 \(T\) 到 \(S\) 连一条容量为 \(inf\) 的边,让流量循环一下,这就不平衡了。
接下来跑无源汇有上下界可行流模板即可。
9. 有源汇上下界最大流
- 跑一个有源汇上下界可行流,如果满流则设其答案为 \(flow_1\),否则无解。
- 删去从原图汇点到原图源点的边(即边 \((T, S)\))。
- 跑从原图源点到原图汇点的最大流,设答案为 \(flow_2\),则总答案 \(ans = flow_1 + flow_2\).