一句话概括专题训练的效果:
专题训练好!!!
专题训练好!!!
专题训练好!!!
(好吧是3句)
第一个介绍的专题是大网络流专题(好古老的专题呀)。这个元老级专题的质量很高(好像所有专题的题目质量都很高),涉及了许多方面的知识。
网络流的基础知识,包括各种定理,可以看看这儿。
前段时间里刷了几道此专题的练习,接触了费用流、ZKW费用流、匈牙利算法等等。
在此,对这个专题进行介绍与总结。
首先介绍的是网络流的基础——最大流。
最大流,顾名思义,就是要让网络中的总流量最大。
这里,先讲讲SAP算法+GAP优化。(对于EK算法和普通增广路算法这里不作介绍)
SAP算法,其实就是在找增广路的时候给每个点记录一个高度标号,每次增广只走两边的点的高度相差为1的边(即满足条件 h[v]==h[u]+1 的边),且每次流完都用该点能走到的点的高度的最大值来更新该点的高度标号。该算法的理论时间复杂度是 O(n2m) 的,但在实践中,加了优化后,时间复杂度远远低于理论值,而且代码很短,是最实用的最大流算法之一。
根据SAP算法的特点,我们注意到,当某次增广时,最大流可能已经求出,因此算法做了很多无用功。这启示我们若高度标号之间存在间隙,就说明不可能再有增广路,算法就可以提前终止。这就是GAP优化。事实证明,GAP优化使程序的效率提高了不少。
这是一个十分优秀的算法。
其实还可以加上当前弧优化:
当前弧优化,就是指在增广时给每个点记录一条当前弧。然后,每次从当前弧开始增广,每找到一条可行弧就把它设为该点的当前弧。当前弧优化的作用也是十分显著的(尤其是稠密图),有一道叫圈地计划的题,GAP跑了953ms,几乎是压线过了,加上当前弧之后只跑了70ms。(lalala)
现在给出SAP+GAP+当前弧优化的标程(以usaco草地排水为例):
#include<cstdio>
#include<algorithm>
const int N=1010,M=500010,INF=int(1e9);
int tot,to[M*2],next[M*2],head[N],tail[N];
int m,n,S,T,c[N][N],h[N],vh[N];
using namespace std;
void link(int u,int v)
{
to[++tot]=v;
next[tot]=0;
//
if(!head[u]) head[u]=tot;
else next[tail[u]]=tot;
tail[u]=tot;
//当前弧
}
int aug(int v,int flow)
{
if(v==T) return flow;
int minh=n+1;
for(int i=head[v],j=tail[v];i;j=i,i=next[i])//当前弧
{
int u=to[i];
if(c[v][u])
{
if(h[v]==h[u]+1)
{
int f=aug(u,min(flow,c[v][u]));
if(f>0)
{
c[v][u]-=f;
c[u][v]+=f;
//
next[tail[v]]=head[v];
next[j]=0;
head[v]=i,tail[v]=j;
//当前弧
return f;
}
if(h[S]>n) return 0;
}
minh=min(minh,h[u]+1);
}
}
//
if(--vh[h[v]]==0) h[S]=n+1;
h[v]=minh;
vh[h[v]]++;
//GAP
return 0;
}
int main()
{
int u,v,w;
scanf("%d %d",&m,&n);
S=1,T=n;
for(int i=1;i<=m;i++)
{
scanf("%d %d %d",&u,&v,&w);
if(!c[u][v]) link(u,v),link(v,u);
c[u][v]+=w;
}
int ans=0;
vh[0]=n;
while(h[S]<=n) ans+=aug(S,INF);
printf("%d",ans);
}
割,顾名思义,就是要在图中割掉一些边。但是割掉的边有限制条件:割完这些边之后这个图是“断开”的,即没有一条路径能从S走到T。这些边组成的边集就是割。
使得割中的边权和最小便是最小割。最小割模型也是一个非常重要的模型。
这里不加证明地给出一个定理。
最小割=最大流
图的 m 染色:一个图中,在保证每对相邻的点的颜色都互不相同的前提下,使颜色总数为 m 的染色。
二分图:一个图可以 2 染色。
二分图最大匹配:两种不同颜色的点按图中连的边进行配对,配对过的点就不能再进行配对,这时要求成功配对的点对数最大。具体可以参考这儿。
二分图匹配,可以用网络流来实现。但是,网络流的方法实现比较麻烦。在这里,介绍一种简单、高效、神奇的算法——匈牙利算法。
匈牙利算法是由匈牙利数学家Edmonds于1965年提出,因而得名。
这里有一道模板题:【Usaco 2005 NOV Gold】小行星群,题目描述自行搜索。
先给出标:
#include<cstdio>
#include<algorithm>
#define N 1010
using namespace std;
int st,a[N][N],b[N],bz[N];
bool match(int v)
{
if(bz[v]==st) return 0;
bz[v]=st;
for(int i=1;i<=a[v][0];i++)
{
int u=a[v][i];
if(!b[u] || match(b[u]))
{
b[u]=v;
return 1;
}
}
return 0;
}
int main()
{
int n,k,x,y,ans=0;
scanf("%d %d",&n,&k);
for(int i=1;i<=k;i++)
{
scanf("%d %d",&x,&y);
y+=n;
a[x][++a[x][0]]=y;
a[y][++a[y][0]]=x;
}
for(st=1;st<=n;st++)
if(match(st)) ans++;
printf("%d",ans);
}
怎么样,是不是很短。
给点的颜色规定为黑、白两种。用 b[i] 表示 i 与 b[i] 进行了匹配。
现在讲讲思路:对于每个黑点 v ,都尝试去匹配(它连出的点一定是白点,原因看定义)。如果它连向的白点 u 目前没有匹配到,就说明当前匹配成功, b[u]=v; 或者,尝试把 b[u] “赶走”,若能赶走,也说明匹配成功。否则说明匹配失败。每匹配成功一次就把 ans+1 ,最后 ans 就是最大匹配数。由于 n 个点全部匹配一遍,每次匹配最多走 m 条边,所以时间复杂度是 O(nm) 。
一个点可以覆盖与它相邻的边。选出一些点,使得图中的每条边都被覆盖,就是一个点覆盖集。这个定义等价于让每条边连接的两个顶点中至少选出一个。点覆盖集是一个点集。而让选出的点最少,就是最小点覆盖集。让选出的点的权值和最小,就是最小点权覆盖集。
这个问题的约束条件是边:每条边都要被覆盖。把问题转化成最小割模型。原图中边的关系不变。新增S,T点,S向X连边,Y向T连边( (X,Y)=E )。这样一条从S到T的路径必然是这样的形式:S->u->v->T( u∈X,v∈Y )。为了体现约束条件,可以令 (S,u) 的边权为 u 点的点权。类似,令 (v,T) 的边权为 v 点的点权。然后人为地让 (u,v) 不在割中存在,即令 (u,v) 的边权为 +∞ ,这样一条从S到T的路径中 (S,u),(v,T) 两边必有一边被割,这就刚好满足了约束条件。而最小割恰好是最小化点权,所以这个最小割模型就是问题的答案。
选出一些点,使这些点任意两点在原图中都不相邻,选出的这些点就是一个独立集。该定义的限制条件等价于对于 (u,v)∈E , u,v 不可同时在独立集中。而点数最多的独立集就是最大点独立集。让选出的点的权值和最大就是最大点权独立集。
可以证明,最大化点权独立集等价于最小化点权覆盖集。所以最大点权独立集就可以用点权总和减去最小点权覆盖集。
一笔画问题是经典的欧拉路问题。而最小路径覆盖问题,形象地说,是最小化“画的笔数”。比如说最少3笔画完一个图,则最小路径数是3。而“画一笔”的时候只能走边,而且一个点不能重复经过。注意这里的边可能退化成一个单独的点。可以证明,最小路径覆盖数=原图点数-最大匹配数。
费用流,比普通的网络流多加了边上的“费用”,即一条边不仅有容量,还有费用。
而要在保证最大流的前提下使费用最小,就是最小费用最大流->链接(当然,也有最大费用最大流)。
对于最小费用最大流的解法,我目前只会两种——SPFA的暴力求解&高大上的ZKW费用流。
ZKW费用流由ZKW大神所创(Orz)。
现在讲讲SPFA怎么跑费用流。
流程图:
每次跑SPFA的时候要记录 S 到 T 的路径,然后流量显然为路径上残量的最小值,记为 minr 。本次增广总费用就是 dis[T]∗minr (其实就是 每条边的费用依次乘上流量 的总和)。
进入重点了!
令 dis[i] 表示 i 到 T 的距离(最小费用)。然后对于每一条由 u 连向 v 的边,必有
dis[u]<=dis[v]+cost<u,v>
(想一想,为什么?)
若 dis[u]<dis[v]+cost<u,v> ,则沿着 <u,v> 走(在目前)肯定不是最优的。因为依据上面的不等式,这样会比不等式左右两边相等时走的费用更多。所以,在增广时只走满足
dis[u]=dis[v]+cost<u,v>
的弧,就能保证走的路径最优,少走了许多无用的弧。
其实本人认为ZKW费用流借鉴了SAP的思想, dis[i] 其实就相当于SAP里面的 h[i] ,一个是距离标号,一个是高度标号。贪心地走最优弧。
但是如果没有一条弧满足条件,就要修改 dis 了。怎么修改呢?
设成功增广的点集为 U ,暂时未增广的点集为 V ,我们的任务是修改 U 里面的 dis 使 U 里面的点能增广出新的点。设 u∈U,v∈V ,为了让 <u,v> 满足等式, dis[u] 应该等于 dis[v]+cost<u,v> ,即应该增加 dis[v]+cost<u,v>−dis[u] 。为了不跳过可行的点,增加值要取最小那一个。最后暴力查找修改即可。如果修改不了了,整个算法就结束。
#include<cstdio>
#include<cstring>
#include<algorithm>
#define fo(i,a,b) for(int i=a;i<=b;i++)
using namespace std;
const int N=2010,M=N*N,INF=2147483647;
struct edge
{
int to,next,r,w;//r:容量 w:费用
}a[M];
int n,m,S,T,tot,ans1,ans2,last[N],dis[N];
bool bz[N];
void link(int u,int v,int r,int w)
{
a[++tot].to=v;
a[tot].next=last[u];
a[tot].r=r;
a[tot].w=w;
last[u]=tot;
}
int aug(int v,int flow)
{
bz[v]=1;
if(v==T)
{
ans1+=flow;
ans2+=dis[S]*flow;
return flow;
}
for(int i=last[v];i;i=a[i].next)
{
int u=a[i].to;
if(a[i].r && !bz[u] && dis[v]==dis[u]+a[i].w)
{
int f=aug(u,min(a[i].r,flow));
if(f)
{
a[i].r-=f;
a[i^1].r+=f;
return f;
}
}
}
return 0;
}
bool change()
{
int minh=INF;
fo(i,S,T)
if(bz[i])
for(int j=last[i];j;j=a[j].next)
{
int u=a[j].to;
if(a[j].r && !bz[u]) minh=min(minh,dis[u]+a[j].w-dis[i]);
}
if(minh==INF) return 0;
fo(i,S,T)
if(bz[i])
{
dis[i]+=minh;
bz[i]=0;
}
return 1;
}
int main()
{
int u,v,r,w;
scanf("%d %d",&n,&m);
S=1,T=n,tot=1;
fo(i,1,m)
{
scanf("%d %d %d %d",&u,&v,&r,&w);
link(u,v,r,w);
link(v,u,0,-w);
}
do
{
while(aug(S,INF)) memset(bz,0,sizeof(bz));
}
while(change());
printf("%d %d",ans1,ans2);//ans1:maxflow ans2:mincost
}
网络流专题终于结束了,但是还需多做做题熟练算法,还有许多东东需要深究。专题的推进会继续进行下去。
Roads untraveled