网络流重制版:最小费用最大流以及其正确性,还有各种算法的个人SB分析
费用流的定义
有没有考虑过,如果一条边还有费用呢???
就像带权二分图匹配那样子。
给出定义,\(cost(i,j)\)为这条弧的花费。
那么不仅要在最大化流量的同时(优先级最高),最小化\(cost(i,j)*f(i,j)\)。
可以发现,如果图外面存在一个负环,那么这个负环会有流量,且会影响答案。
请注意:最小费用流没有严格要求流最大,所以本篇文章讲的是最小费用最大流。
大概做法
首先,依旧是找增广路,但是呢,挑选依据不同了,改成了以最小费用的路径为挑选依据了(可以证明这样的挑选方法是\(100\)%亿是正确的,可以跑到最小费用)。
同时呢,反向边的定义也要改一下了,既然你经过反向边的时候流量会被消除,那么费用是不是也要取负?
当然,这样会丢失层数一个非常重要的性质,就是如果一条路径经过另外一条路径的反向边,他们交换,使得互相不干扰,并不会改变长度和,但是如果是层数,交换少了两条边,会少\(2\)。
所以,正反向边的费用和为\(0\),因此,费用流随随便便就会出现\(0\)环的情况。
事实上,一般情况下,网络流的建图要求刚开始的时候不存在负环(可以证明这种情况在后面增广的时候也同样不存在负环)。
当然,不用担心,这些证明在后面都会补上的。
算法讲解
讲到这里,你应该默认每次最小费用就是对的了(证明往后翻)。
MCMF算法
非常的简单粗暴,直接用\(SPFA\)增广就行了(\(Dijkstra\)不行,因为中间可能存在负环)。
时间复杂度:\(O(nmf)\)(\(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费用流
这个有个非常有意思的故事:相传是\(ZKW\)神在赛场上遇到费用流的题目脑补了这个算法,但是怕错没打,后来出来实现了一下发现可以!!!
具体你会发现\(MCMF\)其实其最短路是非严格递增的,所以可以一次性直接把相同长度的最短路一次性跑完,简单来说就是像\(Dinic\)和\(EK\)一样,然后码一下即可。
需要注意的是用\(v\)数组保存一下这个点有没有被走过。
- 因为最短路中如果存在\(0\)环不用\(v\)数组记录这个点在不在路径上可能会陷入死循环。
- 因为不存在负环,所以在负环上浪费时间是非常没有意义的。
时间复杂度依旧是丑陋的\(O(nmf)\)。
当然,我的\(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(a[a[k].other].c>0/*由于是倒着搜的,所以边也要反向边*/ && (a[a[k].other].k+d[x]<d[a[k].y] || d[a[k].y]==-1))/*判断边是否可行并更新*/
{
d[a[k].y]=a[a[k].other].k+d[x];/*更新*/
int y=a[k].y;
if(v[y]==false)
{
v[y]=true;
list[tail]=y;
tail++;
if(tail==n+1)tail=1;
}
}
}
head++;
if(head==n+1)head=1;
v[x]=false;
}
return d[st]!=-1;/*返回bool值*/
}
inline ll mymin(ll x,ll y){return x<y?x:y;}/*找最小值*/
long long find(int x,ll f)
{
v[x]=true;
if(x==ed){v[x]=false;return f;}
ll ans=0,t=0;
for(int k=last[x];k;k=a[k].next)
{
int y=a[k].y;
if(v[y]==false/*这个点没走过才可以走,否则更新边的流量是会Balabala*/ && a[k].c>0 && d[x]-a[k].k==d[y]/*类似分层的操作*/ && ans<f)
{
ans+=t=find(y,mymin(a[k].c,f-ans));/*是不是很眼熟?*/
a[k].c-=t;a[a[k].other].c+=t;cost+=t*a[k].k;
}
}
if(ans==f)v[x]=false;//这个地方一定要加这个优化,原因和最大流类似
return ans;
}
int main()
{
scanf("%d%d%d%d",&n,&m,&st,&ed);
for(int i=1;i<=m;i++)
{
int x,y;
ll z,l;
scanf("%d%d%lld%lld",&x,&y,&z,&l);
ins(x,y,z,l);
}
ll zans=0;
while(spfa()==true)/*建图完成!*/
{
zans+=find(st,ll(999999999999999));/*多次查找,找出所有增光路哦*/
}
printf("%lld %lld",zans,cost);
return 0;
}
原始对偶算法
这个名字真的神奇,不用管这么高大上的算法。
实际上就是用某种神奇的方法,使得\(Dijkstra\)可以在费用流上跑。
但是不要期望其可以在普通的最短路上用,因为其先决条件是跑一遍其余的最短路。。。
首先,对于每个点,我们给其标一个势能\(p\),然后把边\((i,j)\)的\(Cost\)设定为\(p(i)+cost(i,j)-p(j)\),同时图中用\(Cost\)作为费用,而一条\(st\)到\(ed\)最短路就是原本的最短路减去\(p(ed)\)。
具体可以看一下证明,对于一条路径,我们将其点标号为\(1,2,3,...,k\)。
那么就是\(p(1)+cost(1,2)-p(2)+p(2)+cost(2,3)-p(3)+...-p(k)=d[ed]+p(ed)\)。
于是就有神犇发现了,\(p\)数组变成\(d\)数组就可以满足新添加的边都是非负的。
因为有以下性质:
对于边\((i,j)\),\(dis[i]+cost(i,j)≥dis[j]\),即\(dis[i]+cost(i,j)-dis[j]≥0\)。
但是关键是,在增广完之后,不是会有些边增广掉了,导致这些边的\(dis\)在下次改变了,那岂不是每次跑完增广,就要重新\(SPFA\)?
不,其实这次增广完之后,直接用增广前的\(dis\)作势能就行了。
为什么?
首先,这条边\((i,j)\)能被走仅当\(dis[i]+cost(i,j)=dis[j]\),所以如果这条边被增广,那么新的\((j,i)\)的费用为:\(dis[j]-cost(i,j)=dis[i]\),所以就是\(0\),而如果没有被走过,显然成立。
时间复杂度:\(O(mlognf)\)
#include<cstdio>
#include<cstring>
#include<queue>
#define N 5100
#define M 110000
using namespace std;
typedef long long LL;
typedef pair<LL,int> PII;
template<class T>
inline T mymin(T x,T y){return x<y?x:y;}
int n,m;
struct node
{
int y,next;
LL c,d;
}a[M];int len=1,last[N];
inline void ins_node(int x,int y,LL c,LL d){len++;a[len].y=y;a[len].c=c;a[len].d=d;a[len].next=last[x];last[x]=len;}
inline void ins(int x,int y,LL c,LL d){ins_node(x,y,c,d);ins_node(y,x,0,-d);}
int st,ed;
//dij
LL d[N],p[N]/*势能函数*/;
bool v[N];//是否访问过
priority_queue<PII,vector<PII>,greater<PII> >fuck;
inline bool DIJ()
{
while(!fuck.empty())fuck.pop();
memset(d,20,sizeof(d));d[st]=0;
memset(v,0,sizeof(v));
fuck.push(make_pair(0,st));
while(!fuck.empty())
{
PII id=fuck.top();fuck.pop();
if(v[id.second])continue;
v[id.second]=1;
int x=id.second;
for(int k=last[x];k;k=a[k].next)
{
int y=a[k].y;
if(!v[y] && d[y]>d[x]+a[k].d+p[x]-p[y] && a[k].c>0)
{
d[y]=d[x]+a[k].d+p[x]-p[y];
fuck.push(make_pair(d[y],y));
}
}
}
return v[ed];
}
int list[N],head,tail;
bool spfa()
{
memset(d,20,sizeof(d));d[st]=0;
list[head=1]=st;tail=2;
memset(v,0,sizeof(v));v[st]=1;
while(head!=tail)
{
int x=list[head++];if(head==n+1)head=1;
v[x]=0;
for(int k=last[x];k;k=a[k].next)
{
int y=a[k].y;
if(d[y]>d[x]+a[k].d && a[k].c>0)
{
d[y]=d[x]+a[k].d;
if(!v[y])
{
list[tail++]=y;if(tail==n+1)tail=1;
v[y]=1;
}
}
}
}
return d[ed]!=d[0];
}
LL cost=0;
LL dfs(int x,LL f)
{
if(x==ed)return f;
v[x]=1;
LL s=0,t;
for(int k=last[x];k;k=a[k].next)
{
int y=a[k].y;
if(a[k].c && d[y]==d[x]+a[k].d+p[x]-p[y] && !v[y])
{
s+=t=dfs(y,mymin(f-s,a[k].c));
a[k].c-=t;a[k^1].c+=t;
cost+=a[k].d*t;
if(s==f){v[x]=0;return s;}
}
}
return s;
}
int main()
{
scanf("%d%d%d%d",&n,&m,&st,&ed);
for(int i=1;i<=m;i++)
{
int x,y;LL c,d;
scanf("%d%d%lld%lld",&x,&y,&c,&d);
ins(x,y,c,d);
}
LL ans=0;
if(spfa())
{
do
{
memset(v,0,sizeof(v));
ans+=dfs(st,LL(99999999999999));
for(int i=1;i<=n;i++)p[i]+=d[i];
}while(DIJ());
}
printf("%lld %lld\n",ans,cost);
return 0;
}
细节、性质以及证明
s<f优化
可以发现,对于\(v[x]\),如果\(s≠f\)其是不会还原变回\(0\)的,这个优化在一般性质证明中默认没有,因为这个优化可能会影响性质的正确性。
为什么需要这个优化?
理由和\(Dinic\)一个道理,类似的数据就可以卡死。
当流量为i时,且图中不存在负环,那么此时是流量为i时最小费用
必要性:很明显,非常的明显,因为负环的话直接在这个环上增加\(1\),绝对不会改变\(\sum\limits_{(st,i)∈E}f(st,i)\)。
充分性:如果存在一个更小费用的\(f_{i}'\),用\(f_{i}'-f_{i}\)得到一个网络(对于一条边\((i,j)∈E\),这条边在网络中的容量为左边的\(f\)减去右边的\(f\),如果小于\(0\),则方向取反且取负,其他边不允考虑,基本上在证明的过程中都是不怎么考虑除\(E\)以外的边的),这个网络是\(G_{f_{i}}\)的一个残余网络,这个网络肯定存在负环,因为\(cost_{f_{i}'}<cost_{f_{i}}\)。
关于最短路增广正确性证明
注意这里使用的是\(MCMF\)。
设第\(i-1\)次增广后的流为\(f_1\),然后第\(i\)次增广为\(f_2\),但是存在一个流\(|f_{3}|=|f_{2}|,cost_{f_3}<cost_{f_2}\)。
这个时候,我们用上篇网络流类似的证明方法:
尝试用归纳法证明,在\(f_{1}\)绝对是最小费用的情况下:
\(f_2-f_1\)得到一个网络,这个网络是残余网络\(G_{f_{1}}\)的子图,这个网络中只存在一条\(st\)到\(ed\)的增广路径\(p_1\),现在用类似的方法,\(f_3-f_1\),得到的图便是一条增广路径\(p_2\)和一坨圈,因为\(p_2\)的费用≥\(p_1\)的费用,所以这些环中一定有负环,所以\(f_{1}\)绝对不是最小费用流,矛盾,证毕。(很明显原图的\(f\),流量为\(0\),\(cost=0\),所以一定是最小费用流)
增广过程中不存在负环
由上面的证明就可以得到。
当然,非要特别特别特别严谨的证明,还有一种方法,就是考虑负环的生成条件,然后考虑最后一条经过这个负环的增广路,然后证明这条增广路存在更短的情况即可。
关于最短路非严格单调递增
这个其实有个非常简单的方法:
跟上次证明\(h\)数组一个套路,同样开始证明\(d\)数组。
这两玩意有个非常相同的地方,对于\(d[x]\),如果其实从\(d[y]\)继承来的,那么\(st\)到\(y\)的最短路其实在\(st\)到\(x\)的最短路的上,而\(h\)也是。
所以,考虑第\(i\)次增广时,\(d\)数组变成了\(d'\)数组,然后设\(x\)为第一个\(d'[x]<d[x]\)的位置,这里的第一个指的是在增广后的图中对于\(st\)到\(x\)的最短路中,存在一条路径上的所有的点\(i\)都满足\(d[i]'≥d[i]\)。
考虑这条最短路为\(st⇝y→x\)。
如果\((y,x)\)这条边增广前就存在,那么成立。
如果\((y,x)\)这条边是在增广时增加的,那么:
\((x,y)\)是上次的增广路,设边费用为\(k\),有:
\(d[x]+k=d[y],d[x']-(-k)=d[y]'≥d[y]≥d[x]+k\)。
矛盾,所以不成立。
所以\(MCMF\)算法是可以非严格递增的,记得有道省选题就用到了这个性质。
ZKW只要在合法网络中的路径都会被增广
首先,把\(s<f\)优化去掉,这个优化可能影响正确性。
什么叫合法网络?
对于\((i,j)\),如果\(dis[i]+cost(i,j)=dis[j]\),那么这条边计入合法网络。
当然,如果在讨论中其\(f-c=0\),那么这条边认为是无意义的,不予讨论。
不难发现,一条边和其反向弧都在这个网络当中,所以这个网络应当在把\(0\)环缩成一个点时,其应该是个有向无环图。
引理:任何一种增广方案都一定可以转化成一种在合法网络中就能增广的、互不干扰(即不走互相反向边)的方案。
解释一下,什么叫在合法网络中就能增广,即在增广一条边时不在其对应的反向边加上对应的流量,也能完整的把这个流量跑完。
实际上这两个形容词描述的是同个东西。(应该)
证明也非常简单,把这个合法网络看成一个普通的网络(正向弧就是这个网络中有意义的边),然后跑出容量网络,根据上次最大流的一个性质分解路径即可。
说完了引理,讲讲这个的证明:
假如合法网络\(DFS\)之后还剩增广路径\(p\),如果其走过了别的增广路径的反向边,则交换,最终对应在原图上。
对于增广路径\(p\),如果其在被\(DFS\)访问之前,这条路径就被一条路径\(q\)经过了其的边(这里的经过必须满流才叫经过,因为只有满流才有影响)了,那么,我们就通过反向边,改变一下\(p\)即可,但是怎么证明改变后的\(p\)依旧满足能被访问到?步骤如下:
- 对于路径\(p\),拆为路径:\(st->x->...->ed\),如果\(x\)没有作为\(st\)的后继点访问过,则如果\(q\)经过的边肯定不是\(st->x\)(因为没有负环),则路径\(p\)依旧满足没有被访问过(且因为流量大于\(0\),未来会被访问到)。
- 如果\(x\)被访问到了,那么其在\(DFS\)栈中,那么设路径中其后继点为\(y\),把路径改为:\(st->y->...->ed\)(认为\(->x->\)是一条边)。
证毕。
当然,通过反向边改变一下路径这句话非常的神奇。
注意:\(p\)是通过反向边交换回来的,那么肯定也有方法换回去:
因为我们其实就是重新模拟一遍\(DFS\)罢了。
关于ZKW最短路严格递增性质
\(ZKW\)最神奇的事情是什么?
反向边可以在这一次就投入作用,\(dis[x]+cost(x,y)=dis[y]\),\(dis[y]+(-cost(x,y))=dis[x]\),可以发现,这两个是等价的,所以说,\(ZKW\)的反向边可以在\(DFS\)的时候就直接投入战斗。
首先,对于一条增广路\(p\),第\(i\)次增广后,\(p\)是同样的长度但没有被增广,如果\(p\)走了第\(i\)次\(DFS\)的增广路径的反向边,那么交换。
设路径上\(x\)到\(st\)的长度为\(d[x]\),设原图中的最短路为\(d[x]'\),如果\(d[x]>d[x]'\),那么可以把原图中到\(x\)的最短路和增广路中\(x\)到\(ed\)的路径合在一起,如果存在环,直接去掉,费用绝对会更加优秀,这与前面的性质相矛盾。
所以这条路径在合法网络中。危
证毕。
参考资料
def mcmf_worst_instance(k):
inf = 5 * 2 ** k // 4
print("%d %d" % (2 * k + 2, k * (k + 1) + 1))
for i in range(k):
print("%d %d %d %d" % (1, i + 2, [1, 3][i] if i < 2 else 5 << (i - 2), 0))
print("%d %d %d %d" % (2, k + 2, inf, 0))
for i in range(k):
for j in range(k):
if i == j: continue
print("%d %d %d %d" % (i + 2, k + j + 2, inf, (1 << max(i, j)) - 1))
for i in range(k):
print("%d %d %d %d" % (i + 2 + k, 2 * k + 2, 2 if i < 2 else 5 << (i - 2), 0))
mcmf_worst_instance(17) # |V| = 36
网络单纯形的参考资料,但是听说还是指数级的复杂度,没什么用,还挺难懂的
坑
注意:这是给我自己看的,读者应该看不懂,看懂且有证明的私信我。
证明s<f优化的正确性或者反例
伪证
如果一个点的s<f,那么直接把这个点封起来不会有任何问题,但是,如果s<f不封起来,是有可能下次增广的时候到s继续得到增广路径的,具体就是A型图,且如果不加入这个优化的话时间复杂度可以卡到2^n次幂 H
ZKW的每个环节跑到的费用相同
从终点跑和起点跑的时间不同之处(从ed开始跑最短路只要存在0环即可,如果一开始不存在,那么后面也可以存在(共享0环))
原始对偶同样可能再构造的时候存在0环,不管是天然就有的,还是后来居上的,卡法(即不加\(s<f\)优化的卡法)。
弧优化