http://trp.jlu.edu.cn/software/net/lssx/4/4.38.htm
http://www.cnblogs.com/zen_chou/archive/0001/01/01/1525841.html
一、 引言
图论这门古老而又年轻的学科在信息学竞赛中占据了相当大的比重。其中,网络流算法经常在题目中出现。网络流涵盖的知识非常丰富,从基本的最小割最大流定理到网络的许多变形再到最高标号预流推进的六个优化等等,同学们在平时需要多多涉猎这方面的知识,不断积累,才能应对题目的各种变化。
随着信息学竞赛的不断发展,其题目的难度以及考察范围都不断增大。现在,对于一些新出现的题目,仅仅掌握最朴素的网络流算法并不足以解决问题。本文针对一些数据规模比较大的网络流题目详细介绍了基于分层思想的3个网络流算法,并通过列举和比较说明了其在解题中的应用,而对一些基础的知识,如最小割最大流定理等,没有作具体阐释,大家可以在许多其他网络流资料中找到。
二、预备概念
2.1剩余图的概念
给定一个流量网络、源点、汇点、容量函数,以及其上的流量函数。我们这样定义对应的剩余图:剩余图中的点集与流量网络中的点集相同,即。对于流量网络中的任一条边,若,那么边,这条边在剩余图中的权值为;同时,若那么边,这条边在剩余图中的权值为。
我们可以发现,流量网络中的每条边在剩余图中都化作一条或二条边。剩余图中的每条边都表示在原流量网络中能沿其方向增广。剩余图的权值函数表示在流量网络中能够沿着的方向增广大小为的流量。所以在剩余图中,从源点到汇点的任意一条简单路径都对应着一条增广路,路径上每条边的权值的最小值即为能够一次增广的最大流量。
2.2顶点的层次
在剩余图中,我们把从源点到点的最短路径长度称作点的层次,记为。源点的层次为0。在下面这张剩余图中:
每个点旁边的数字即表示该点在图中的层次。
2.3层次图的概念
我们这样定义层次图:对于剩余图中的一条边,当且仅当时,边;
直观地讲,层次图是建立在剩余图基础之上的一张“最短路图”。从源点开始,在层次图中沿着边不管怎么走,经过的路径一定是终点在剩余图中的最短路。
2.4阻塞流的概念
在流量网络中存在一可行流,当该网络的层次图中不存在增广路时,我们称流函数为层次图的阻塞流。
三、最短路径增值算法(MPLA)的步骤及复杂度分析
3.1算法步骤
之前我们讲到的层次图将被应用在最短路径增值算法中。首先,我们看一下最短路径增值算法的步骤:
1、初始化流量,计算出剩余图
2、根据剩余图计算层次图。若汇点不在层次图内,则算法结束
3、在层次图内不断用bfs增广,直到层次图内没有增广路为止
4、转步骤2
算法中,2、3步被循环执行,我们将执行2、3步的一次循环称为一个阶段。每个阶段中,我们首先根据剩余图建立层次图,然后不断用bfs在层次图内增广,寻找阻塞流。增广完毕后,进入下一个阶段。这样不断重复,直到汇点不在层次图内出现为止。汇点不在层次图内意味着在剩余图中不存在一条从源点到汇点的路径,即没有增广路。
在程序实现的时候,层次图并不用被“建”出来,我们只需对每个顶点标记层次,增广的时候,判断边是否满足这一约束即可。
3.2定理的证明
定理:对于有n个点的流量网络,在最短路径增值算法中,最多有n个阶段。
也就是说,在算法中层次图最多被建立n次。证明这个定理有助于我们进行算法复杂度分析。
证明:
在建立完层次图以后,假设从源点到汇点的最短路径长度为k,我们将层次图中所有的点分到k+1个集合中,第i个集合为,如下图所示:
在剩余图中,存在着2类边。
第一类:从第个集合中的顶点连到第个集合中的顶点
第二类:从第个集合中的顶点连到第个集合中的顶点
在层次图中,只存在第一类边,这是由层次图的性质决定的。我们所要找的增广路中的边也必定是第一类边。
当我们对一条增广路径增广后,会删除一条或多条增广路中的饱和边,也就是第一类边;而同时会在剩余图中加入一些与增广路径中的边反向的边。这些新加入的边一定是第二类边。如下图所示,在剩余图(a)中,找到一条从左向右的增广路径,能够增广的流量大小为2。增广后的结果是剩余图(b)。可以发现,在剩余图(a)里面,中间一条红色第一类边在增广后饱和而被删除了,同时,在剩余图(b)中,新增了2条绿色的第二类边。
当我们在层次图中找到阻塞流之后,层次图中就不存在从第一个集合一步一步往下走,最后达到第k+1个集合的长为k的路径了。而此时不在层次图中的边都是第二类边。我们可以发现,这个时候在剩余图中的最短路径一定是这样:从源点开始,往下一步一步走,走到某个集合后沿着第二类边向上退至某个集合,再继续一步一步向下走,到某个集合又向上退…………直到走到汇点。
因为必然会经过第二类边,而经过的第一类边的数量>=k,所以路径总长度一定大于k。这即是下一个阶段的最短路径长度。
由此,我们得出了一个结论:
结论:层次图中增广路径长度随阶段而严格递增。
因为增广路径长度最短是1,最长是n-1 ,再算上汇点不在层次图内的最后一次,层次图最多被建造n次,所以最短路径增值算法最多有n个阶段。
证毕。
3.3复杂度分析
3.3.1建造层次图的复杂度分析
我们将复杂度分析分为建层次图和找增广路两部分讨论。
前面已经证明了,在最短路径增值算法中,最多建n个层次图,每个层次图用bfs一次遍历即可得到。一次bfs遍历的复杂度为,所以建层次图的总复杂度为。
3.3.2增广复杂度分析
我们首先分析在每一阶段中找增广路的复杂度。
注意到每增广一次,层次图中必定有一条边会被删除。层次图中最多有m条边,所以可以认为最多增广m次。在最短路径增广中,我们用bfs来增广。一次增广的复杂度为。其中为bfs的花费,为修改流量的花费。
所以在每一阶段的复杂度为
这样,得到找增广路总的复杂度为
最短路径增值算法的总复杂度即为建层次图的总复杂度与找增广路的总复杂度之和,为。
四、Dinic算法的步骤以及复杂度分析
4.1算法步骤
Dinic算法的思想也是分阶段地在层次图中增广。它与最短路径增值算法不同之处是:在Dinic算法中,我们用一个dfs过程代替多次bfs来寻找阻塞流。下面给出其算法步骤:
1、初始化流量,计算出剩余图
2、根据剩余图计算层次图。若汇点不在层次图内,则算法结束
3、在层次图内用一次dfs过程增广
4、转步骤2
在Dinic的算法步骤中,只有第三步与最短路径增值算法不同。之后我们会发现,dfs过程将会使算法的效率较之MPLA有非常大的提高。
下面是dfs的过程:
pßs;
While outdegree(s)>0
ußp.top;
if u<>t
if outdegree(u)>0
设(u,v)为层次图中的一条边;
pßp,v;
else
从p和层次图中删除点u,
以及和u连接的所有边;
else
增广p(删除了p中的饱和边);
令p.top为p中从s可到达的最后顶点;
end while
在程序里,p表示找到的增广路径,p.top为路径中的最后一个顶点。一开始,p中只有源点。
整个While循环分为2个操作。如果p的最后一个顶点为汇点,也就是说找到了增广路,那么对p增广,注意到增广后一定有一条或多条p中的边被删除了。这时,我们使增广路径后退至p中从源点可到达的最后一个顶点。
如果p的最后一个顶点不为汇点,那么观察最后那个的顶点u 。若在层次图中存在从u连出的一条边,比如(u,v),我们就将顶点v放入路径p中,继续dfs遍历;否则,点u对之后的dfs遍历就没有用了,我们将点u以及层次图中连到u的所有边删除,并且在p中后退一个点。
Dfs过程将会不断重复这2个操作,直到从源点连出的边全部被删除为止。
下面给出一个dfs的图例,图中,红边代表找到的增广路p中的边。
4.2复杂度分析
和在最短路径增值算法中的证明一样,Dinic算法最多被分为n个阶段。
这样首先可以得到Dinic算法中建立层次图的总复杂度仍是。
我们再来分析dfs过程的总复杂度。在每一阶段,将dfs分成2部分分析:
pßs;
While outdegree(s)>0
ußp.top;
if u<>t
if outdegree(u)>0
设(u,v)为层次图中的一条边;
pßp,v;
else
从p和层次图中删除点u,
以及和u连接的所有边;
else
增广p(删除了p中的饱和边);
令p.top为p中从s可到达的最后顶点;
end while
(1) 修改增广路的流量并后退的花费:(即为代码中红框对应的部分)
在3.3.2小节中我们讲到过,在每一阶段,最多增广m次,每次修改流量的费用为。而一次增广后在增广路中后退的费用也为。所以在每一阶段,修改增广路以及后退的复杂度为。
(2) Dfs遍历时的前进与后退:(即为代码中篮框对应的部分)
我们将用到3.1小节中的知识。
在dfs遍历时,如果当前路径的最后一个顶点能够继续扩展,则一定是沿着第一类边向汇点前进了一步。因为增广路径长度最长为n,所以最多连续前进n步后就会遇到汇点。在前进的过程中,可能会遇到没有边能够沿着继续前进的情况,这时,我们将路径中的最后一个点在层次图中删除并出栈。
注意到每后退一次必定会删除一个点,所以后退的次数最多为n次。在每一阶段中,后退的复杂度为。
假设在最坏情况下,所有的点最后均被删除,一共后退了n次,这也就意味着,有n次的前进被“无情”地退了回来,这n次前进操作都打了水漂。除去这n次前进和n次后退,其余的前进都对最后找到增广路作了贡献。增广路最多找m次,每次最多前进n个点。所以所有前进操作最多为次,复杂度为。
于是我们得到:在每一阶段中,dfs遍历时前进与后退的花费为。
综合以上二点,一次dfs的复杂度为。因为最多进行n次dfs,所以在Dinic算法中找增广路的总复杂度为,这也是Dinic算法的总复杂度。
代码:效率较高的
#include<stdio.h>
#include<string.h>
#define inf 0x3fffffff
#define N 600
struct node
{
int u,v,flow,next;
}map[4*N];
int n,m,s,t,adj[N],dis[N],q[N],num;
int min(int a,int b)
{
return a<b?a:b;
}
void insert(int u,int v,int w)
{
map[num].u=u;
map[num].v=v;
map[num].flow=w;
map[num].next=adj[u];
adj[u]=num;
num++;
map[num].u=v;
map[num].v=u;
map[num].flow=0;
map[num].next=adj[v];
adj[v]=num;
num++;
}
int bfs()
{
int i,x,v,tail=0,head=0;
memset(dis,0,sizeof(dis));
dis[s]=1;
q[tail++]=s;
while(head<tail)
{
x=q[head++];
for(i=adj[x];i!=-1;i=map[i].next)
{
v=map[i].v;
if(map[i].flow&&!dis[v])
{
dis[v]=dis[x]+1;
if(v==t)
return 1;
q[tail++]=v;
}
}
}
return 0;
}
int dfs(int s,int limit)
{
if(s==t)
return limit;
int i,temp,cost=0,v;
for(i=adj[s];i!=-1;i=map[i].next)
{
v=map[i].v;
if(map[i].flow&&dis[s]==dis[v]-1)
{
temp=dfs(v,min(limit-cost,map[i].flow));
if(temp>0)
{
map[i].flow-=temp;
map[i^1].flow+=temp;
cost+=temp;
if(limit==cost)
break;
}
else
dis[v]=-1;
}
}
return cost;
}
int dinic()
{
int ans=0;
while(bfs())
ans+=dfs(s,inf);
return ans;
}
int main()
{
while(~scanf("%d%d",&m,&n))
{
int u,v,w;
memset(adj,-1,sizeof(adj));
num=0;
s=1;t=n;
while(m--)
{
scanf("%d%d%d",&u,&v,&w);
insert(u,v,w);
}
printf("%d\n",dinic());
}
return 0;
}
效率有点低的:
#include<stdio.h>
#include<string.h>
#include<queue>
#define N 300
#define inf 0x3fffffff
using namespace std;
int min(int a,int b)
{
return a<b?a:b;
}
int dis[N],n;//dis[i],表示到原点s的层数
int flow[N][N];
int bfs()// 重新建图(按层数建图)
{
int i;
memset(dis,-1,sizeof(dis));
dis[1]=0;
queue<int>q;
q.push(1);
while(!q.empty())
{
int k=q.front();
q.pop();
for(i=1;i<=n;i++)
{
if(flow[k][i]>0&&dis[i]<0)// 如果可以到达但还没有访问
{
dis[i]=dis[k]+1;
q.push(i);
}
}
}
if(dis[n]>0)
return 1;
else
return 0;
}
int dfs(int x,int mx)// 查找路径上的最小的流量
{
int i,a;
if(x==n) return mx;
for(i=1;i<=n;i++)
{
if(flow[x][i]&&dis[i]==dis[x]+1&&(a=dfs(i,min(mx,flow[x][i]))))
{
flow[x][i]-=a;
flow[i][x]+=a;
return a;
}
}
return 0;
}
int main()
{
int i,m,u,v,w,ans,res;
while(scanf("%d%d",&m,&n)!=EOF)
{
memset(flow,0,sizeof(flow));
for(i=0;i<m;i++)
{
scanf("%d%d%d",&u,&v,&w);
flow[u][v]+=w;
}
ans=0;
while(bfs())
{
while(res=dfs(1,inf))
ans+=res;
}
printf("%d\n",ans);
}
return 0;
}