先看一道题(选自usaco题库4.2.1):
时间限制: 1000 ms 空间限制: 128000 KB 具体限制
题目描述
在农夫约翰的农场上,每逢下雨,贝茜最喜欢的三叶草地就积聚了一潭水。这意味着草地被水淹没了,并且小草要继续生长还要花相当长一段时间。因此,农夫约翰修建了一套排水系统来使贝茜的草地免除被大水淹没的烦恼(不用担心,雨水会流向附近的一条小溪)。作为一名一流的技师,农夫约翰已经在每条排水沟的一端安上了控制器,这样他可以控制流入排水沟的水流量。
农夫约翰知道每一条排水沟每分钟可以流过的水量,和排水系统的准确布局(起点为水潭而终点为小溪的一张网)。需要注意的是,有些时候从一处到另一处不只有一条排水沟。
根据这些信息,计算从水潭排水到小溪的最大流量。对于给出的每条排水沟,雨水只能沿着一个方向流动,注意可能会出现雨水环形流动的情形。
输入
第1行: 两个用空格分开的整数N (0 <= N <= 500000) 和 M (2 <= M <= 1000)。N是农夫约翰已经挖好的排水沟的数量,M是排水沟交叉点的数量。交点1是水潭,交点M是小溪。
第二行到第N+1行: 每行有三个整数,Si, Ei, 和 Ci。Si 和 Ei (1 <= Si, Ei <= M) 指明排水沟两端的交点,雨水从Si 流向Ei。Ci (0 <= Ci <= 10,000,000)是这条排水沟的最大容量。
输出
输出一个整数,即排水的最大流量。
样例输入
5 4
1 2 40
1 4 20
2 4 20
2 3 30
3 4 10
样例输出
50
数据范围限制
N (0 <= N <= 500000)
M (2 <= M <= 1000)
这道题看了看,发现用之前所学的算法都没有什么思路,那么,我们现在就引进一种新的算法:网络流。而网络流又分为很多种:最大流,最小割,费用流等,这一道题用的是最大流。
何为最大流?
抽象地讲是给你一个网络,有一个源点和汇点,现在要从源点放水,水从一条边流到另一个点,但最多只能流一个值的水,下一个点又这样流向另一个点,问汇点最多会流入多少水?
知道了这些,我们就了解一些概念:
-
定理:对于有n个点的流量网络,在最短路径增值算法中,最多有n个阶段。
也就是说,在算法中层次图最多被建立n次。证明这个定理有助于我们进行算法复杂度分析。
证明:
在建立完层次图以后,假设从源点到汇点的最短路径长度为k,我们将层次图中所有的点分到k+1个集合中,第i个集合为 {顶点u|level(u)=i−1} ,如下图所示:
在剩余图中,存在着2类边。
在层次图中,只存在第一类边,这是由层次图的性质决定的。我们所要找的增广路中的边也必定是第一类边。
当我们对一条增广路径增广后,会删除一条或多条增广路中的饱和边,也就是第一类边;而同时会在剩余图中加入一些与增广路径中的边反向的边。这些新加入的边一定是第二类边。如下图所示,在剩余图(a)中,找到一条从左向右的增广路径,能够增广的流量大小为2。增广后的结果是剩余图(b)。可以发现,在剩余图(a)里面,中间一条红色第一类边在增广后饱和而被删除了,同时,在剩余图(b)中,新增了2条绿色的第二类边。
当我们在层次图中找到阻塞流之后,层次图中就不存在从第一个集合一步一步往下走,最后达到第k+1个集合的长为k的路径了。而此时不在层次图中的边都是第二类边。我们可以发现,这个时候在剩余图中的最短路径一定是这样:从源点开始,往下一步一步走,走到某个集合后沿着第二类边向上退至某个集合,再继续一步一步向下走,到某个集合又向上退…………直到走到汇点。
因为必然会经过第二类边,而经过的第一类边的数量>=k,所以路径总长度一定大于k。这即是下一个阶段的最短路径长度。
由此,我们得出了一个结论:
结论:层次图中增广路径长度随阶段而严格递增。
因为增广路径长度最短是1,最长是n-1 ,再算上汇点不在层次图内的最后一次,层次图最多被建造n次,所以最短路径增值算法最多有n个阶段。
证毕。
我们首先分析在每一阶段中找增广路的复杂度。
注意到每增广一次,层次图中必定有一条边会被删除。层次图中最多有m条边,所以可以认为最多增广m次。在最短路径增广中,我们用bfs来增广。一次增广的复杂度为O(n+m),其中O(m)为bfs的花费,O(n)为修改流量的话费。所以在每一阶段的复杂度为 O(m(n+m))=O(m2)
这样,得到找增广路总的复杂度为 O(nm2)
最短路径增值算法的总复杂度即为建层次图的总复杂度与找增广路的总复杂度之和,为 O(nm2)
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
和在最短路径增值算法中的证明一样,Dinic算法最多被分为n个阶段。
这样首先可以得到Dinic算法中建立层次图的总复杂度仍是O(nm)。
我们再来分析dfs过程的总复杂度。在每一阶段,将dfs分成2部分分析:
p←s;
While outdegree(s)>0
u←p.top;
//1{
if u<>t
if outdegree(u)>0
设(u,v)为层次图中的一条边;
p←p,v;
else
从p和层次图中删除点u,
以及和u连接的所有边;
//}
//2{
else
增广p(删除了p中的饱和边);
令p.top为p中从s可到达的最后顶点;
//}
end while
综合以上二点,一次dfs的复杂度为 O(nm) ,因为最多进行n次dfs,所以在Dinic算法中找增广路的总复杂度为 O(n2m)
#include
#include
#include
using namespace std;
const int maxn = 100005,maxm = 100005;
struct node
{
int to,next,flow;//目标点,下一条边,剩余流量
node(void){}
node(int a,int b,int c) : to(a),next(b),flow(c){}
}e[maxm * 2];//正向+反向弧
int d[maxn];//距离标号,距离原点的最短距离
int final[maxn],cur[maxn];
int n,m,tot,s,t;
void link(int u,int v,int c)
{
e[++ tot] = node(v,final[u],c),final[u] = tot;
e[++ tot] = node(u,final[v],0),final[v] = tot;
// 2 3
//这里保证了正,反向弧的标号连续,那么对于第i条边,其反向弧就是i^1
}
bool bfs()
{
//用bfs找到每个点的距离标号
static int que[maxn];
for(int i = s;i <= t;i ++) d[i] = -1,cur[i] = final[i];
d[s] = 0;
que[1] = s;
for(int fi = 1,en = 1;fi <= en;fi ++)
{
int u = que[fi];
for(int i = final[u];i;i = e[i].next)
if (e[i].flow > 0 && d[e[i].to] == -1)
{
d[e[i].to] = d[u] + 1;
que[++ en] = e[i].to;
}
}
return d[t] != -1;
}
int dfs(int now,int flow)
{
if (now == t) return flow;//流完,退出
int use = 0;
for(int i = cur[now];i;i = e[i].next)
{
cur[now] = i;
if (e[i].flow > 0 && d[e[i].to] == d[now] + 1/*只能沿着最短路走*/)
{
int tmp = dfs(e[i].to,min(e[i].flow,flow - use));
use += tmp,e[i].flow -= tmp,e[i ^ 1].flow += tmp;
if (flow == use) return use;
}
}
return use;
}
int main()
{
tot = 1;//计算反向弧时更方便
scanf("%d%d", &m, &n);//n为点数,m为边数
s = 1,t = n;//假定原点为s,汇点为t
for(int i = 1;i <= m;i ++)
{
int u,v,c;
scanf("%d%d%d", &u, &v, &c);//一条从u到v,流量为c的边
link(u,v,c);
}
int ans = 0;
for(;bfs()/*假如找不到证明无法增广*/;)
ans += dfs(s,1 << 30);
printf("%d\n", ans);
return 0;
}
SAP算法,其实就是在找增广路的时候给每个点记录一个高度标号,每次增广只走两边的点的高度相差为1的边(即满足条件 level(v)=level(u)+1 的边),且每次流完都用该点能走到的点的高度的最大值来更新该点的高度标号。该算法的理论时间复杂度是 O(n2m) 的,但在实践中,加了优化后,时间复杂度远远低于理论值,而且代码很短,是最实用的最大流算法之一。
根据SAP算法的特点,我们注意到,当某次增广时,最大流可能已经求出,因此算法做了很多无用功。这启示我们若高度标号之间存在间隙,就说明不可能再有增广路,算法就可以提前终止。这就是GAP优化。事实证明,GAP优化使程序的效率提高了不少。
这是一个十分优秀的算法。
当前弧优化,就是指在增广时给每个点记录一条当前弧。然后,每次从当前弧开始增广,每找到一条可行弧就把它设为该点的当前弧。当前弧优化的作用也是十分显著的(尤其是稠密图)
现在给出SAP+GAP+当前弧优化的标程(以usaco草地排水为例):
#include
#include
#include
using namespace std;
const int N=1010,M=500010;
int to[2*M],next[2*M],fir[N],las[N],c[M*2],n,m,top=1,ans=0,h[N],vh[N];
void Con(int,int);
int dg(int,int);
int main()
{
scanf("%d%d",&m,&n);
for(int i=1;i<=m;i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
Con(x,y);
c[top]+=z;
Con(y,x);
}
vh[0]=n;
while(h[1]<=n)ans+=dg(1,int(1e9));
printf("%d",ans);
}
int dg(int w,int v)
{
if(w==n)return v;
int Get=n+1;
for(int i=fir[w],j=las[w];i;j=i,i=next[i])
{
int x=to[i];
if(c[i])
{
if(h[w]==h[x]+1)
{
int f=dg(x,min(v,c[i]));
if(f>0)
{
c[i]-=f;
c[i^1]+=f;
//当前弧
next[las[w]]=fir[w];
next[j]=0;
fir[w]=i,las[w]=j;
//
return f;
}
if(h[1]>n)return 0;
}
Get=min(Get,h[x]+1);
}
}
if(--vh[h[w]]==0)h[1]=n+1;
h[w]=Get;
vh[h[w]]++;
return 0;
}
void Con(int x,int y)
{
to[++top]=y;
next[top]=0;
if(fir[x])next[las[x]]=top;else fir[x]=top;
las[x]=top;
}
还有,我们将边用前向星存起来,这个边的位置用另一个数组表示能过的流量,也就是说,u[i]到v[i]这条边的流量存入c[i],而不是c[u[i]][v[i]],它的反向边的流量为c[i^1],
由于我们讨论的时间复杂度属于最坏情况,所以实际所需时间远远不及 O(mn2) 甚至1000个点的图在1s内能跑过去
首先,我们定义,对于一个图,我们将一些边删掉,使得原图( G(E,V) )的点( E )分成两个集合 S和T,有S∪T=E且S∩T=∅,并且满足s∈S,t∈T ,那么称(S,T)构成网络G(E,V)的割,定义割边
CUT(S,T){l(x,y)∈V且x∈S,y∈T或x∈T,y∈S)}
正向割边 CUT+(S,T){l(x,y)∈V且x∈S,y∈T}
反向割边 CUT−(S,T){l(x,y)∈V且x∈T,y∈S}
割的容量,即为正向割边的容量和 cut(S,T)=∑x∈S,y∈Tc(x,y)
正向流量 flow+(S,T)=∑l(x,y)∈CUT+(S,T)f(x,y)
反向流量 flow−(S,T)=∑l(x,y)∈CUT−(S,T)f(x,y)
那么我们定义最小割
定理:最大流=最小割
由于本蒟蒻学识并不高,所以只能给出一个比较感性的证明:
证明1:
∵任意一条流≤最小割且存在一条流=最小割 ∴最大流=最小割
证明2:
我们要使由原点至汇点的路径断开,那么最小为此路径的流;
此时再找另一条路径的割边,有两种情况:
1. 割边不在两条路径的相同部分,即为流
2. 割边在两条路径的相同部分,那么两条路径割边相同,即为两条路径最大流
所以最小割=最大流
好,我们现在就先上几道题:
①最大权闭合子图
首先对于一个图G(E,V), Ci 表示编号为i的点的权
我们定义一个集合 S{i|i∈E,∀j∈E,(i,j)∈Vj∈S} 为图G的一个闭合子图
闭合子图的权 f(S)=∑i∈SCi
那么要你求一个图的最大权闭合子图的权
这道题可以用最小割模型:
1. 我们把源点连向权为正数的点,边权为这个点的点权,割掉表示选的闭合子图没有这个点
2. 我们把权为负数的点连向汇点,边权为点权的相反数,割掉表示选的闭合子图有这个点
3. 把原图的边权赋为Max,表示这些边不能割
跑一边最大流,将总正点权减去答案即可
为什么是正确的:
1. 定义的正确性:首先我们想到最大的权,如果不要这个正数点或要一个负数点就相当于割掉一条边,要使权最大那么久要使割最小,再用所有正权点权和减它,符合最小割定义
2. 答案的正确性:我们规定割掉连向源点的边表示不要这个点,那么割的流量会加上这个值,答案相应减少这个值;由于答案之前没有计入负点权,所以我们当选择一个负点时,答案应在原来基础上减去它的相反数,正好割的流量多了它的相反数。
3. 选取子集的正确性:我们求了答案,那么怎么保证符合闭合子图的定义呢?根据构图来说,只要源点和汇点连通,那么所选取的子集一定不是闭合子图;只要源点汇点不连通,那么选取的一定是闭合子图。感性证明:如果从一个正数X,走到另一个正数Y,如果源点连向X但不连向Y,很显然取X和Y会更优,自然排除只选X不选Y;若存在一条从源到汇的路径,由源点走向X,Y走向汇点,那么所选的子集中含X但不含有Y,也就说明闭合子图肯定有一节点连向非集合的节点,不符;若有X连向汇点但Y不连汇点,Y连向X也就是说闭合子图存在Y不存在X,如果Y与源点连通,则源点与汇点连通,不符。证毕。
②狼和羊的故事