Unverified Commit 173499b7 authored by Shuhao Zhang's avatar Shuhao Zhang Committed by GitHub
Browse files

max-flow added

parent 36da0842
Loading
Loading
Loading
Loading
+221 −0
Original line number Diff line number Diff line
## 增广路

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

**增广路**指的是,从源点到汇点,只要有 $flow$ ($flow>0$) 流过去,这条路就是增广路。在一些最大流算法中,就是将这些路**增广**(意思就是走掉这条路,带走的流量肯定就是这条路的最小流量),如图:

![](./images/flow1.png)

我们从 $4$ 到 $3$,肯定可以先从流量为 $20$ 的这条边先走。那么这条边就被走掉了,不能再选,总的流量为 $20$(现在)。然后我们可以这样选择:

1.  $4\rightarrow2\rightarrow3$ 这条**增广路**的总流量为 $20$。到 $2$ 的时候还是 $30$,到 $3$ 了就只有 $20$ 了。

2.  $4\rightarrow2\rightarrow1\rightarrow3$ 这样子我们就很好的保留了 $30$ 的流量。

所以我们这张图的最大流就应该是 $20+30=50$。

求最大流是很简单的,接下来讲解求最大流的 $3$ 种方法。

### Edmond-Karp 动能算法($EK$ 算法)

这个算法很简单,就是 BFS**找增广路**,然后对其进行**增广**。你可能会问,怎么找?怎么增广?

1.  找? 我们就从源点一直 BFS 走来走去,碰到汇点就停,然后增广(每一条路都要增广)。我们在 BFS 的时候就注意一下流量合不合法就可以了。

2.  增广?其实就是按照我们找的增广路在重新走一遍。走的时候把这条路的能够成的最大流量减一减,然后给答案加上最小流量就可以了。

再讲一下**反向边**。增广的时候要注意建造反向边,原因是这条路不一定是最优的,这样子程序可以进行反悔。假如我们对这条路进行增广了,那么其中的每一条边的反向边的流量就是它的流量。

![](./images/flow2.png)

讲一下一些小细节。如果你是用邻接矩阵的话,反向边直接就是从 $table[x,y]$ 变成 $table[y,x]$。如果是常用的链式前向星,那么在加入边的时候就要先加入反向边。那么在用的时候呢,我们直接 $i\operatorname{xor}1$ 就可以了 ($i$ 为边的编号)。为什么呢? 相信大家都是知道 $\operatorname{xor}$ 的,那么我们在加入正向边后加入反向边,就是靠近的,所以可以使用 $\operatorname{xor}$。我们还要注意一开始的编号要设置为 $tot=1$,因为边要从编号 $2$ 开始,这样子 $\operatorname{xor}$ 对编号 $2,3$ 的边才有效果。

EK 算法的时间复杂度为 $O(n^2m)$(其中 $n$ 为点数,$m$ 为边数)。效率还有很大提升空间。

```cpp
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#define INF 0x3f3f3f3f
using namespace std;
struct edge
{
 int v,w,next;
}e[200005];
struct node
{
 int v,e;
}p[10005];
int head[10005],vis[10005];
int n,m,s,t,cnt=1;
void addedge(int u,int v,int w)
{
 e[++cnt].v=v;
 e[cnt].w=w;
 e[cnt].next=head[u];
 head[u]=cnt;
}
bool bfs()
{
 queue<int> q;
 memset(p,0,sizeof(p));
 memset(vis,0,sizeof(vis));
 vis[s]=1;
 q.push(s);
 while(!q.empty())
 {
  int cur=q.front();
  q.pop();
  for(int i=head[cur];i;i=e[i].next)
   if((!vis[e[i].v])&&e[i].w)
   {
    p[e[i].v].v=cur;
    p[e[i].v].e=i;
    if(e[i].v==t)return 1;
    vis[e[i].v]=1;
    q.push(e[i].v);
   }
 }
 return 0;
}
int main()
{
 scanf("%d%d%d%d",&n,&m,&s,&t);
 for(int i=1;i<=m;i++)
 {
  int u,v,w;
  scanf("%d%d%d",&u,&v,&w);
  addedge(u,v,w);
  addedge(v,u,0);
 }
 int ans=0;
 while(bfs())
 {
  int minw=INF;
  for(int i=t;i!=s;i=p[i].v)
   minw=min(minw,e[p[i].e].w);
  for(int i=t;i!=s;i=p[i].v)
  {
   e[p[i].e].w-=minw;
   e[p[i].e^1].w+=minw;
  }
  ans+=minw;
 }
 printf("%d\n",ans);
 return 0;
}
```

### Dinic

**Dinic 算法**的过程是这样的:每次增广前,我们先用 BFS 来将图分层。设源点的层数为 0,那么一个点的层数便是它离源点的最近距离。

通过分层,我们可以干两件事情:

1.  如果不存在到汇点的增广路(即汇点的层数不存在),我们即可停止增广。
2.  确保我们找到的增广路是最短的。(原因见下文)

接下来是 DFS 找增广路的过程。

我们每次找增广路的时候,都只找比当前点层数多 1 的点进行增广(这样就可以确保我们找到的增广路是最短的)。

Dinic 算法有两个优化:

1.  **多路增广**:每次找到一条增广路的时候,如果残余流量没有用完怎么办呢?我们可以利用残余部分流量,再找出一条增广路。这样就可以在一次 DFS 中找出多条增广路,大大提高了算法的效率。
2.  **当前弧优化**:如果一条边已经被增广过,那么它就没有可能被增广第二次。那么,我们下一次进行增广的时候,就可以不必再走那些已经被增广过的边。

设点数为 $n$,边数为 $m$,那么 Dinic 算法的时间复杂度是 $O(nm^2)$,在稀疏图上效率和 EK 算法相当,但在稠密图上效率要比 EK 算法高很多。

特别地,在求解二分图最大匹配问题时,可以证明 Dinic 算法的时间复杂度是 $O(n \sqrt{m})$。

```cpp
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#define INF 0x3f3f3f3f
using namespace std;
struct edge
{
 int v,w,next;
}e[200005];
int n,m,s,t,cnt=1;
int head[100005],dep[100005],vis[100005],cur[100005];
void addedge(int u,int v,int w)
{
 e[++cnt].v=v;
 e[cnt].w=w;
 e[cnt].next=head[u];
 head[u]=cnt;
}
bool bfs()
{
 queue<int> q;
 memset(dep,INF,sizeof(dep));
 memset(vis,0,sizeof(vis));
 memcpy(cur,head,sizeof(head));
 dep[s]=0;
 vis[s]=1;
 q.push(s);
 while(!q.empty())
 {
  int p=q.front();
  q.pop();
  vis[p]=0;
  for(int i=head[p];i;i=e[i].next)
   if(dep[e[i].v]>dep[p]+1&&e[i].w)
   {
    dep[e[i].v]=dep[p]+1;
    if(!vis[e[i].v])
    {
     vis[e[i].v]=1;
     q.push(e[i].v);
    }
   }
 }
 if(dep[t]==INF)return 0;
 return 1;
}
int dfs(int p,int w)
{
 if(p==t)return w;
 int used=0;//已经使用的流量
 for(int i=cur[p];i;i=e[i].next)//每条边都尝试找一次增广路
 {
  cur[p]=i;//当前弧优化
  if(dep[e[i].v]==dep[p]+1&&e[i].w)
  {
   int flow=dfs(e[i].v,min(w-used,e[i].w));
   if(flow)
   {
    used+=flow;
    e[i].w-=flow;
    e[i^1].w+=flow;
    if(used==w)break;//残余流量用尽了就停止增广
   }
  }
 }
 return used;
}
int main()
{
 scanf("%d%d%d%d",&n,&m,&s,&t);
 for(int i=1;i<=m;i++)
 {
  int u,v,w;
  scanf("%d%d%d",&u,&v,&w);
  addedge(u,v,w);
  addedge(v,u,0);
 }
 int ans=0;
 while(bfs())
  ans+=dfs(s,INF);
 printf("%d\n",ans);
 return 0;
}
```

### ISAP

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