预流推进的几种算法
Lagoon
2019-01-20 13:51:50
### 前言
本文介绍几个比较优秀的预流推进算法,包括朴素的ISAP算法,HLPP算法和自己脑补的LCT维护ISAP的算法。
### 警告:各种网络流算法的复杂度及其退化的可能性
Dinic算法及朴素的ISAP算法:$O(n^2 m)$ ,不加当前弧优化:$O(n m^2)$ 。
队列维护的的ISAP算法:$O(n^3)$ ,不加当前弧(或者叫允许弧)优化:$O(n^2 m)$ 。
HLPP算法:$ O(n^2 \sqrt m) $ ,不加当前弧(或者叫允许弧)优化:$O(n m \sqrt m)$ ,用堆维护优先队列:$ O(n^2 \sqrt m logn) $ 。
如果两者都是那就是$ O(nm \sqrt m logn) $。然而网上几乎所有代码都有这两个问题,这就是为什么很多预流推进的程序跑得还没Dinic快的原因。
LCT维护的ISAP算法: $O(nm logn)$ 但由于常数太大目前没有找到很好的用途。
##### 前置技能:
网络流的基本定义,Dinic算法,LCT(如果要看LCT维护ISAP的算法的话)。
## 朴素的ISAP算法
我们首先回忆一下Dinic算法的做法:每次从源点出发bfs出一个层次图,然后只在这个层次图上跑流。
而ISAP沿用了这种思想,但不同之处就是ISAP改为从汇点出发bfs出层次图,接下来动态维护这个层次图。
具体来说,ISAP首先bfs出每一个点和汇点的距离,寻找增广路时只沿着满足$d[u]=d[v]+1$ 的边 $u→v$(这样的边称为允许弧)走,如果找不到这样的边,说明这个点的距离“过时了”,需要重新找一个距离,而这个距离显然是当前残量网络中与点 $u$ 相邻的点 $v$ 中最小的 $d[v]+1$ ,即 $d[u]=min_{v,(u,v)∈E}d[v] +1$ 。而这样做的正确性也是显然的(可以参照Dinic算法)。
#### 复杂度证明:
首先,每一个点最多被重标号 $O(n)$ ,因为每个点与汇点的距离不超过 $n$ ,对于一次推流,我们把它分为“完全推流”(一次性流完整条边的容量),和“不完全推流”。
对于一次“完全推流”,我们知道,这条边 $(u,v)$ (包括它的反向边)可能再被推流当且仅当点 $u$ 或点 $v$ 被重标号。所以每一条边至多被“完全推流” $O(n)$ 次,而完全推流的总数为 $O(nm)$ 。
而对于一次增广,至少有一次“完全推流”,所以增广的次数也是 $O(nm)$ 的,而一次增广最多流 $O(n)$ 次,所以推流的总数是 $O(n^2m)$ 的。(但不存允许弧的话就会变成 $O(nm^2)$ )
### ISAP的几个优化:
##### 1、当前弧/允许弧优化:
对于每一个点用领接表(开O2的话可以用vector)存它的允许弧,这样不用多余地访问不允许推流的边,而允许弧改变当且仅当一个点被重标号了,所以每次重标号的时候修改一下,这样是 $O(nm)$ 的,不影响程序整体效率。
##### 2、GAP优化:
开一个桶存每一个高度有几个点,重标号的时候看一下这个高度是否已经没有点了,如果是的话就不用再流了,因为这意味着图出现了断层。
### 代码:
(暂无,其实大家没必要去写朴素的ISAP,快来写下面的HLPP吧)
## HLPP算法
施工中。。。(等我期末考完吧)
### 代码:
```
#include<iostream>
#include<cstdlib>
#include<cstdio>
#include<cstring>
#include<map>
using namespace std;
#define re register
#define cmin(a,b) (a>(b)?a=(b):0)
#define cmax(a,b) (a<(b)?a=(b):0)
#define dmin(a,b) ((a)<(b)?(a):(b))
const int inf=1<<30,MaxN=10100,MaxM=121000;
struct edge{int to,v;edge*nx,*rec,*nx1;}e[MaxM<<1];edge*last[MaxN],*cur[MaxN],*cnt=e;
int h[MaxN],gap[MaxN],q[MaxN],n,s,t,n1,g[MaxN],nd[MaxN];
long long w[MaxN];
inline void addE(re int a,re int b,re int c)
{
*++cnt=(edge){b,c,last[a]};last[a]=cnt;cnt->rec=cnt+1;
*++cnt=(edge){a,0,last[b]};last[b]=cnt;cnt->rec=cnt-1;
}
inline void addV(re int a,re int h){nd[a]=g[h];g[h]=a;}
void bfs()
{
re int x,ta=2;q[1]=t;
memset(h,0,(n+1)<<2);h[t]=1;
for(re int i=1;i<ta;i++)
{
x=q[i];if(x==s)continue;
for(re edge*j=last[x];j;j=j->nx)if(!h[j->to])
h[j->to]=h[x]+1,q[ta++]=j->to;
}
}
void hlpp()
{
bfs();h[s]=n+1;re int ta=0,x,d;memset(w,0,(n+1)<<2);
for(re int i=1;i<=n;i++)
{
for(re edge*j=last[i];j;j=j->nx)if(h[j->to]==h[i]-1)
j->nx1=cur[i],cur[i]=j;
gap[h[i]]++;
}
for(re edge*j=last[s];j;j=j->nx)if(j->v&&h[j->to])
addV(j->to,h[j->to]),cmax(ta,h[j->to]),
w[j->to]+=j->v,j->rec->v=j->v,j->v=0;
while(ta)
{
x=g[ta];g[ta]=nd[x];nd[x]=0;
if(h[x]>n)continue;
for(re edge*&j=cur[x];j&&w[x];j=j->nx1)//推流
if(j->v&&h[j->to]==h[x]-1){
d=dmin(j->v,w[x]);if(!w[j->to]&&j->to!=t)
addV(j->to,h[j->to]),cmax(ta,h[j->to]);
j->v-=d,j->rec->v+=d;w[x]-=d;w[j->to]+=d;
if(!w[x])break;
}
if(w[x])//重标号
{
gap[h[x]]--;
if(!gap[h[x]])//gap优化
{
for(re int i=1;i<=n;i++)if(h[i]>h[x])gap[h[i]]=0,h[i]=n+1;h[x]=n+1;
while(ta&&!g[ta])ta--;continue;
}
h[x]=n+1;
for(re edge*j=last[x];j;j=j->nx)
if(j->v)cmin(h[x],h[j->to]+1);gap[h[x]]++;
for(re edge*j=last[x];j;j=j->nx)
if(j->v)if(j->v&&h[j->to]==h[x]-1)j->nx1=cur[x],cur[x]=j;
else if(j->rec->v&&h[j->to]==h[x]+1)j->rec->nx1=cur[j->to],cur[j->to]=j->rec;
addV(x,h[x]),cmax(ta,h[x]);
}while(ta&&!g[ta])ta--;
}
}
int main()
{
re int m,x,y,c;
scanf("%d%d%d%d",&n,&m,&s,&t);
for(re int i=1;i<=m;i++)scanf("%d%d%d",&x,&y,&c),addE(x,y,c);
hlpp();
printf("%lld\n",w[t]);
}
```
## LCT维护ISAP(口胡警告!!)
#### 前言
可能因为常数太大,太不实用了,我从来在网上没见过这个算法。只有维基上提了一下(只讲了复杂度,没有讲做法),于是我花了几天时间把它给脑补出来,也没有自己实现过。所以可能会有问题,有问题请帮忙指出一下。
#### 正文
让我们再回忆一下关于ISAP算法的复杂度,我们发现大部分时间都花在增广上面,而增广的实质是在找到一条路径的最小边权 $v$ , $ans += v$ ,然后路径上的边权全部减去 $v$ ,接着进行重标号。
这样暴力做是 $O(n)$ 的,浪费了大量的时间,我们想想有没有能够快速维护这个操作的数据结构呢?
有!LCT!
我们从汇点以外所有点取出一条允许弧建成一颗树(这样做显然会建成树),然后我们的操作就变成了:
1、找到源点 $s$ 到汇点 $t$ 的链上的最小值 $v$ , $ans+=v$ 。
2、源点 $s$ 到汇点 $t$ 的链上的所有边边权 $-= v$ 。
3、删除(cut掉)所有链上边权为 $0$ 的边。
4、对于所有“被删边的点”找到下一条允许弧link上了(这样操作完显然显然还会是树)。
5、若有点找不到“允许弧”,则重标号,cut掉所有指向它的边并执行步骤4。
(事实上3、4、5是同时执行的)
6、如果有一个点找不到“允许弧”,直接删除这个的并cut掉所有指向它的边,然后执行步骤4。
7、当图出现断层时,终止操作。
根据我在上面ISAP部分的证明,link和cut的次数都是 $O(nm)$ 的,所以总复杂度是 $O(nmlogn)$ 的。(然而LCT的常数啊。。。)
##### 关于反向边的处理:
显然地,一条边和它的反向边不可能同时为“允许弧”,所以我们只需要在cut掉一条边的时候再处理反向边就好了。
### 代码:
(暂无,可能得等挺久才会来填这个坑的吧。。。)