网络流简记
更新日志
2024/12/31:开工。添加网络流概念以及EK算法2025/01/01:添加Dinic算法,最小割与费用流
2025/01/02:添加最小割方案求法
2025/01/02:添加上下界网络流问题
概念
官方定义
网络
一种特殊有向图,有一个源点 \(s\) 与汇点 \(t\)。
图中每一条边都具有容量 \(c\),也就是流经流量上限。不存在的边 \(c=0\)。
可以视作流水,从源点开始进水(无限或有限),通过一条条边流开,每条边的尺寸限定了流量。
要求所有流都要汇入汇点。也就是流量守恒。
流
一个整体,大概就是所有有流水的边。
割
两个点集合 \(S,T\),满足 \(S\cup T=V\) 且 \(S\cap T=\varnothing\),同时 \(s\in S,t\in T\)。
一个割的容量是 \(\sum\limits_{u\in S}\sum\limits_{v\in T}c(u,v)\)。
形象化的,就是将点集分成两个集合,割的容量就是连接两个集合的边权之和。
最大流
问题
求一个网络的最大流量。
也就是每条边流量之和。
Ford-Fulkerson 增广
贪心思想。
每次找到一条从 \(s\) 到 \(t\) 的路径,称之为增广路,给它加上 \(w\),也就是增广操作,那么流量就加上了 \(w\)。
为了保证最优,我们加入反悔操作。具体的,每一条边都建立一条反边,反边空闲的流量就是正边已走的流量,这样走反边就相当于反悔了。
正确性证明略。
EK算法
直接模拟 Ford-Fulkerson 算法。
具体的,每次找一条 \(s-t\) 最短路(不是权值和最小,单纯的经过路程最小),保证每一条可行路径均被取到,然后每找到一条路径就操作一次:
- 给路径中每一条边的流量加上可行的最大值(路径中最小的剩余流量),同时给答案也加上。
- 别忘了给反边相应减去对应值。
具体地,BFS 即可。证明略。
复杂度 \(O(nm^2)\)。
模板
struct EK{
int n,cnt;
int hd[N];
struct edge{
int ne,to;
ll cap,flow;
}ed[M*2];
int p[N];ll a[N];
queue<int> q;
void init(int num){
n=num;
cnt=0;
rep(i,0,n)hd[i]=-1;
}
void adde(int a,int b,ll cap){
ed[cnt].ne=hd[a];hd[a]=cnt;
ed[cnt].to=b;
ed[cnt].cap=cap;
ed[cnt].flow=0;
cnt++;
ed[cnt].ne=hd[b];hd[b]=cnt;
ed[cnt].to=a;
ed[cnt].cap=0;
ed[cnt].flow=0;
cnt++;
}
bool bfs(int s,int t){
rep(i,0,n)a[i]=0;
while(!q.empty())q.pop();
a[s]=INF;
q.push(s);
while(!q.empty()){
int now=q.front();q.pop();
for(int e=hd[now];~e;e=ed[e].ne){
int nxt=ed[e].to;
if(!a[nxt]&&ed[e].cap>ed[e].flow){
a[nxt]=min(a[now],ed[e].cap-ed[e].flow);
p[nxt]=e;
q.push(nxt);
}
}
if(a[t])return 1;
}
return 0;
}
ll maxflow(int s,int t){
ll res=0;
while(bfs(s,t)){
res+=a[t];
for(int now=t;now!=s;now=ed[p[now]^1].to){
ed[p[now]].flow+=a[t];
ed[p[now]^1].flow-=a[t];
}
}
return res;
}
};
Dinic算法
首先重建分层图。
具体地,先BFS一遍分层,然后每个点只保留其到下一层的边,而删去其到上一层或同层的边。
随后,在分层图上,每次找到流量最大的增广流(即为阻塞流。这里的增广流是指整个流的路径并集,而非单条路径),将其加入答案、更新流。
直到不存在阻塞流为止,此时就是答案。
当前弧优化
这个优化可以保证复杂度。
具体地,我们维护每个点第一个还有余量的出边,防止每次都从头开始遍历。
表现在代码中,我们只需要在遍历边的时候加上引用即可。
但注意求新的阻塞流的时候需要复原。以上优化仅用于求同一阻塞流时。
模板
struct Dinic{
int n,cnt;
int hd[N],fr[N];
struct edge{
int ne,to;
ll cap,flow;
}ed[M*2];
int dp[N];
queue<int> q;
void init(int num){
n=num;
cnt=0;
rep(i,0,n)hd[i]=-1;
}
void adde(int a,int b,ll cap){
ed[cnt].ne=hd[a];hd[a]=cnt;
ed[cnt].to=b;
ed[cnt].cap=cap;
ed[cnt].flow=0;
cnt++;
ed[cnt].ne=hd[b];hd[b]=cnt;
ed[cnt].to=a;
ed[cnt].cap=0;
ed[cnt].flow=0;
cnt++;
}
int bfs(int s,int t){
rep(i,0,n)dp[i]=0;
dp[s]=1;
q.push(s);
while(!q.empty()){
int now=q.front();q.pop();
for(int e=hd[now];~e;e=ed[e].ne){
int nxt=ed[e].to;
if(ed[e].cap>ed[e].flow&&!dp[nxt]){
dp[nxt]=dp[now]+1;
q.push(nxt);
}
}
}
return dp[t];
}
ll dfs(int now,int t,ll flow){
if(now==t||!flow)return flow;
ll res=0;
for(int &e=fr[now];~e;e=ed[e].ne){
int nxt=ed[e].to;
if(ed[e].cap>ed[e].flow&&dp[nxt]==dp[now]+1){
ll f=dfs(nxt,t,min(flow-res,ed[e].cap-ed[e].flow));
res+=f;
ed[e].flow+=f;
ed[e^1].flow-=f;
if(res==flow)break;
}
}
return res;
}
ll maxflow(int s,int t){
ll res=0;
while(bfs(s,t)){
rep(i,0,n)fr[i]=hd[i];
res+=dfs(s,t,INF);
}
return res;
}
};
最小割
问题
求容量最小的割。
解决
根据最大流最小割定理,最大流就是最小割。证明略。
方案
从源点开始,每次走有余量的边,走到的点都在 \(S\) 中。
费用流
问题
每条边多了一个属性单位费用 \(w\)。
流经的费用就是 \(f(u,v)\times w(u,v)\)。
\(w(u,v)=-w(v,u)\)
要求最大流前提下最小化费用的问题就是最小费用最大流。
SPP 算法
\(\text{SPP}\) 贪心算法,每次找增广路的时候优先找费用最小的即可。
SPFA
通过 \(\text{SPFA}\) 解决负边权最短路问题。注意不可以有负环。
直接把两个算法中找增广路部分(\(\text{BFS}\))换成 \(\text{SPFA}\) 即可。
EK模板
其他部分没有什么变化,最后回溯时顺便加上代价即可。
struct SPP_EK{
int n,cnt;
int hd[N];
struct edge{
int ne,to;
ll cap,flow,val;
}ed[M*2];
int p[N];ll a[N];ll dis[N];
queue<int> q;bool inq[N];
void init(int num){
n=num;
cnt=0;
rep(i,0,n)hd[i]=-1;
}
void adde(int a,int b,ll cap,ll val){
ed[cnt].ne=hd[a];hd[a]=cnt;
ed[cnt].to=b;
ed[cnt].cap=cap;
ed[cnt].flow=0;
ed[cnt].val=val;
cnt++;
ed[cnt].ne=hd[b];hd[b]=cnt;
ed[cnt].to=a;
ed[cnt].cap=0;
ed[cnt].flow=0;
ed[cnt].val=-val;
cnt++;
}
bool spfa(int s,int t){
rep(i,0,n)a[i]=0,dis[i]=INF,inq[i]=0;
a[s]=INF;dis[s]=0;
q.push(s);
inq[s]=1;
while(!q.empty()){
int now=q.front();q.pop();
inq[now]=0;
for(int e=hd[now];~e;e=ed[e].ne){
int nxt=ed[e].to;
if(dis[nxt]>dis[now]+ed[e].val&&ed[e].cap>ed[e].flow){
dis[nxt]=dis[now]+ed[e].val;
a[nxt]=min(a[now],ed[e].cap-ed[e].flow);
p[nxt]=e;
if(!inq[nxt])q.push(nxt),inq[nxt]=1;
}
}
}
return a[t];
}
pll maxflow_mincost(int s,int t){
ll flow=0,cost=0;
while(spfa(s,t)){
flow+=a[t];
for(int now=t;now!=s;now=ed[p[now]^1].to){
ed[p[now]].flow+=a[t];
ed[p[now]^1].flow-=a[t];
cost+=ed[p[now]].val*a[t];
}
}
return {flow,cost};
}
};
Dinic模板
通过 \(dis\) 判断是否为最短增广路,每次只在最短增广路上增广。
struct SPP_DC{
int n,cnt;
int hd[N],fr[N];
struct edge{
int ne,to;
ll cap,flow,val;
}ed[M*2];
ll dis[N];int dp[N];
queue<int> q;bool inq[N];
void init(int num){
n=num;
cnt=0;
rep(i,0,n)hd[i]=-1;
}
void adde(int a,int b,ll cap,ll val){
ed[cnt].ne=hd[a];hd[a]=cnt;
ed[cnt].to=b;
ed[cnt].cap=cap;
ed[cnt].flow=0;
ed[cnt].val=val;
cnt++;
ed[cnt].ne=hd[b];hd[b]=cnt;
ed[cnt].to=a;
ed[cnt].cap=0;
ed[cnt].flow=0;
ed[cnt].val=-val;
cnt++;
}
bool spfa(int s,int t){
rep(i,0,n)inq[i]=0,dis[i]=INF;
dis[s]=0;
q.push(s);
inq[s]=1;
while(!q.empty()){
int now=q.front();q.pop();
inq[now]=0;
for(int e=hd[now];~e;e=ed[e].ne){
int nxt=ed[e].to;
if(ed[e].cap>ed[e].flow&&dis[nxt]>dis[now]+ed[e].val){
dis[nxt]=dis[now]+ed[e].val;
dp[nxt]=dp[now]+1;
if(!inq[nxt])q.push(nxt),inq[nxt]=1;
}
}
}
return dis[t]!=INF;
}
pll dfs(int now,int t,ll flow){
if(now==t||!flow)return {flow,0};
pll res={0,0};
for(int &e=fr[now];~e;e=ed[e].ne){
int nxt=ed[e].to;
if(ed[e].cap>ed[e].flow&&dis[nxt]==dis[now]+ed[e].val&&dp[nxt]==dp[now]+1){
ll f,c;
tie(f,c)=dfs(nxt,t,min(flow-res.fir,ed[e].cap-ed[e].flow));
res.fir+=f;res.sec+=c;
ed[e].flow+=f;
ed[e^1].flow-=f;
res.sec+=ed[e].val*f;
if(res.fir==flow)break;
}
}
return res;
}
pll maxflow_mincost(int s,int t){
ll flow=0,cost=0;
while(spfa(s,t)){
rep(i,0,n)fr[i]=hd[i];
auto res=dfs(s,t,INF);
flow+=res.fir;cost+=res.sec;
}
return {flow,cost};
}
};
上下界
无源汇
可行流
判断是否存在可行流量。
使用求最大流解决,我们考虑消去下界,这样问题就转化成了普通的网络流问题(还缺源点和汇点)。
具体的,对于每个点,上下界同时减去下界即可。
这时候我们发现流量不守恒了,计算出当前节点流量守恒需要的流量 \(d_i=(流入的下界之和-流出的下界之和\)),考虑构建一个附加源点和附加汇点,若 \(d_i<0\),则向汇点连一条 \(-d_i\) 容量的边,否则从源点连一条 \(d_i\) 容量的边,从而消去多出的出流量或入流量。
最后对附加源点到附加汇点跑一遍最大流即可,若所有附加边全部满流(判断附加源点的出边即可),则存在可行流。
此时每一条边的流量就是求出的流量加上它的下界。
有源汇
可行流
我们可以把这个问题转化成无源汇可行流问题。
具体的,连一条 \(t\rightarrow s\) 的上界为 \(\infty\) 下界为 \(0\) 的边,然后整个图就无源汇了。
若有解,那么可行流流量就是上面那条附加流的流量。
最大流
我们先跑一遍可行流。
这时候我们只需要榨干获取整个网络的剩余流量即可。
先删除所有附加边(事实上,只需要删除那个连接源点与汇点的边即可,因为别的附加边都满流了)。
然后在残量网络里用给出的源点和汇点再跑一遍最大流,二者相加即可。
最小流
类似地,我们退掉所有没有的流量就行。
更具体地,残量网络里跑一遍 \(t\rightarrow s\) 的最大流,可行流答案减去多余最大流就是最小流。
费用流
没有区别,把上面所有部分跑最大流的算法改成对应的费用流算法就行了。
模板(Dinic)
不推荐额外封装,直接在外部模拟,用上面的板子就行。
下面给出一个非费用流的封装示例。
struct BoundDinic{
int n,cnt;
int hd[N],fr[N];
struct edge{
int ne,to;
ll low,cap,flow;
}ed[M*2+N*2*2+2];
int dp[N];
queue<int> q;
void init(int num){
n=num;
cnt=0;
rep(i,0,n)hd[i]=-1;
}
void adde(int a,int b,ll low,ll cap){
ed[cnt].ne=hd[a];hd[a]=cnt;
ed[cnt].to=b;
ed[cnt].low=low;ed[cnt].cap=cap;
ed[cnt].flow=0;
cnt++;
ed[cnt].ne=hd[b];hd[b]=cnt;
ed[cnt].to=a;
ed[cnt].low=0;ed[cnt].cap=0;
ed[cnt].flow=0;
cnt++;
}
int bfs(int s,int t){
rep(i,0,n)dp[i]=0;
dp[s]=1;
q.push(s);
while(!q.empty()){
int now=q.front();q.pop();
for(int e=hd[now];~e;e=ed[e].ne){
int nxt=ed[e].to;
if(ed[e].cap>ed[e].flow&&!dp[nxt]){
dp[nxt]=dp[now]+1;
q.push(nxt);
}
}
}
return dp[t];
}
ll dfs(int now,int t,ll flow){
if(now==t||!flow)return flow;
ll res=0;
for(int &e=fr[now];~e;e=ed[e].ne){
int nxt=ed[e].to;
if(ed[e].cap>ed[e].flow&&dp[nxt]==dp[now]+1){
ll f=dfs(nxt,t,min(flow-res,ed[e].cap-ed[e].flow));
res+=f;
ed[e].flow+=f;
ed[e^1].flow-=f;
if(res==flow)break;
}
}
return res;
}
ll maxflow(int s,int t){
ll res=0;
while(bfs(s,t)){
rep(i,0,n)fr[i]=hd[i];
res+=dfs(s,t,INF);
}
return res;
}
ll d[N];
void LoopInit(int &s,int &t){
s=n+1,t=n+2;
hd[s]=hd[t]=-1;
rep(i,0,cnt-1){
if(i&1)d[ed[i].to]-=ed[i^1].low;
else d[ed[i].to]+=ed[i].low;
ed[i].cap-=ed[i].low;
}
rep(i,0,n){
if(d[i]>0)adde(s,i,0,d[i]);
if(d[i]<0)adde(i,t,0,-d[i]);
}
n+=2;
}
bool LoopPossibleFlow(){
int s,t;
LoopInit(s,t);
maxflow(s,t);
for(int e=hd[s];~e;e=ed[e].ne){
if(ed[e].flow<ed[e].cap)return 0;
}
return 1;
}
int loop;
ll RootPossibleFlow(int s,int t){
loop=cnt;
adde(t,s,0,INF);
if(!LoopPossibleFlow())return -1;
else return ed[loop].flow+ed[loop].low;
}
ll RootMaxFlow(int s,int t){
ll res=RootPossibleFlow(s,t);
if(res==-1)return -1;
ed[loop].cap=ed[loop].flow=0;
ed[loop^1].cap=ed[loop^1].flow=0;
return res+maxflow(s,t);
}
ll RootMinFlow(int s,int t){
ll res=RootPossibleFlow(s,t);
if(res==-1)return -1;
ed[loop].cap=ed[loop].flow=0;
ed[loop^1].cap=ed[loop^1].flow=0;
return res-maxflow(t,s);
}
};
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】