网络最大流
Tip:括号内的文字为术语。
莱阳一中的同学要参加 NOI 啦。老师得到了一张交通图(网络),节点为城市, S S S 为起点(源点), T T T 为终点(汇点)。
为了控制名额,所以有必要对每条道路经过的人数进行限制(容量)。比如下图, 1 → 3 1\to 3 1→3 的道路只允许两个人经过,而且道路的限制是永久的而不是单位时间的限制,比如今天过去了一个人(实际流量),那么限制(残留容量)变成了 1 1 1,明天又过去两个人是不可以的。
可以发现这张图中有许多条 S → T S \to T S→T 的路径(流)。那么,一条 S → T S \to T S→T 的路径(流)最多通过多少人呢?当然是这条路径(流)上,最小的边权(短板)。
那么能去打 NOI 的人数最多为多少(网络最大流)就是我们的问题。
最小割最大流定理
Tip:如果你只想学板子,可以跳过这里。
dzd 知道了有许多人想拿 Au,感觉自己经费不足,所以想炸掉一些路,图中没有流。这就是割,那么最小割要求选择的边集的权值之和尽可能小,也就是在每条流上选短板。
所以有了一个定理:最小割最大流定理。即为最大流 = = = 最小割。
暴力 Ford-Fulkerson
中国地大物博,交通线众多,想依靠人类的智慧是不可能的。看来只能求计算机了,但是计算机像需要算法,而不是计算机自己手算一下。
那么我们给计算机想一个可靠的算法。如图,计算机找了一个人,随便找了一条流,将经过的边的容量减去一。计算机发现,走完这条流后,实际流量多了一。所以,给这条流起名叫:增广路。
可爱的计算机走完这条增广路后,发现自己无路可走了,其实走
S
→
1
→
T
S \to 1 \to T
S→1→T 和
S
→
2
→
T
S \to 2 \to T
S→2→T 才是最大流。那可怎么办?已经有人回去爆搜了。
所以还是靠人类的智慧——反向边。在找到的增光路上,在路径上相邻的两个点添加一条反向边,边的流量等于这次过去的人数,允许其它路径过来。
如上图,紫色的有向边就是反向边。TA 有什么用呢?举个栗子,计算机再一次从
S
S
S 出发找增广路。它从
S
→
2
→
1
→
T
S\to 2 \to 1 \to T
S→2→1→T,又找出了一条增光路,又建了一个反向边(紫色边的反向边即为原边,直接修改即可,所以只加了一条
1
→
T
1 \to T
1→T 的反向边)。重点来了:
当第二次的增广走 2 → 1 2 \to 1 2→1 这条反向边的时候,就相当于把 1 → 3 1 \to 3 1→3 这条正向边已经用了的流量给退了回去,不走 1 → 2 1 \to 2 1→2 这条边,而改走从 2 2 2 点出发走其他的路也就是 1 → T 1 \to T 1→T。同时本来在 2 → T 2 \to T 2→T 上的流量由 S → 2 → T S \to 2 \to T S→2→T 这条路来接管。
是不是像小时候妈妈说的:知错就改就是最大流好孩子。
所以,无数实践已经证明:如果不断的找增光路,并提供反悔的机会,那么最后的流量即为最大流。那么这也是 Ford-Fulkerson 算法。
在实现时,建图的时候就把反向边建起来,等用到的时候直接作出修改即可。对于建边的小细节在代码中说明。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 4, M = 2e5 + 5;
int n, m, s, t, tot = 1, head[N], vis[N];
/*
十进制 二进制
0 00
1 01
2 10
3 11
4 20
可以发现从 1 开始建边,一个边与它的反边的二进制只有最后一位不同。
*/
struct edge{
int to, nxt, w;
}e[M];
void addedge(int x, int y, int w) {
e[++tot].to = y; e[tot].w = w; e[tot].nxt = head[x]; head[x] = tot;
}
int dfs(int x, int flow) { //在没有走到汇点前,我们不知道流量是多少,所以flow是动态更新的
if (x == t) return flow;//走到汇点返回本次增广的流量
vis[x] = 1;
for (int i = head[x]; i; i = e[i].nxt) {
int y = e[i].to, w = e[i].w;
if (w && !vis[y]){ //不能重复经过,如果到的点没有残余可以用的流量,那么走过去也没用
int t = dfs(y, min(flow, w));
if (t > 0) { //顺着流过去,要受一路上最小容量的限制
e[i].w -= t; e[i ^ 1].w += t; //此边残余容量减小并建立反向边
return t;
}
}
}
return 0; //无法到汇点
}
int main() {
scanf("%d %d %d %d", &n, &m, &s, &t);
for (int i = 1; i <= m; ++i) {
int x, y, w; scanf("%d %d %d", &x, &y, &w);
addedge(x, y, w); addedge(y, x, 0);
/*反向边开始容量为0,表示不允许平白无故走反向边
只有正向边流量过来以后,才提供返还流量的机会*/
}
int res = 0, ans = 0;
while (memset(vis, 0, sizeof(vis)) && (res = dfs(s, 2e9 /*假设flow很大*/)) > 0) ans += res;
printf("%d\n", ans);
return 0;
}
上图概述了 FF 算法的原理,那么想要卡 FF 也很容易,只要构造一张这样的图就可以卡。所以 FF 最坏的时间复杂度为 O ( n 2 m ) O(n^2m) O(n2m)
Dinic + 当前弧优化
刚刚知道了卡 FF 的思路,因为 FF 每次只找一条增广路,所以构造一个长得像菊花图的网络就可以卡,那么遇到许多分叉的情况,能不能不走分叉之前的路呢?这就是 Dinic 的核心——多路增广。
x x x 点通过一条边,向 y y y 输出流量以后,y 会尝试到达汇点(到达汇点才真正增广),然后 y y y 返回实际增广的流量。这时,如果 x x x 还有没用完的流量,就继续尝试输出到其它边,而不是等下一次尝试增广。但是要警惕绕远路、甚至绕回的情况,不加管制的话极易发生。怎么管?
那么就要用到分层图了,我们让每个点只流向它下一层的点 即可。下图概述了 Dinic 算法的原理
那么当前弧优化是对 Dinic 的改进。每次增广一条路后可以看做榨干了这条路,既然榨干了就没有再增广的可能了。但如果每次都扫描这些边是很浪费时间的。那我们就记录一下榨取到那条边了,然后下一次直接从这条边开始增广,就可以节省大量的时间。
#include <bits/stdc++.h>
using namespace std;
#define re register
#define F first
#define S second
typedef long long ll;
typedef pair<int, int> P;
const int N = 1e5 + 5, M = 2e6 + 5;
const int INF = 0x3f3f3f3f;
inline int read() {
int X = 0,w = 0; char ch = 0;
while(!isdigit(ch)) {w |= ch == '-';ch = getchar();}
while(isdigit(ch)) X = (X << 3) + (X << 1) + (ch ^ 48),ch = getchar();
return w ? -X : X;
}
struct edge{
int to, nxt, w;
}e[M];
int head[N], cur[N], tot = 1, vis[N];
int n, m, s, t, dep[N];
void addedge(int x, int y, int w){
e[++tot].to = y; e[tot].w = w; e[tot].nxt = head[x]; head[x] = tot;
}
bool bfs(){ //每一次都要预处理分层图,因为每次增广后残余流量会改变。
memcpy(cur, head, sizeof(head));
memset(dep, 0, sizeof(dep)); dep[s] = 1; //一定要初始化
queue <int> q; q.push(s);
while (!q.empty()){
int x = q.front(); q.pop();
for (int i = head[x]; i; i = e[i].nxt){
int y = e[i].to, w = e[i].w;
if (w && !dep[y]){ //如果有残余流量(没有的话谁也过不去) 并且这个点是第一次到达
dep[y] = dep[x] + 1;
q.push(y);
}
}
}
return dep[t];//t 的深度不为 0,就是搜到了汇点
}
int dfs(int x, int flow){
if (x == t) return flow;
int sum = 0;
for (int i = cur[x]; i; i = e[i].nxt) { //当前弧优化
cur[x] = i; if (flow == 0) return sum;
int y = e[i].to, w = e[i].w;
if (w && dep[y] == dep[x] + 1){//仅允许流向下一层
int t = dfs(y, min(flow, w));
flow -= t; sum += t;
e[i].w -= t; e[i ^ 1].w += t;
}
}
if (!sum) dep[x] = 0; //我与终点(顺着残量网络)不连通的话,那么上一层的点请别给我流量
return sum;
}
int main(){
n = read(), m = read(), s = read(), t = read();
for (int i = 1; i <= m; i++){
int x = read(), y = read(), w = read();
addedge(x, y, w); addedge(y, x, 0);
}
int ans = 0;
while (bfs()) ans += dfs(s, 0x3f3f3f3f);
printf("%d\n", ans);
return 0;
}
Dinic + 当前弧优化的理论复杂度也是 O ( n 2 m ) O(n^2m) O(n2m),实现时远远达不到。
最小费用最大流
现在每条道路要收费了,都有一个费用 w w w,如果有 k k k 个人经过这条道路,那么要交 k × w k \times w k×w 的过路费。现在,老师希望能在有尽可能多的人参加 NOI 的前提下,花的钱之和最小。
那么不说废话,做法比较简单。把费用当成边权,每次找到 S → T S \to T S→T 的最短路径,然后让这条路径的流量尽可能的饱满。用 spfa 实现即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 4, M = 2e5 + 5;
bool vis[N];//在这个 spfa 中,我们不能用 dis 判重,需要用 vis 判重 一个是因为代价可能为 0 或 负数,主要原因是我们要把 dis 赋值为 INF
int n, m, s, t, dis[N], pre[N], last[N], flow[N], maxflow, mincost;
/*
flow[i] 到第 i 个点的最大流量。
dis[i] 为优先流量最大的情况下,到达第 i 点的最小的单位花费(不是最终花费,计算的时候还要乘上) 。
pre[i] 第 i 个点在流量最大的情况下的前驱。
last[i] 第 i 个点在流量最大的情况下的前一条边。
*/
int head[N], tot = 1;
queue <int> q;
struct edge{ // 存图有所变化
int to, nxt, z, w;
}e[M];
void addedge(int x, int y, int z, int w) {
e[++tot].to = y; e[tot].z = z; e[tot].w = w; e[tot].nxt = head[x]; head[x] = tot;
}
bool spfa(){
memset(dis, 0x7f, sizeof(dis)); memset(flow, 0x7f, sizeof(flow)); memset(vis, 0, sizeof(vis)); // 初始化, 0x7f 是 memset 能赋的最大值了
q.push(s); vis[s] = 1; dis[s] = 0; pre[t] = -1;
while (!q.empty()){
int x = q.front(); q.pop(); vis[x] = 0;
for (int i = head[x]; i; i = e[i].nxt){
int y = e[i].to, z = e[i].z, w = e[i].w;
if (z && dis[x] + w < dis[y]) { //spfa核心代码
dis[y] = dis[x] + w;
pre[y] = x; last[y] = i;
flow[y] = min(flow[x], e[i].z);
if (!vis[y]){
vis[y] = 1;
q.push(y);
}
}
}
}
return pre[t] != -1;
}
int main()
{
scanf("%d%d%d%d",&n, &m, &s, &t);
for (int i = 1; i <= m; i++)
{
int x, y, z, w; scanf("%d%d%d%d",&x, &y, &z, &w);
addedge(x, y, z, w); addedge(y, x, 0, -w);
//反边的流量为0,花费是相反数 ,为什么是相反数想想就明白了
}
while (spfa())
{
int x = t;
maxflow += flow[t];
mincost += flow[t] * dis[t];
while (x != s) //从汇点一直回溯到源点
{
e[last[x]].z -= flow[t];
e[last[x] ^ 1].z += flow[t];
x = pre[x];
}
}
printf("%d %d\n",maxflow, mincost);
return 0;
}