【网络流】费用流(基于Capacity Scaling)
目录
简介
原理
代码
引用资料
简介
费用流问题就是要求在所有最大流之中,找到费用最大/最小的问题。
下面重点讨论最小费用最大流。
原理
先给出大概的做法:
在残留网络上沿着最短路(边权即费用)增广,直到得到最大流(无法再增广),那么,假如图中没有负圈,这样的最大流的费用是最小的。
下面证明正确性:
假如存在具有相同流量比 费用更小的流 ,考察 ,可知它由若干个圈组成,因为 的费用为负,故在这些圈中至少存在一个负圈。
也就是说, 是最小费用流 残留网络中不存在负圈
基于上面的结论,可以利用归纳法证明上述做法的正确性。
- 对于流量为 的流 ,残留网络即为原图,故只要不存在负圈,那么 就是最小费用流。
- 下证:若流量为 的流 为最小费用流,则其通过 到 的最短路增广得到的 是流量为 的最小费用流。
假设存在 满足 ,而 对应的路径又是 的最短路,所以 对应的路径一定存在负圈,与 为最小费用流矛盾。
那么,如果图中存在负圈,如何解决呢?
此时我们需要对问题进行转化,从 连一条容量为 ,费用为 的虚边得到无源汇流。注意到如果存在增广路,则一定可以通过这条路径以及虚边使得费用更小,反之,如果不存在增广路,则在原图相应地得到了最小费用最大流。
下面只需求无源汇流的最小费用。
我们采取 capacity scaling 解决这个问题:
它利用了流本身的性质,实现减少增广的次数的目的。
这个性质是:将原图中每条边的容量乘二后,最小费用流每条边的流量分别乘二。
因此,在求出 为边容量的最小费用流 后,将 对应的边容量乘二,然后对每条二进制中相应位为 的边进行容量加一即可得到 ,最后我们即可得到相应边 容量为 对应的最小费用流。
复杂度:如果采用 SPFA ,那么最坏复杂度是 。( 为边的最大容量)
使用 Dijkstra 的复杂度可能会优秀一点,但是编写难度较大,可以看看下面引用资料 1,介绍较为详细。
代码
#include<bits/stdc++.h>
using namespace std;
inline int read()
{
int x=0,y=1;char c=getchar();
while (c<'0'||c>'9') {if (c=='-') y=-1;c=getchar();}
while (c>='0'&&c<='9') x=x*10+c-'0',c=getchar();
return x*y;
}
const int N=205, M=10005<<1, INF=0x3f3f3f3f;
int n, m, S, T;
struct node{
int to, c, rc, w, next; // c 表示容量(进行 capacity scaling ),rc 表示原图容量,w 表示费用。
}e[M];
int h[N], tot;
void add(int u, int v, int rc, int w){
e[tot].to=v, e[tot].rc=rc, e[tot].w=w, e[tot].next=h[u], h[u]=tot++;
e[tot].to=u, e[tot].rc=0, e[tot].w=-w, e[tot].next=h[v], h[v]=tot++;
}
int d[N], pre[N];
bool vis[N];
int q[N];
void spfa(int s){
memset(vis, false, sizeof vis);
memset(d, 0x3f, sizeof d);
memset(pre, -1, sizeof pre);
int tt=0, hh=0;
d[s]=0, vis[s]=true, q[tt++]=s;
while(tt!=hh){
int hd=q[hh++]; if(hh==N) hh=0;
vis[hd]=false;
for(int i=h[hd]; ~i; i=e[i].next){
int go=e[i].to;
if(e[i].c && d[go]>d[hd]+e[i].w){
d[go]=d[hd]+e[i].w;
pre[go]=i;
if(!vis[go]){
vis[go]=true;
q[tt++]=go; if(tt==N) tt=0;
}
}
}
}
}
void add_one_cap(int id){
// 优化,有流量的话,不可能关于这条边还存在负圈,直接更新后 return。
if(e[id].c){
e[id].c++;
return;
}
int u=e[id^1].to, v=e[id].to; // from and to
spfa(v);
if(d[u]<INF && d[u]+e[id].w<0){
e[id^1].c++;
int x=u;
while(x!=v){
int t=pre[x];
e[t].c--, e[t^1].c++;
x=e[t^1].to;
}
}else e[id].c++;
}
int main(){
memset(h, -1, sizeof h);
n=read(), m=read(), S=read(), T=read();
while(m--){
int u, v, rc, w; u=read(), v=read(), rc=read(), w=read();
add(u, v, rc, w);
}
add(T, S, INF, -INF);
for(int i=10; i>=0; i--){
for(int j=0; j<tot; j++) e[j].c<<=1;
for(int j=0; j<tot; j+=2) if(e[j].rc>>i&1) add_one_cap(j); // 传入边的编号 id,进行 +1 容量操作。
}
int cost=0;
for(int i=0; i<tot-2; i+=2) cost+=e[i].w*e[i^1].c;
cout<<e[tot-1].c<<' '<<cost<<endl;
return 0;
}
当然,如果题目中不存在负圈,那么直接使用 SPFA 即可。
```cpp
#include<bits/stdc++.h>
using namespace std;
inline int read()
{
int x=0,y=1;char c=getchar();
while (c<'0'||c>'9') {if (c=='-') y=-1;c=getchar();}
while (c>='0'&&c<='9') x=x*10+c-'0',c=getchar();
return x*y;
}
const int N=205, M=10005<<1, INF=0x3f3f3f3f;
int n, m, S, T;
struct node{
int to, c, rc, w, next; // c 表示容量(进行 capacity scaling ),rc 表示原图容量,w 表示费用。
}e[M];
int h[N], tot;
void add(int u, int v, int rc, int w){
e[tot].to=v, e[tot].rc=rc, e[tot].w=w, e[tot].next=h[u], h[u]=tot++;
e[tot].to=u, e[tot].rc=0, e[tot].w=-w, e[tot].next=h[v], h[v]=tot++;
}
int d[N], pre[N];
bool vis[N];
int q[N];
void spfa(int s){
memset(vis, false, sizeof vis);
memset(d, 0x3f, sizeof d);
memset(pre, -1, sizeof pre);
int tt=0, hh=0;
d[s]=0, vis[s]=true, q[tt++]=s;
while(tt!=hh){
int hd=q[hh++]; if(hh==N) hh=0;
vis[hd]=false;
for(int i=h[hd]; ~i; i=e[i].next){
int go=e[i].to;
if(e[i].c && d[go]>d[hd]+e[i].w){
d[go]=d[hd]+e[i].w;
pre[go]=i;
if(!vis[go]){
vis[go]=true;
q[tt++]=go; if(tt==N) tt=0;
}
}
}
}
}
void add_one_cap(int id){
// 优化,有流量的话,不可能关于这条边还存在负圈,直接更新后 return。
if(e[id].c){
e[id].c++;
return;
}
int u=e[id^1].to, v=e[id].to; // from and to
spfa(v);
if(d[u]<INF && d[u]+e[id].w<0){
e[id^1].c++;
int x=u;
while(x!=v){
int t=pre[x];
e[t].c--, e[t^1].c++;
x=e[t^1].to;
}
}else e[id].c++;
}
int main(){
memset(h, -1, sizeof h);
n=read(), m=read(), S=read(), T=read();
while(m--){
int u, v, rc, w; u=read(), v=read(), rc=read(), w=read();
add(u, v, rc, w);
}
add(T, S, INF, -INF);
for(int i=32; i>=0; i--){ // 取决于 logU 的大小
for(int j=0; j<tot; j++) e[j].c<<=1;
for(int j=0; j<tot; j+=2) if(e[j].rc>>i&1) add_one_cap(j); // 传入边的编号 id,进行 +1 容量操作。
}
int cost=0;
for(int i=0; i<tot-2; i+=2) cost+=e[i].w*e[i^1].c;
cout<<e[tot-1].c<<' '<<cost<<endl;
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效