HDU -3549 (最大网络流——Edmonds_Karp算法+Dinic算法)

HDU -3549 (最大网络流)

Problem Description

Network flow is a well-known difficult problem for ACMers. Given a graph, your task is to find out the maximum flow for the weighted directed graph.

Input

The first line of input contains an integer T, denoting the number of test cases.
For each test case, the first line contains two integers N and M, denoting the number of vertexes and edges in the graph. (2 <= N <= 15, 0 <= M <= 1000)
Next M lines, each line contains three integers X, Y and C, there is an edge from X to Y and the capacity of it is C. (1 <= X, Y <= N, 1 <= C <= 1000)

Output

For each test cases, you should output the maximum flow from source 1 to sink N.

Sample Input

2
3 2
1 2 1
2 3 1
3 3
1 2 1
2 3 1
1 3 1

Sample Output

Case 1: 1
Case 2: 2

  • 题目大意:
    裸的最大网络流
    这是我写的第一个网络流的问题。在这里详细的介绍一下
  • 最大网络流:
    首先是个有向图 G=(V,E),每一个边都有一个权值,我们把它记为c(u,v),是这个边上的最大流量限制。我们形象的解释一波: 在这里插入图片描述
    如图:从A —— D 之间有多条管道连着,我们的目的是从A向D来输送油(液体黄金呐)。每条边上的权值代表管道”粗度“,比如 A-B 这条管道,最多能通过5个单位粗度的油。我们要求的是从A到D最多能输送多少油(单位时间内),我们要把这个过程想象成动态的,就是说管道里一直流动着油。这就是我们最大网络流的定义了。【菜鸟的拙见,如有错误的地方,欢迎指正】。
  • 基础知识储备:
    因为解决这个问题,要用到一些术语,我们先来解释一波术语。
    1.源点
    就是往外输送油的点,也就是只出不进的点(大公无私),就是上图的A点
    2.汇点
    只进不出的点(最自私的点),就是上图的D点。
    3.边流量
    我们用f(u,v)表示边流量。意思是当前u–v这条当前管道里的流动的油
    4.容量
    我们用c(u,v)表示容量,就是u–v这条管道的“粗度”,也就是我们图里的边权值
    –因为已经解释了几个名词了,我在这个地方插入一下网络流的性质
    a.f(u,v)<=c(u,v) 这个还用解释嘛?就是流过管子的流量肯定得小于它的“粗度”啊。
    b.f(u,v)=-f(v,u) 我们把它类比速度来看,正向和反向大小相等,方向相反
    c.除了源点和汇点其他的点,流入的总值等于流出的总值。 这个也好理解,这些中间节点只是一些中转点不会有流量的积攒。
    5.流量
    源点流出了多少单位的油(等价于汇点接收到了多少油,因为中间的结点是不会积攒油的,进多少,出多少,中间结点只是一个中转站)
    6.残留网络
    假设我们已经确定了一个流量f.(首先我们要明确,一定会存在一个流量的,至少存在一个f=0的流量吧。。虽然这个流量没有意义,但是趋势存在)。然后我们把每条边的c(u,v)都减去这个流量f 形成的图我们就c称为残留网络。比如f=1时 上图的残留网络就是下图
    在这里插入图片描述
    7.增广路
    残留子图找一条能从源点到汇点的路径,然后我们定义cf§为这条增广路的流量值,cf§=min(增广路的路径上最小的边),这个很好理解把,还是用运输油的管道来打比方!在这里插入图片描述
    这就比如时运输油的那一条路,他能运输的量肯定是由最细的那个来决定呀。
    这里有一个定理 f时最大流 就等价于 残留网络中找不到 增广路 这也是我们解决这个问题的一个核心。


BB了这么多,还没有说到点子上,到底怎么解决这个问题呢?
我们来模拟一波,希望能让你深入了解一下
借用一下大佬的图(在文末会注明出处)
在这里插入图片描述
这是初始的状态 ,剩余就是这条边还能经过多少。这个图就是先假想出f=0,然后,我们开始找残留网络里的增广路了。怎么找呢?用bfs找呀。我们可以找这么一条 A–B--C 最多能增广 2 个单位。安排上,图就变成这样了
在这里插入图片描述
然后继续找增广路 A-B-D-C 然后能增广1个单位,再安排上
在这里插入图片描述
z再找A-D-C 增广 3 个单位,安排上
在这里插入图片描述
然后我们发现,这时候找不到增广路了。那就结束了,说明这个最大流就是 2+1+3=6.本来可以愉快的结束这个问题了,但是我们发现一个问题,如果我们先找了A−B−D−C 这条增广路的话在这里插入图片描述
然后我们再增广 A-D-C 这时候只能增广 1 了。
在这里插入图片描述
然后想再找增广路,发现找不到了~找不到了!! 也就是说这样的话我们的f=3+1=4 结果明显不对。这就说明我们的算法还有些瑕疵,毕竟在bfs找增广路的时候没q确定哪条最优。

所以这时候我们,引入一波反向弧的概念
就是每次你找到增广路,就把这条路上所有的边都加一条反向边,权值是你增广的值。至于我们为什么这么做呢,简单的来说,我们就是给我们的程序提供一个反悔的机会,具体咋反悔呢,这样说太抽象,没法理解,我们还是就之前的例子继续说。
如果刚开始选了A-B-D-C
增广的值是3 然后我们把路径上的都加一条反向边 q权值为3.
在这里插入图片描述
然后我们再找 增广路的时候就能找 A-D-B-C 增广的值是2.因为 D-B之间加了一条边
然后再增广 A-D-C 增广的值是1
(就不画图了 太麻烦了)
这时候结果还是 f=6 完美的解决了之前的问题。
接下来就是代码实现了

  • 代码实现细节——1.Edmonds_Karp:
    1.存图我们用邻接矩阵存mp[][]
    2.为了实现反向弧操作,我们为每一个结点都给一个pre[i],表示i的前一个是啥,pre数组初始化为-1,然后我们找到增广路后,往前回溯,加反向边。当然还要把之前边的权值更新一下 就是-flow
void update(int u,int flow)///找到增广路之后将在增广路上的边-flow 反向+flow
{
	while(pre[u]!=-1)//回溯加反向边
	{
		mp[pre[u]][u]-=flow;
		mp[u][pre[u]]+=flow;
		u=pre[u];
	}
}

3.我们怎样找增广路呢?
我们用BFS找增广路
用个队列q 来一层一层的装经过的结点,然后如果能找到终点了就是找到增广路了,如果找了一遍pre[t]=-1就是没有找到与终点连接的边,就自然没找到增广路啦。就return false

bool find_fath_bfs(int s,int t)//用BFS找增广路的流量
{
	memset(vis,0,sizeof(vis));
	memset(pre,-1,sizeof(pre));
	vis[s]=1;
    minn=INF;
	queue<int>q;
	q.push(s);
	while(!q.empty())
	{
		int cur=q.front();
		q.pop();
		if(cur==t)//找到增广路
			break;

		for(int i=1;i<=n;i++)
		{
			if(vis[i]==0&&mp[cur][i]!=0)
			{
				q.push(i);
				minn=min(minn,mp[cur][i]);

				pre[i]=cur;
				vis[i]=1;
			}
		}			
	}
	
	if(pre[t]==-1)//与汇点没有相连的点??那还搞个毛
	{
		return false; 
	 } 
	return true;
}

4.接下来就是我们Edmonds_Karp算法的核心部分了。我们不停的找增广路,不停的更新我们的new_flow. 这个值就是我们找的增广路增广的值,然后加到我们的max_flow里,一直找增广路,找呀找呀找呀,一直到找不到增广路为止,当然找到后还要回溯更新边,并加上那个反向的边。

int Edmonds_Karp(int s,int t)
{
	int new_flow=0;
	int max_flow=0;
	while(find_fath_bfs(s,t))
	{
		new_flow=minn;
		max_flow+=new_flow;
		update(t,new_flow);
	}
	return max_flow;
}

因为我太菜了…………………………
这个破东西我看了一晚上,那个进化版的最大流我还没搞定,等搞定了再回来更新博客。
注:我借鉴的大佬的博客
https://blog.csdn.net/vonmax007/article/details/64921089#commentBox
https://blog.csdn.net/LiRewriter/article/details/78759337

  • AC代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
#define INF 0x3f3f3f3f
using namespace std;
const int maxn=1e3+10;
int mp[maxn][maxn];///用来存图
int vis[maxn];//标记数组,在bfs的时候标记是否经过这个点
int pre[maxn];///用来存一个结点的前一个结点,为了后来方便回溯的
int n,m;
int minn=INF;
void update(int u,int flow)///找到增广路之后将在增广路上的边-flow 反向+flow
{
	while(pre[u]!=-1)//回溯加反向边
	{
		mp[pre[u]][u]-=flow;
		mp[u][pre[u]]+=flow;
		u=pre[u];
	}
}

bool find_fath_bfs(int s,int t)//用BFS找增广路的流量
{
	memset(vis,0,sizeof(vis));
	memset(pre,-1,sizeof(pre));
	vis[s]=1;
    minn=INF;
	queue<int>q;
	q.push(s);
	while(!q.empty())
	{
		int cur=q.front();
		q.pop();
		if(cur==t)//找到增广路
			break;

		for(int i=1;i<=n;i++)
		{
			if(vis[i]==0&&mp[cur][i]!=0)
			{
				q.push(i);
				minn=min(minn,mp[cur][i]);

				pre[i]=cur;
				vis[i]=1;
			}
		}			
	}
	
	if(pre[t]==-1)//与汇点没有相连的点??那还搞个毛
	{
		return false; 
	 } 
	return true;
}

int Edmonds_Karp(int s,int t)
{
	int new_flow=0;
	int max_flow=0;
	while(find_fath_bfs(s,t))
	{
		new_flow=minn;
		max_flow+=new_flow;
		update(t,new_flow);
	}
	return max_flow;
}
int main()
{
	int T;
	cin>>T;
	int cas=1;
	while(T--)
	{
		cin>>n>>m;
		memset(mp,0,sizeof(mp));
		for(int i=1;i<=m;i++)
		{
			int a,b,w;
			cin>>a>>b>>w;
			mp[a][b]+=w;
		}
//		for(int i=1;i<=n;i++)
//		{
//			for(int j=1;j<=n;j++)
//				cout<<mp[i][j]<<" ";
//			cout<<endl;
//		}
		int ans=Edmonds_Karp(1,n);
		cout<<"Case "<<cas++<<": " <<ans<<endl;
	}
	return 0;
}

Edmonds_Karp算法有个明显得缺陷,请看这个图
在这里插入图片描述
我们找一条增广路,并加上反向边
在这里插入图片描述
然后我们再继续增广 找到 S-U-V-T 并加上反向边
V在这里插入图片描述
然后我们发现我们又能增广 S-V-U-T ,然后又能增广S-U-V-T
本来很简单得两条路径解决的问题,我们在这里需要多进行了好多次,浪费了大量的时间,所以说这个Edmonds_Karp算法并不高效。
所以我们在Edmonds_Karpd的基础上引入新的算法—Dinic算法.

  • Dinic算法
    大致思路就是把所有的点按距离源点的远近分层,然后找增广路的时候每一步都找下一层的点。这样就完美的避开了Edmonds_Karpd所造成的时间浪费问题。
    大佬博客
    他介绍的Dinic算法非常详细也非常清晰。
    AC代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
#define inf 0x3f3f3f3f
using namespace std;
const int maxn=1e4+10;
class Graph
{
private:
	int s,t;//源点 汇点
	int cnt;
	int head[maxn];
	int next[maxn];//连接到下一个点
	int V[maxn];///每一条边指向的点
	int W[maxn];///每一条边的残量
	int depth[maxn];//分层的深度
public:
	int n;
	void init(int nn,int ss,int tt)
	{
		n=nn;
		s=ss;
		t=tt;
		cnt=0;
		memset(head,-1,sizeof(head));
		memset(next,-1,sizeof(next));
		return ;
	}
	void _Add(int u,int v,int w )
	{
		next[cnt]=head[u];
		V[cnt]=v;
		W[cnt]=w;
		head[u]=cnt++;
	}
	void Add_Edge(int u,int v,int w)
	{
		_Add(u,v,w);
		_Add(v,u,0);//反向边
	}
	int dfs(int u,int dist)///表示当前的点,dist表示当前的流量
	{
		if(u==t)//找到汇点也就是找到了增广路
			return dist;
		for(int i=head[u];i!=-1;i=next[i])
		{
			if(depth[V[i]]==depth[u]+1&&W[i]!=0)//前面这个判断条件是确保找的是下一层,后面这个是判断它残留量大于零
			{
				int di=dfs(V[i],min(dist,W[i]));//往下dfs
				if(di>0)
				{
					W[i]-=di;
					W[i^1]+=di;//反向边
					return di;
				}
			}
		}
		return 0;//找不到增广路
	}
	bool bfs()//分层
	{
		queue<int>q;
		while(!q.empty())//清空队列
			q.pop();
		memset(depth,0,sizeof(depth));
		depth[s]=1;
		q.push(s);
		while(q.size())
		{
			int u=q.front();
			q.pop();
			for(int i=head[u];i!=-1;i=next[i])
			{
				if(depth[V[i]]==0 &&W[i]>0)///这个点还没分层且残量大于0
				{
					depth[V[i]]=depth[u]+1;
					q.push(V[i]);
				}
			}
		}
		if(depth[t]==0)//汇点没深度说明 没有分层图
			return false;
		return true;
	}
	int Dinic()
	{
		int ans=0;
		while(bfs())//能分层
		{
			while(int d=dfs(s,inf))//有增广路
				ans+=d;
		}
		return ans;
	}

};
int main()
{
	int t;
	cin>>t;
	int n,m;
	Graph G;
	int cas=1;
	while(t--)
	{

		cin>>n>>m;
		G.init(n,1,n);
		while(m--)
		{
			int a,b,c;
			cin>>a>>b>>c;
			G.Add_Edge(a,b,c);
		}
		cout<<"Case "<<cas++<<": "<<G.Dinic()<<endl;
	}
	return 0;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值