学习心得 - 算法 - 网络流
定义
- 网络(一个特殊有向图):
- 边权变为了流量(容量)记作 \(\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\)。 我们就要算出这个最大的答案。
\(4\to 2\) 流水,只剩下 \(30\) 的水量了。\(4\to 3\) 流水,汇点 \(3\) 接受到了 \(20\) 的水量。这时先让 \(2\to3\),\(2\) 点剩余 \(10\),汇点 \(3\) 接受 \(20\)。最后流 \(2\to 1\to 3\),汇点接受 \(10\),总计 \(50\)。当然你也可以让 \(2 \to 1\to 3\) 流走全部的 \(30\) 水,总计也是 \(50\)。
我们负责计算出一个决策,流出最多的水。这个过程就是最大流。
EK 算法
我们先定义一个增广路为一条 \(s\) 到 \(t\) 的所有剩余流量(就是 \(\text{c}(u,v)-\text{f}(u,v)\))都 \(>0\),这就是一条增广路。那么 EK 算法就是 BFS 增广,更新最大流量到没有增广路。
具体的,对于所有还有剩余流量的边,考虑找一条路径,其剩余流量的最小值 \(\min_c\) 就是最大流量可以增加的值。但是这显然不能贪心(被多条增广路包含),那么考虑反悔,可以通过建反向边达成要求。之后走反向边就可以做到撤销操作,因为跑反边的时候,把多余的流量退回了。
复杂度 \(\mathcal O(nm^2)\)。
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}\))的时候会出问题。
#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\) 是一个多项式,最大流)
唉主播尝试不用链式前向星结果调不出来,下面放个链式前向星。
#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。
// 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 了。
最小割建模转化
原始问题:为了使得源点和汇点不连通,最少要割几条边。
首先我们引入一个最大流最小割定理,这个定理同样可以用于证明最大流正确。具体证明请看这里,这里不再重写。
那么最小割其实就是就最大流,因为最大流的割同时也最小。
其余建模见听课笔记。
文章同步载入于洛谷,详见文章下方链接。
本文来自博客园,作者:Ivan422,转载请注明原文链接:https://www.cnblogs.com/Exfish/p/23269921。

浙公网安备 33010602011771号