文章目录
费用流的定义
有没有考虑过,如果一条边还有费用呢???
就像带权二分图匹配那样子。
给出定义, c o s t ( i , j ) cost(i,j) cost(i,j)为这条弧的花费。
那么不仅要在最大化流量的同时(优先级最高),最小化 c o s t ( i , j ) ∗ f ( i , j ) cost(i,j)*f(i,j) cost(i,j)∗f(i,j)。
可以发现,如果图外面存在一个负环,那么这个负环会有流量,且会影响答案。
请注意:最小费用流没有严格要求流最大,所以本篇文章讲的是最小费用最大流。
大概做法
首先,依旧是找增广路,但是呢,挑选依据不同了,改成了以最小费用的路径为挑选依据了(可以证明这样的挑选方法是 100 100 100%亿是正确的,可以跑到最小费用)。
同时呢,反向边的定义也要改一下了,既然你经过反向边的时候流量会被消除,那么费用是不是也要取负?
当然,这样会丢失层数一个非常重要的性质,就是如果一条路径经过另外一条路径的反向边,他们交换,使得互相不干扰,并不会改变长度和,但是如果是层数,交换少了两条边,会少 2 2 2。
所以,正反向边的费用和为 0 0 0,因此,费用流随随便便就会出现 0 0 0环的情况。
事实上,一般情况下,网络流的建图要求刚开始的时候不存在负环(可以证明这种情况在后面增广的时候也同样不存在负环)。
当然,不用担心,这些证明在后面都会补上的。
算法讲解
讲到这里,你应该默认每次最小费用就是对的了(证明往后翻)。
MCMF算法
非常的简单粗暴,直接用 S P F A SPFA SPFA增广就行了( D i j k s t r a Dijkstra Dijkstra不行,因为中间可能存在负环)。
时间复杂度: O ( n m f ) O(nmf) O(nmf)( f f f为流量)
#include<cstdio>
#include<cstring>
#define N 5100
#define M 1100000
using namespace std;
typedef long long ll;
struct node
{
int y,next,other;ll c,k;
}a[M];int last[N],len,n,m,st,ed;
int qian[N],b[N],list[N],head=1,tail=2;
ll flow[N],dis[N];
bool v[N];
ll zans=0,cost=0;
inline ll mymin(ll x,ll y){
return x<y?x:y;}
inline void ins(int x,int y,ll c,ll k)
{
len++;
a[len].y=y;a[len].c=c;a[len].k=k;
a[len].next=last[x];last[x]=len;
len++;
a[len].y=x;a[len].c=0;a[len].k=-k;
a[len].next=last[y];last[y]=len;
a[len].other=len-1;
a[len-1].other=len;
}
inline bool spfa()
{
memset(v,false,sizeof(v));v[st]=true;
head=1;tail=2;list[1]=st;
memset(dis,63,sizeof(dis));dis[st]=0;
b[ed]=-1;
while(head!=tail)
{
int x=list[head];
for(register int k=last[x];k;k=a[k].next)
{
int y=a[k].y;
if(a[k].c>0 && dis[x]+a[k].k<dis[y])
{
dis[y]=dis[x]+a[k].k;
flow[y]=mymin(a[k].c,flow[x]);
qian[y]=x;b[y]=k;
if(v[y]==false)
{
v[y]=true;
if(dis[list[head+1]]>dis[y])
{
int lpl=head-1;
if(lpl==0)lpl=n;
list[lpl]=list[head];
list[head]=y;head=lpl;
}
else
{
list[tail]=y;
tail++;
if(tail==n+1)tail=1;
}
}
}
}
head++;
if(head==n+1)head=1;
v[x]=false;
}
if(b[ed]!=-1)//找到增广路径
{
int y=ed,root=0;
while(y!=st)
{
root=b[y];y=qian[y];
a[root].c-=flow[ed];
a[a[root].other].c+=flow[ed];
}
zans+=flow[ed];
cost+=flow[ed]*dis[ed];
}
return b[ed]!=-1;
}
int main()
{
scanf("%d%d%d%d",&n,&m,&st,&ed);
for(register int i=1;i<=m;i++)
{
int x,y;
ll z,k;
scanf("%d%d%lld%lld",&x,&y,&z,&k);
ins(x,y,z,k);
}
flow[st]=ll(999999999999999);
while(spfa()==true);
printf("%lld %lld",zans,cost);
return 0;
}
ZKW费用流
这个有个非常有意思的故事:相传是 Z K W ZKW ZKW神在赛场上遇到费用流的题目脑补了这个算法,但是怕错没打,后来出来实现了一下发现可以!!!
具体你会发现 M C M F MCMF MCMF其实其最短路是非严格递增的,所以可以一次性直接把相同长度的最短路一次性跑完,简单来说就是像 D i n i c Dinic Dinic和 E K EK EK一样,然后码一下即可。
需要注意的是用 v v v数组保存一下这个点有没有被走过。
- 因为最短路中如果存在 0 0 0环不用 v v v数组记录这个点在不在路径上可能会陷入死循环。
- 因为不存在负环,所以在负环上浪费时间是非常没有意义的。
时间复杂度依旧是丑陋的 O ( n m f ) O(nmf) O(nmf)。
当然,我的 Z K W ZKW ZKW的写法和常人不同。
#include<cstdio>
#include<cstring>
using namespace std;
typedef long long ll;
struct node
{
int y,next,other;
ll c,k;
}a[201000];int last[5100],len;
long long d[5100];
bool v[5100];
int n,m,st,ed;
ll cost=0;
inline void ins(int x,int y,ll c,ll k)
{
len++;
a[len].y=y;a[len].c=c;a[len].k=k;
a[len].next=last[x];last[x]=len;
len++;
a[len].y=x;a[len].c=0;a[len].k=-k;
a[len].next=last[y];last[y]=len;
a[len-1].other=len;
a[len].other=len-1;
}
int list[5100],head,tail;/*队列*/
inline bool spfa()
{
memset(v,false,sizeof(v));v[ed]=true;/*判断是否进入队列*/
memset(d,-1,sizeof(d));d[ed]=0;/*从终点到这里要多少费用*/
head=1;tail=2;list[head]=ed;/*从终点出发*/
while(head!=tail)
{
int x=list[head];
for(int k=last[x];k;k=a[k].next)
{
if<