算法导论学习笔记第26章 & acm专题训练7——最大流

白昼怎懂夜的黑 提交于 2020-03-10 00:09:44

26最大流

1.研究的问题
可以把最大流问题用货运公司的运货来模拟。有一个源点持续不断地产生新货物,并通过有限条道路运往一个汇点,每条道路有限定的容量,且进入一个节点的速度和出一个节点的速度相同。求源点到汇点的最大速率。

2.运用算法条件
容量值为非负数,对于两个节点,u,v,(u,v)与(v,u)至多存在一个,如果不连通,令c(u,v)=0,不允许自循环,图必须连通.
c(u,v)指的是容量,f(u,v)指的是流量

3.使实际情况满足条件的修改
(1)解决双向边问题
对于双向边(u,v),添加一个新的节点v’,将c(v,u)=0,连通(v,v’),(v’,u),让它们的容量等于之前的c(v,u)。
(2)解决多个源节点与多个汇点的问题
设立一个超级源节点s和超级汇点t,让超级源节点s到每个源节点的流量为无穷大,设立一个超级汇点,让每个汇点到超级汇点t的流量为无穷大。

4.Ford-Fulkerson方法
算法核心:沿着增广路径重复增加路径上的流量,直到
先引入三个概念
(1)残存网络
由原图G诱导出来的新图Gf
由那些仍有空间对流量进行调整的边构成
边cf(u,v)=c(u,v)-f(u,v)
为了表示对一个正流量(u,v)的缩减,将边(v,u)加入G,将其残存容量设置为f(u,v),一条边的反向流量最多将其正向流量抵消
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
(2)增广路径
一条从源节点s到汇点t的简单路径
残存容量是一条路上最小的cf(u,v)
在这里插入图片描述
在这里插入图片描述
(3)流网络的切割
最大流最小切割定理:一个流是最大流当且仅当其残存网络不包含任何增广路径。

算法详解
在这里插入图片描述
dfs的思想
以不存在增广路径为终止条件
找到一条增广路径后,将这条路上最小的值设为k
将这条路上每条边的值-k,每条边的反向边的值+k
最后终点指向前面各点的值之和即为答案

经典例题
模板题
hdoj1532
第一步:建图
用vector数组建图

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAX = 123123;
#define INF  0x3f3f3f3f
struct edge//边的结构体
{
    int to, cap, rev;//到达的点,边的容量,反向边
};
vector <edge> G[MAX];//二维
bool used[MAX];//dfs的时候标记是否被访问过
void add_edge(int from, int to, int cap)//建边
{
    struct edge a;
    //正向建立
    a.to = to;
    a.cap = cap;
    a.rev = G[to].size();
    G[from].push_back(a);
    //反向建立,反向边
    a.to = from;
    a.cap = 0;
    a.rev = G[from].size()-1;//对应正向边。
    G[to].push_back(a);
}

第二步:最大流F-F算法

int max_flow(int s, int t)
{
    int flow = 0;//记录要输出的最大流量
    for(;;)
    {
        memset(used, 0, sizeof(used));//标记值清空
        int f = dfs(s, t, INF);//找到此时存在的一条边的最大流量
        if(f==0)//此时说明已经没有符合的条件了
            return flow;//返回最大流量
        flow += f;//继续加。。
    }
}

第三步:dfs

int dfs(int v, int t, int f)//寻找从v到t的最大流量
{
    if(v==t)//找到终点,返回这条路径上的最大流
        return f;
    used[v] = true;//标记访问过
    for(int i=0; i<G[v].size(); i++)//遍历从v出发的每一条边
    {
        edge &e = G[v][i];//找到这个边
        if(!used[e.to]&&e.cap>0)//如果到达的点没有被访问过,并且这条边还可以流水,有容量
        {
            int d = dfs(e.to, t, min(f, e.cap));//继续dfs,注意最大流量是min(此条边的容量,之前的最小的容量)
            if(d>0)//如果存在这条边
            {
                e.cap -= d;//将容量减少
                G[e.to][e.rev].cap += d;//反向边的容量增加
                return d;//返回最大流量
            }
        }
    }
    return 0;//否则0
}

全部程序

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAX = 123123;
#define INF  0x3f3f3f3f
struct edge//边的结构体
{
    int to, cap, rev;//到达的点,边的容量,反向边
};
vector <edge> G[MAX];//二维
bool used[MAX];//dfs的时候标记是否被访问过
void add_edge(int from, int to, int cap)//建边
{
    struct edge a;
    //正向建立
    a.to = to;
    a.cap = cap;
    a.rev = G[to].size();
    G[from].push_back(a);
    //反向建立,反向边
    a.to = from;
    a.cap = 0;
    a.rev = G[from].size()-1;//对应正向边。
    G[to].push_back(a);
}
int dfs(int v, int t, int f)//寻找从v到t的最大流量
{
    if(v==t)//找到终点,返回这条路径上的最大流
        return f;
    used[v] = true;//标记访问过
    for(int i=0; i<G[v].size(); i++)//遍历从v出发的每一条边
    {
        edge &e = G[v][i];//找到这个边
        if(!used[e.to]&&e.cap>0)//如果到达的点没有被访问过,并且这条边还可以流水,有容量
        {
            int d = dfs(e.to, t, min(f, e.cap));//继续dfs,注意最大流量是min(此条边的容量,之前的最小的容量)
            if(d>0)//如果存在这条边
            {
                e.cap -= d;//将容量减少
                G[e.to][e.rev].cap += d;//反向边的容量增加
                return d;//返回最大流量
            }
        }
    }
    return 0;//否则0
}
int max_flow(int s, int t)
{
    int flow = 0;//记录要输出的最大流量
    for(;;)
    {
        memset(used, 0, sizeof(used));//标记值清空
        int f = dfs(s, t, INF);//找到此时存在的一条边的最大流量
        if(f==0)//此时说明已经没有符合的条件了
            return flow;//返回最大流量
        flow += f;//继续加。。
    }
}
int main()
{
    int n,m,t;
	scanf("%d",&t);
	for(int T=1;T<=t;T++)
    {
    	scanf("%d %d", &m, &n);
        for(int i=0;i<m;i++)
        {
            G[i].clear();//注意清
        }
        for(int i=0; i<n; i++)
        {
            int a, b, c;
            scanf("%d %d %d", &a, &b, &c);
            add_edge(a, b, c);
        }
        int w = max_flow(1, m);//寻找从1到m的最大流量。
        printf("Case %d: %d\n",T,w);
    }
    return 0;
}

5.Edmonds-Karp 算法
EK算法是FF算法的优化,将dfs换成bfs
时间复杂度(v*e^2)

int delta[MaxN]; /* s到i的增广路径上残余流量最小值 */
int pre[MaxN]; /* s到i的增广路径中的上一个点 */
int r[MaxN][MaxN]; /* 残余流量 */
int s, t, n; /* 源点、汇点、点的总数 */
int bfs(){
	memset(delta,0,sizeof delta); /* 未访问 */
	queue<int> q; q.push(s);
	while(not q.empty()){
		int x = q.front(); q.pop();
		for(int i=1; i<=n; ++i)
			if(delta[i] == 0 and r[x][i] > 0){
				delta[i] = min(delta[x],r[x][i]);
				pre[i] = x;
				if(i == t) return delta[t];
				/* 已经找到汇点,提前退出 */
				q.push(i);
			}
	}
	return 0; /* 无法增广 */
}
int EK(){
	int maxFlow = 0;
	while(true){
		int d = bfs();
		if(d == 0) return maxFlow;
		for(int i=t; i!=s; i=pre[i]){
			r[pre[i]][i] -= d;
			r[i][pre[i]] += d;
			/* “反对称性” */
		}
		maxFlow += d;
	}
	return maxFlow;
}

6.dinic算法
时间复杂度:o(v^2*e)
EK算法的优化
它总是寻找最短的增广路径(通过结点数少),并沿着这条路径更新流。最短增广路径的长度在增广过程中始终不会变短,所以无需每次找增广路前都进行一次bfs。可以先进行一次bfs,按各个点被发现的顺序建立分层图,然后我们在进行dfs找到最短的增广路径,即增广的方向就是先被发现点指向后被发现的点。当没有新的最短增广路径时,意味着需要扩大最短增广路径的长度。此时再进行一次bfs,顺便可以检测是否还有通向汇点的路径。每一次bfs建立分层图的时间复杂度都是O(E),每一步最短增广路径的长度至少增加1,最多增加到∣V∣−1。

int d[MaxN], q[MaxN];
bool bfs(int s,int t){
	for(int i=1; i<=n; ++i) d[i] = -1;
	d[s] = 0; int *head = q, *tail = q;
	*(tail ++) = s;
	while(head != tail){
		int x = *(head ++);
		for(int i=1; i<=n; ++i)
			if(d[i] == -1 and c[x][i] > 0){
				d[i] = d[x]+1;
				*(tail ++) = i;
			}
	}
	return d[t] != -1; /* 存在增广路 */
}
int dfs_T;
int dfs(int x,int inFlow){
	if(x == dfs_T) return inFlow;
	int sum = 0;
	for(int i=1,delta; i<=n; ++i)
		if(d[i] == d[x]+1 and c[x][i] > 0){
			delta = dfs(i,min(inFlow-sum,c[x][i]));
			c[x][i] -= delta, c[i][x] += delta;
			if((sum += delta) == inFlow) break;
		}
	return sum;
}
int dinic(int s,int t){
	int maxFlow = 0; dfs_T = t;
	while(bfs(s,t)) maxFlow += dfs(s,infty);
	return maxFlow;
}

7.isap算法
时间复杂度(v^2*e)
EK算法的优化
加入了维持标记
当某一个标记的数目为0时即返回当前流量。循环停止

int c[MAXN][MAXN]; // 残留网络 
int d[MAXN]; // d[]:距离标号 
int vd[MAXN]; // vd[]:标号为i的结点个数 
int S, T, n; /* 源、汇、顶点数 */
int dfs(int i,int inFlow){
	// i:顶点, inFlow:最大有多大的流进入i 
	int j, sum = 0, mind = n-1, delta;
	if(i == T) // 到达汇点 
		return inFlow; /* 返回值为有多大的流进入T */
	for(j = 1;j <= n; j++) // 枚举i的邻接点 
		if(c[i][j] > 0) { // 如果有边到j 
			if(d[i] == d[j]+1){// (i,j) in E' 
				delta = dfs(j,min(inFlow-sum,c[i][j]));
				/* inFlow-sum:在i点剩下的流量; c[i,j]:这条边的容量 */
				// 递归增广,返回沿(i,j)的实际增广量 
				c[i][j] -= delta; // 更新残留网络 
				c[j][i] += delta; /* 反对称性 */
				sum += delta; // sum记录已经增广的流量
				if(d[S] >= n)
				// 结束,向上一层返回经过i的实际增广量 
					return sum;
				if(sum == inFlow) break;
				// 已经到达可增广上界,提前跳出 
			}
			if (d[j] < mind) mind = d[j];
			// 更新最小的邻接点标号 
		}
	if(sum == 0) { // 如果从i点无法增广 
		vd[d[i]] --; // 标号为d[i]的结点数-1 
		if(vd[d[i]] == 0) // GAP优化 
			d[S] = n; /* break标记 */
		d[i] = mind + 1; // 更新标号 
		vd[d[i]] ++; // 新标号的结点数+1 
	}
	return sum; // 向上一层返回经过i的实际增广量 
}
int isap(){
	int maxFlow = 0;
	memset(d,0,sizeof d);
	/* 显然,d全部为0是合法的 */
	memset(vd,0,sizeof vd);
	vd[0] = n; // all vertexes 
	while(d[S] < n)
		maxFlow += dfs(S,INF);
	return maxFlow;
}

实际应用

1、裸的最大流

2、二分图的最大匹配:建一个点S,连到二分图的集合A中;建一个点T,连到二分图的集合B中。再将所有的集合A中的点与集合B中的点相连。全部边权设为1,跑一遍最大流,结果即为二分图的最大匹配

3、最小割:在单源单汇流量图中,最大流等于最小割

4、求最大权闭合图:最大权值=正点权之和-最小割

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!