费用流

分析

目前只会EK(毕竟难点在构图)

如果原图中不存在负环,则之后的残余网络中不可能出现负环

构图的关键:在最大流的基础上最小费用(最大流必须跑满流起着限制作用,这是关键中的关键)

PS:最好每次都保证建双向边,之前有一道题由于是无向图想当然地建了单向边,导致钩钩了

inline void add(int u,int v,int k,int l) {
	to[++cnt]=v,nxt[cnt]=he[u],he[u]=cnt;
	w[cnt]=k,ex[cnt]=l;
}
inline void adde(int u,int v,int k,int l) {
	add(u,v,k,l),add(v,u,0,-l);
}
inline bool spfa() {
	for(int i=1;i<=n;i++) {
		d[i]=INF; fl[i]=0;
	}
	int l=1,r=1; d[q[1]=S]=0; 
	while(l<=r) {
		int u=q[l%n]; l++; fl[u]=0;
		for(int e=he[u];e;e=nxt[e]) {
			int v=to[e];
			if(w[e]&&d[v]>d[u]+ex[e]) {
				d[v]=d[u]+ex[e],pre[v]=e;
				if(!fl[v]){
					fl[v]=1;
					r++,q[r%n]=v;
				} 
			}
		}
	}
	return d[T]!=INF;
}

inline void dfs() {
	int u=T,flow=INF;
	for(;u!=S;u=to[pre[u]^1]) {
		flow=min(flow,w[pre[u]]);
	}
	for(u=T;u!=S;u=to[pre[u]^1]) {
		w[pre[u]]-=flow;
		w[pre[u]^1]+=flow;
	}
	sum+=flow; ans+=flow*d[T];
}

例题1(裂边)

Luogu P2488 [SDOI2011]工作安排

如果费用一定,则是费用流板子

费用是分段函数,且单调增,所以可以裂边,或者动态加边(前者方便)

int main(){
	scanf("%d%d",&m,&n); S=n+m+1,T=S+1;
	cnt=1;
	for(int i=1;i<=n;i++) {
		int t; scanf("%d",&t);
		adde(i+m,T,t,0);
	}
	for(int i=1;i<=m;i++) {
		for(int j=1;j<=n;j++) {
			int op; scanf("%d",&op);
			if(op) adde(i,j+m,INF,0);
		}
	}
 	for(int i=1;i<=m;i++) {
 		int num; scanf("%d",&num);
		for(int j=1;j<=num;j++) {
			scanf("%d",&a[j]);
		}
		a[num+1]=INF;
		for(int j=1;j<=num+1;j++) {
			int t; scanf("%d",&t);
			adde(S,i,a[j]-a[j-1],t);
		}
	}	
	while(spfa()) dfs();
	printf("%lld\n",ans);
	return 0;
}

例题2

Luogu P1251 餐巾计划问题

为了维护一天需要\(r_i\)块餐巾,需要将一天裂成早上和晚上两个点

想法1:将早上和晚上用流量为\(r_i\),费用为0的边,然后早上和\(S\)连流量正无穷,费用为\(p\)的边,晚上和\(T\)连流量正无穷费用为\(0\) 的边,然后再安排洗毛巾

这感觉是正确的,结果是错误无比,因为答案并不是在最大流时取到

想法2:将\(S\)点向晚上连流量为\(r_i\),费用为0的边,再让早上和\(T\)连流量为\(r_i\),费用为0的边,要使早上有毛巾,从\(S\)点向早上连流量为\(INF\),费用为\(p\)的边,然后安排洗毛巾和毛巾的传递

相当于枚举的是早上的毛巾情况,然后建晚上的点来辅助分析,最大流的情况保证了所有早上的点都满流

int main(){
	scanf("%d",&n); 
	for(int i=1;i<=n;i++) scanf("%d",&c[i]); 
	scanf("%d%d%d%d%d",&f,&a,&fa,&b,&fb); S=n+n+1,T=S+1;
	cnt=1;
	for(int i=1;i<=n;i++) {
		int t=c[i];
		adde(S,i+n,t,0);
		adde(i,T,t,0);
		adde(S,i,INF,f);
		if(i!=n) adde(i,i+1,INF,0);
		if(i+a<=n) adde(i+n,i+a,INF,fa);
		if(i+b<=n) adde(i+n,i+b,INF,fb);
	}	
	while(spfa()) dfs();
	printf("%lld\n",ans);
	return 0;
}

例题3

ybtoj 放置棋子

答案不具有二分性,\(n\leq 40\),所以考虑枚举行的最多个数\(Lim\),求在条件1和\(Lim\)约束下的棋子数\(ans\)

则条件2:

\[\frac{Lim}{ans}\leq \frac{A}{B} \]

显然\(ans\)越大越有可能符合条件2

\(\therefore\)求在条件1和\(Lim\)约束下的最大棋子数,可以把行列分开建节点

正难则反,考虑求全部可放置的节点-最少不要的节点数

接下来是神仙构图:将\(S\)向所有行连边,流量为每行最多放的节点数,权值为0,表示\(All\)

将所有列向\(T\)连边,流量为每列最多放的节点数,权值为0,表示\(All\)

\(i\)行向\(i\)列连边,流量为\(INF\),权值为0,流过\(k\)流量表示保留\(k\)个点

\((x,y)\)表示\(x\)行向\(y\)列连边,流量为1,权值为1,流过表示删掉该点

必须满流

int main() {
    int A, B;
    scanf("%d%d%d", &n, &A, &B);
    int id = 0;
    while (n || A || B) {
        printf("Case %d: ", ++id);
        S = n + n + 1, T = S + 1;
        int Ans = -1, yl = 0, sum = 0;
        memset(mx, 0, sizeof(mx));
        for (int i = 1; i <= n; i++) {
            scanf("%s", s[i] + 1);
            for (int j = 1; j <= n; j++) {
                if (s[i][j] == '.' || s[i][j] == 'C')
                    mx[i]++, mx[j + n]++, sum++;
                if (s[i][j] == 'C')
                    yl++;
            }
        }
        int lim = 0;
        for (int i = 1; i <= n; i++) {
            int ss = 0;
            for (int j = 1; j <= n; j++) ss += s[i][j] == 'C';
            lim = max(lim, ss);
        }
        for (int i = 1; i <= n; i++) {
            int ss = 0;
            for (int j = 1; j <= n; j++) ss += s[j][i] == 'C';
            lim = max(lim, ss);
        }
        for (; lim <= n; lim++) {
            cnt = 1;
            memset(he, 0, sizeof(he));
            for (int i = 1; i <= n; i++) {
                adde(S, i, mx[i], 0);
                adde(i + n, T, mx[i + n], 0);
                adde(i, i + n, lim, 0);
            }
            for (int i = 1; i <= n; i++) {
                for (int j = 1; j <= n; j++) {
                    if (s[i][j] == '.') {
                        adde(i, j + n, 1, 1);
                    }
                }
            }
            ans = num = 0;
            while (spfa()) dfs();
            ans = sum - ans;
            if (num == sum && lim * B <= A * ans)
                Ans = max(Ans, ans);
        }
        if (Ans == -1)
            puts("impossible");
        else
            printf("%d\n", Ans - yl);
        scanf("%d%d%d", &n, &A, &B);
    }
    return 0;
}

例题4(裂点)

ybtoj 订单处理

假设一台机器有\(K\)个订单,所需时间分别为\(c[i]\),则其花费的时间为\(\sum_{i=1}^{K}c[i]\times i\)

要求时间最小,则\(c[i]\)应单调增,所以\(c[i]\times i\)单调增,所以裂点后可以保证订单按次序

所以可以把一台机器裂成\(n\)个点,分别表示1倍权值,2倍权值\(\cdots\)

然后通过建边保证每个权值倍数只能用1次,最小费用最大流

int main(){
int Tim; scanf("%d",&Tim);
while(Tim--) { 
	scanf("%d%d",&n,&m); S=n*(m+1)+1,T=S+1;
	cnt=1; ans=0;
	memset(he,0,sizeof(he));
	for(int i=1;i<=n;i++) {
		for(int j=1;j<=m;j++) {
			int t; scanf("%d",&t);
			for(int k=1;k<=n;k++) {
				adde(id(j,k),i,1,t*k);
			}
		}
		adde(i,T,1,0);
	}
	for(int i=1;i<=m;i++) {
		for(int j=1;j<=n;j++) {
			adde(S,id(i,j),1,0);
		}
	}
	while(spfa()) dfs();
	printf("%.6lf\n",(double)ans/n);
}
return 0;
}

例题5

Luogu P4068 [SDOI2016]数字配对

这是一张二分图,可以用约数个数的奇偶性划分

连边后跑最大费用最大流

因为每次EK找的是最大费用的流,所以当前的费用和\(<0\)即可结束

inline bool dfs() {
	int u=T,flow=INF;
	for(;u!=S;u=to[pre[u]^1]) {
		flow=min(flow,w[pre[u]]);
	}
	for(u=T;u!=S;u=to[pre[u]^1]) {
		w[pre[u]]-=flow;
		w[pre[u]^1]+=flow;
	}
	if(ans+d[T]*flow<0) {
		sum+=ans/(-d[T]);
		return 0;
	} 
	ans+=d[T]*flow;
	sum+=flow;
	return 1;
}

inline int fj(int x) {
	int u=x,ret=0;
	for(int i=2;i<=sqrt(x);i++) {
		while(u%i==0) {
			u/=i; ret++;
		}
	}
	if(u>1) ret++;
	return ret;
}

int main(){
	scanf("%d",&n); S=n+1,T=S+1,cnt=1; 
	for(int i=1;i<=n;i++) {
		scanf("%d",&a[i]); co[i]=fj(a[i]);
	}
	for(int i=1;i<=n;i++) {
		scanf("%d",&b[i]); 
	}
	for(int i=1;i<=n;i++) {
		scanf("%d",&c[i]); 
	}
	for(int i=1;i<=n;i++) {
		if(co[i]&1) {
			for(int j=1;j<=n;j++) {
				if(abs(co[i]-co[j])==1&&(a[i]%a[j]==0||a[j]%a[i]==0)) {
					adde(i,j,INF,(ll)c[i]*c[j]);
				}
			}
			adde(S,i,b[i],0);
		} else {
			adde(i,T,b[i],0);
		}
	}
	while(spfa()) {
		if(!dfs()) break;
	}
	printf("%d\n",sum);
	return 0;
}

例题6

ybtoj 避难方案

先按题意建出残余网络,spfa找负环,然后沿负环流一遍

注意:spfa找负环是一定要\(>n+1\),防止的是1个节点

inline int spfa() {
	for(int i=1;i<=T;i++) {
		d[i]=INF; fl[i]=0;
	}
	d[T]=0; q.push(T);
	while(!q.empty()) {
		int u=q.front(); q.pop(); fl[u]=0;
		for(int e=he[u];e;e=nxt[e]) {
			int v=to[e];
			if(w[e]&&d[v]>d[u]+ex[e]) {
				d[v]=d[u]+ex[e],pre[v]=e;
				if(!fl[v]){
					fl[v]=1;q.push(v);
					vis[v]++;
					if(vis[v]>n+1) {
						return v;
					}
				} 
			}
		}
	}
	return -1;
}

struct A{int a,b,c; }a[N],b[N];
int main(){
	scanf("%d%d",&n,&m); S=n+m+1,T=S+1,cnt=1;
	for(int i=1;i<=n;i++) {
		scanf("%d%d%d",&a[i].a,&a[i].b,&a[i].c);
	}
	for(int i=1;i<=m;i++) {
		scanf("%d%d%d",&b[i].a,&b[i].b,&b[i].c);
	}
	for(int i=1;i<=n;i++) {
		for(int j=1;j<=m;j++) {
			int t; scanf("%d",&t);
			add(i,j+n,INF,abs(a[i].a-b[j].a)+abs(a[i].b-b[j].b)+1);
			add(j+n,i,t,-abs(a[i].a-b[j].a)-abs(a[i].b-b[j].b)-1);
			c[j]+=t;
			E[i][j]=t;
		}	
		adde(i,S,a[i].c,0);
	}
	for(int i=1;i<=m;i++) {
		add(i+n,T,b[i].c-c[i],0),add(T,i+n,c[i],0); 
	}
	int ans=spfa();
	if(ans==-1) printf("OPTIMAL");
		else {
			puts("SUBOPTIMAL");
			memset(fl,0,sizeof(fl));
			for(;!fl[ans];ans=to[pre[ans]^1]) {
				fl[ans]=1;
			}
			memset(fl,0,sizeof(fl));
			int flow=INF;
			for(;!fl[ans];ans=to[pre[ans]^1]) {
				flow=min(flow,w[pre[ans]]);
				fl[ans]=1;
			}
			int v=ans;
			if(ex[pre[v]]>0) {
					E[to[pre[v]^1]][v-n]+=flow;
				} else {
					E[v][to[pre[v]^1]-n]-=flow;
				}
			v=to[pre[v]^1];
			for(;v!=ans;v=to[pre[v]^1]) {
				if(ex[pre[v]]>0) {
					E[to[pre[v]^1]][v-n]+=flow;
				} else {
					E[v][to[pre[v]^1]-n]-=flow;
				}
			}
			for(int i=1;i<=n;i++) {
				for(int j=1;j<m;j++) {
					printf("%d ",E[i][j]);
				}
				printf("%d\n",E[i][m]);
			}
		}
	return 0;
}
posted @ 2021-03-15 09:17  wwwsfff  阅读(120)  评论(0编辑  收藏  举报