安塔的ISAP和HLPP学习笔记
木木!
2019-09-25 16:50:16
# 前置知识
关于这些知识,可能会提及,但是不会详细介绍。请确认你已掌握以下知识:
+ [Dinic算法](https://www.cnblogs.com/linzhengmin/p/9313216.html)
+ 网络流的基本用语,如残量网络
+ [在`priority_queue`里面使用自定义`cmp`函数](https://blog.csdn.net/qq_30339595/article/details/79566788)、
# ISAP
`ISAP`,全称`Improved Shortest Augment Path`。这里提到它`Improved`,指它不需要在残量网络上重复执行`bfs`。它的基本思路是,一次`BFS`确定标号之后边流边修改。
由于要修改,而对于一个点,我们可以很容易地枚举它的出边,但是除非存反图,没有办法枚举入边。所以标号不再是到`s`的最短距离,而是到`t`的最短距离。这样可以很方便地进行修改。
首先做一次类似`Dinic`的计算。如果传进来的`flow`还有剩余,就需要提高标号以便于继续找增广路。由于标号的范围可以认为是`1-n`的,所以可以直接给标号`+1`。
按照这个思路,我们写出来的`isap`类似于一个`while`套`Dinic`。大概长这样:
(递归版)
```cpp
int isap(int s,int flow)
{
if(s==t)
{
return flow;
}
int flowed = 0;
while(flow && tag[s]<n)
{
for(int p=beg[s]; p; p=nxt[p])
{
if(ci[p] && tag[ed[p]]==tag[s]-1)
{
int f = isap(ed[p],min(ci[p],flow));
flow -= f;
flowed += f;
ci[p] -= f;
ci[p^1] += f;
if(!flow)
{
return flowed;
}
}
}
++tag[s];
}
return flowed;
}
```
这个就是`isap`的基本骨架了。当然,`Dinic`的运行速度有一半都是它的优化贡献的。`isap`同样也需要有很多优化。
## 1.优化-当前弧优化
这里的`ISAP`同样可以加当前弧优化,和`Dinic`的一样。
这里要注意一点:在`++tag[s]`的时候,必须将当前弧还原(`cnt[s]=beg[s]`)。
## 2.优化-gap优化
> 如果标号出现断层,说明当前已经不存在一个能够从s到t的增广路了。此时可以直接结束算法。
——这句话是对非递归版`ISAP`说的。
对于递归版`ISAP`,同样可以维护一个标号数量数组`tagn[]`,每次更改标号时候,如果对应标号被改没了,可以直接`break`。但是要注意,不能直接结束算法,因为前面可能还有点能补上来。
由于比较抽象,附代码。
```cpp
int isap(int s,int flow)
{
if(s==t)
{
return flow;
}
if(!tag[s] || tag[s]>n) //小优化
{
return 0;
}
int flowed = 0;
while(flow && tag[s]<n)
{
for(int p=cnt[s]; p; p=nxt[p])
{
if(ci[p] && tag[ed[p]]==tag[s]-1)
{
int f = isap(ed[p],min(ci[p],flow));
flow -= f;
flowed += f;
ci[p] -= f;
ci[p^1] += f;
if(!flow)
{
cnt[s] = p;
return flowed;
}
}
}
--tagn[tag[s]];
if(!tagn[tag[s]]) //直接退出
{
++tag[s];
++tagn[tag[s]];
return flowed;
}
++tag[s];
++tagn[tag[s]];
cnt[s] = beg[s];
}
return flowed;
}
```
## 3.修正-vis优化
如果你直接拿这份代码去交[P3376模板](https://www.luogu.org/problem/P3376),你会发现你`TLE`了一个点。
因为访问一个点之后,它可能没有任何高度合适的点,所以它就会将`tag`自增,然后再访问下一个点。下一个点可能也没有任何点,因此它也会将`tag`自增。由此,我们发现这个过程可以一直重复。更重要的是,其中的某一个点可能在`tag`自增后比中间还在递归栈内的某一个点的`tag`大,因此可能会形成环。
由于我们可能会重复访问一个点多达$\Theta(n)$次,这个增广路的长度最大是$\Theta(n^2)$的,因此你就会华丽丽地`TLE`。
修正方法是,增加一个`vis`数组,以避免重复访问某一个点导致增广路过长。
然后就是最终版代码:
```cpp
#include <queue>
#include <cstdio>
using namespace std;
int beg[10005];
int ed[200005];
int ci[200005];
int nxt[200005];
int top = 1;
void addedge(int a,int b,int c)
{
++top;
ed[top] = b;
ci[top] = c;
nxt[top] = beg[a];
beg[a] = top;
}
int n,t;
int tag[10005];
int cnt[10005];
int vis[10005];
int tagn[20005];
void rbfs()
{
for(int i=1; i<=n; ++i)
{
cnt[i] = beg[i];
}
tag[t] = 1;
tagn[1] = 1;
queue<int> q;
q.push(t);
while(!q.empty())
{
int th = q.front();
q.pop();
for(int p=beg[th]; p; p=nxt[p])
{
if(!ci[p] && !tag[ed[p]])
{
tag[ed[p]] = tag[th]+1;
++tagn[tag[ed[p]]];
q.push(ed[p]);
}
}
}
}
int isap(int s,int flow)
{
if(s==t)
{
return flow;
}
if(!tag[s] || tag[s]>n)
{
return 0;
}
vis[s] = 1;
int flowed = 0;
while(flow && tag[s]<n)
{
for(int p=cnt[s]; p; p=nxt[p])
{
if(ci[p] && !vis[ed[p]] && tag[ed[p]]==tag[s]-1)
{
int f = isap(ed[p],min(ci[p],flow));
flow -= f;
flowed += f;
ci[p] -= f;
ci[p^1] += f;
if(!flow)
{
cnt[s] = p;
vis[s] = 0;
return flowed;
}
}
}
--tagn[tag[s]];
if(!tagn[tag[s]])
{
return flowed;
}
++tag[s];
++tagn[tag[s]];
cnt[s] = beg[s];
}
vis[s] = 0;
return flowed;
}
int main()
{
int m,s;
scanf("%d%d%d%d",&n,&m,&s,&t);
for(int i=1; i<=m; ++i)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
addedge(a,b,c);
addedge(b,a,0);
}
rbfs();
printf("%d",isap(s,0x7f7f7f7f));
}
```
# HLPP
这里讲解`HLPP`的方式和途径和别的博客不一样。
显然,我们并没有将`ISAP`的所有优化搞完。如果同一个点被调用多次,我们可以将这些调用合并起来。即,`isap(s,3)`连着`isap(s,5)`等价于`isap(s,8)`。
合并调用,类似于线段树的合并操作,可以用`lazy_algorithm`的思想。我们开一个队列,存下那些调用,而不是每一次都着急地运行这些调用。
当然,我们也不能和上面一样使用`isap`的返回值了。不过这不是大问题。
```cpp
void hlpp(int s)
{
flow[s] = 0x7f7f7f7f7f7f7f7f;
queue<int> q;
q.push(s);
while(!q.empty())
{
int th = q.top();
q.pop();
while(flow[th] && tag[th]<n)
{
for(int p=beg[th]; p&&flow[th]; p=nxt[p])
{
if(ci[p] && tag[ed[p]]==tag[th]-1)
{
int f = min((long long)ci[p],flow[th]);
flow[ed[p]] += f;
ci[p] -= f;
flow[th] -= f;
ci[p^1] += f;
if(ed[p]!=t)
{
pq.push(ed[p]);
}
}
}
if(flow[th])
{
++tag[th];
}
}
}
}
```
`flow[]`数组就相当于调用`isap`时的参数`flow`。这里使用的是没有加任何优化的`isap`形式,得到了`hlpp`的骨架。当然,这时候它还不能被称作`HLPP`,因为缺了个最关键的优化。
我们约定,对于一个点做一次类似于`isap`的操作称为“推流”,而在那个类似`isap`的过程中给一个点的`flow[]`增加称为“推向”
## 1.优化-最高标号优化
> 预流推进+最高标号优化=最高标号预流推进(HLPP)
由于我们应用了`lazy_algorithm`,我们可以自由地决定先对哪些点推流。显然,如果可以的话,我们希望对标号尽量高的点推流。因为这样的话,可以尽量快地让一个点达成`tag[s]=n+1`。(“迫害原则”)
当然,你可以决定先对`flow[]`大的点推流,也可以决定先对`tag[]`小的点推流。你也可以对各种方案都试试,然后选择效果最好的。
Cheriyan和Maheshwari在1988年证明了先对`tag[]`大的点推流的复杂度为$O(n^2\sqrt{m})$,所以,我们只讲先对`tag[]`大的点推流的做法。
另外一个理由就是,先对`tag[]`大的点推流,就是先对远离`t`的点推流。这样,就像挤牙膏一样,把流都往`t`推。
至于实现原理,只需要使用`priority_queue`就好了。
## 2.优化-inq优化
类似`SLF`优化的`SPFA`,一个点只需要进队一次,所以可以记录`inq`数组,防止重复进队。
## 3.优化-当前弧优化
与`ISAP`的当前弧优化差不多。由于我们实现的是加`lazy_algorithm`的`ISAP`,当前弧优化可以完美兼容。
## 4.优化-gap优化
这里,出现断层仍旧不可以直接结束代码。但是,如果出现断层,可以确定`tag`比断层大的所有点都不能推流到`t`了。所以,可以直接将他们的`tag[]`变成`n+1`,来表示这个点已经废了。
附HLPP代码:
```cpp
#include <queue>
#include <cstdio>
using namespace std;
int beg[10005];
int ed[240005];
int nxt[240005];
int ci[240005];
int top = 1;
void addedge(int a,int b,int c)
{
++top;
ed[top] = b;
ci[top] = c;
nxt[top] = beg[a];
beg[a] = top;
}
int n,t;
int tag[10005];
int inq[10005];
int cnt[10005];
int tagn[10005];
long long flow[10005];
void rbfs()
{
for(int i=1; i<=n; ++i)
{
cnt[i] = beg[i];
}
tag[t] = 1;
queue<int> q;
q.push(t);
tagn[1] = 1;
while(!q.empty())
{
int th = q.front();
q.pop();
for(int p=beg[th]; p; p=nxt[p])
{
if(!tag[ed[p]])
{
tag[ed[p]] = tag[th]+1;
++tagn[tag[ed[p]]];
q.push(ed[p]);
}
}
}
}
class cmp
{
public:
bool operator() (int a,int b)
{
return tag[a]<tag[b];
}
};
void hlpp(int s)
{
flow[s] = 0x7f7f7f7f7f7f7f7f;
priority_queue<int,vector<int>,cmp> pq;
inq[s] = 1;
pq.push(s);
while(!pq.empty())
{
int th = pq.top();
inq[th] = 0;
pq.pop();
while(flow[th] && tag[th]<n)
{
for(int p=cnt[th]; p; p=nxt[p])
{
if(ci[p] && tag[ed[p]]==tag[th]-1)
{
int f = min((long long)ci[p],flow[th]);
flow[ed[p]] += f;
ci[p] -= f;
flow[th] -= f;
ci[p^1] += f;
if(ed[p]!=t && !inq[ed[p]])
{
inq[ed[p]] = 1;
pq.push(ed[p]);
}
if(!flow[th])
{
cnt[th] = p;
break;
}
}
}
if(flow[th])
{
--tagn[tag[th]];
if(!tagn[tag[th]])
{
for(int i=1; i<=n; ++i)
{
if(tag[i]>=tag[th])
{
tag[i] = n+1;
}
}
break;
}
++tag[th];
++tagn[tag[th]];
cnt[th] = beg[th];
}
}
}
}
int main()
{
int m,s;
scanf("%d%d%d%d",&n,&m,&s,&t);
for(int i=1; i<=m; ++i)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
addedge(a,b,c);
addedge(b,a,0);
}
rbfs();
hlpp(s);
printf("%lld\n",flow[t]);
}
```
# 总结
ISAP和HLPP在[P3376](https://www.luogu.org/problem/P3376)下跑速差不多,`HLPP`加那三个优化会稍快一些。`ISAP`比`HLPP`好写一些。在[P4722](https://www.luogu.org/problem/P4722),`HLPP`几乎是卡着最坏复杂度过的,`ISAP`根本过不了。
具体而言,在网络流算法里面,`HLPP`几乎肯定是首选。如果不想码太多代码,可以选择`ISAP`。如果还不想码太多代码,可以选择`Dinic`。一般而言,`Dinic`已经足够。
# 参考资料
感谢以下作者的无私付出:
+ [究级的最大流算法:ISAP与HLPP -钱逸凡](https://www.luogu.org/blog/ONE-PIECE/jiu-ji-di-zui-tai-liu-suan-fa-isap-yu-hlpp)
+ [最大流算法-ISAP -permui](https://www.cnblogs.com/owenyu/p/6852664.html)