网络单纯形算法学习笔记
参考链接
网络单纯形算法解说【翻译】 - zhiyin123123 - 博客园
前言
阅读本篇,你不需要了解单纯形算法,但是你需要了解费用流相关概念。
如果你会使用 SPFA + EK 算法(一种 SSP 算法)求解费用流问题,并想学会更高效的算法,那么你适合阅读本篇。
约定
- 如无特殊说明,使用
代表图中的点数和边数,用 代表源点和汇点。
介绍
网络单纯形算法可以直接解决如下问题:
- 有源汇最小费用最大流问题。图中可能有负圈。
它能做到期望
介绍以下大致思路。首先,连接一条
- 在图中找到一个负圈,使得其所有边均能增加流量,然后找到最大的流量
,让负圈上的所有边的流量增加 ,并均不超出限制。
直到找不到负圈为止。最后,把一开始添加的
该思路中,添加的辅助边
该算法的正确性基于:费用流的局部最优解就是全局最优解。
正文
维护生成树解
为了方便讨论,先做如下定义。
- 对于边
,定义它和其对应的虚拟反边构成一个边对。 - 对于边对
,若 的流量可以增加(而不超过限制), 的流量也可以增加,则称其为自由边对。 - 对于一个边对组成的圈,如果组成它的所有边对都是自由边对,则称其为自由圈。
在算法的过程中,我们会不让自由圈出现,因为总有一种推流方式,让该圈的费用变得更小。
我们要维护一个该图的生成树(由边对构成)(不妨设图联通),满足
- 生成树外的边对均不是自由边对。
这样,就一定没有自由圈啦。(因为生成树无圈)
为了方便,我们强制认为,生成树上的边对
然后,我们就要不断在图中找负圈了。设生成树为
设图为
- 图
中无负圈,当且仅当对于所有边对 ,图 中无负圈。
如果它是正确的,我们就可以不断的消除和树有关的负圈来正确求解了。
证明其实很简单,画画图写几个不等式就能搞定。
假如在算法的执行过程中,我们找到了边对
我们不断找到这样的边
维护生成树的具体实现
在维护生成树时,首先要解决的问题就是,对于任意一条边对
不妨先给
然后,我们还要维护从树中加边和删边,用 LCT?太难写了,直接单次
还有,对于一条边,我们要找出它和生成树构成的圈包含哪些边,这样才能推流。可以使用单次
如何找到边使得 有负圈
实际上,有如下可行方法:
- 循环遍历所有边对
,如果发现 中有负圈,那么就立刻选择它推流。 - 暴力遍历所有边对
,找到 中负圈费用最小的,用它推流。 - 将边对分为
组( 可能取 ),循环遍历每个组,在组中选择产生负圈权值最小的边,用它推流。
选第
如何避免死循环
容易发现,这个算法有可能会产生死循环,具体来讲,有如下两个原因:
- 有的时候,推流减少的费用可能为
. - 用
推流并加入 中后,有可能有多条圈中的边对是非自由边对,不知道删哪一个。
下面是解决方案,不过我不会证明其正确性:
- 用
(设其为 )推流并加入 中后,设 和 在 中的 LCA 为 ,则想象你从 沿着圈上的边走到了 ,再走到 ,再走到了 ,如此绕一圈,然后,删掉你遇到的第一个非自由边对。
该算法能爆切的题目
暴力建图就能过,还比正解跑得快:P5331 [SNOI2019] 通信 - 洛谷
代码实现
#include<bits/stdc++.h>
template<int Node_cnt,int Edge_cnt,typename cost_t,typename flow_t>
class min_cost_max_flow_t{
template<typename Tp>
static bool tomin(Tp &x,const Tp &y){if(y<x){x=y; return 1;} return 0;}
static constexpr int NSI=Node_cnt,ESI=(Edge_cnt+1)*2;
struct ed_t{int nxt,to; flow_t f; cost_t c;};
int n,m,S,T;
std::vector<char> vis;
std::vector<int> hd,Fa,Fe,dep,buf;
std::vector<cost_t> dis;
std::vector<ed_t> E;
void build(int it,int fa,int fe){
vis[it]=1;
Fa[it]=fa; Fe[it]=fe;
for(int j=hd[it];j!=0;j=E[j].nxt)if(!vis[E[j].to]){
build(E[j].to,it,j^1);
}
return ;
}
void upd(int it){
if(it==0||vis[it]) return ;
vis[it]=1;
upd(Fa[it]);
dep[it]=dep[Fa[it]]+1;
dis[it]=dis[Fa[it]]+E[Fe[it]].c;
return ;
}
int getlca(int x,int y){
while(x!=y) (dep[y]>=dep[x])?y=Fa[y]:x=Fa[x];
return x;
}
void pushflow(int j){
int l=NSI,r=NSI;
buf[l]=j;
int u=E[j^1].to,v=E[j].to;
int lca=getlca(u,v);
for(int i=u;i!=lca;i=Fa[i]) buf[--l]=Fe[i]^1;
for(int i=v;i!=lca;i=Fa[i]) buf[++r]=Fe[i];
int del=NSI; cost_t f=E[j].f;
for(int i=l;i<=r;i++) if(tomin(f,E[buf[i]].f)) del=i;
for(int i=l;i<=r;i++){int s=buf[i]; E[s].f-=f; E[s^1].f+=f;}
if(del==NSI) return ;
if(del<NSI){
for(int i=del+1;i<=NSI;i++){
int s=buf[i];
Fa[E[s^1].to]=E[s].to;
Fe[E[s^1].to]=s;
}
}else{
for(int i=NSI;i<=del-1;i++){
int s=buf[i];
Fa[E[s].to]=E[s^1].to;
Fe[E[s].to]=s^1;
}
}
std::fill(vis.begin()+1,vis.begin()+n+1,0);
return ;
}
public:
min_cost_max_flow_t():n(0),m(1),S(0),T(0),
vis(NSI+5,0),hd(NSI+5,0),Fa(NSI+5,0),Fe(NSI+5,0),dep(NSI+5,0),buf(NSI*2+5,0),
dis(NSI+5,0),E(ESI+5){}
void connect(int u,int v,flow_t f,cost_t c){
E[++m]={hd[u],v,f,c}; hd[u]=m;
E[++m]={hd[v],u,0,-c}; hd[v]=m;
return ;
}
std::pair<flow_t,cost_t> solve(int _n,int _s,int _t){
n=_n; S=_s, T=_t;
flow_t sflow=0; cost_t scost=0;
for(int j=2;j<=m;j++){
auto &e=E[j];
sflow+=e.f; if(e.c>0) scost+=e.c;
}
connect(S,T,sflow,scost+1);
std::swap(E[m-1].f,E[m].f);
flow_t flow=0; cost_t cost=0;
build(S,0,0); std::fill(vis.begin()+1,vis.begin()+n+1,0);
while(1){
bool ok=0;
for(int j=2;j<=m;j++){
auto &e=E[j];
if(e.f>0){
int u=E[j^1].to,v=e.to;
upd(u); upd(v);
if(e.c-dis[u]+dis[v]<0){
pushflow(j);
ok=1;
}
}
}
if(!ok) break;
}
for(int j=3;j<m;j+=2){
auto &e=E[j];
if(e.to==S) flow+=e.f;
if(E[j^1].to==S) flow-=e.f;
cost+=-e.c*e.f;
}
return {flow,cost};
}
};
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通