专栏文章
最大流与Dijkstra做费用流
算法·理论参与者 47已保存评论 59
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 59 条
- 当前快照
- 1 份
- 快照标识符
- @mhz5rlo3
- 此快照首次捕获于
- 2025/11/15 01:55 4 个月前
- 此快照最后确认于
- 2025/11/29 05:25 3 个月前
0x00网络流简介
网络流是一个有向图,其中每条边均有一个非负的容量值,记为.如果则可以规定.网络流中有两个特殊的顶点,即源点和汇点,源点可以提供无限的流量,而汇点可以接受无限的流量.
与网络流相关的一个概念是流.设为网络的源点,为汇点,那么的流是一个函数,满足以下性质:
容量限制:,满足;
反对称性:,满足;
流守恒性:,满足。
流的值定义为
而最大流就是值最大的流.
可以形象地理解为:源点是一个水库,汇点是一个用水量无限的用户,其它点是中转站,而边就是连接三者的水管.一个流就是一种合法的输水方案(每条水管运送水量在其容量之内,除了源点不会莫名其妙地出现水),它的值就是用户接受到的水量,而最大流就是用户接受水量最大的输水方案.
残留网络是由可以容纳更多流的边组成的,即如果中的一条边没有满流,那么它就是残留网络中的一条边.
一条增广路则是指中从到的一条路径.
0x01最大流算法:以Dinic算法为例
以下定义:
为边的边权
为连接的边的边权
为边的费用 为起点到点的距离
为源点
为汇点
Dinic算法大致步骤:
1.用BFS给图标号,每个点的标号为到源点的最近距离(只走剩余容量大于0的边).
2.若BFS无法到达汇点则算法结束.
3.DFS一遍找出一条从源点到汇点的增广路,DFS时只走标号比当前点大1的点.
4.若没有这样的路径则返回1,否则将此路计入答案,并更新反向边,重复3.
反向边的概念:
假设有这样一个图(边上的数字代表其容量)

现在有一条2到5的路径

对于路径上每一条边(u,v),建立一条(v,u),容量为经过(u,v)的流量的边(图中绿色边)

假设又有一条6到1的边

那么最终的图是这样的(每条边上:(容量,流量))

例子:

BFS标号

DFS

更新反向边

由于无路可走,再BFS并DFS

更新反向边

无路可走,退出
最终:

PS:每条边上(容量,流量)
Code(P3376)
CPP#define out(i,a,now) for(int i=a.be[now],to=a.v[i];i;i=a.ne[i],to=a.v[i])
#define fo(i,a,b) for(i=a;i<=b;i++)
struct adj_list
{
LL be[maxn],ne[maxm<<2],v[maxm<<2],w[maxm<<2],cnt;
void init()
{
qk(be); cnt=1;
}
void add(LL x,LL y,LL z)
{
v[++cnt]=y;
ne[cnt]=be[x];
w[cnt]=z;
be[x]=cnt;
}
}v;
LL bfs()//标号
{
queue<LL> q;
q.push(t);
qk(id);
id[t]=1;
while (!q.empty())
{
LL now=q.front();
q.pop();
if (now==s) break;
out(i,v,now)
if (v.w[i^1] && !id[to])
{
id[to]=id[now]+1;
q.push(to);
}
}
return id[s];//判断是否有可行流
}
LL dfs(LL now,LL flow)//寻找可行流
{
if (now==t) return flow;
LL miflow;
out(i,v,now)
{
LL wei=v.w[i];
if (wei && id[to]==id[now]-1)
{
miflow=dfs(to,min(flow,wei));
if (miflow)
{
v.w[i]-=miflow;
v.w[i^1]+=miflow;//增加反向边流量
return miflow;
}
}
}
return 0;
}
LL dinic()
{
LL res=0;
while (bfs())
{
LL temp;
while (temp=dfs(s,LLONG_MAX>>1))
res+=temp;
}
return res;
}
int main()
{
scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
LL i;
LL x,y,z;
v.init();
fo(i,1,m)
{
scanf("%lld%lld%lld",&x,&y,&z);
v.add(x,y,z);
v.add(y,x,0);
}
LL ans=dinic();
printf("%lld",ans);
}
0x02最小费用最大流算法
现在你需要为流经一条边的单位流量支付费用,其实只需将最大流算法中的DFS改为求最短路,找到一条从源点到汇点每条边费用之和最小的路并更新答案.
鉴于现在SPFA人人喊打,以下给出一种使用dijkstra算法的方法.
由于费用可能为负数,普通的dijkstra算法无法实现.因此我们考虑对原图做一次SPFA求出源点到每个点的最短路,之后我们为每个点赋点权为,并视每条连接的边的边权为,由于对于任意两点,有,所以,这样一来新图上的就等于(对于路径上除起点和终点以外的点,其入边的与出边的抵消),由于每次跑最短路时都是不变的,所以求出了也就求出了(,其实很显然)
但是跑完之后需要加入反向边,原来的可能会不适用,所以我们需要更新
对于最短路上每一条连接的边,显然有
从而
所以我们只要对于每个点将加上即可.
由于
h[S]又是一个常量(就是)
所以还是一开始我们所希望的,对于每个点将加上不会破坏每条边边权不为负的特性.
CPP#define out(i,a,now) for(int i=a.be[now],to=a.v[i];~i;i=a.ne[i],to=a.v[i])
#define fo(i,a,b) for(i=a;i<=b;i++)
struct adj_list
{
LL be[maxn],ne[maxm<<4],v[maxm<<4],w[maxm<<4],c[maxm<<4],cnt;
void init()
{
memset(be,-1,sizeof(be));
cnt=0;
}
void add(LL pre,LL nxt,LL wei,LL cost)
{
v[cnt]=nxt;
ne[cnt]=be[pre];
be[pre]=cnt;
w[cnt]=wei;
c[cnt]=cost;
++cnt;
}
};
struct NetWork
{
adj_list v;
LL dis[maxn],h[maxn],pre_node[maxn],pre_edge[maxn],inque[maxn];
void SPFA(LL be,LL en)
{
queue<LL> q;
qk(inque);
LL i;
fo(i,1,n) h[i]=LLONG_MAX>>1;
h[be]=0; inque[be]=1;
q.push(be);
while (!q.empty())
{
LL now=q.front();
q.pop();
out(i,v,now)
if (h[now]+v.c[i]<h[to] && v.w[i])
{
h[to]=h[now]+v.c[i];
if (!inque[to])
{
q.push(to);
inque[to]=1;
}
}
inque[now]=0;
}
}
void MCMF(LL s,LL t,LL &flow,LL &cost)
{
flow=cost=0;
SPFA(s,t);//用一次SPFA更新h数组
while (1)
{
pre_node[s]=pre_edge[s]=-1;//开始堆优化dijkstra算法
LL i;
fo(i,1,n) dis[i]=LLONG_MAX>>1;
dis[s]=0;
priority_queue<Pair,vector<Pair>,greater<Pair> > pq;
pq.push(mp(0,s));
while(!pq.empty())
{
Pair now=pq.top();
pq.pop();
if (now.first!=dis[now.second]) continue;
if (now.second==t) break;
out(i,v,now.second)
{
LL len=v.c[i]+h[now.second]-h[to];
if (v.w[i] && dis[now.second]+len<dis[to])
{
dis[to]=dis[now.second]+len;
pq.push(mp(dis[to],to));
pre_node[to]=now.second;//记录从哪个点来
pre_edge[to]=i;//记录从哪条边来
}
}
}
if (dis[t]>=(LLONG_MAX>>1)) break;
LL miflow=LLONG_MAX>>1;
for(i=t;i!=s;i=pre_node[i])
cmin(miflow,v.w[pre_edge[i]]);//找到路径上流量最小值
for(i=t;~i;i=pre_node[i])
{
v.w[pre_edge[i]]-=miflow;
v.w[pre_edge[i]^1]+=miflow;//更新反向边
}
flow+=miflow;
cost+=miflow*(h[t]+dis[t]);
fo(i,1,n) h[i]=min(h[i]+dis[i],LLONG_MAX>>1);//更新h数组
}
}
void sol()
{
v.init();
LL i,x,y,z,w;
scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
fo(i,1,m)
{
scanf("%lld%lld%lld%lld",&x,&y,&z,&w);
v.add(x,y,z,w);
v.add(y,x,0,-w);
}
LL flow,cost;
MCMF(s,t,flow,cost);
printf("%lld %lld",flow,cost);
}
}solver;
int main()
{
solver.sol();
return 0;
}
0x03最小割最大流定理
的一个割把点集分为两部分,其中源点S属于其中一部分,汇点T属于另外一部分.割是的边集的一个子集,每条边的两个端点分别属于不同的点集.割的容量定义为割的每条边的容量和,最小割就是容量最小的一个割.
例子:

黄色的边就是一个割
介绍一下网络流中最重要的定理之一------最小割最大流定理:
如果是中一个流,则以下条件是等价的:
1.是的一个最大流
2.残留网络不包含增广路
3.存在一个割,等于其容量
那么如何借此求出最小割呢?我们看可以先求出最大流,以S为起点在残留网络上DFS,搜索到的点就是包含S的一部分,剩余的则是另一部分,而最小割即是连接两部分的所有边.
0x04典型例题
1.飞行员配对方案问题(P2756)
一个二分图最大匹配问题,源点向所有外籍飞行员连容量为1的边,外籍飞行员向可以配对的英国飞行员连容量为1的边,英国飞行员向汇点连容量为1的边,网络最大流即是答案,若外籍飞行员连向英国飞行员的边满流,说明这一对被选中.
2.太空飞行计划问题(P2762)
一个最大权闭合图问题,详细说明可以见胡伯涛的《最小割模型在信息学竞赛中的应用》.
简要说一下方法:源点向实验连容量为利润的边,实验向所需器材连容量为无穷大的边,器材向汇点连容量为花费的边,所有实验利润之和减去网络最小割的容量即是答案,方案可以按照上文方法输出.
3.最小路径覆盖问题(P2764)
寻找最小路径条数等价于为尽可能多的点找到后继,于是就可以用一种类似于二分图匹配的方法求解.将点拆为,源点向所有连容量为1的边,所有向汇点连容量为1的边,若原图上有边,则向连容量为1的边.答案为总点数减去网络最大流(网络最大流就是找到了后继的点数),若向的边满流,说明路径上是的后继.
相关推荐
评论
共 59 条评论,欢迎与作者交流。
正在加载评论...