浅谈网络流
网络流是什么?
网络流(network-flows)是一种类比水流的解决问题方法,与线性规划密切相关。网络流的理论和应用在不断发展,出现了具有增益的流、多终端流、多商品流以及网络流的分解与合成等新课题。网络流的应用已遍及通讯、运输、电力、工程规划、任务分派、设备更新以及计算机辅助设计等众多领域。
图论中的一种理论与方法,研究网络上的一类最优化问题 。1955年 ,T.E.哈里斯在研究铁路最大通量时首先提出在一个给定的网络上寻求两点间最大运输量的问题。1956年,L.R. 福特和 D.R. 富尔克森等人给出了解决这类问题的算法,从而建立了网络流理论。所谓网络或容量网络指的是一个连通的赋权有向图 D= (V、E、C) , 其中V 是该图的顶点集,E是有向边(即弧)集,C是弧上的容量。此外顶点集中包括一个起点和一个终点。网络上的流就是由起点流向终点的可行流,这是定义在网络上的非负函数,它一方面受到容量的限制,另一方面除去起点和终点以外,在所有中途点要求保持流入量和流出量是平衡的。如果把下图看作一个公路网,顶点v1…v6表示6座城镇,每条边上的权数表示两城镇间的公路长度。现在要问 :若从起点v1将物资运送到终点v6去 ,应选择那条路线才能使总运输距离最短?这样一类问题称为最短路问题 。 如果把上图看作一个输油管道网 , v1 表示发送点,v6表示接收点,其他点表示中转站 ,各边的权数表示该段管道的最大输送量。现在要问怎样安排输油线路才能使从v1到v6的总运输量为最大。这样的问题称为最大流问题。
可以说,以上都是废话~
最大流-最小割
1.Dinic
Dinic算法是网络流最大流的优化算法之一,每一步对原图进行分层,然后用DFS求增广路。时间复杂度是O(n^2*m),Dinic算法最多被分为n个阶段,每个阶段包括建层次网络和寻找增广路两部分。
Dinic算法的思想是分阶段地在层次网络中增广。它与最短增广路算法不同之处是:最短增广路每个阶段执行完一次BFS增广后,要重新启动BFS从源点Vs开始寻找另一条增广路;而在Dinic算法中,只需一次BFS过程就可以实现多次增广。
层次图
层次图,就是把原图中的点按照点到源的距离分“层”,只保留不同层之间的边的图。
算法流程
1、根据残量网络计算层次图。
2、在层次图中使用DFS进行增广直到不存在增广路。
3、重复以上步骤直到无法增广。
时间复杂度
因为在Dinic的执行过程中,每次重新分层,汇点所在的层次是严格递增的,而n个点的层次图最多有n层,所以最多重新分层n次。在同一个层次图中,因为每条增广路都有一个瓶颈,而两次增广的瓶颈不可能相同,所以增广路最多m条。搜索每一条增广路时,前进和回溯都最多n次,所以这两者造成的时间复杂度是O(nm);而沿着同一条边(i,j)不可能枚举两次,因为第一次枚举时要么这条边的容量已经用尽,要么点j到汇不存在通路从而可将其从这一层次图中删除。综上所述,Dinic算法时间复杂度的理论上界是\(O(n^2m)\)。
#include<cstdio>
#include<iostream>
#include<cstring>
#define INF 1e10
using namespace std;
struct moon{
long long next,last,len,tov;
};
moon edge[1001];
long long n,m,ans,tot,sum;
int dis[201];//最短路(用于优化)
int f[10001];//队列
void insert(int x,int y,int z)
{
edge[++tot].tov=y;
edge[tot].len=z;
edge[tot].next=edge[x].last;
edge[x].last=tot;
}
bool bfs()
{
int head=0,tail=1,x,y;
memset(dis,0xff,sizeof(dis));//初值为-1
f[1]=1;dis[1]=0;
while(head<tail)
{
x=f[++head];
for (int i=edge[x].last;i;i=edge[i].next)
{
y=edge[i].tov;
if(dis[y]<0&&edge[i].len>0)
{
dis[y]=dis[x]+1;
f[++tail]=y;
}
}
}
return dis[m]>0;//看看是否还能到达汇点
}
int dfs(int t,long long s)
{
if(t==m) return s;
int y;
long long flow=0;
for (int i=edge[t].last;i;i=edge[i].next)
{
y=edge[i].tov;
if(edge[i].len>0&&dis[y]==dis[t]+1)
{
flow=dfs(y,min(s,edge[i].len))//最可行流量
if(flow)
{
edge[i].len-=flow;
edge[i^1].len+=flow;//i^1表示这条边的反向边
return flow;
}
}
}
}
int main()
{
scanf("%lld%lld",&n,&m);
int i,j,x,y,z;tot=1;
for (i=1;i<=n;++i)
{
scanf("%d%d%d",&x,&y,&z);
insert(x,y,z);//正向边
insert(y,x,0);//反向边
}
while(bfs())
{
sum=dfs(1,INF);
while (sum)
{
ans+=sum;
sum=dfs(1,INF);
}
}
printf("%lld\n",ans);
}
2.SAP
一个比较优秀的算法。
它和dinic的区别在于,dinic要每次bfs找出是否还有增广路,但是SAP不用,它的距离标号直接再每次更新是会加。答题思想和Dinic差不多。
时间复杂度
理论的时间复杂度是\(O(n^2m)\),但是再实际应用中是远远达不到这个复杂度的。
优化:
- cur(当前弧)优化,每次已经流满了的弧就不在走了。
- GAP优化,每次如果距离标号显示断层了,说明不可能再有流可以到达后面了,直接退出。
#include<cstdio>
#include<iostream>
#define INF 1<<30
using namespace std;
int n,m,x,y,z,tot;
int b[1001];//距离标号
int gap[1001];//gap[i]表示距离标号为i的点的个数
int cur[1001];//当前弧优化
int last[1001],tov[1000010],next[1000010],len[1000010];
int ans,S/*源点*/,T/*汇点*/;
void insert(int x,int y,int z)
{
tov[++tot]=y;
len[tot]=z;
next[tot]=last[x];
last[x]=tot;
}
int flow(int t,int Flow/*表示从S留到当前点的流量*/)
{
if(t==T) return Flow;
int have=0;/*表示当前点已经流到汇点的流量*/
for (int i=cur[t];i;i=next[i])
{
int y=tov[i];
if(len[i]>0&&b[t]==b[y]+1)
{
cur[t]=i;
int now=flow(y,min(Flow-have,len[i]));
len[i]-=now;len[i^1]+=now;
have+=now;
}
if(Flow==have) return have;
}
cur[t]=last[t];
if(--gap[b[t]]==0) b[S]=n;
++gap[++b[t]];
return have;
}
int main()
{
scanf("%d%d",&n,&m);
int i,j;tot=1;
for (i=1;i<=n;++i)
{
scanf("%d%d%d",&x,&y,&z);
insert(x,y,z);
insert(y,x,0);
}
S=1,T=gap[0]=m;
while (b[S]<m)
ans+=flow(S,INF);
printf("%d\n",ans);
}