学习心得 - 算法 - 网络流
ExFish
·
·
算法·理论
建议查看续集。
定义
- 网络(一个特殊有向图):
- 边权变为了流量(容量)记作 \text{c}(u,v),注意当 u 与 v 之间没有直接连边,那么 \text{c}(u,v)=0。
- 有两个特殊点:源点 s,汇点 t,要求 s\neq t。
- 流(一个边集函数):
- 对于流满足 0\leq \text{f}(u,v)\leq \text{c}(u,v)。
- 除了 s 和 t,一个点出入的总流量应为 0,即 \text{F}(u)=\sum_p \text{f}(p,u)-\sum_p \text{f}(u,p)=0(这个表达式其实也是净流量)。原因很好想,就是一进一出。
- 对于一个流 \text{f},它的净流量 |f| 为 \text{F}(s) 或 -\text{F}(t)。
- 我们把图 G 分为 S,T 两部分,要是一个流的 s\in S 且 t\in T,那么 {S,T} 是 G 的 s-t 割。这个东西的容量为 ||S,T||=\sum_{u\in S}\sum_{v\in T} \text{c}(u,v)。
2026/01/23:我忘记写残量网络是什么了,下面是它的定义:流完水后还有流量的边所组成的图叫做残量网络。
最大流
找一个流 \text{f} 使得图总流量最大。怎么说呢,分析下样例吧。
在 4 注无限多的水,只会在 3 流出 50。 我们就要算出这个最大的答案。
我们负责计算出一个决策,流出最多的水。这个过程就是最大流。
### EK 算法
我们先定义一个**增广路**为一条 $s$ 到 $t$ 的所有剩余流量(就是 $\text{c}(u,v)-\text{f}(u,v)$)都 $>0$,这就是一条增广路。那么 EK 算法就是 **BFS 增广,更新最大流量到没有增广路。**
具体的,对于所有**还有剩余流量的边**,考虑找一条路径,其剩余流量的最小值 $\min_c$ 就是最大流量可以增加的值。但是这显然不能贪心(被**多条**增广路包含),那么考虑**反悔**,可以通过建**反向边**达成要求。之后走反向边就可以做到**撤销**操作,因为跑反边的时候,把**多余的流量退回了**。
复杂度 $\mathcal O(nm^2)$。
```cpp
namespace EK{
const int N=205;
const ll INF=(1ll<<31)-1;
ll e[N][N],pre[N],flow[N];
queue<int>q;
ll bfs(int s,int t){
memset(pre,-1,sizeof(pre));
flow[s]=INF;
pre[s]=0;
while(q.size())
q.pop();
q.push(s);
while(q.size()){
int f=q.front();
q.pop();
if(f==t)
break;
for(int i=1;i<=n;i++)
if(i!=s&&e[f][i]>0&&pre[i]==-1){
pre[i]=f;
flow[i]=min(flow[f],e[f][i]);
q.push(i);
}
}
if(pre[t]==-1)
return -1; // no new
return flow[t];
}
ll maxflow(int s,int t){
ll mf=0;
while(1){
ll nowf=bfs(s,t);
if(nowf==-1)
break;
for(int p=t;p!=s;p=pre[p]){
int frm=pre[p];
e[frm][p]-=nowf;
e[p][frm]+=nowf; // reverse edge
}
mf+=nowf;
}
return mf;
}
};
```
### Dinic 算法
是 EK 的优化,复杂度是 $\mathcal O(n^2m)$。这玩意像迭代加深一样,结合了深搜和广搜,一次计算了**多条**增广路,优化复杂度。
如何写呢?我们考虑先使用**广搜分层**,这样构造出来的分层图可以限制深搜范围。然后我们考虑直接上深搜增广。之所以不在 EK 里直接上深搜,是因为这样会出现**多次迭代**,在边权大($w<2^{31}$)的时候会出问题。
```cpp
#include<bits/stdc++.h>
using namespace std;
const int N=205,M=5e3+10;
#define ll long long
int n,m;
namespace ListMap{
int cnt=1,head[N];
struct edges{
int to,nxt;
ll w;
}e[2*M];
void Add(int u,int v,ll w){
++cnt;
e[cnt]={v,head[u],w};
head[u]=cnt;
}
};
using namespace ListMap;
namespace Dinic{
const ll INF=(1ll<<31)-1;
int now[N],dep[N];
queue<int>q;
// now 当前弧优化
int s,t;
int bfs(){ // count dep.
for(int i=1;i<=n;i++)
dep[i]=INF;
dep[s]=0;
now[s]=head[s];
while(q.size())
q.pop();
q.push(s);
while(q.size()){
int f=q.front();
q.pop();
for(int i=head[f];i>0;i=e[i].nxt){
int v=e[i].to;
if(e[i].w>0&&dep[v]==INF){
q.push(v);
now[v]=head[v];
dep[v]=dep[f]+1;
if(v==t)
return 1;
}
}
}
return 0;
}
ll dfs(int p,ll flow){
if(p==t)
return flow;
ll nowf=0,cost;
for(int i=now[p];i>0&&flow>0;i=e[i].nxt){
int v=e[i].to;
now[p]=i;
if(e[i].w>0&&(dep[v]==dep[p]+1)){ // only next.
cost=dfs(v,min(flow,e[i].w));
if(!cost) // No new dfs.
dep[v]=INF;
e[i].w -=cost;
e[i^1].w +=cost; // reverse edge.
nowf +=cost;
flow -=cost;
}
}
return nowf;
}
ll maxflow(){
ll ans=0;
while(bfs())
ans+=dfs(s,INF);
return ans;
}
};
using namespace Dinic;
signed main(){
cin>>n>>m>>s>>t;
for(int i=1;i<=m;i++){
int u,v,w;
cin>>u>>v>>w;
Add(u,v,w);
Add(v,u,0); // reverse edge.
}
cout<<maxflow();
return 0;
}
```
### 最小费用最大流
一个网络流问题,就是每一条边增加了一个费用,我们想想多一个限制怎么做,同样这个题也有两种主流算法,都被称作 $\tt SSP$。这是一个通过贪心思想,不断选择最小费用增广的算法。
### SSP 算法(EK + SPFA)
是的,它又回来了。这个题可以直接把 `BFS` 改成 `SPFA`。复杂度变为 $\mathcal O(nmf)$。($f$ 是一个多项式,最大流)
唉主播尝试不用链式前向星结果调不出来,下面放个链式前向星。
```cpp
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=5e3+10,M=1e5+10;
int n,m,s,t;
namespace ListMap{
int cnt,head[N];
struct edges{
int to,nxt;
int w,g;
}e[2*M];
void Add(int u,int v,int w,int g){
e[cnt]={v,head[u],w,g};
head[u]=cnt++;
}
};
using namespace ListMap;
namespace EK{
const int N=5e3+10;
const ll INF=0x3f3f3f3f;
int pre[N],flow[N],dep[N],vis[N];
int cnt[N],q[N];
bool spfa(int s,int t){
memset(dep,0x3f,sizeof(dep));
memset(flow,0,sizeof(flow));
memset(cnt,0,sizeof(cnt));
flow[s]=INF;
dep[s]=0;
q[0]=s;
int fr=0,rr=1;
while(fr!=rr){
int f=q[fr++];
if(fr==N)fr=0;
vis[f]=0;
for(int i=head[f];~i;i=e[i].nxt){
int v=e[i].to;
if(e[i].w>0&&dep[v]>dep[f]+e[i].g){
dep[v]=dep[f]+e[i].g;
pre[v]=i;
flow[v]=min(flow[f],e[i].w);
if(!vis[v]){
q[rr++]=v;
if(rr==N)rr=0;
vis[v]=1;
}
}
}
}
return flow[t]>0;
}
pair<ll,ll> maxflow(int s,int t){
ll mf=0,mc=0;
while(spfa(s,t)){
int nowf=flow[t];
mf+=nowf;
mc+=nowf*dep[t];
for(int p=t;p!=s;p=e[pre[p]^1].to){
e[pre[p]].w-=nowf;
e[pre[p]^1].w+=nowf;
}
}
return{mf,mc};
}
};
using namespace EK;
signed main(){
cin>>n>>m>>s>>t;
memset(head,-1,sizeof(head));
for(int i=1;i<=m;i++){
ll u,v,w,g;
cin>>u>>v>>w>>g;
Add(u,v,w,+g);
Add(v,u,0,-g);
}
auto res=maxflow(s,t);
cout<<res.first<<' '<<res.second;
return 0;
}
```
### SSP 算法(Dinic + SPFA)
同理也是替换 `BFS` 为 `SPFA`。
```cpp
// PID: Luogu P4043
// URL: https://www.luogu.com.cn/problem/P4043
// ML: 125 MB
// TL: 1000 ms
// Author: Ivan422
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ld long double
#define db double
#define fi first
#define se second
#define pb push_back
const ll P=998244353;
const int N=2e4+10, M=1e6+10;
namespace ListMap{
int cnt,head[N];
struct edges{
int to,nxt;
int w,g;
}e[2*M];
void ADD(int u,int v,int w,int g){
e[cnt]={v,head[u],w,g};
head[u]=cnt++;
}
void add(int u,int v,int w,int g){
ADD(u,v,w,g);
ADD(v,u,0,-g);
}
};
using namespace ListMap;
namespace Dinic{
const int INF=0x3f3f3f3f;
int pre[N],dep[N],vis[N],now[N];
int cnt[N],q[N];
bool spfa(int s,int t){
for(int i=s;i<=t;i++)
dep[i]=INF, vis[i]=0, now[i]=head[i];
dep[s]=0;
q[0]=s;
int fr=0,rr=1;
while(fr!=rr){
int f=q[fr++];
if(fr==N)fr=0;
vis[f]=0;
for(int i=head[f];~i;i=e[i].nxt){
int v=e[i].to;
if(e[i].w>0&&dep[v]>dep[f]+e[i].g){
dep[v]=dep[f]+e[i].g;
if(!vis[v]){
q[rr++]=v;
if(rr==N)rr=0;
vis[v]=1;
}
}
}
}
return dep[t]!=INF;
}
int mc;
int dfs(int p,int f,int t){
if(!f||p==t) return f;
int nowf=0, flow;
vis[p]=1;
for(int &i=now[p];~i;i=e[i].nxt){
int v=e[i].to;
if(!vis[v]&&e[i].w&&dep[v]==dep[p]+e[i].g){
flow=dfs(v,min(f-nowf,e[i].w),t);
if(!flow) continue;
nowf+=flow;
e[i].w-=flow, e[i^1].w+=flow;
mc+=flow*e[i].g;
if(nowf==f) break;
}
}
vis[p]=0;
return nowf;
}
int maxflow(int s,int t){
int mf=0; mc=0;
while(spfa(s,t))
mf+=dfs(s,INF,t);
return mc;
}
};
int n,m,s,t,a[N],nd;
using namespace Dinic;
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
memset(head,-1,sizeof(head));
cin>>n;
s=0, t=N-1; nd=n;
add(s,1,INF,0);
for(int i=1;i<=n;i++){
int k; cin>>k;
while(k--){
int b,w; cin>>b>>w;
int e0=++nd; int e1=++nd;
add(i,e0,INF,w);
add(e0,e1,INF,0);
add(e1,b,INF,0);
add(s,e1,1,0);
add(e0,t,1,0);
}
}
maxflow(s,t); cout<<mc;
return 0;
}
```
### Primal-Dual 原始对偶算法
因为 SPFA 它死了。所以我们考虑使用 Johnson 全源最短路的势能分析方法来制作。然后就可以变为 EK/Dinic + Dijkstra 了。
### 最小割建模转化
原始问题:为了使得源点和汇点不连通,最少要割几条边。
首先我们引入一个**最大流最小割定理**,这个定理同样可以用于证明最大流正确。具体证明请看[这里](https://oi-wiki.org/graph/flow/max-flow/#%E6%9C%80%E5%A4%A7%E6%B5%81%E6%9C%80%E5%B0%8F%E5%89%B2%E5%AE%9A%E7%90%86),这里不再重写。
那么最小割其实就是就最大流,因为最大流的割同时也最小。
其余建模见听课笔记。