学习心得 - 算法 - 网络流

· · 算法·理论

建议查看续集。

定义

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),这里不再重写。 那么最小割其实就是就最大流,因为最大流的割同时也最小。 其余建模见听课笔记。