基于增广路的最大流算法

最大流算法

基础概念

网络:一个有向图 G ( V , E ) G(V,E) G(V,E)

容量:边权值 c ( u , v ) c(u,v) c(u,v)表示有向边 < u , v > <u,v> <u,v>上最大能够承载的流量

< u , v > <u,v> <u,v>存在时 c ( u , v ) c(u,v) c(u,v)输入给定

< u , v > <u,v> <u,v>不存在时, c ( u , v ) = 0 c(u,v)=0 c(u,v)=0

源点:所有流量的发出点

在最大流问题中源点的流量一般设置为无穷大

汇点:所有流量的终点

流量:函数 f ( u , v ) f(u,v) f(u,v)称为边 < u , v > <u,v> <u,v>的流量

流量需要满足:

1. f ( u , v ) ≤ ( u , v ) f(u,v)\leq (u,v) f(u,v)(u,v),即边上的实际流量小于容量

2. f ( u , v ) = − f ( v , u ) f(u,v)=-f(v,u) f(u,v)=f(v,u),即每条边的流量与其反边流量之和为0

3.从源点流出的流量等于汇点流入的流量,任意一点接收和排出的流量相等

剩余容量: c f ( u , v ) = c ( u , v ) − f ( u , v ) c_f(u,v)=c(u,v)-f(u,v) cf(u,v)=c(u,v)f(u,v)为边 < u , v > <u,v> <u,v>上的剩余流量

残量网络:原图G中保留所有顶点还有剩余流量大于0的边,以及加上剩余流量大于0的反边

注意原图中不存在的反边经过变化之后也有可能存在与残量网络之中

已知正向边容量为 c c c只要正向边已经有流量 f f f,即正向边为 f / c f/c f/c

反向边为 − f / 0 -f/0 f/0,此时反向边的剩余容量为 c f 反 = c 反 − f 反 = 0 − ( − f ) = f > 0 c_{f反}=c_反-f_反=0-(-f)=f>0 cf=cf=0(f)=f>0,那么这条反向边就应当存在于参量网络中

也就是说只要正向边上存在流量则反向边一定存在于残量网络

正向边上流量等于容量时正向边不会出现在残量网络

增广路:在原图 G G G或者参量网络 G f G_f Gf中如果存在一条从原点到汇点的路径(显然路径上每条边的剩余容量大于0),则该路径称为增广路

所谓增广路就是能够从源点到汇点找到一条还可以通过流量的路

举例理解参量网络和增广路:

4为起点,3为汇点,开始时每条边都没有通过流量,原图 G G G为:

显然 4 → 3 4\rightarrow 3 43是一条增广路,可以从源点4施加20单位流量,直接经过 4 → 3 4\rightarrow 3 43到达汇点3

然后获得残量网络 G 1 G_1 G1,边上的符号为 f / c f/c f/c,即流量/容量

然后 4 → 2 → 1 → 3 4\rightarrow 2\rightarrow 1\rightarrow 3 4213为一条增广路,显然从4发出20单位的流量能够经该条路径到达3,如此之后得到的残量网络 G 2 G_2 G2

以下代码均已通过洛谷P3376测试

Ford-Fulkerson 增广路算法

FF暴力算法

暴力算法是裸DFS算法,执行若干轮DFS,每轮DFS的目的是找到一条(只能找一条,不能找多了)增广路,计算从该条增广路上汇点可以获得的最大容量,并且延路修改路径上边的流量状态.

若干论DFS结束的标志是:某次从s开始的DFS返回0即没有找到s到t的增广路则算法停止

1.设计状态:

​ DFS的两个参数:当前结点u,能够流进当前结点u的最大流量flow

2.状态转移:

​ 遍历u能够到达的(指 < u , v > <u,v> <u,v>上残余容量大于0)的后继结点v,从v开始DFS,如果v能够拓展到汇点t则说明u可以拓展到汇点t,于是找到了一条u到t的增广路,向递归函数的上一级报告最大能够到达t的流量

​ 如果从u的所有后继结点开始的DFS都没法拓展到t则说明u到t没有增广路,向递归函数的上一级报告失败(警示以后不要再试图从u到t寻找增广路)

3.结束条件:

​ 如果DFS到当前结点为汇点t,则报告成功,返回另一个参数flow给上级递归函数,即流入汇点的流量

#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
#include <cstring>
using namespace std;
const long long  INF = 1e12;
const int maxn = 1e3;
struct Edge {
	int from, to;
	long long capacity, flow;
	Edge(const int &f, const int &t, const long long  &c) {
		from = f;
		to = t;
		capacity = c;
		flow = 0;
	}
	void addFlow(const long long  &adder) {
		flow += adder;
	}
	void loseFlow(const long long  &adder) {
		flow -= adder;
	}
	bool hasRemainCapacity() {
		return capacity > flow;
	}
	long long getRemainCapacity() {
		return capacity - flow;
	}
};
struct Solve {
	vector<Edge> edges;
	vector<int> G[maxn];
	bool visited[maxn];//visited数组标记一轮DFS中某个节点是否已经被访问过,避免重复访问陷入循环
	int s, t;//记录s源点,t汇点
	void addEdge(const int &u, const int &v, const int &w) {
		edges.push_back(Edge(u, v, w));
		edges.push_back(Edge(v, u, 0));
		G[u].push_back(edges.size() - 2);
		G[v].push_back(edges.size() - 1);
	}
	long long getMaxFlow(const int &start, const int &finish) {
		s = start;
		t = finish;
		long long maxFlow = 0;//累加汇点流量
		long long remaining = 0;//临时量,保存一次DFS之后汇点可以接收到的最大流量
		while (true) {
			memset(visited, false, sizeof(visited));//每次DFS之前将所有结点标记为未访问过
			remaining = DFS(s, INF);
			if (remaining == 0)//当某一次汇点没有接收到流量则算法结束
				break;
			maxFlow += remaining;
		}
		return maxFlow;
	}
	long long DFS(const int &u, const long long &flow) {//从u开始DFS,流入u的流量为flow
        //也就是u结点最多可以向外排出flow的流量
		if (u == t) {//结束条件:当前结点 u为汇点t,流入汇点的流量就是增广路上每条边需要承载的流量
			return flow;
		}
		int v;
		long long remaining = 0;//remaining为临时量,记录从u的后继结点开始的DFS能够提供给汇点的流量
		visited[u] = true;//记录u结点已经访问过
		for (auto i : G[u]) {//遍历u结点的后继边数组
			Edge &e = edges[i];
			v = e.to;//v为从u经过边i到达的后继结点v
			if (visited[v] == true || e.hasRemainCapacity() == false)
				continue;
            //只有v没有被访问过并且<u,v>这条边上有剩余流量时才可以考虑从v开始进行DFS
			remaining = DFS(v, min(flow, e.getRemainCapacity()));
            					//从v开始时流量受<u,v>剩余容量和u点流量的双重约束
			if (remaining > 0) {
                //remaining>0意味着从v开始的DFS可以到达汇点t并且可以向汇点提供remaining大小的流量
                //又<u,v>有意义,因此从u到t的增广路为:(u,t)=(u,v)+(v,t)
				e.addFlow(remaining);//更新(u,v)的剩余容量状态(实际上是增加流量)
                					//v与其后继节点之间的增广路的更新在刚才的DFS下一级递归函数中已经完成
				edges[i ^ 1].loseFlow(remaining);//反边失去流量
				return remaining;
			}
		}
		return 0;
	}
} solve;

int u, v;
long long w;
int n, m, s, t;
signed main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> u >> v >> w;
		solve.addEdge(u, v, w);
	}
	cout << solve.getMaxFlow(1, m);
	return 0;
}

Edmonds-Karp算法

进行若干轮BFS,目的是寻找增广路,每轮BFS只要是找到了增广路,则沿路更新路径上的边流量(包括反边流量),然后在残量网络上进行下一轮BFS,详见注释

struct Edge {//边结构体
	int from, to;//<from,to>
	long long capacity, flow;//容量,流量
	Edge(const int &f, const int &t, const long long &c) {//构造函数,从 f到t有一条容量为c的管道
		from = f;
		to = t;
		capacity = c;
		flow = 0;//初始流量为0
	}
	void addFlow(const long long &adder) {//增加流量,值为adder
		flow += adder;
	}
	void loseFlow(const long long &loser) {//丢失流量,值为loser
		flow -= loser;
	}
	bool hasRemainCapacity() {//判断当前边是否还有剩余容量
		return capacity > flow;
	}
	long long getRemainCapacity() {//获取当前边的剩余容量
		return capacity - flow;
	}
	friend ostream &operator<<(ostream &os, const Edge &e) {
		os << "<" << e.from << "," << e.to << ">capacity=" << e.capacity << ",flow=" << e.flow;
		return os;
	}
};

struct EK {
	vector<Edge> edges;//vector存边数组
	vector<int>postEdges[maxn];//结点后继边的编号数组
	long long a[maxn];//a数组用于最大流算法getMaxFlow,作用是记录   能够流入a的最大流量
	int pre[maxn];//pre数组用于最大流算法getMaxFlow,作用是记录结点是从哪条边拓展过来的,便于追溯增广路
	void addEdge(const int &u, const int &v, const long long &w) {
		edges.push_back(Edge(u, v, w));//正向边
		edges.push_back(Edge(v, u, 0));//反向边
        //存反向边的作用是使得最大流算法中取增广路时可以返回,当正向边有流量时,反向边会减少相应的流量,使得后来的增广路可以取反边
		postEdges[u].push_back(edges.size() - 2);//u发出的边中新增正向边,实际上保存的是正向边在edges中的下标
		postEdges[v].push_back(edges.size() - 1);//v发出的边中新增反向边
	}
	long long getMaxFlow(const int &s, const int &t) {//最大流算法,起点s,终点t
		queue<int>q;
		int u, v;
		long long flow = 0;//最大流记录,这是一个累加量,累加的是全部增广路能够提供给汇点的流量和
		while (true) {//外圈循环停止的条件是再也找不到增广路
			memset(a, 0, sizeof(a));//为本轮寻找增广路初始化条件,
			while (!q.empty()) {
				q.pop();
			}
			q.push(s);//源点进入队列
			a[s] = INF;//源点流量置为无穷大的实际意义是源点的流量取之不尽
			while (!q.empty()) {//内圈while是BFS的过程,模拟的是走迷宫一样寻找s到t的通路
                
				u = q.front();
				q.pop();
				for (auto i : postEdges[u]) {//i遍历u发出的边,i为边编号
					Edge &e = edges[i];
					v = e.to;
                    //如果a[v]不为0说明BFS已经拓展到v点,无需重新拓展
					if (!a[v] && e.hasRemainCapacity()) {//判断通路(可以继续通过流量)的条件是有剩余容量
						a[v] = min(e.getRemainCapacity(), a[u]);
                        /*
                        能够到达v的流量收到两个限制
                        
                        一方面是从u到v的管道有多粗,即使u是一片大海,
                        但是<u,v>是一根吸管,那么到达v的流量就得受吸管粗细限制
                        
                        另一方面是u点的流量有多大,如果u点的流量少得可怜,
                        那么即使<u,v>是一根粗管也只能流过那一点流量
                        
                        这里显然应该取两个限制量的交集
                        */
						pre[v] = i;//记录增广路上的v结点是从编号为i的边拓展过来的
						q.push(v);//v点进入队列,即从v出发进行宽度搜索是 有可能 到达t的
					}
				}
				if (a[t]){//如果从u出发拓展完后继结点之后,发现已经有流量流到终点t了,那么就找到了一条增广路
					//此时应当跳出BFS,处理刚刚找到的增广路,处理完后再进行下一轮 BFS
                    break;
                  }
			}
         
			if (a[t] == 0) {
                   /*如果内圈的BFS全部执行完毕,也就相当于从s点开始走迷宫,
            		a[t]=0相当于残余网络中没有从s到t的增广路了
            		那么增广路算法就结束了
            		*/
				break;
			} else {//否则即a[t]!=0说明至少是本轮找到了一条增广路,需要首先处理该条增广路,
				for (u = t; u != s; u = edges[pre[u]].from) {
                    //增广路上的所有正向管道都应当获得a[t]的流量增量,反向管道都应该失去a[t]的流量
                    //为什么是a[t]?因为最终到达终点t的流量是a[t],这条增广路上的每条边必都是a[t]
					Edge &thisEdge = edges[pre[u]];
					Edge &thatEdge = edges[pre[u] ^ 1];
					thisEdge.addFlow(a[t]);
					thatEdge.loseFlow(a[t]);
				}
				flow += a[t];
			}
			//至于是否还存在存在增广路,本轮无法决定,需要外圈while重新循环一次找新的增广路
		}

		return flow;
	}

} ek;

Dinic算法

每次DFS之前首先进行一次BFS.

作用是:为每个结点标上层数,并且判断本轮从s出发能否到达t.

标记层数的作用是,使得从u开始的DFS只考虑拓展到下一层结点,找最短的增广路.

然后DFS算法相对于暴力的DFS算法有两个优化:

1.多路增广

暴力算法中只要找到一条增广路就立刻停止搜索,转而处理该条增广路

Dinic算法中从u找到一条增广路,但是u点流量减去增广路流量之后仍然有剩余流量,这时候就可以从u考虑开辟其他道路成为增广路,直到u点流量全部耗尽为止

2.当前弧优化

一条边只能被增广一次,下一次增广时不再考虑已经增广到的边

具体的实现是用一个 c u r [ x ] cur[x] cur[x]配合记录从x发出的各条边( G [ x ] G[x] G[x]中的编号)中,前n-1条已经被增广过了,当前应直接从第cur[x]=n条边开始增广

我一开始的疑问时:

一条边被增广,但是不一定发挥了它的最大效力啊,如果流过很少的流量也算是增广过了,如何确保它发挥了最大效力呢?

比如:源点为1,汇点为5,在 1 → 2 → 4 → 5 1\rightarrow 2\rightarrow 4 \rightarrow5 1245这条增广路被找到后, < 4 , 5 > <4,5> <4,5>这条边是否已经被标记为增广过了

但实际上程序是这样运行的:

1 → 2 → 4 → 5 1\rightarrow 2\rightarrow 4 \rightarrow5 1245这条增广路被找到后,流入4结点的流量是20,由于 < 4 , 5 > <4,5> <4,5>容量是30完全可以容纳20的流量,于是4点的流量全部流出,4点的剩余流量(注意不是剩余容量)降为0,当经过某条后继边增广后,结点的剩余容量降为0,则该条后继边不标记为增广过了

 int DFS(int x, int a) {//从x结点开始的DFS,流入x的流量为a
    if (x == t || a == 0) return a;//如果x为汇点t或者x点流入流量为0则返回
    int flow = 0, f;//flow是x结点可以排出的流量,不管a灌给x多少流量,x流出的流量是固定的;
     				//f是临时量,用于获取x的后继节点的流出量
    for (int& i = cur[x]; i < G[x].size(); i++) {//注意i的起点时cur[x],并且是引用类型,i++就是cur[x]++
      Edge& e = edges[G[x][i]];
      if (d[x] + 1 == d[e.to] && (f = DFS(e.to, min(a, e.cap - e.flow))) > 0) {
          //保证选取下一个结点在x的下一层
        e.flow += f;//正向边<u,v>获得f的流量
        edges[G[x][i] ^ 1].flow -= f;
        flow += f;//x流出流量flow增加f
        a -= f;//x可以分配的流量a减少f
        if (a == 0) break;//当x点的剩余  流量  降为0,最后一条访问过的边不标记为访问过了
      }
    }
    return flow;
  }

完整代码:

#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
#include <cstring>//memset头
using namespace std;
const int maxn = 1e4;
const long long INF = 1e12;

struct Edge {
	int from, to;
	long long capacity, flow;
	Edge(const int &f, const int &t, const long long &c) {
		from = f;
		to = t;
		capacity = c;
		flow = 0;
	}
	void addFlow(const long long &adder) {
		flow += adder;
	}
	void loseFlow(const long long &loser) {
		flow -= loser;
	}
	bool hasRemainCapacity()const {
		return capacity - flow > 0;
	}
	long long getRemainCapacity()const {
		return capacity - flow;
	}
};

struct Dinic {
	vector<Edge> edges;
	vector<int> postEdges[maxn];
	int depth[maxn];//记录层数数组
	int current[maxn];//current[u]记录u结点发出的边已经增广到哪一条
	int s, t;//记录起点终点
	void addEdge(const int &u, const int &v, const long long &w) {
		edges.push_back(Edge(u, v, w));
		edges.push_back(Edge(v, u, 0));
		postEdges[u].push_back(edges.size() - 2);
		postEdges[v].push_back(edges.size() - 1);
	}
	long long getMaxFlow(const int &s, const int &t) {
		this->s = s;
		this->t = t;
		long long maxFlow = 0;
		while (BFS()) {//只要BFS返回true则一定存在至少一条增广路,需要DFS处理
			memset(current, 0, sizeof(current));
            //每次BFS过后层结构会变,已经访问过的边,另一头的结点将不会被BFS标记层次,current需要置零,
            //此时进行DFS的边都是没有增广过的边
			maxFlow += DFS(s, INF);
		}
		return maxFlow;
	}
	bool BFS() {
		memset(depth, 0, sizeof(depth));
		queue<int> q;
		depth[s] = 1;
		int u, v;
		q.push(s);
		while (!q.empty()) {
			u = q.front();
			q.pop();
			for (auto i : postEdges[u]) {
				Edge &e = edges[i];
				v = e.to;
				if (!depth[v] && e.hasRemainCapacity()) {
                    //前件确定v的深度最小,后件将没有剩余容量的边剔除
					depth[v] = depth[u] + 1;
					q.push(v);
				}
			}
		}
		return depth[t];//如果返回值为真则depth[t]>0则至少存在一条s到t的增广路
	}
	long long DFS(const int &u, long long in) {//从u开始的DFS,有in流量流进u点
		if (u == t || in == 0)
			return in;
		int v;
		long long out = 0;//累积量,在多路增广过程中累计当前结点可以排出的流量
		long long next_out = 0;//临时量,下级结点可以排出的流量,也就是当前结点可以排给下级结点的流量
		for (int &i = current[u]; i < postEdges[u].size(); i++) {//i从current[u]开始遍历,当前弧优化
			Edge &e = edges[postEdges[u][i]];
			v = e.to;
			if (depth[v] == depth[u] + 1 ) {
				next_out = DFS(v, min(in, e.getRemainCapacity()));
				if (next_out == 0)
					continue;
				out += next_out;
				in -= next_out;
				e.addFlow(next_out);//正向边增加next_out的流量
				edges[postEdges[u][i] ^ 1].loseFlow(next_out);
                
                //如果从当前边拓展的增广路会耗尽u点的流量,则无法判断该增广路是否可以继续承载流量,
                //此时current应停留在当前边位置,供下次遍历继续
				if (in == 0)
					break;
			}
		}
		return out;
	}


} dinic;

int n, m, s, t;
int u, v;
long long w;
int main() {
	cin >> n >> m >> s >> t;
	for (int i = 1; i <= m; i++) {
		cin >> u >> v >> w;
		dinic.addEdge(u, v, w);
	}
	cout << dinic.getMaxFlow(s, t);
	return 0;
}

ISAP和预流推进算法尚未学习

MATLAB中调用最大流函数

以如下图为例子,建立有向图
在这里插入图片描述
EK算法的运行结果为

5 7 1 5
1 2 10
1 4 20
2 3 15
4 2 10
4 3 20
4 5 10
3 5 5
15

使用matlab:

>> from=[1,1,2,4,4,4,3]

from =

     1     1     2     4     4     4     3

>> to=[2,4,3,2,3,5,5]

to =

     2     4     3     2     3     5     5

>> value=[10,20,15,10,20,10,5]

value =

    10    20    15    10    20    10     5

>> G=digraph(from,to,value)

G = 

  digraph - 属性:

    Edges: [7×2 table]
    Nodes: [5×0 table]

>> s=1

s =

     1

>> t=5

t =

     5

>> mf=maxflow(G,s,t)

mf =

    15
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

灰球球

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值