1. 程式人生 > 實用技巧 >網路流重製版:基於 Capacity Scaling 的弱多項式複雜度最小費用流演算法

網路流重製版:基於 Capacity Scaling 的弱多項式複雜度最小費用流演算法

目錄

前言

Ouuan Orz

當然,先說一下弱多項式是啥?

OI 界中叫做 Dinic 和 EK 的那兩個最大流演算法,把其中的 BFS 改成求最短路,複雜度都是與值域多項式相關的,即複雜度是偽多項式的。

多項式複雜度有弱多項式和強多項式兩種,弱多項式就是關於輸入長度( \(n\)\(m\) 之類的,以及 \(log 值域\))為多項式複雜度,強多項式就是在加減乘除為 \(O(1)\) 時複雜度關於資料規模為多項式(就是說跟值域完全無關,只和 \(n, m\) 之類的相關,複雜度關於 \(n, m\) 之類的為多項式)。

當然,這時\(Ouuan\)說的。

演算法講解

要求

  1. \(st\)無入邊,\(ed\)無出邊。
  2. 可以有負環。

無源匯最小費用流

對於有源匯最小費用最大流的定義我們改一下:

沒有\(st\)\(ed\),同時最小化\(\sum\limits_{(i,j)∈E}cost(i,j)*f(i,j)\)

當然,這個時候你可能會好奇這個我們講的有源匯最小費用最大流有個\(der\)的關係?

那如果我們從\(ed\)\(st\)連線一條無限小的邊,使得流量越多越好,然後後面減掉就行了。

做法

首先,這個流是最小費用當且僅當圖中不存在負環,證明和上篇重製版最小費用最大流部落格雷同,不予以贅述。

首先明白一個定理:如果把每條邊容量乘\(2\)

,則對應流量乘\(2\),最小費用乘\(2\),因為乘\(2\)一定不存在負環。

那麼接下來就非常簡單了,把邊權拆成二進位制,維護殘餘網路\(G\),一開始\(G\)中的容量和流量都為\(0\),然後二進位制從高到低掃描,每一位把所有邊的容量和流量乘\(2\),但是需要注意,有些邊這一位二進位制可能為\(1\),因此這條邊會加入到殘餘網路中,這就非常的蛋疼了,好的方法是這條邊是\((x,y)\),在加入前從\(y\)跑一遍最短路,如果\(d[x]+cost(x,y)<0\),那麼就不加入,並且把\(y\)\(x\)的最短路的流量全部減一,當然,如果這條邊原本就存在,則直接流量\(+1\)即可。

至於為什麼直接跑最短路即可,因為我們維護的殘餘網路中一定沒有負環啊。

時間複雜度:\(O(nm^2\log{U})\)\(U\)是邊中的最大流量。

例題:https://uoj.ac/problem/487

#include<cstdio>
#include<cstring>
#define  N  5100
#define  M  110000
using  namespace  std;
typedef  long  long  LL;
template<class  T>
inline  T  mymin(T  x,T  y){return  x<y?x:y;}
template<class  T>
inline  T  mymax(T  x,T  y){return  x>y?x:y;}
int  n,m;
struct  node
{
	int  x,y,next;
	LL  c/*表示它們現在現有的流量*/,d;
}a[M];int  len=1,last[N];
LL  cap[N];//表示它們原本的流量 
inline  void  ins_node(int  x,int  y,LL  c,LL  d){len++;a[len].x=x;a[len].y=y;a[len].d=d;cap[len]=c;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);}
//SPFA
LL  d[N];
int  list[N],head,tail,pre[N];
bool  v[N],vv[N];
void  spfa(int  st,int  ed)
{
	list[head=1]=st;tail=2;
	memset(d,20,sizeof(d));d[st]=0;
	memset(pre,0,sizeof(pre));
	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(a[k].c>0  &&  d[y]>d[x]+a[k].d)
			{
				d[y]=d[x]+a[k].d;
				pre[y]=k;
				if(!v[y])
				{
					v[y]=1;
					list[tail++]=y;
					if(tail==n+1)tail=1;
				}
			}
		}
	}
}
LL  cost=0,ans=0,ffuck=0;
void  trash(int  st,int  ed)
{
	LL  mmax=0/*,sum=0用來記錄st-ed新增的那一條邊應該是多少*/,summ=0;
	for(int  i=2;i<=len;i++)
	{
//		if(a[i].d>0)sum+=a[i].d;
		summ+=cap[i];
		mmax=mymax(mmax,cap[i]);
	}
	ins(ed,st,summ,-(LL)999999999);
	mmax=mymax(mmax,cap[len-1]);
	int  l=1;
	while(((LL)1<<l)-1<mmax)l++;
	
	for(int  ll=l;ll>=1;ll--)//從高位到低位開始掃描二進位制 
	{
		cost<<=1;
		LL  shit=((LL)1<<(ll-1));
		memset(vv,0,sizeof(vv));
		for(int  i=2;i<=len;i++)
		{
			a[i].c<<=1;
			if(a[i].c)a[i].c+=(cap[i]&shit)>0,vv[i]=1;
		}
		//對於所有已經存在的邊不用掃描 
		for(int  i=2;i<=len;i++)
		{
			if(vv[i])continue;
			if(cap[i]&shit)//反向邊絕對不會進來 
			{
				int  x=a[i].x,y=a[i].y;
				spfa(y,x);
				if(d[x]+a[i].d<0/*負環!!!!*/)
				{
					cost+=a[i].d;
					x=pre[x];
					while(x)
					{
						cost+=a[x].d;
						a[x].c--;a[x^1].c++;
						x=pre[a[x].x];
					}
					a[i^1].c++;
				}
				else  a[i].c++;
			}
		}
	}
	for(int  k=last[st];k;k=a[k].next)
	{
		if(!(k&1))ans+=cap[k]-a[k].c;
	}
	printf("%lld %lld\n",ans,cost-(cap[len-1]-a[len-1].c)*a[len-1].d+ffuck);
}
int  main()
{
	int  st,ed;
	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);
		if(x==y)
		{
			if(d<0)ffuck+=c*d;//自環 
		}
		else  ins(x,y,c,d);
	}
	trash(st,ed);
	return  0;
}

細節

無限小?

邊權?

總結

放心,因為比賽一般都是構圖題,難以卡掉\(ZKW\)\(EK\),所以,大膽的,放心的用\(ZKW\)吧,學這個演算法就圖一樂。

參考資料

洛谷的討論

弱多項式複雜度演算法非常非常好的常考資料,真的非常非常好

要準備NOIP啦,賽後補充Dij的做法,還有億點細節補充。

程式碼也要補點註釋。

賽前還是直接打個簡單思路就去準備比賽了。