网络及网络流
什么是网络?网络其实就是有向带权图。为什么要叫网络,是因为权值是容量,容量意味着可以在单位时间内经过的上限,但是可以比上限小。
有向图=点集+有向边集
一个实例:运输网络
网络定义
* 一个有向图 G=(V,E); V代表点的集合,E代表边的集合。*
* 有两个特别的点:源点s、汇点t;*
* 图中每条边(u,v)∈E,有一个非负值的容量C(u,v)*
记为 G=(V,E,C)
网络三要素:点、边、容量
可行流定义:
是网络G上的一个“流”,即每条边上有个“流量”P(u,v),要满足下面两个条件:
流的容量限制—弧:
0≤P(u,v)≤C(u,v)
(对于任意弧(u,v)——有向边)
流的平衡—点:
除源点和汇点,对任意中间点有:流入u的“流量”与流出u的“流量”相等。即:
网络的流量:源点的净流出“流量” 或 汇点的净流入“流量”。即:
注意,我们这里说的流量是一种速率,而不是指总量。联系上面所说的实例,下面是一个流量为1的可行流:
标准的网络流图应该是这么画的:
tips:事实上,打一个简单的比方,我们把路径看成一根根水管,水管的容积就是容量限制,通过的水流就是流量,显然通过的水流不能超过水管的容积(不然就炸水管啦!!),下面要讲的最大流问题就是水管中最大能流过的水。
最大流问题
寻找网络G上可能的最大流量(和一个有最大流量的可行流方案),即为网络G上的最大流问题。
最大流算法的核心—增广路径
我们先来看一个网络流:
退流的概念—后向弧
仔细分析上图,我们发现,流量是可以增加的:
把一个流量弧(B,C)和(C,T)上的流退回到B点,改道从B-D-E-T走,就可以增加流量了,如下图:
上图不能“直接寻找增大流的路径”,是因为当初有些弧上的流选择不“恰当”,要“退流”。
一种直观的想法就是:调整或重新搜索“当初的选择”—难!
能不能保留以前的工作,而在这之上改进呢?经过专家们研究发现,加入退流思想—后向弧,就能再次“直接寻找增大流的路径”。
增广路径(可改进路径)的定义
若P是网络中连结源点s和汇点t的一条路,我们定义路的方向是从s到t,则路上的弧有两种:
前向弧—弧的方向与路的方向一致。前向弧的全体记为P+;
后向弧—弧的方向与路的方向相反。后向弧的全体记为P-;
设F是一个可行流,P 是从s到t的一条路,若P满足下列条件:
在P+的所有前向弧(u,v)上,
0≤f(u,v)<C(u,v)
;
在P-的所有后向弧(u,v)上,
0<f(u,v)≤C(u,v)
;
则称P是关于可行流F的一条可增广路径。
本图中:S-A-C-B-D-E-T 为一增广路径。其中(C,B)为后向弧,其它为前向弧。
在增广路径上的改进算法:
1) 求路径可改变量
2)修改增广路径上的流量
附加网络1—残留网络
由于要考虑前向弧、后向弧,分析、描述时不简洁,在图2.1上直观看也不容易看出增广路径。
因此我们把已经有的流量从容量中分离出来表示,引入一个与原问题等价的附加网络1:残留网络。
其中,前向弧(黑色)上的容量为“剩余容量”=C(u,v)-f(u,v);后向弧(红色双线)上的容量为“可退流量”=f(v,u)。
改造后的网如下:
在这张图上,我们找增广路径显的非常直观了!
结合增广路径,我们有如下定理:
最大流定理
如果残留网络上找不到增广路径,则当前流为最大流;反之,如果当前流不为最大流,则一定有增广路径。
(虽然我觉得这是一个很扯皮的定理,但是确实是最大流算法的核心)
证明涉及最小割概念,下文会讲……
用图示流程来表示最大流算法:
基于这种思想的算法,关键之处在于怎样找增广路径。常用方法有:
深度搜索dfs :Ford-Fulkerson 算法,也是入门算法。
宽度搜索bfs
优先搜索pfs—即类似Dijkstra算法的标号法。
入门的板子这里就不给出了,这里给出Dinic算法的代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=10000010;
int read()
{
int sum=0,flag=1;
char c;
for(;c<'0'||c>'9';c=getchar())if(c=='-') flag=-1;
for(;c>='0'&&c<='9';c=getchar())sum=(sum<<1)+(sum<<3)+c-'0';
return sum*flag;
}
int fd;
bool check=true;
struct edge
{
int y,next,v;
}e[MAXN];
int link[MAXN];
int tot=1;
int n,m;
int d[MAXN];
int ans;
int head,tail,q[MAXN];
void insert(int x,int y,int z)
{
e[++tot].next=link[x];e[tot].v=z;e[tot].y=y;link[x]=tot;
e[++tot].next=link[y];e[tot].v=0;e[tot].y=x;link[y]=tot;
}
void init()
{
m=read();n=read();
int x,y,z;
for(int i=1;i<=m;++i)
{
x=read();y=read();z=read();
insert(x,y,z);
}
}
void bfs() //按层次分层
{
memset(d,-1,sizeof(d));
head=tail=1;
q[1]=1;d[1]=0;
while(head<=tail)//开始bfs,确定每个点的level。
{
int k=q[head];
for(int i=link[k];i;i=e[i].next)
if(e[i].v&&d[e[i].y]<0)
{
d[e[i].y]=d[k]+1;
q[++tail]=e[i].y;
}
head++;
}
if(d[n]!=-1) check=true;//边界,如果从s到t没有路径,已经不可增广。
}
int dfs(int k,int l)//l是上一层传过来的
{
if(k==n) return l;
int maxl=0;//maxl 表示当前点已经增广的最大流量。
for(int i=link[k];i&&maxl<l;i=e[i].next)//maxl<l也是个很不错的剪枝
//如果当前点能增广的流量比前面传过来的流量还大,表明,流量瓶颈点在前面。
{
if(d[e[i].y]==d[k]+1&&e[i].v)
{
if(fd=dfs(e[i].y,min(e[i].v,l-maxl)))
//注意这里是l-maxl,仔细思考为什么?
{
maxl+=fd;
e[i].v-=fd;
e[i^1].v+=fd;
}
}
}
if(!maxl) d[k]=-1;//如果当前点不可增广,下一次就不会再找。可行性剪枝。
return maxl;
}
void dinic()
{
while(check)
{
check=false;
bfs();
if(!check) break;
while(fd=dfs(1,MAXN)) ans+=fd;
}
}
int main()
{
init();
dinic();
printf("%d",ans);
return 0;
}
最小割
to be continued……