网络流相关算法整理

基本问题

给定一个网络与源点S,汇点T,求最大流

Dinic

由增广路(一条 S → T S \to T ST的路径,且在路径上的所有弧剩余容量大于0)定理得

一个剩余网络没有增广路当且仅当它是原网络的最大流所对应的剩余网络之一

(不会证充分性…)
我们可以找增广路增广,直到没有增广路为止。
每次进行一次仅包含可行边的(剩余容量大于0的边)BFS,将原图按照到汇点的弧的数量下界分层。(在这里直接找一条增广路是没有问题的,但是这就退化成了另一种更差的算法)
在BFS出来的距离标号的基础上找增广路,保证路上相邻点的距离标号差为1,避免找不到增广路而回溯。
不难看出每次BFS后能走的增广路不止一条,为了优化我们进行“倒水”,走完所有增广路为止。

上界时间复杂度为 O ( n 2 m ) O(n^2m) O(n2m),理由如下:

  1. 考虑到每一次汇点的距离标号都是严格递增的,所以最多增广N次。
  2. DFS一次最多有M条增广路,因为每一条增广路至少有一条边的剩余容量被流光,而且当层次图不重设时,其反向弧不可能被走到
  3. 每一条增广路最多经过N个点。

由分析不难看出实际复杂度要比上界要小不少,所以要有梦想

##优化
当前弧优化(必须要加,不然会被卡)。
记录一下点X的前几条弧已经被流过了,下一次没有必要再试重走。
反向弧对当前层次图无影响!
##CODE

//jzoj4087. 网络吞吐量(network),dinic+当前弧(优化效果不明显)
#include 
#include 
#include 
#define min(a,b) ((a)>(b)?(b):(a))
#define MAX (1LL<<60)
#define maxn 510
#define maxm 100100 
typedef long long ll;
using namespace std;
ll n,m;
ll c[maxn],dis[maxn*2];
ll g[maxn][maxn],f[maxn*2][maxn*2];
ll d[maxn][maxn];
ll cur[maxn*2];

void floyed() {
	for (ll i=1; i<=n; i++) d[i][i]=0;
	for (ll k=1; k<=n; k++)
		for (ll i=1; i<=n; i++) if (k!=i) 
			for (ll j=1; j<=n; j++) if (k!=j && i!=j) d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
}
ll q[maxn*2],h,t;

bool bfs() {
	memset(dis,127,sizeof dis);
	h=t=0;
	dis[1]=0;
	q[++t]=1;
	while (h<t) {
		ll now=q[++h];
		for (ll i=1; i<=n*2; i++) {
			if (f[now][i]>0 && dis[i]==dis[0]) {
				dis[i]=dis[now]+1;
				q[++t]=i;
			}
		}
	}
	return dis[n]!=dis[0];
}
ll go(ll x,ll flow) {
	if (x==n*2) return flow;
	ll use=0;
	if (cur[x]==0) cur[x]=1;
	for (ll i=cur[x]; i<=2*n; i++) {
		cur[x]=i;
		if (f[x][i]>0 && dis[x]+1==dis[i]) {
			ll u=go(i,min(flow-use,f[x][i]));
			f[x][i]-=u,f[i][x]+=u;
			use+=u;
			if (use==flow) return use;
		}
	}
	return use;
}
ll dinic() {
	ll ans=0;
	while (bfs()) {
		memset(cur,0,sizeof cur);
		ans+=go(1,MAX);
	}
	return ans;
}
int main() {
	memset(g,127/3,sizeof g);
	freopen("network.in","r",stdin);
	freopen("network.out","w",stdout);
	cin>>n>>m;
	ll a,b,dd;
	for (ll i=1; i<=m; i++) {
		scanf("%lld %lld %lld",&a,&b,&dd);
		g[a][b]=g[b][a]=min(g[a][b],dd);
	}
	memcpy(d,g,sizeof g);
	for (ll i=1; i<=n; i++) {
		scanf("%lld",&c[i]);
		f[i][n+i]=c[i];
	}
	f[1][n+1]=MAX;
	f[n][n+n]=MAX;
	floyed();
	for (ll i=1; i<=n; i++)
		for (ll j=1; j<=n; j++) 
			if (i!=j && g[i][j]!=g[0][0] && d[1][i]+g[i][j]+d[j][n]==d[1][n]) {
				f[n+i][j]=MAX;
			}
	cout<<dinic()<<endl;
}

#SAP
SAP有两个含义:

  1. 基于最短增广路的算法,这里指的是一类算法(包括dinic)。
  2. Dinic的一种优化算法(我是这么理解的),这里指的是一种算法

与Dinic最大的不同是,其距离标号不再用BFS重构。而是动态维护。维护方法也很简单:显而易见地,不存在一个点x的前驱点y使得la[y]+1=la[x]时,更新la[x]=min(la[y])+1。
但是注意,我们是从S顺推到T,这就意味着我们必须在点x时检查点x的后继。这不自然而且复杂度高,所以我们换成:la[x]表示点x到汇点的弧数下界。这是等价的表达,我们就可以在处理完点x的所有后继点后,再检查点x。
相当于每次走过的点层数抬高1,然后就会一圈圈扩散至T,意会一下。

这个算法的结束:当la[S]大于总点数时。
理论上初始标号要用反向BFS求得,实际可全部设为0,但对时间可能会有少许影响。
上界时间复杂度依旧是 O ( n 2 m ) O(n^2m) O(n2m),证明同dinic,但实际效率要比dinic优秀许多。

优化

当前弧优化:同dinic.

(重要)GAP
这其实不算一种优化,是算法结束的条件。
若以la[S]大于总点数为条件结束,需要时间太久了。观察发现,当不存在x使得la[x]=p时,任何点显然无法更新为p+1。那么也就不存在增广路。于是结束算法。
这个条件在la[S]大于总点数之前满足。

int go(int x,int fl) {
	if (x==T) return fl;
	int used=0;
	for (int i=final[x]; i; i=nex[i]) if (f[i] && la[to[i]]+1==la[x]) {
		int suc=go(to[i],min(fl-used,f[i]));
		f[i]-=suc,f[i^1]+=suc;
		used+=suc;
		if (used==fl) return fl;
	}
	if (--gap[la[x]++]==0) la[0]=inf;
	else ++gap[la[x]];
	return used;
}
int maxflow() {
	int ans=0; memset(gap,0,sizeof gap),memset(la,0,sizeof la);
	gap[0]=n*n+2;
	while (la[0]!=inf)
		ans+=go(S,inf);
	return ans; 
}

基本问题2

给定一个费用网络与源点S,汇点T,求最小费用最大流。
##SPFA法求费用流(单路增广)
按照dinic的想法,每次找一条增广路增广。但要保证该增广路在当前增广路集中是费用之和最小的(最优费用增广路,dinic找的是最短增广路)。即在保证最大流的情况下考虑费用

注意反向边的费用应为原费用的相反数,保证过程可逆。

每一次使用spfa求解最优增广路。注意会导致0权环问题。

如何解决?见下文

据说只要原网络(不包括反向弧)中无负权环,那么求解费用流中途也不会出现负权环。(yali某大哥ppt上结论)

假设最大流流量为K,那么上界时间复杂度 O ( K ∗ s p f a ) → O ( K ∗ E ) O(K*spfa)\to O(K*E) O(Kspfa)O(KE)
为什么最多增广K次,考虑极端情况: 每次最优增广路仅能增广1流量。

//jzoj3739. 【TJOI2014】匹配(match) spfa求费用流
#include 
#include 
#include 
#define min(a,b) ((a)>(b)?(b):(a))
#define maxn 100
using namespace std;
int h[maxn][maxn],n,ans;
int tot,S,T;
int fr[maxn*maxn*2],to[maxn*maxn*2],head[maxn*2],next[maxn*maxn*2],pre[maxn*2];
int f[maxn*maxn*2],w[maxn*maxn*2];
int dis[maxn*2];
int bh[maxn][maxn];
int q[maxn*maxn*100],he,t,vis[maxn*maxn*100];
int todel[maxn*maxn*2][2];

int link(int x,int y,int ff,int ww) {
	//cout<0 && dis[ti]>dis[now]+w[i]) {
				pre[ti]=i;
				dis[ti]=dis[now]+w[i];
				if (vis[ti]==0) {
					q[++t]=ti;
					vis[ti]=1;
				}
			}
		}
	}
	return !(dis[T]==dis[0]);
}
void initEdge(int ex,int ey) {
	memset(head,0,sizeof head);
	tot=1;
	for (int i=1; i<=n; i++) for (int j=1; j<=n; j++) {
		if (ex==i && ey==j+n) continue;
		link(i,n+j,1,-h[i][j]);
		link(n+j,i,0,+h[i][j]);
	}
	S=n+n+1,T=S+1;
	for (int i=1; i<=n; i++) link(S,i,1,0),link(i,S,0,0);
	for (int i=1; i<=n; i++) link(n+i,T,1,0),link(T,n+i,0,0);
}
int minCostMaxFlow() {
	int mi,ans=0;
	while (spfa()) {
		mi=99999999;
		for (int i=pre[T]; i; i=pre[fr[i]]) mi=min(mi,f[i]);
		for (int i=pre[T]; i; i=pre[fr[i]]) {
			f[i]-=mi; f[i^1]+=mi;
			ans+=w[i]*mi;
		}
	}
	return ans;
}
int savedF[maxn*maxn*2];
int main() {
	freopen("match.in","r",stdin);
	freopen("match.out","w",stdout);
	cin>>n;
	for (int i=1; i<=n; i++) for (int j=1; j<=n; j++) scanf("%d",&h[i][j]);
	initEdge(0,0);
	memcpy(savedF,f,sizeof f);
	cout<<(ans=-minCostMaxFlow())<

SPFA法求费用流(多路增广)UPD:2018/7/16

之前发现像dinic一样倒水会出现0权环的问题。
其实解决方法也很简单,在最短路图上按bfs步数分下层,然后只走层数递增1的边即可。
(infleaking:这玩意儿我打了n次了绝对没问题)
效率会比单路增广高很多。
PS : 保证无重点当费用都差不多的时候有时间问题。

ll ans,dis[N],Q[N*10],vis[N];
ll step[N];
void spfa() {
	memset(dis,127,sizeof dis);
	dis[S] = 0;
	int h = 0, t = 0;
	Q[++t] = S;
	while (h < t) {
		int x = Q[++h];
		vis[x] = 0;
		for (int i = final[x]; i; i=nex[i]) if (f[i]) {
			int y = to[i];
			if (dis[x] + cost[i] < dis[y]) {
				dis[y] = dis[x] + cost[i];
				if (!vis[y]) {
					vis[y] = 1;
					Q[++t] = y;
				}
			}
		}
	}
	
	h = 0, t = 0; vis[S] = 1;
	Q[++t] = S;
	while (h < t) {
		int x = Q[++h];
		for (int i = final[x]; i; i=nex[i]) if (f[i]) {
			int y = to[i];
			if (!vis[y] && dis[x] + cost[i] == dis[y]) {
				step[y] = step[x] + 1;
				vis[y] = 1;
				Q[++t] = y;	
			}
		}
	}
	memset(vis,0,sizeof vis);
}

#define min(a,b) ((a)>(b)?(b):(a))
#define abs(x) ((x)>(0)?(x):-(x))
int s[N];
int go(int x,int wa) {
//	cout<
	if (wa == 0) 
		return 0;
	if (x == T) {
//		cout<
		return wa;
	}
	
	s[++s[0]] = x;
	int used = 0;
	for (int i = final[x]; i; i=nex[i]) if (f[i]) {
		int y = to[i];
		if (step[x] + 1 == step[y] && dis[x] + cost[i] == dis[y]) {
			int take = go(y, min(wa - used, f[i]));
			f[i] -= take;
			f[i^1] += take;
			ans += (ll)cost[i] * take;
			used += take;
			if (wa == used) break;
		}
	}
	s[0]--;
	vis[x] = 0;
	return used;
}

ZKW费用流 (玄学)

用重标号思想维护最短路,只走满足费用最短路的边。

当一次增广完毕时,将当前S能走到的点的距离标号抬高一个最小值,使得有更多的边可以被增广。时间复杂度同为 O ( K ∗ E ) O(K*E) O(KE)
适合费用区间小,流量大的稠密图。稀疏图用spfa较好。
jzoj1094 乘法用log转加法,最小费用最大流 费用取反 转最大费用最大流。
注意vis[T]不能为1,否则会导致提前结束

#include 
#include 
#include 
#include 
#define E 150*150*2
#define N 500
#define ADD 100
#define ERR 0.00001
using namespace std;
typedef double db;

int n,m,tot=1,S,T;
double w[E],dis[N];
int to[E],next[E],final[N],f[E];
bool vis[N];
bool equal(db a,db b) {return abs(a-b)<=ERR;}
void link0(int x,int y,int f0,db w0) {
	to[++tot]=y; next[tot]=final[x]; final[x]=tot;
	f[tot]=f0, w[tot]=w0;
}
void link(int x,int y,int f,db w) {link0(x,y,f,ADD-w),link0(y,x,0,w-ADD);}
int go(int x,int fl) {
	if (x==T) return fl;
	vis[x]=1; int rest=fl;
	for (int i=final[x]; i; i=next[i]) if (f[i]) {
		int y=to[i];
		if (!vis[y] && equal(dis[y]+w[i],dis[x])) {
			int h=go(y,min(f[i],rest));
			f[i]-=h,f[i^1]+=h;
			rest-=h; if (rest==0) return fl;
		}
	}
	return fl-rest;
}
bool change() {
	db minh=(1<<30);
	for (int i=1; i<=T; i++) if (vis[i]) 
		for (int j=final[i]; j; j=next[j]) 
			if (f[j] && !vis[to[j]]) 
				minh=min(minh,(dis[to[j]]+w[j])-dis[i]);
	if (equal(minh,1<<30)) return 0;
	for (int i=1; i<=T; i++) if (vis[i]) vis[i]=0,dis[i]+=minh;
	return 1;
}
int main() {
	freopen("1094.in","r",stdin);
	freopen("1094.out","w",stdout);
	
	tot=1; int p;
	cin>>n>>m;
	for (int i=1; i<=n; i++) for (int j=1; j<=m; j++) {
		scanf("%d",&p);
		if (p==0) link(i,n+j,1,-20);
		else link(i,n+j,1,log2(p));
	}
	S=n+m+1,T=S+1;
	int c;
	for (int i=1; i<=n; i++) scanf("%d",&c), link(S,i,c,0);
	for (int i=1; i<=m; i++) scanf("%d",&c), link(n+i,T,c,0);
	do go(S,1<<30); while (change());
	int o=2;
	for (int i=1; i<=n; i++) {
		for (int j=1; j<=m; j++,o+=2) 
			if (f[o]==0) printf("1"); else printf("0");
		printf("\n");
	}
}

你可能感兴趣的:(新内容,网络流)