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

The Link of My Video

Problem Link

网络最大流

Tip:括号内的文字为术语。

莱阳一中的同学要参加 NOI 啦。老师得到了一张交通图(网络),节点为城市, S S S 为起点(源点), T T T 为终点(汇点)。

为了控制名额,所以有必要对每条道路经过的人数进行限制(容量)。比如下图, 1 → 3 1\to 3 13 的道路只允许两个人经过,而且道路的限制是永久的而不是单位时间的限制,比如今天过去了一个人(实际流量),那么限制(残留容量)变成了 1 1 1,明天又过去两个人是不可以的。

可以发现这张图中有许多条 S → T S \to T ST 的路径(流)。那么,一条 S → T S \to T ST 的路径(流)最多通过多少人呢?当然是这条路径(流)上,最小的边权(短板)。

那么能去打 NOI 的人数最多为多少(网络最大流)就是我们的问题。

在这里插入图片描述

最小割最大流定理

Tip:如果你只想学板子,可以跳过这里。

dzd 知道了有许多人想拿 Au,感觉自己经费不足,所以想炸掉一些路,图中没有流。这就是割,那么最小割要求选择的边集的权值之和尽可能小,也就是在每条流上选短板。

所以有了一个定理:最小割最大流定理。即为最大流 = = = 最小割。

暴力 Ford-Fulkerson

中国地大物博,交通线众多,想依靠人类的智慧是不可能的。看来只能求计算机了,但是计算机像需要算法,而不是计算机自己手算一下。

那么我们给计算机想一个可靠的算法。如图,计算机找了一个人,随便找了一条流,将经过的边的容量减去一。计算机发现,走完这条流后,实际流量多了一。所以,给这条流起名叫:增广路

在这里插入图片描述
可爱的计算机走完这条增广路后,发现自己无路可走了,其实走 S → 1 → T S \to 1 \to T S1T S → 2 → T S \to 2 \to T S2T 才是最大流。那可怎么办?已经有人回去爆搜了。

所以还是靠人类的智慧——反向边。在找到的增光路上,在路径上相邻的两个点添加一条反向边,边的流量等于这次过去的人数,允许其它路径过来

在这里插入图片描述
如上图,紫色的有向边就是反向边。TA 有什么用呢?举个栗子,计算机再一次从 S S S 出发找增广路。它从 S → 2 → 1 → T S\to 2 \to 1 \to T S21T,又找出了一条增光路,又建了一个反向边(紫色边的反向边即为原边,直接修改即可,所以只加了一条 1 → T 1 \to T 1T 的反向边)。重点来了:

当第二次的增广走 2 → 1 2 \to 1 21 这条反向边的时候,就相当于把 1 → 3 1 \to 3 13 这条正向边已经用了的流量给退了回去,不走 1 → 2 1 \to 2 12 这条边,而改走从 2 2 2 点出发走其他的路也就是 1 → T 1 \to T 1T。同时本来在 2 → T 2 \to T 2T 上的流量由 S → 2 → T S \to 2 \to T S2T 这条路来接管。

是不是像小时候妈妈说的:知错就改就是最大流好孩子。

所以,无数实践已经证明:如果不断的找增光路,并提供反悔的机会,那么最后的流量即为最大流。那么这也是 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 ST 的最短路径,然后让这条路径的流量尽可能的饱满。用 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;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值