大常数 Dinic,背后是什么?

cirnovsky /

看向这样一份 Dinic 的实现:

#include<bits/stdc++.h>
using namespace std;
template<typename T> struct network {
	const int n;
	struct Arc {
		int to; size_t rev; T w;
	};
	vector<vector<Arc>> e;
	vector<int> lev,iter;
	network(int n):n(n),lev(n),iter(n),e(n) {}
	void add(int one,int ano,T cap) {
		assert(1<=one && one<=n && 1<=ano && ano<=n);
		e[one-1].push_back((Arc){ano-1,e[ano-1].size()+(one==ano),cap});
		e[ano-1].push_back((Arc){one-1,e[one-1].size()-1,0});
	}
	bool bfs(int s,int t) {
		queue<int,list<int>> q;
		lev.assign(n,0);
		for(q.push(s),lev[s]=1; q.size(); q.pop()) {
			for(int now=q.front(),i=iter[now]=0,y; i<int(e[now].size()); ++i) {
				if(now==t)	return 1;
				if(!lev[y=e[now][i].to] && e[now][i].w)	q.push(y),lev[y]=lev[now]+1;
			} 
		}
		return lev[t];
	}
	T dfs(int now,T f,int t) {
		if(now==t)	return f;
		T res=0,tt;
		for(int& i=iter[now],y; i<int(e[now].size()); ++i) {
		    if(!f)	break;
			if(lev[y=e[now][i].to]==lev[now]+1 && e[now][i].w && (tt=dfs(y,min(f,e[now][i].w),t))) {
				e[now][i].w-=tt; e[y][e[now][i].rev].w+=tt; f-=tt; res+=tt;
			}
		}
		if(!res)	lev[now]=0;
		return res;
	}
	T get(int s,int t) {
		assert(1<=s && s<=n && 1<=t && t<=n);
		T res=0,f;
		while(bfs(s-1,t-1)) {
			if((f=dfs(s-1,numeric_limits<T>::max(),t-1)))	res+=f;
			else	break;
		}
		return res;
	}
};
signed main() {
	int n,m,s,t;
	scanf("%d %d %d %d",&n,&m,&s,&t);
	network<long long> G(n);
	for(int i=0,x,y,z; i<m; ++i) {
		scanf("%d %d %d",&x,&y,&z);
		G.add(x,y,z);
	}
	printf("%lld\n",G.get(s,t));
	return 0;
};

它在洛谷的最大流模板中取得了 sum runtime 440ms 的傻逼成绩,卡了我至少 4 题的常。

让我们来看看为什么这份实现跑得这么慢。

首先我使用了 std::size_t 来储存反向边的编号,然而实际上 std::size_tstd::vector<Type>::size_type) 在 64-bits 机器中是 long long unsigned int 的别名,也即,这个跑得比 int 慢多了。

其次是 std::queue<int, list<int>>,虽然 -Wallace- 在其 STL 博客中表示这将比 std::queue<int, deque<int>> 更快,但事实上至少在这里它跑得很慢……

最后是

for(int& i=iter[now],y; i<int(e[now].size()); ++i) {
	if(!f)	break;
	if(lev[y=e[now][i].to]==lev[now]+1 && e[now][i].w && (tt=dfs(y,min(f,e[now][i].w),t))) {
		e[now][i].w-=tt; e[y][e[now][i].rev].w+=tt; f-=tt; res+=tt;
	}
}

在经过实验对比(指,摸到 loj 势力上看了下为啥神户的板子跑这么快)后,我们惊讶的发现,下面代码所呈现的跑进了 50ms

for(int& i=iter[now],y; i<int(e[now].size()); ++i) {
	if(lev[y=e[now][i].to]==lev[now]+1 && e[now][i].w && (tt=dfs(y,min(f,e[now][i].w),t))) {
		e[now][i].w-=tt; e[y][e[now][i].rev].w+=tt; f-=tt; res+=tt;
		if(!f)	break;
	}
}

起初我以为是我的 ff 可能变成负数,但理论和事实都给了我两耳巴子。然后我怀疑是 std::vector<Type>::size() 的问题,但依然没有区别。

最后的怀疑是 ++i 的效率问题

for(int& i=iter[now],y; i<int(e[now].size()); ++i) {
	if(lev[y=e[now][i].to]==lev[now]+1 && e[now][i].w && (tt=dfs(y,min(f,e[now][i].w),t))) {
		e[now][i].w-=tt; e[y][e[now][i].rev].w+=tt; f-=tt; res+=tt;
		if(!f)	{++i; break;}
	}
}

飘成了 400ms,破案了?

不,再看

for(int i=iter[now],y; i<int(e[now].size()); ++i) {
	iter[now]=i;
	if(lev[y=e[now][i].to]==lev[now]+1 && e[now][i].w && (tt=dfs(y,min(f,e[now][i].w),t))) {
		e[now][i].w-=tt; e[y][e[now][i].rev].w+=tt; f-=tt; res+=tt;
		if(!f)	{++i; break;}
	}
}

又跑进了 50ms,现在明了了,iterstd::vector<int>)的寻址太慢,并且剪枝剪掉的情况确实比较多,所以效率很低。