Unverified Commit cea40681 authored by Yaoyao's avatar Yaoyao Committed by GitHub
Browse files

补充网络流最大流

添加了网络和流的定义
说明了两种主流的求解最大流算法
添加HLPP算法(最高标准预留推进算法),配了gif和png的图
parent e1c1af9a
Loading
Loading
Loading
Loading
+254 −2
Original line number Diff line number Diff line
## 网络

网络是指一个有向图 $G=(V,E)$

每条边 $(u,v)\in E$ 都有一个权值 $c(u,v)$,称之为容量(Capacity),当 $(u,v)\notin E$ 时有 $c(u,v)=0$.

其中有两个特殊的点:源点 $s\in V$ 和汇点 $t\in V,(s\neq t)$.

## 流

设 $f(u,v)$ 定义在二元组 $(u\in V,v\in V)$ 上的实数函数且满足

1. **容量限制**:$f(u,v)\leq c(u,v)$
2. **斜对称性**:$f(u,v)=-f(v,u)$
3. **流守恒性**:$\forall x\in V-\{s,t\},\sum_{(u,x)\in E}f(u,x)=\sum_{(x,v)\in E}f(x,v)$

那么 $f$ 称为网络 $G$ 的流函数

对于 $(u,v)\in E$,$f(u,v)$ 称为边的**流量**,$c(u,v)-f(u,v)$ 称为边的**剩余容量**

整个网络的流量为 $\sum_{(s,v)\in E}f(s,v)$,即**从源点发出的所有流量之和**

*注*:流函数的完整定义为
$$
f(u,v)=\left\{\begin{split}
&f(u,v)&,(u,v)\in E\\
&-f(v,u)&,(v,u)\in E\\
&0&,(u,v)\notin E,(v,u)\notin E
\end{split}\right.
$$

## 定义

我们有一张图,要求从源点流向汇点的最大流量(可以有很多条路到达汇点),就是我们的最大流问题。

求解最大流问题有三种常见算法:Edmonds-Karp 算法,Dinic 算法,ISAP 算法。
## 最大流主流的两种方法

### Ford-Fulkerson 方法

## 增广路
该方法通过寻找增广路来更新最大流

有 $EK,dinic,SAP,ISAP$ 主流算法

### Push-Relabel 方法

该方法在求解过程中忽略流守恒性,并每次对一个结点更新信息,以求解最大流

有 $HLPP$ 的主流算法

## FF增广路算法

求解最大流之前,我们先认识以下增广路的概念。

@@ -202,3 +245,212 @@ int main() {
### ISAP

这个是 SAP 算法的加强版 (Improved)。

## PR预留推进算法

推送 - 重贴标签算法通过对单个结点的更新操作,直到没有结点需要更新来求解最大流

算法过程维护的流函数不一定保持流守恒性,对于一个结点,我们允许进入结点的流超过流出结点的流,超过的部分被称为结点 $u(u\in V-\{s,t\})$ 的**超额流**$e(u)$:
$$
e(u)=\sum_{(x,u)\in E}f(x,u)-\sum_{(u,y)\in E}f(u,y)
$$
若 $e(u)>0$,称结点 $u$**溢出**.

推送 - 重贴标签算法维护每个结点的高度 $h(u)$,并且规定溢出的结点 $u$ 如果要推送超额流,只能向高度小于 $u$ 的结点推送;如果 $u$ 没有相邻的高度小于 $u$ 的结点,就修改 $u$ 的高度(重贴标签).

#### 高度函数

准确地说,推送 - 重贴标签维护以下的一个映射 $h:V\to \mathbb{N}$:

- $h(s)=|V|,h(t)=0$
- $\forall (u,v)\in E_f,h(u)\leq h(v)+1$.

则称 $h$ 是残存网络 $G_f=(V_f,E_f)$ 的高度函数。

引理 1:设 $G_f$ 上的高度函数为 $h$,对于任意两个结点 $u,v\in V$,如果 $h(u)>h(v)+1$,则 $(u,v)$ 不是 $G_f$ 中的边。

算法只会在 $h(u)=h(v)+1$ 的边执行推送。

#### 推送 -Push

适用条件:结点 $u$ 溢出,且存在结点 $v((u,v)\in V_f,c(u,v)-f(u,v)>0,h(u)=h(v)+1)$,则 push 操作适用于 $(u,v)$.

于是,我们尽可能将超额流从 $u$ 推送到 $v$,推送过程中我们只关心超额流和 $c(u,v)-f(u,v)$ 的最小值,不关心 $v$ 是否溢出。

如果 $(u,v)$ 在推送完之后满流,将其从残存网络中删除

#### 重贴标签 -Relabel

适用条件:如果结点 $u$ 溢出,且 $\forall (u,v)\in E_f,h(u)\leq h(v)$,则 relabel 操作适用于 $u$.

则将 $h(u)$ 更新为 $min_{(u,v)\in E_f}h(v)+1$ 即可。

#### 初始化

$$
\begin{split}
&\forall (u,v)\in E,&f(u,v)=\left\{\begin{split}
&c(u,v)&,u=s\\
&0&,u\neq s\\
\end{split}\right..
\\
&\forall u\in V,&h(u)=\left\{\begin{split}
&|V|&,u=s\\
&0&,u\neq s\\
\end{split}\right.
,e(u)=\sum_{(x,u)\in E}f(x,u)-\sum_{(u,y)\in E}f(u,y).
\end{split}.
$$

上述将 $(s,v)\in E$ 充满流,并将 $h(s)$ 抬高,使得 $(s,v)\notin E_f$,因为 $h(s)>h(v)$,而且 $(s,v)$ 毕竟满流,没必要留在残存网络中;上述还将 $e(s)$ 初始化为 $\sum_{(s,v)\in E}f(s,v)$ 的相反数。

#### 通用执行框架

无需按照特定顺序,执行以下过程:

- 只要存在结点 $u$ 满足 push 或 relabel 的条件,就执行对应的操作。

如图,每个结点中间表示编号,左下表示高度值 $h(u)$,右下表示超额流 $e(u)$,结点颜色的深度也表示结点的高度;边权表示 $c(u,v)-f(u,v)$,绿色的边表示满足 $h(u)=h(v)+1$ 的边 $(u,v)$(即残存网络的边 $E_f$):

![p1](https://hexo-source-1257756441.cos.ap-chengdu.myqcloud.com/2018/12/21/2148.png)

整个算法我们大致浏览一下过程,这里笔者使用的是一个暴力算法,即暴力扫描是否有溢出的结点,有就更新

![p2](https://hexo-source-1257756441.cos.ap-chengdu.myqcloud.com/2018/12/21/2149.gif)

最后的结果

![p3](https://hexo-source-1257756441.cos.ap-chengdu.myqcloud.com/2018/12/21/2150.png)

可以发现,最后的超额流一部分回到了 $s$,且除了源点汇点,其他结点都没有溢出;这时的流函数 $f$ 满足流守恒性,为最大流,即 $e(t)$.

#### 核心代码

```cpp
const int N=1e4+4,M=1e5+5,INF=0x3f3f3f3f;
int n,m,s,t,maxflow,tot;
int ht[N],ex[N];
void init(){// 初始化
	for(int i=h[s];i;i=e[i].nex){const int &v=e[i].t;
		ex[v]=e[i].v,ex[s]-=ex[v],e[i^1].v=e[i].v,e[i].v=0;
	}
	ht[s]=n;
}
bool push(int ed){const int &u=e[ed^1].t,&v=e[ed].t;
	int flow=min(ex[u],e[ed].v);
	ex[u]-=flow,ex[v]+=flow,e[ed].v-=flow,e[ed^1].v+=flow;
	return ex[u];// 如果 u 仍溢出,返回 1
}
void relabel(int u){
	ht[u]=INF;
	for(int i=h[u];i;i=e[i].nex)if(e[i].v)ht[u]=min(ht[u],ht[e[i].t]);
	++ht[u];
}
```

### HLPP 算法

最高标号预流推进算法(High Level Preflow Push)是基于推送 - 重贴标签算法的优先队列实现,该算法优先推送高度高的溢出的结点,算法算法复杂度 $O(n^2\sqrt m)$.

具体地说,HLPP 维护以下过程:

1. 初始化(基于推送 - 重贴标签算法)
2. 选择溢出结点(除 $s,t$)中高度最高的结点 $u$,并对它所有可以推送的边进行推送;
3. 如果 $u$ 仍溢出,对它重贴标签,回到 2.
4. 如果没有溢出的结点,算法结束

#### BFS 优化

HLPP 的上界为 $O(n^2\sqrt m)$,但在使用时卡得比较紧;我们可以在初始化高度的时候进行优化

具体来说,我们初始化 $h(u)$ 为 $u$ 到 $t$ 的最短距离;特别地,$h(s)=n$.

在 BFS 的同时我们顺便检查图的联通性,排除无解的情况

#### GAP 优化

HLPP 推送的条件是 $h(u)=h(v)+1$,而如果在算法的某一时刻,$h(u)=t$ 的结点个数为 0,那么对于 $h(u)>t$ 的结点就永远无法推送超额流到 $t$,因此只能送回 $s$,那么我们就在这时直接让他们的高度变成 $n+1$,以尽快推送回 $s$,减少重贴标签的操作

#### [LuoguP4722] 【模板】最大流 加强版 / 预流推进

```cpp
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
const int N=1e4+4,M=2e5+5,INF=0x3f3f3f3f;
int n,m,s,t;

struct qxx{int nex,t,v;};
qxx e[M*2];
int h[N],cnt=1;
void add_path(int f,int t,int v){e[++cnt]=(qxx){h[f],t,v},h[f]=cnt;}
void add_flow(int f,int t,int v){add_path(f,t,v);add_path(t,f,0);}

int ht[N],ex[N],gap[N];// 高度;超额流;gap 优化
bool bfs_init(){
    memset(ht,0x3f,sizeof(ht));
    queue<int> q;
    q.push(t),ht[t]=0;
    while(q.size()){// 反向 BFS, 遇到没有访问过的结点就入队
        int u=q.front();q.pop();
        for(int i=h[u];i;i=e[i].nex){const int &v=e[i].t;
            if(e[i^1].v&&ht[v]>ht[u]+1)ht[v]=ht[u]+1,q.push(v);
        }
    }
    return ht[s]!=INF;// 如果图不连通,返回 0
}
struct cmp{bool operator()(int a,int b)const{return ht[a]<ht[b];}};// 伪装排序函数
priority_queue<int,vector<int>,cmp> pq;// 将需要推送的结点以高度高的优先
bool vis[N];// 是否在优先队列中
int push(int u){// 尽可能通过能够推送的边推送超额流
    for(int i=h[u];i;i=e[i].nex){const int &v=e[i].t,&w=e[i].v;
        if(!w||ht[u]!=ht[v]+1)continue;
        int k=min(w,ex[u]);// 取到剩余容量和超额流的最小值
        ex[u]-=k,ex[v]+=k,e[i].v-=k,e[i^1].v+=k;//push
        if(v!=s&&v!=t&&!vis[v])pq.push(v),vis[v]=1;// 推送之后,v 必然溢出,则入堆,等待被推送
        if(!ex[u])return 0;// 如果已经推送完就返回
    }return 1;
}
void relabel(int u){// 重贴标签(高度)
    ht[u]=INF;
    for(int i=h[u];i;i=e[i].nex)if(e[i].v)ht[u]=min(ht[u],ht[e[i].t]);
    ++ht[u];
}
int hlpp(){// 返回最大流
    if(!bfs_init())return 0;// 图不连通
    ht[s]=n;
    memset(gap,0,sizeof(gap));
    for(int i=1;i<=n;i++)if(ht[i]!=INF)gap[ht[i]]++;// 初始化 gap
    for(int i=h[s];i;i=e[i].nex){const int v=e[i].t,w=e[i].v;// 队列初始化
        if(!w)continue;
        ex[s]-=w,ex[v]+=w,e[i].v-=w,e[i^1].v+=w;// 注意取消 w 的引用
        if(v!=s&&v!=t&&!vis[v])pq.push(v),vis[v]=1;// 入队
    }
    while(pq.size()){
        int u=pq.top();pq.pop(),vis[u]=0;
        while(push(u)){// 仍然溢出
            // 如果 u 结点原来所在的高度没有结点了,相当于出现断层
            if(!--gap[ht[u]])for(int i=1;i<=n;i++)
                if(i!=s&&i!=t&&ht[i]>ht[u]&&ht[i]<n+1)ht[i]=n+1;
            relabel(u);++gap[ht[u]];// 新的高度,更新 gap
        }
    }
    return ex[t];
}
int main(){
    scanf("%d%d%d%d",&n,&m,&s,&t);
    for(int i=1,u,v,w;i<=m;i++){
        scanf("%d%d%d",&u,&v,&w);
        add_flow(u,v,w);
    }
    printf("%d",hlpp());
    return 0;
}
```

感受一下运行过程

![HLPP](https://hexo-source-1257756441.cos.ap-chengdu.myqcloud.com/2018/12/22/1152.png)

其中 $pic13$ 到 $pic14$ 执行了 $Relabel(4)$,并进行了 GAP 优化