網路流重製版:基於 Capacity Scaling 的弱多項式複雜度最小費用流演算法
前言
Ouuan Orz
當然,先說一下弱多項式是啥?
OI 界中叫做 Dinic 和 EK 的那兩個最大流演算法,把其中的 BFS 改成求最短路,複雜度都是與值域多項式相關的,即複雜度是偽多項式的。
多項式複雜度有弱多項式和強多項式兩種,弱多項式就是關於輸入長度( \(n\)、 \(m\) 之類的,以及 \(log 值域\))為多項式複雜度,強多項式就是在加減乘除為 \(O(1)\) 時複雜度關於資料規模為多項式(就是說跟值域完全無關,只和 \(n, m\) 之類的相關,複雜度關於 \(n, m\) 之類的為多項式)。
當然,這時\(Ouuan\)說的。
演算法講解
要求
- \(st\)無入邊,\(ed\)無出邊。
- 可以有負環。
無源匯最小費用流
對於有源匯最小費用最大流的定義我們改一下:
沒有\(st\)和\(ed\),同時最小化\(\sum\limits_{(i,j)∈E}cost(i,j)*f(i,j)\)。
當然,這個時候你可能會好奇這個我們講的有源匯最小費用最大流有個\(der\)的關係?
那如果我們從\(ed\)向\(st\)連線一條無限小的邊,使得流量越多越好,然後後面減掉就行了。
做法
首先,這個流是最小費用當且僅當圖中不存在負環,證明和上篇重製版最小費用最大流部落格雷同,不予以贅述。
首先明白一個定理:如果把每條邊容量乘\(2\)
那麼接下來就非常簡單了,把邊權拆成二進位制,維護殘餘網路\(G\),一開始\(G\)中的容量和流量都為\(0\),然後二進位制從高到低掃描,每一位把所有邊的容量和流量乘\(2\),但是需要注意,有些邊這一位二進位制可能為\(1\),因此這條邊會加入到殘餘網路中,這就非常的蛋疼了,好的方法是這條邊是\((x,y)\),在加入前從\(y\)跑一遍最短路,如果\(d[x]+cost(x,y)<0\),那麼就不加入,並且把\(y\)到\(x\)的最短路的流量全部減一,當然,如果這條邊原本就存在,則直接流量\(+1\)即可。
至於為什麼直接跑最短路即可,因為我們維護的殘餘網路中一定沒有負環啊。
時間複雜度:\(O(nm^2\log{U})\),\(U\)是邊中的最大流量。
#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的做法,還有億點細節補充。
程式碼也要補點註釋。
賽前還是直接打個簡單思路就去準備比賽了。