网络流--最大流,最小割,费用流问题

例题:

  1. P1231 教辅的组成

  2. P2598 [ZJOI2009]狼和羊的故事

  3. P4016 负载平衡问题

  • 最大流:

贪心流程:

  1. 找一条\(s\)\(t\)的只经过\(f(e)< c(e)\) 的边的路径。

  2. 如果不存在满足条件的路径,则算法结束。否则,沿着该路径尽可能地增加\(f(e)\),返回第(1)步。这一步骤称作增广。

反悔机制:

  1. 只利用满足\(f(e)< c(e)\)\(e\)\(f(e)> 0\)\(e\)的反向边\(rev(e)\) ,寻找一条\(s\)\(t\)的路径。

  2. 如果不存在满足条件的路径,则算法结束。否则,沿着该路径尽可能地增加流,返回(1)。

核心代码:

bool BFS(int s){
	queue<int> q;
	q.push(s);memset(dis,0,sizeof(dis));dis[0] = 1;
	while (!q.empty()){
		int x = q.front();q.pop();
		for (int i = head[x];i;i = ed[i].nxt){
			int to = ed[i].to;
			if (dis[to]||!ed[i].w) continue;
			dis[to] = dis[x] + 1;
			q.push(to);
		}
	}
	return dis[n1+n2+n1+n3+1];
}
int DFS(int x,int limit){
	if (!limit||x == n1+n2+n1+n3+1) return limit;
	int res = 0;
	for (int &i = head[x];i;i = ed[i].nxt){
		int to = ed[i].to;
		if (dis[to] != dis[x]+1) continue;
		int tmp = DFS(to,min(limit,ed[i].w));
		limit -= tmp,ed[i].w -= tmp,ed[i^1].w += tmp,res += tmp;
		if (!limit) break;
	}
	return res;
}
int main(){
	for (int i = 0;i <= n1+n2+n1+n3+1;i++) cur[i] = head[i];
	while (BFS(0)){
		while (tmp = DFS(0,inf)) ans += tmp;
		for (int i = 0;i <= n1+n2+n1+n3+1;i++) head[i] = cur[i];
	}
	printf("%d\n",ans);
	return 0;
}

最小割:

性质: 最小割 = 最大流

费用流:

在求解最大流时,我们在残量网络上不断贪心地增广而得到了最大流。现在边上多了费用,为了使最后总费用最小,我们自然能想到每次贪心地在残量网络上,沿着费用最小的那条路进行增广,此时残量网络中的反向边费用应该是原边费用的相反数,以保证退流操作是可逆、正确的。

核心代码:

bool SPFA(int s){
	queue<int> q;
	memset(vis,0,sizeof(vis));
	for (int i = 1;i <= n;i++) dis[i] = inf;
	q.push(s);dis[s] = 0;vis[s] = 1;
	while (!q.empty()){
		int x = q.front();q.pop();
		vis[x] = 0;
		for (int i = head[x];i;i = ed[i].nxt){
			int to = ed[i].to;
			if (!ed[i].lim) continue;
			if (dis[to] > dis[x] + ed[i].w){
				dis[to] = dis[x] + ed[i].w;
				pre1[to] = x,pre2[to] = i;
				if (!vis[to]){
					vis[to] = 1;
					q.push(to);
				}
			} 
		}
	}
	return dis[t] != inf;
}
int main(){
	n = read(),m = read(),s = read(),t = read();
	for (int i = 1;i <= m;i++){
		int u = read(),v = read(),w = read(),c = read();
		add(u,v,w,c),add(v,u,0,-c);
	}
	int ans1 = 0,ans2 = 0;
	while (SPFA(s)){
		int tmp = inf;
		for (int i = t;i != s;i = pre1[i]) tmp = min(tmp,ed[pre2[i]].lim);	
		ans1 += tmp,ans2 += tmp*dis[t];
		for(int i = t;i != s;i = pre1[i]) ed[pre2[i]].lim -= tmp,ed[pre2[i]^1].lim += tmp;
	}
	printf("%d %d\n",ans1,ans2);
	return 0;
}

例题:

对于第一道题,想要尽可能的多组出书的配套方案,我们可以联想到最大流,那么问题就在于如何连边,因为给出的都是书和练习册,或者书和答案之间的关系,所以我们把书放在中间,练习册和答案分别和他连边。

但是这就出现了一个问题,每本书只能选一次,怎样才能保证这个问题呢???我们把书拆成两个点,中间连一条边权为1的边就可以解决了。

code:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
int read(){
	int x = 1,a = 0;char ch = getchar();
	while (ch < '0'||ch > '9'){if (ch == '-') x = -1;ch = getchar();}
	while (ch >= '0'&&ch <= '9'){a = a*10+ch-'0';ch = getchar();}
	return x*a;
}
const int maxn = 1e5+10,inf = 1e9+7;
int n1,n2,n3;
int m1,m2;
struct node{
	int to,nxt,w;
}ed[maxn*4];
int head[maxn*4],tot = 1,cur[maxn*4];
void add(int u,int to,int w){
	ed[++tot].to = to;
	ed[tot].w = w;
	ed[tot].nxt = head[u];
	head[u] = tot;
}
int dis[maxn*4];
bool BFS(int s){
	queue<int> q;
	q.push(s);memset(dis,0,sizeof(dis));dis[0] = 1;
	while (!q.empty()){
		int x = q.front();q.pop();
		for (int i = head[x];i;i = ed[i].nxt){
			int to = ed[i].to;
			if (dis[to]||!ed[i].w) continue;
			dis[to] = dis[x] + 1;
			q.push(to);
		}
	}
	return dis[n1+n2+n1+n3+1];
}
int DFS(int x,int limit){
	if (!limit||x == n1+n2+n1+n3+1) return limit;
	int res = 0;
	for (int &i = head[x];i;i = ed[i].nxt){
		int to = ed[i].to;
		if (dis[to] != dis[x]+1) continue;
		int tmp = DFS(to,min(limit,ed[i].w));
		limit -= tmp,ed[i].w -= tmp,ed[i^1].w += tmp,res += tmp;
		if (!limit) break;
	}
	return res;
}
int main(){
	n1 = read(),n2 = read(),n3 = read();
	m1 = read();
	for (int i = 1;i <= m1;i++){
		int x = read(),y = read();
		add(y,x+n2,1),add(x+n2,y,0);
	}
	m2 = read();
	for (int i = 1;i <= m2;i++){
		int x = read(),y = read();
		add(x+n2+n1,y+n1+n1+n2,1),add(y+n1+n1+n2,x+n1+n2,0);
	}
	for (int i = 1;i <= n1;i++){
		add(i+n2,i+n2+n1,1),add(i+n2+n1,i+n2,0);
	}
	for (int i = 1;i <= n2;i++) add(0,i,1),add(i,0,0);
	for (int i = 1;i <= n3;i++) add(i+n1+n2+n1,n1+n2+n1+n3+1,1),add(n1+n2+n1+n3+1,i+n1+n2+n1,0);
	int ans = 0,tmp;
	for (int i = 0;i <= n1+n2+n1+n3+1;i++) cur[i] = head[i];
	while (BFS(0)){
		while (tmp = DFS(0,inf)) ans += tmp;
		for (int i = 0;i <= n1+n2+n1+n3+1;i++) head[i] = cur[i];
	}
	printf("%d\n",ans);
	return 0;
}

第二道题主要看我们如何能把问题转化为最小割,连出一个四联通图后,建立源点和汇点,把羊向源点连边,狼向汇点连边,边权全为\(inf\),这样我们在求最小割的时候一定不会断掉他(因为他太大了啊)

正确性: 狼和羊能相遇的充分条件是能通过狼和羊亮点从源点走到汇点,如果现在不连通,且断掉的不是连接源点和汇点这些边,那么一定是狼和羊之间的路径

code:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
int read(){
	int  x = 1,a = 0;char ch = getchar();
	while (ch < '0'||ch > '9'){if (ch == '-') x = -1;ch = getchar();}
	while (ch >= '0'&&ch <= '9'){a = a*10+ch-'0';ch = getchar();}
	return x*a;
}
const int maxn = 1000*1000+10,inf = 1e9+7;
int n,m;
int s,t;
struct node{
	int to,nxt,w;
}ed[maxn*4];
int head[maxn*4],tot = 1,cur[maxn*4];
void add(int u,int to,int w){
	ed[++tot].to = to;
	ed[tot].nxt = head[u];
	ed[tot].w = w;
	head[u] = tot;
}
int dis[maxn*4];
bool BFS(int s){
	queue<int> q;
	q.push(s);memset(dis,0,sizeof(dis));dis[0] = 1;
	while (!q.empty()){
		int x = q.front();q.pop();
		for (int i = head[x];i;i = ed[i].nxt){
			int to = ed[i].to;
			if (dis[to]||!ed[i].w) continue;
			dis[to] = dis[x] + 1;
			q.push(to);
		}
	}
	return dis[t];
}
int DFS(int x,int limit){
	if (!limit||x == t) return limit;
	int res = 0;
	for (int &i = head[x];i;i = ed[i].nxt){
		int to = ed[i].to;
		if (dis[to] != dis[x]+1) continue;
		int tmp = DFS(to,min(limit,ed[i].w));
		limit -= tmp,ed[i].w -= tmp,ed[i^1].w += tmp,res += tmp;
		if (!limit) break;
	}
	return res;
}
int main(){
	n =  read(),m = read();
	s = 0,t = (1+n)*m+1;
	for (int i = 1;i <= n;i++){
		for (int j = 1;j <= m;j++){
			int x = read();
			int pos1 = i*m+j,pos2 = (i-1)*m+j,pos3 = (i+1)*m+j,pos4 = i*m+j-1,pos5 = i*m+j+1;
			if (i > 1) add(pos1,pos2,1),add(pos2,pos1,0);
			if (i < n) add(pos1,pos3,1),add(pos3,pos1,0);
			if (j > 1) add(pos1,pos4,1),add(pos4,pos1,0);
			if (j < m) add(pos1,pos5,1),add(pos5,pos1,0);
			if (x == 1) add(pos1,t,inf),add(t,pos1,0);
			if (x == 2) add(s,pos1,inf),add(pos1,s,0);
		}	
	}
	int tmp,ans = 0;
	for (int i = s;i <= t;i++) cur[i] = head[i];
	while (BFS(0)){
		while (tmp = DFS(0,inf)) ans += tmp;
		for (int i = s;i <= t;i++) head[i] = cur[i];
	}
	printf("%d\n",ans);
	return 0;
}

第三题而言重点还是在建模,贪心的想,多的我们移走,少的我们补充,正好的我们不动,那么建边就分为以下几种情况:

  1. 比平均值大的仓库,从源点向他连一条流量为\(a_i-sum\)的边,费用为0,说明要从他移出

  2. 比平均值小的仓库,从他向汇点连一条流量为\(sum-a_i\)的边,费用为0,说明要补充他

  3. 相邻仓库间建一条流量为\(inf\),费用为1的边,可以无限移动,但是与源点汇点连的边限制了他们

code:

#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
int read(){
	int x = 1,a = 0;char ch = getchar();
	while (ch < '0'||ch > '9'){if (ch == '-') x = -1;ch =  getchar();}
	while (ch >= '0'&&ch <= '9'){a = a*10+ch-'0';ch = getchar();}
	return x*a;
}
const int maxn = 500,inf = 1e9+7;
int n,a[maxn],s,t;
struct node{
	int to,nxt,w,lim;
}ed[maxn*4];
int head[maxn*4],tot = 1;
void add(int u,int to,int lim,int w){
	ed[++tot].to = to;
	ed[tot].w = w;
	ed[tot].lim = lim;
	ed[tot].nxt = head[u];
	head[u] = tot;
}
int vis[maxn],dis[maxn],pre1[maxn],pre2[maxn];
bool SPFA(int s){
	queue<int> q;q.push(s);
	memset(vis,0,sizeof(vis));vis[s] = 1;
	for (int i = 0;i <= n+1;i++) dis[i] = inf;dis[s] = 0;
	while (!q.empty()){	
		int x = q.front();q.pop();
		vis[x] = 0;
		for (int i = head[x];i;i = ed[i].nxt){
			int to = ed[i].to;
			if (!ed[i].lim) continue;
			if (dis[to] > dis[x]+ed[i].w){
				dis[to] = dis[x] + ed[i].w;
				pre1[to] = x,pre2[to] = i;
				if (!vis[to]){
					vis[to] =  1;
					q.push(to);
				}
			}
		}
	}
	return dis[t] != inf;
}
int main(){
	n = read();
	s = 0,t = n+1;
	int sum = 0;
	for (int i = 1;i <= n;i++) a[i] = read(),sum += a[i];
	sum /= n;
	for (int i = 1;i <= n;i++){
		if (a[i] < sum) add(i,t,sum-a[i],0),add(t,i,0,0);
		if (a[i] > sum) add(s,i,a[i]-sum,0),add(i,s,0,0);
		if (i > 1) add(i,i-1,inf,1),add(i-1,i,0,-1);
		if (i < n) add(i,i+1,inf,1),add(i+1,i,0,-1);
	}
	add(1,n,inf,1),add(n,1,0,-1);
	add(n,1,inf,1),add(1,n,0,-1);
	int ans = 0;
	while (SPFA(s)){
		int tmp = inf;
		for (int i = t;i != s;i = pre1[i]) tmp = min(tmp,ed[pre2[i]].lim);
		ans += tmp*dis[t];
		for (int i = t;i != s;i = pre1[i]) ed[pre2[i]].lim -= tmp,ed[pre2[i]^1].lim += tmp;
	}
	printf("%d\n",ans);
	return 0;
}
posted @ 2020-11-16 12:47  小又又yyyy  阅读(236)  评论(1编辑  收藏  举报