网络流学习笔记
网络流,这个曾经只是专业知识的东西现在也逐渐进入寻常百姓家了。其实网络流并不神秘(但的确很神奇),可以将它理解为是一个管道网络,从一个源点出发,最后汇集到一个汇点,中间经过了许多容量大小不一的管道。一般来说,能抽象出网络流的题目在转成网络后求的不外乎是最大流(有时候也会求最小流,不过那是在流量有下界的时候),最小割,以及费用流。下面先对一些名词作简单的介绍:
1.流量限度:一个管道的极限流量,上限是最高流量,下限是最低流量。
2.残量网络:一个管道在已经流过一些流量以后还能流的流量。
3.反向边:设网络中非源非汇的两个顶点u、v,流量限度为c(u,v),先从u流向v f个流量(f<c(u,v)),然后设置一个流量为f的反向边从v流向u。反向边其实就是一条后路,以防你的程序选择了一条错误的道路却无法改正,在程序发现走错后就会从反向边走回来。“反向边的作用就是给程序一个反悔的机会”。
4.增广路:在残量网络中的一条从源点到汇点的路径,可以通过不断地增广来求最大流。
5.割:切断一些管道,使得从源点无法通向汇点,每一条被切断的管道的流量限度之和就是这个割的代价。
接下来是一些定理和性质:
1.流量守恒:除去源点和汇点外,流入其他任意一个节点的流量等于从这个点流出的流量,即每个节点都不会储存流量,开始从源点流出的流量最后一点都不剩的流进了汇点。
2.增广路定理:对于一个流f,在它的残量网络中再也找不到增广路,那么它就是原网络的最大流。
3.最小割最大流定理:一个网络中的最小割等于它的最大流。(这个非常重要,许多题目都是通过转成最小割,再转到求最大流上)
4.一条从源点到汇点的路的最大流量由这条路上的最小流量限制决定。
一、最大流
最大流是最基础的网络流算法,很多题目都是直接或间接地求网络的最大流,让我们从最基本的算法看起:
1.Ford-Fulkerson算法
这是求最大流的最基础的算法,不过因为它的时间复杂度比较高(O(n^4)),所以在实际应用中一般不用它,但是通过学习它,我们可以掌握最大流的基础求法,为后面学习SAP和DINIC打好基础。
FF算法的思想是现在原网络中找到一条从源点流向汇点的可行流,然后在残量网络中不断寻找增广路,直到没有增广路为止,根据增广路定理,可知此时已经找到了最大流。
以下的图可以十分形象的展示FF算法的运转过程:(注:图片摘自leolin_的专栏)
2.Edmonds-Karp算法(EK)
EK是以FF为基础的算法,只是对FF有了一些小的优化,就是用BFS的方法来求增广路,这样相比较起来会优一些。Ek就是通过不断用BFS增广,直到达到最大流,不过它的效率也不是很高(O(V E^2),V是点的数量,E是边的数量),所以一般都是把它当做练习,而实践中也不常用。
3.ISAP算法
重点来了,基本上大家都是使用这种方法解决最大流问题的,而且ISAP具有许多用力的优化,从而使得它具有良好的时间效率,同时编程的复杂度也不高,裸的代码(只加了一个优化)只有60行左右,通俗易懂。
优化:
1.gap优化:开一个gap数组保存距离,开始都是零,然后逐渐更新,当一次更新完后出现断层(gap[i]=0)时,这就代表源点到汇点之间已经不连通了,直接跳出。
2.邻接表优化:如果点的数量过多,邻接矩阵存不下了,就需要用到邻接表(当然也可以用next数组,写成链表也没有问题)。邻接表存储的是边的信息,一般存的是出发点,终点,价值和下一条边的序号。使用邻接表的话可以降低空间复杂度,但是会比较难写,所以邻接矩阵能存下就尽量写邻接矩阵吧。
3.当前弧优化:在使用邻接表增广的时候,保存第一个可行的弧,这样下次在搜到这条边的时候就直接从这个可行的弧开始搜起。
下面是只使用了gap优化的ISAP:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
int n,m,a[101][101]={0},pre[101],dis[101]={0},gap[101]={0};
int ISAP(int S,int T)
{
memset(pre,-1,sizeof(pre));
pre[S]=S; gap[0]=T;
int minn,u=S,v,k,maxn=0;
while (dis[S]<T)
{
for (v=1; v<=T; v++)
if (dis[u]==dis[v]+1 && a[u][v]>0) break;
if (v<=T)
{
pre[v]=u; u=v;
if (v==T)
{
k=0x7fffffff;
for (int i=v; i!=S; i=pre[i])
k=min(k,a[pre[i]][i]);
maxn+=k;
for (int i=v; i!=S; i=pre[i])
{
a[pre[i]][i]-=k;
a[i][pre[i]]+=k;
}
u=S;
}
}
else
{
minn=T;
for (v=1; v<=T; v++)
if (a[u][v]>0) minn=min(minn,dis[v]);
gap[dis[u]]--;
if (!gap[dis[u]]) break;
dis[u]=minn+1; gap[dis[u]]++; u=pre[u];
}
}
return maxn;
}
int main()
{
scanf("%d%d",&n,&m);
for (int i=1; i<=m; i++)
{
int x,y,v;
scanf("%d%d%d",&x,&y,&v);
a[x][y]+=v;//邻接矩阵
}
printf("%d\n",ISAP(1,n));//此处1为源点,n为汇点
return 0;
}
4.预留推进算法(push_relabel)
预留推进算法经过优化就是压入重标记算法,先简述一下预留推进的思想,与其他算法相比,预留推进的一个最大特点是它每次都会给下一条边最大的流量(要么给到满,要么给到自己这里流量空了为止),这样一步一步推进。它会对每个节点维护一个高度,开始时源点高度最高,流量设为无穷大,然后向低的地方流下去,每次把向下已经流满却还有剩余流量的节点(源汇点除外)提升高度,使得能过继续流,如此往复,直到所有的非源非汇节点流量都变成零,此时算法结束,求出最大流。代码如下:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
#include<queue>
using namespace std;
int c[101][101],h[101],y[101],n;//c表示残量网络,h是高度函数,y是顶点的余流
void init()
{
scanf("%d",&n);
for (int i=1; i<=n; i++)
for (int j=1; j<=n; j++)
scanf("%d",&c[i][j]);
memset(h,0,sizeof(h));
memset(y,0,sizeof(y));
h[1]=n+1; y[1]=0x7fffffff;//此处1为源点,开始高度最高,余流为无穷大
}
int work(int s,int t)
{
int ans=0;
queue<int> qq;
qq.push(s);
while (!qq.empty())
{
int u=qq.front();
qq.pop();
for (int i=1; i<=t; i++)
{
int k;
if (c[u][i]<y[u]) k=c[u][i];
else k=y[u];
if (k>0 && (u==s || h[u]==h[i]+1))
{
c[u][i]-=k; c[i][u]+=k;
if (i==t) ans+=k;
y[u]-=k; y[i]+=k;
if (i!=s && i!=t) qq.push(i);
}
}
if (u!=s && u!=t && y[u]>0)//若还有余流,则提高高度,重新入队
{
h[u]++;
qq.push(u);
}
}
return ans;
}
int main()
{
init();//预处理
printf("%d\n",work(1,n));
return 0;
}
二、费用流(最小费用最大流)
费用流的求法与最大流求法相类似,这里不多描述,下面是邻接表实现的最小费用最大流模板(有借鉴神犇模板):
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
struct point
{
int v,cap,cost,next,reverse;
}edge[200001];
int n,m,ans=0,k,eh[200001],que[200001],pre[200001],dis[200001];
bool vis[200001];
void init()
{
memset(eh,0,sizeof(eh));
memset(que,0,sizeof(que));
memset(pre,-1,sizeof(pre));
memset(dis,0,sizeof(dis));
}
void add(int u,int v,int cap,int cost)
{
edge[k].v=v; edge[k].cap=cap; edge[k].cost=cost;
edge[k].next=eh[u]; edge[k].reverse=k+1;
eh[u]=k++;
edge[k].v=u; edge[k].cap=0; edge[k].cost=-cost;
edge[k].next=eh[v]; edge[k].reverse=k++;
}
bool spfa()
{
int head=0,tail=1;
for (int i=0; i<=n; i++)
{
dis[i]=0x7fffffff;
vis[i]=false;
}
dis[0]=que[0]=0; vis[0]=true;
while (head<tail)
{
int u=que[head++];
for (int i=eh[u]; i!=0; i=edge[i].next)
{
int v=edge[i].v;
if (edge[i].cap && dis[v]>dis[u]+edge[i].cost)
{
dis[v]=dis[u]+edge[i].cost;
pre[v]=i;
if (!vis[v])
{
vis[v]=true;
que[tail++]=v;
}
}
}
vis[u]=false;
}
if (dis[n]==0x7fffffff) return false;
return true;
}
void work()
{
int u,s=0x7fffffff;
for (u=n; u!=0; u=edge[edge[pre[u]].reverse].v)
s=min(s,edge[pre[u]].cap);
for (u=n; u!=0; u=edge[edge[pre[u]].reverse].v)
{
edge[pre[u]].cap-=s; edge[edge[pre[u]].reverse].cap+=s;
ans+=s*edge[pre[u]].cost;
}
}
int main()
{
scanf("%d%d",&n,&m);
init();
for (int i=1; i<=m; i++)
{
int x,y,cap,cost;
scanf("%d%d%d",&x,&y,&cap,&cost);
add(x,y,cap,cost); add(y,x,cap,cost);
}
while (spfa()) work();
printf("%d\n",ans);
return 0;
}
下面是邻接矩阵实现的:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
int a[101][101],b[101][101];
int n,ans=0,k,que[101],pre[101],dis[101];
bool vis[101];
void init()
{
memset(que,0,sizeof(que));
memset(pre,-1,sizeof(pre));
memset(dis,0,sizeof(dis));
memset(a,0,sizeof(a));
memset(b,0,sizeof(b));
}
bool spfa()
{
int head=0,tail=1;
for (int i=0; i<=n; i++)
{
dis[i]=0x7fffffff;
vis[i]=false;
}
dis[0]=que[0]=0; vis[0]=true;
while (head<tail)
{
int u=que[head++];
for (int i=0; i<=n; i++)
if (a[u][i] && dis[i]>dis[u]+b[u][i])
{
dis[i]=dis[u]+b[u][i];
pre[i]=u;
if (!vis[i])
{
vis[i]=true;
que[tail++]=i;
if (tail==n) tail=0;
}
}
vis[u]=false;
head++;
if (head==n) head=0;
}
if (dis[n]==0x7fffffff) return false;
return true;
}
void work()
{
int s=0x7fffffff;
for (int u=n; u!=0; u=pre[u])
s=min(s,a[pre[u]][u]);
for (int u=n; u!=0; u=pre[u])
{
a[pre[u]][u]-=s; a[u][pre[u]]+=s;
ans+=s*b[pre[u]][u];
}
}
int main()
{
scanf("%d",&n);
init();
for (int i=1; i<=n; i++)
for (int j=1; j<=n; j++)
scanf("%d%d",&a[i][j],&b[i][j]);
while (spfa()) work();
printf("%d\n",ans);
return 0;
}
最后,总结一下个人做题经验:在做网络流题目的时候,建模是最重要的,只要建好了模,直接修改模板即可。还是要多练习才能掌握技巧。