首先介绍的是网络流的基础——最大流。
最大流,顾名思义,就是要让网络中的总流量最大。
这里,先讲讲SAP算法+GAP优化。
SAP算法,其实就是在找增广路的时候给每个点记录一个高度标号,每次增广只走两边的点的高度相差为1的边(即满足条件 h[v]==h[u]+1 h [ v ] == h [ u ] + 1 的边),且每次流完都用该点能走到的点的高度的最大值来更新该点的高度标号。该算法的理论时间复杂度是 O(n2m) O ( n 2 m ) 的,但在实践中,加了优化后,时间复杂度远远低于理论值,而且代码很短,是最实用的最大流算法之一。
根据SAP算法的特点,我们注意到,当某次增广时,最大流可能已经求出,因此算法做了很多无用功。这启示我们若高度标号之间存在间隙,就说明不可能再有增广路,算法就可以提前终止。这就是GAP优化。事实证明,GAP优化使程序的效率提高了不少。
其实还可以加上当前弧优化:
当前弧优化,就是指在增广时给每个点记录一条当前弧。然后,每次从当前弧开始增广,每找到一条可行弧就把它设为该点的当前弧。当前弧优化的作用也是十分显著的(尤其是稠密图),有一道叫圈地计划的题,GAP跑了953ms,几乎是压线过了,加上当前弧之后只跑了70ms。
现在给出SAP+GAP+当前弧优化的标程(以usaco草地排水为例):
#include
#include
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。这些边组成的边集就是割。
使得割中的边权和最小便是最小割。最小割模型也是一个非常重要的模型。
这里不加证明地给出一个定理。
最小割=最大流
#include
#include
#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] b [ i ] 表示 i i 与 b[i] b [ i ] 进行了匹配。
现在讲讲思路:对于每个黑点 v v ,都尝试去匹配(它连出的点一定是白点,原因看定义)。如果它连向的白点 u u 目前没有匹配到,就说明当前匹配成功, b[u]=v; b [ u ] = v ; 或者,尝试把 b[u] b [ u ] “赶走”,若能赶走,也说明匹配成功。否则说明匹配失败。每匹配成功一次就把 ans+1 a n s + 1 ,最后 ans a n s 就是最大匹配数。由于 n n 个点全部匹配一遍,每次匹配最多走 m m 条边,所以时间复杂度是 O(nm) O ( n m ) 。
一个点可以覆盖与它相邻的边。选出一些点,使得无向图中的每条边都被覆盖,就是一个点覆盖集。这个定义等价于让每条边连接的两个顶点中至少选出一个。点覆盖集是一个点集。而让选出的点最少,就是最小点覆盖集。让选出的点的权值和最小,就是最小点权覆盖集。这是经典的NPC问题。
这个问题的约束条件是边:每条边都要被覆盖。把问题转化成最小割模型。原图中边的关系不变。新增S,T点,S向X连边,Y向T连边( (X,Y)∈E ( X , Y ) ∈ E )。这样一条从S到T的路径必然是这样的形式:S->u->v->T( u∈X,v∈Y u ∈ X , v ∈ Y )。为了体现约束条件,可以令 (S,u) ( S , u ) 的边权为 u u 点的点权。类似,令 (v,T) ( v , T ) 的边权为 v v 点的点权。然后人为地让 (u,v) ( u , v ) 不在割中存在,即令 (u,v) ( u , v ) 的边权为 +∞ + ∞ ,这样一条从S到T的路径中 (S,u),(v,T) ( S , u ) , ( v , T ) 两边必有一边被割,这就刚好满足了约束条件。而最小割恰好是最小化点权,所以这个最小割模型就是问题的答案。
不带权,答案等于最大匹配
选出一些点,使这些点任意两点在原无向图中都不相邻,选出的这些点就是一个独立集。该定义的限制条件等价于对于 (u,v)∈E ( u , v ) ∈ E , u,v u , v 不可同时在独立集中。而点数最多的独立集就是最大点独立集。让选出的点的权值和最大就是最大点权独立集。这亦是经典的NPC问题
可以证明,最大化点权独立集等价于最小化点权覆盖集。所以最大点权独立集就可以用点权总和减去最小点权覆盖集。感性理解起来挺显然的233
不带权,答案等于n-最大匹配
选择边,边能覆盖两个端点。要求所有点都被覆盖
听说最小边覆盖=最大点独立=n-最大匹配
定义:一个点带权的有向图,选出权值和最大的子图,满足图中点的所有后继也在子图中
构图:S向正权点连 ai a i ,负权点向T连 |ai| | a i | ,原图中的边容量为 +∞ + ∞
拿正权点权值和减去最小割就是答案
图的 m m 染色:一个图中,在保证每对相邻的点的颜色都互不相同的前提下,使颜色总数为 m m 的染色。
二分图:一个图可以 2 2 染色。
二分图最大匹配:两种不同颜色的点按图中连的边进行配对,配对过的点就不能再进行配对,这时要求成功配对的点对数最大。具体可以参考这儿。
用尽可能少的路径覆盖图中所有点。
DAG转二分图,每个点拆成X部,Y部,若DAG中存在边a->b,二分图中X部中的a向Y部的b连边。
显然答案等于n-最大匹配
一样转二分图,只不过连边是,只要x能到达y(不要求直接有边相连),在二分图中就连边。
答案等于n-最大匹配
反链定义:一个点集,其中两两点互不可达
根据Dilworth定理,最大反链=最小路径覆盖
Dilworth定理的对偶定理,最小反链覆盖=最长路径
丢链接跑
每次跑SPFA的时候要记录 S S 到 T T 的路径,然后流量显然为路径上残量的最小值,记为 minr m i n r 。本次增广总费用就是 dis[T]∗minr d i s [ T ] ∗ m i n r (其实就是 每条边的费用依次乘上流量 的总和)。
令 dis[i] d i s [ i ] 表示 i i 到 T T 的距离(最小费用)。然后对于每一条由 u u 连向 v v 的边,必有
dis[u]<=dis[v]+cost<u,v> d i s [ u ] <= d i s [ v ] + c o s t < u , v >
若 dis[u]<dis[v]+cost<u,v> d i s [ u ] < d i s [ v ] + c o s t < u , v > ,则沿着 <u,v> < u , v > 走(在目前)肯定不是最优的。因为依据上面的不等式,这样会比不等式左右两边相等时走的费用更多。所以,在增广时只走满足
dis[u]=dis[v]+cost<u,v> d i s [ u ] = d i s [ v ] + c o s t < u , v >
的弧,就能保证走的路径最优,少走了许多无用的弧。
其实本人认为ZKW费用流借鉴了SAP的思想, dis[i] d i s [ i ] 其实就相当于SAP里面的 h[i] h [ i ] ,一个是距离标号,一个是高度标号。贪心地走最优弧。
但是如果没有一条弧满足条件,就要修改 dis d i s 了。怎么修改呢?
设成功增广的点集为 U U ,暂时未增广的点集为 V V ,我们的任务是修改 U U 里面的 dis d i s 使 U U 里面的点能增广出新的点。设 u∈U,v∈V u ∈ U , v ∈ V ,为了让 <u,v> < u , v > 满足等式, dis[u] d i s [ u ] 应该等于 dis[v]+cost<u,v> d i s [ v ] + c o s t < u , v > ,即应该增加 dis[v]+cost<u,v>−dis[u] d i s [ v ] + c o s t < u , v > − d i s [ u ] 。为了不跳过可行的点,增加值要取最小那一个。最后暴力查找修改即可。如果修改不了了,整个算法就结束。
#include
#include
#include
#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
}