HDOJ 1532 Drainage Ditches
网络流对于刚入门算法学习的同学来说是稍有困难的(比如我),于是我打算从一道最基本的最大流例题让自己好好的学习一下网络流。这一篇文章是完完全全的新手入门文章,对于有些许基础的人可能不会有什么帮助,而对于小白来说,可能是叩开网络流的一个起点吧
题目大意
给出n与m,n为有向边数,m为顶点数,求s到t的最大流。
什么是最大流呢?不妨先来看一下题目的input和output:
5 4
1 2 40
1 4 20
2 4 20
2 3 30
3 4 10
50
5条边,4个顶点,我们可以把这个图形画出来:
最大流指的是,4在每个单位时间内可以获得的最大值。
我们把1看做是源头,我们假定源头内包含的值是无穷大的。
按照编号来看,首先有40从1流向2,2就可以有30流向3,3就有10流向4,因为2此时还剩下10,所以可以沿着2->4再流10,最后有1直接流向4的20
边 | 值 |
---|---|
1->2 | 40 |
1->4 | 20 |
2->3 | 30 |
2->4 | 10 |
3->4 | 10 |
这样4总计能得到40。
不对啊,这样在点3的剩下20不是就浪费了吗?看来原因是当初2->3给的值太多了。
为了防止出现这样的错误计算,我们给它反悔的机会,引入反向边的概念,对于每一条边,都可以设立一个反向边。
在上述中,我们已经把点2中的30给了点3,也就是说,现在,在2->3这条边上是有数值流动的,给的太多,我们可以有一种想法,就是可以把给过来的,重新返回去。
下面用具体的代码分析(我现在所使用的为Ford-Fulkerson算法):
//部分源码来自挑战程序设计竞赛1
#include <bits/stdc++.h>
#define MAX_V 210 //根据题目需求改变
#define INF 0x3f3f3f3f //定义无穷大值
using namespace std;
struct edge{ //边的定义:终点,边的容量,以及反向边
int to,cap,rev;
};
vector<edge> G[MAX_V]; //邻接表
int used[MAX_V]; //访问标记数组
int n,m;
void add_edge(int from,int to,int cap){ //添加边的函数
G[from].push_back((edge){to,cap,G[to].size()});
G[to].push_back((edge){from,0,G[from].size()-1});
}
int dfs(int v,int t,int f){ //dfs求解
if(v==t) return f;
used[v] = 1;
for(int i = 0;i<G[v].size();i++){
edge &e=G[v][i];
if(!used[e.to]&&e.cap>0){
int d=dfs(e.to,t,min(f,e.cap));
if(d>0){
e.cap-=d;
G[e.to][e.rev].cap += d;
return d;
}
}
}
return 0;
}
int max_flow(int s,int t){ //求最大流
int flow=0;
while(1){
memset(used,0,sizeof(used));
int f=dfs(s,t,INF);
if(f==0) return flow;
flow+=f;
}
}
int main(){
while(~scanf("%d %d",&n,&m)){
for(int i = 1;i<=n;i++){
int t1,t2,t3;
scanf("%d %d %d",&t1,&t2,&t3); //输入数据
add_edge(t1,t2,t3);
}
printf("%d\n",max_flow(1,m));
for(int i = 1;i<=n;i++){
G[i].clear();
}
}
return 0;
}
我们慢慢分析
struct edge{
int to,cap,rev;
};
void add_edge(int from,int to,int cap){
G[from].push_back((edge){to,cap,G[to].size()});
G[to].push_back((edge){from,0,G[from].size()-1});
}
首先是对边的结构体表示,因为使用的是邻接表,所以对于每一个流出点上的边,只需要记录这些边的终点(to)以及这些边的容量(cap)
我个人觉得,理解的难点在于这个rev。
为什么把反向边设置成G[to].size(),不妨我们设这条边为a->b,我们假设此时b已经拥有了n条以b为起点的边,此时,因为b收到了一条为终点的边,就添加以b为起点,a为终点的反向边,即为第n+1条边,当然了,因为是从0开始计数,实际上是0,1,2…n,所以对于这条新建的边的反向边的编号,就是G[to].size(),同理,对于这条反向边,我们也要对他的反向边也就是原边进行编号,对于a点来说,这是第n条边,在vector中是第n-1条,结果自然就是G[from].size()-1。
当然了,初始化的时候因为还没有开始流动,反向边的容量自然就是0。
int dfs(int v,int t,int f){
if(v==t) return f;
used[v] = 1;
for(int i = 0;i<G[v].size();i++){
edge &e=G[v][i];
if(!used[e.to]&&e.cap>0){
int d=dfs(e.to,t,min(f,e.cap));
if(d>0){
e.cap-=d;
G[e.to][e.rev].cap += d;
return d;
}
}
}
return 0;
}
这一段可以算是算法的核心,我们使用dfs来进行路径的搜索。
该函数有三个参数,分别代表的是起点v,终点t,以及起点v的值的多少。当v和t相等的时候,不流动,直接返回f即可。接下来,把起点的访问数组标记一下,因为我们显然要找的是一条不会重复的路线。之后,我们按照邻接表,从起点开始一条条进行搜索,如果对于一条边的终点我们没有去过并且我们当前的点的余量不为0的时候,我们就可以沿着这条路向这里走。当然了,走的多少还是要看余量和边的限制,取小的那个。我们取d作为流动的数值,如果d>0,就是有数值的流动,我们就降低边的容量,相应的,对于该条边的反向边,我们就提高它的容量。
举一个简单的例子:有A,B两人,A最多能给B 30元,A给了B 10元,那么之后A只能最多给B 20元,B此时因为从A获得了10元,所以B现在最多可以给A10元。
最后返还此次流动的数值d。
接下来,在max_flow函数中只需要不停的使用dfs寻找可行的路径即可,当没有数值的变化,整一个流稳定的时候,就可以退出循环,并且得到了答案。