the Art of Network-flows(网络流24题小结 chapter I)

飞行员配对方案问题

原题链接:洛谷P2756
——题意——
现在有 n ( &lt; 100 ) n(&lt;100) n(<100)名飞行员,其中 1 1 1 m m m为外籍飞行员和 m + 1 m+1 m+1 n n n为英国飞行员,已知外籍飞行员 i i i和英国飞行员 j j j可以配合(一名外籍飞行员可能与多位英国飞行员可以配合),现在需要对外籍飞行员和英国飞行员一一配对,求出最多的匹配队数,并给出匹配方案。
——题解——
这是一道裸的二分图求最大匹配问题,也是网络流24题中最简单的。把飞行员分成外籍和英国籍两部分,每个飞行员看成一个点,对于可以匹配的飞行员之间,由外籍飞行员向英国飞行员连一条容量为 1 1 1的边。建立一个源点,向各个外籍飞行员连一条容量为 1 1 1的边;建立一个汇点,各个英国飞行员向其连一条容量为 1 1 1的边。对于样例,我们建立如下图所示的二分图:
在这里插入图片描述
(图中,所有边流量都为1,从左指向右,红线为匹配方案)
——Code——

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;

const int INF = 0x7fffffff;
struct EDGE{
	int v;
	int c;
	int rev;
	EDGE(int x,int y,int z):v(x),c(y),rev(z){}
};
vector<EDGE> edge[101]; 
queue<int> q;
bool vis[101];
int match[101];
int n,m;

int dinic_dfs(int u,int flow){
	if(u==n+1) return flow;
	vis[u] = true;
	for(unsigned int i=0;i<edge[u].size();++i){
		EDGE &e=edge[u][i];
		if( e.c>0 && !vis[e.v] ){
			int d = dinic_dfs(e.v,min(flow,e.c));
			if( d>0 ){
				e.c -= d;
				edge[e.v][e.rev].c +=d;
				return d;
			}
		}
	}
	return 0;	
}

int main(){
	scanf("%d %d",&m,&n);
	for(int i=1;i<=m;++i){
		edge[0].push_back(EDGE(i,1,0));
		edge[i].push_back(EDGE(0,0,i-1));
	}
	for(int i=m+1;i<=n;++i){
		edge[i].push_back(EDGE(n+1,1,i-m-1));
		edge[n+1].push_back(EDGE(i,0,0));
	}
	int u,v;
	while(~scanf("%d %d",&u,&v)){
		if(u==-1 && v==-1) break;
		edge[u].push_back(EDGE(v,1,edge[v].size()));
		edge[v].push_back(EDGE(u,0,edge[u].size()-1));
	}
	int sumflow = 0;
	while(true){
		memset(vis,0,sizeof(vis));
		int addflow = dinic_dfs(0,INF);
		if(!addflow) break;
		sumflow +=addflow;
	}
	if(!sumflow){
		printf("No Solution!\n");
	}else{
		printf("%d\n",sumflow);
		for(int i=1;i<=m;++i){
			for(unsigned int j=1;j<edge[i].size();++j){
				if( !edge[i][j].c && edge[i][j].v!=0 && edge[i][j].v!=n+1 ){
					match[i] = edge[i][j].v;
					break;
				}	
			}
		}
		for(int i=1;i<=m;++i){
			if(match[i]&&i<match[i])
				printf("%d %d\n",i,match[i]);
		}
	}
	return 0;
}




试题库问题

原题链接:洛谷P2763
——题意——
现在有 ( 2 &lt; = ) k ( &lt; = 20 ) (2&lt;=)k(&lt;=20) (2<=)k(<=20)种题型和 ( k &lt; = ) n ( &lt; = 1000 ) (k&lt;=)n(&lt;=1000) (k<=)n(<=1000)道题目,总共需要抽取 m m m个题目组成一份试卷,其中对于每一种题型都有固定的需求个数,每道题目可能属于几种题型。现在要求找到一种组成试卷的方案。
——题解——
这道题很好建模,把题目看做点集一,题型看做点集二。若某道题从属于某题型,则从题目向题型连一条容量为 1 1 1的有向边。从源点向各个题目连接一条容量为 1 1 1的边,各个题型向汇点连一条容量等同于需求量的边。一条从源点指向汇点的流表示,选取某题目,求把该题归为某题型。如果最后流向汇点的边都满流,则存在可行方案。
——Code——

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <vector>
using namespace std;

const int INF = 0X7FFFFFFF;
struct EDGE{
	int v;
	int c;
	int rev;
	EDGE(int x,int y,int z):v(x),c(y),rev(z){}
};
vector<EDGE> edge[1024];
vector<int>  showAns[21];
bool vis[1024];
int match[1024];
int n,k;
int Dinic_dfs(int u){
	if(u==k+n+2) return 1;
	vis[u] = true;
	for(unsigned int i=0;i<edge[u].size();++i){
		EDGE &e = edge[u][i];
		if( e.c>0 && !vis[e.v] && Dinic_dfs(e.v)){
			e.c -= 1;
			edge[e.v][e.rev].c +=1;
			match[u] = e.v;
			return 1;
		}
	}
	return 0;
}

int main(){
	scanf("%d %d",&k,&n);
	int checkFlow = 0;
	for(int i=1;i<=k;++i){
		int cap;
		scanf("%d",&cap);
		checkFlow += cap;
		edge[n+i].push_back(EDGE(n+k+2,cap,edge[k+n+2].size()));
		edge[k+n+2].push_back(EDGE(n+i,0,edge[n+i].size()-1));
	}
	for(int i=1;i<=n;++i){
		edge[k+n+1].push_back(EDGE(i,1,edge[i].size()));
		edge[i].push_back(EDGE(k+n+1,0,edge[k+n+1].size()-1));
		int num;
		scanf("%d",&num);
		for(int j=1;j<=num;++j){
			int to;
			scanf("%d",&to);
			edge[i].push_back(EDGE(to+n,1,edge[to+n].size()));
			edge[to+n].push_back(EDGE(i,0,edge[i].size()-1));
		}
	}
	int sumFlow = 0;
	while(true){
		memset(vis,0,sizeof vis);
		int addFlow = Dinic_dfs(k+n+1);
		if(!addFlow) break;
		sumFlow += addFlow; 		
	}
	if(sumFlow < checkFlow){
		printf("No Solution!\n");
	}else{
	//	for(int i=1;i<=n;++i){
	//		cout<<match[i]<<" ";
	//	}
		for(int i=1;i<=n;++i){
			if(match[i]){
				showAns[match[i]-n].push_back(i);
			}
		}
		for(int i=1;i<=k;++i){
			printf("%d:",i);
			for(unsigned int j=0;j<showAns[i].size();++j){
				printf(" %d",showAns[i][j]);
			}
			printf("\n");
		}
	}
	return 0;
} 




方格取数问题

原题链接:洛谷P2774
——题意——
给出一个 m ( &lt; = 100 ) m(&lt;=100) m(<=100) n ( &lt; = 100 ) n(&lt;=100) n(<=100)列的棋盘,每个格子上都有一个权值为正的棋子,你需要取走其中任意数量的棋子,取走的棋子之间不能有公共边,求出取出棋子的最大权值和。
——题解——
在建模之前,需要弄明白一些细节。所有棋子的权值总和是一定的,取走棋子的过程,可以看做舍弃其它棋子的过程。取走棋子之间没有公共边,即相邻的棋子最多去掉其中一个,我们当然是希望去掉权值小的那一枚。如此,不妨先把棋盘中的棋子拆成互相没有公共边的两个点集,用一个源点连向点集一中的各个点,各边容量为棋子权值;在用一个汇点,使点集二中的点指向它,容量为棋子权值。对于两个点集,如果棋子之间相邻,则对应的点之间连一条流量 I N F INF INF的边,由点集一连向点集二。这样以来,一条从原点往汇点的流,表示删除两个相邻棋子之间权值较小的棋子。于是,发挥网络流模型可以不断调整的优势,走一遍二分图最大流,即可得出去掉棋子权值和的最小值。以样例为例,建图如下:
在这里插入图片描述
(图中,所有边从左指向右)
——Code——

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <vector>
#define st 0
#define ed m*n+1
using namespace std;

const int INF = 0X7FFFFFFF;
int dx[] = {0,1,0,-1};
int dy[] = {1,0,-1,0};
struct EDGE{
	int v;
	int c;
	int rev;
	EDGE(int x,int y,int z):v(x),c(y),rev(z){}
};
vector<EDGE> edge[10004];
int mp[102][102];
bool vis[10004];
int m,n;

inline int node(int x,int y){return n*(x-1)+y;}
inline void build(int u,int v,int c){
	edge[u].push_back(EDGE(v,c,edge[v].size()));
	edge[v].push_back(EDGE(u,0,edge[u].size()-1));
}
int Dinic_dfs(int u,int flow){
	if(u==ed) return flow;
	vis[u] = true;
	for(unsigned int i=0;i<edge[u].size();++i){
		EDGE &e = edge[u][i];
		if( e.c>0 && !vis[e.v] ){
			int d = Dinic_dfs(e.v,min(flow,e.c));
			if(d>0){
				e.c -= d;
				edge[e.v][e.rev].c += d;
				return d;
			}
		}
	}
	return 0;
}
int main(){
	int sum = 0; 
	scanf("%d %d",&m,&n);
	for(int i=1;i<=m;++i){
		for(int j=1;j<=n;++j){
		 	scanf("%d",&mp[i][j]);
		 	sum += mp[i][j];
		}
	}
	for(int i=1;i<=m;++i){
		for(int j=1;j<=n;++j){
			if((i+j)&1){
				build(st,node(i,j),mp[i][j]);
				for(int k=0;k<4;++k){
					int x = i+dx[k];
					int y = j+dy[k];
					if( x>=1 && x<=m && y>=1 && y<=n ){
						build(node(i,j),node(x,y),INF);
					}
				}
			}else{
				build(node(i,j),ed,mp[i][j]);
			}
		}
	}
	int sumFlow = 0;
	while(true){
		memset(vis,0,sizeof vis);
		int addFlow = Dinic_dfs(st,INF);
		if(!addFlow) break;
		sumFlow += addFlow;
	}
	printf("%d\n",sum-sumFlow);
	return 0;
} 




最小路径覆盖问题

原题链接:洛谷P2764
——题意——
给出一个有向无环图,其中结点数为 n ( &lt; = 150 ) n(&lt;=150) n(<=150),边数为 m ( &lt; = 6000 ) m(&lt;=6000) m(<=6000),要求用最少的路径,使图中所有的点都恰好被经过一次。输出每一条路径各自经过的结点。
——题解——
在学网络流之前,很容易想到这题需要用一种贪心策略来解决,即先找出一条最长的路径,然后再找次长路径,直到图中只剩下一些孤立点。但是,当我们用直接贪心找到一条最长路径时,这条路径上包含哪些点是确定的,或者说存在多条长度相同但是包含结点不同的路径,那么这个时候,怎么保证直接贪心得出的路径就一定包含在最优解之中呢?网络流模型有一个好处,即使当前找到一条路径,在后续增加流量时,依旧能在不改变路径长度的情况下更改这条路径具体包含的结点。本题用网络流模型,可以充分利用到其自动调整的优势。首先处理“每个点只能被经过一次”,把一个结点 i i i拆成两个,分别记为 a i ai ai b i bi bi,同理,把结点集拆成两个集合,分别为A和B。如果点 i i i和点 j j j之间存在有向边,则由 a i ai ai b j bj bj连一条容量为 1 1 1的有向边。源点向所有的 a i ai ai连一条容量为 1 1 1的边,所有的 b i bi bi向汇点连一条容量为 1 1 1的边。从源点到汇点的流表示,结点 i i i连向结点 j j j的边包含在某条路径中。如此一来,输出路径只需要迭代输出每个结点的匹配结点,孤立点输出其自身。以样例为例建图:
在这里插入图片描述
(图中,每条边都从左指向右,且容量为1)

——Code——

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;

struct EDGE{
	int v;
	int c;
	int rev;
	EDGE(int x,int y,int z):v(x),c(y),rev(z){}
};
vector<EDGE> edge[303];
bool vis[303];
int match[303];
int n,m;

bool Dinic_dfs(int u){
	if( u==1 ) return true;
	vis[u] = true;
	for(unsigned int i=0;i<edge[u].size();++i){
		EDGE &e = edge[u][i];
		if( e.c>0 && !vis[e.v] && Dinic_dfs(e.v)){
			e.c -= 1;
			edge[e.v][e.rev].c +=1;
			match[u] = e.v;
			return true;
		}
	}
	return false;
}
void print(int i){
	if(i>>1==0) return;
	printf(" %d",i>>1);
	vis[i-1] = true;
	if(match[i]) print(match[i-1]);
}
int main(){
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;++i){
		edge[0].push_back(EDGE(i<<1,1,edge[i<<1].size()));
		edge[i<<1].push_back(EDGE(0,0,edge[0].size()-1));
		edge[i<<1|1].push_back(EDGE(1,1,edge[1].size()));
		edge[1].push_back(EDGE(i<<1|1,0,edge[i<<1|1].size()-1));
	}
	for(int i=1;i<=m;++i){
		int u;
		int v;
		scanf("%d %d",&u,&v);
		edge[u<<1].push_back(EDGE(v<<1|1,1,edge[v<<1|1].size()));
		edge[v<<1|1].push_back(EDGE(u<<1,0,edge[u<<1].size()-1));
	}
	int ans = 0;
	int node = n;
	while(node--){
		memset(vis,0,sizeof vis);
		if(Dinic_dfs(0)) ++ans;
	}
	memset(vis,0,sizeof vis);
	for(int i=2;i<=n*2;i+=2){
		if(!vis[i]){
			printf("%d",i>>1);
			print(match[i]);
			printf("\n");
		}
	}
	printf("%d\n",n-ans);
	return 0;
}




魔术球问题

——题意——
现在给你 n ( &lt; = 55 ) n(&lt;=55) n(<=55)根柱子,把数字对应的小球叠着放在柱子上。小球之间叠加的规则是:相邻两个小球编号之和为一个平方数。小球的编号从 1 1 1开始,现在需要求出这 n n n根柱子最多能承载多少小球。‘
——题解——
’本题是上面最小路径覆盖问题的一个变式。可以反向思考,对于一个特定数量的小球,最少需要多少根柱子。因此和上题一样,把一个小球拆成两个点,能构成平方数的小球,则从编号小的连向编号大的。每次新加入一个小球计算最小需要的柱子数,如果柱子数大于 n n n,则当前小球编号减 1 1 1即为 n n n根柱子承载的最大数量。利用网络流的优势,新加入的球在之前的残余网络上进行增广,所以复杂度并不会很高。
——Code——

#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
#include <vector>
#include <cmath>
using namespace std;

const int INF = 0x7FFFFFFF;
struct EDGE{
	int v;
	int c;
	int rev;
	EDGE(int x,int y,int z):v(x),c(y),rev(z){}
};
vector<EDGE> edge[4003];
int match[4003];
bool vis[4003];
int n;

bool Dinic_dfs(int u){
	if(u==1) return true;
	vis[u] = true;
	for(unsigned int i=0;i<edge[u].size();++i){
		EDGE &e = edge[u][i];
		if( e.c>0 && !vis[e.v] ){
			if(Dinic_dfs(e.v)){
				e.c -= 1;
				edge[e.v][e.rev].c += 1;
				match[u] = e.v;
				return true;
			}
		}
	}
	return false;
}
void print(int i){
	if( i>>1 == 0 ) return;
	printf(" %d",i>>1);
	vis[i-1] = true;
	if(match[i]) print(match[i-1]);
}

int main(){
	scanf("%d",&n);
	int zhuzi = 0;
	for(int in=1;;++in){
		edge[0].push_back(EDGE(in<<1,1,edge[in<<1].size()));
		edge[in<<1].push_back(EDGE(0,0,edge[0].size()-1));
		edge[in<<1|1].push_back(EDGE(1,1,edge[1].size()));
		edge[1].push_back(EDGE(in<<1|1,0,edge[in<<1|1].size()-1));
		for(int i=1;i<in;++i){
			if((long long)sqrt((double)(i+in))*(long long)sqrt((double)(i+in)) == (long long)i+in){
				edge[i<<1].push_back(EDGE(in<<1|1,1,edge[in<<1|1].size()));
				edge[in<<1|1].push_back(EDGE(i<<1,0,edge[i<<1].size()-1));
			}
		}
		memset(vis,0,sizeof vis);
		if(!Dinic_dfs(0)) ++zhuzi;
		if(zhuzi>n){
			printf("%d\n",in-1);
			memset(vis,0,sizeof vis);
			for(int i=2;i<in*2-1;i+=2){
				if(!vis[i]){
					printf("%d",i>>1);
					print(match[i]);
					printf("\n");
				}
			}
			cout<<endl;
			break;
		}
	}
	return 0;
}

最长不下降子序列问题

原题链接:洛谷P2766
——题意——
给出一个长度为 n ( &lt; = 500 ) n(&lt;=500) n(<=500)的序列, x 1... x n x1...xn x1...xn,求解三个问题:
1)最长不下降子序列的长度;
2)原序列中最多可以取多少个这样的最长子序列;
3)如果 x 1 x1 x1 x n xn xn可以重复利用,最多有多少个最长子序列。
——题解——
第一小问可以用动态规划解决 ( d p ) (dp) (dp),得出最大长度 l e n len len,顺便得出以每个点为结尾的序列的最长子序列长度(相当于一个拓扑序)。
第二小问,利用上面得出的序列,建立一张拓扑排序图(如果 i &gt; j 且 d p [ i ] = = d p [ j ] + 1 且 x [ i ] &gt; = x [ j ] i&gt;j且dp[i]==dp[j]+1且x[i]&gt;=x[j] i>jdp[i]==dp[j]+1x[i]>=x[j] j j j i i i连一条容量为 1 1 1的有向边),源点向 d p = 1 dp=1 dp=1的点连一条容量为 1 1 1的有向边, d p = l e n dp=len dp=len的点向汇点连接一条容量为 1 1 1的有向边。为了保证每个点只被使用一次,把每个点拆成两份, a i ai ai b i bi bi连一条容量为 1 1 1的有向边,若满足拓扑序,则由 b j bj bj连向 a i ai ai
第三小问,再上图的基础上,把 x 1 x1 x1的两个拆分的之间以及与源点的边容量改成INF, x n xn xn同理。注意 n = 1 n=1 n=1时,需要特判,参见代码。
以序列 1 、 5 、 8 、 2 、 6 1、5、8、2、6 15826为例建图:
在这里插入图片描述
(图中所有边从左指向右)
——Code——

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>
#define st 0
#define ed n+1
using namespace std;

const int INF = 0X7FFFFFFF;
struct EDGE{
	int v;
	int c;
	int rev;
	EDGE(int x,int y,int z):v(x),c(y),rev(z){}
};
vector<EDGE> edge[1024];
int iter[1024];
int level[1024];
int num[1024];
int dp[1024];
int n;
int ans1,ans2,ans3;

inline void build(int u,int v,int c){
	edge[u].push_back(EDGE(v,c,edge[v].size()));
	edge[v].push_back(EDGE(u,0,edge[u].size()-1));
}
inline int b(int i){return i+n+1;}
void Dinic_bfs(){
	for(int i=0;i<=n+1;++i){
		level[i] = level[b(i)] = -1;
	}
	level[st] = 1;
	queue<int> q;
	q.push(st);
	while(!q.empty()){
		int u = q.front();
		q.pop();
		for(unsigned int i=0;i<edge[u].size();++i){
			EDGE &e = edge[u][i];
			if(level[e.v]<0 && e.c>0){
				level[e.v] = level[u]+1;
				q.push(e.v);
			}
		}
	}
}
int Dinic_dfs(int u){
	if(u==ed) return 1;
	for(int &i=iter[u];i<(int)edge[u].size();++i){
		EDGE &e = edge[u][i];
		if(e.c>0 && level[u]<level[e.v] && Dinic_dfs(e.v)){
			e.c -= 1;
			edge[e.v][e.rev].c += 1;
			return 1;
		}
	}
	return 0;
}
int Dinic(){
	int ret = 0;
	while(true){
		Dinic_bfs();
		if(level[ed]<0) break;
		memset(iter,0,sizeof iter);
		while(true){
			int addFlow = Dinic_dfs(st);
			if(!addFlow) break;
			ret += addFlow;
		}
	}
	return ret;
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		scanf("%d",&num[i]);
	}
	/************Q1************/
	for(int i=1;i<=n;++i){
		dp[i] = 1;
		for(int j=i-1;j>0;--j){
			if(num[j]<=num[i]) dp[i] = max(dp[i],dp[j]+1); 
		}
		ans1 = max(ans1,dp[i]);
	}
	/************Q2************/
	for(int i=1;i<=n;++i){
		build(i,b(i),1);
		if(dp[i]==1){
			build(st,i,1);
		}
		if(dp[i]==ans1){
			build(b(i),ed,1);
		}
		for(int j=1;j<i;++j){
			if( num[j]<=num[i] && dp[j]+1==dp[i] ){
				build(b(j),i,1);
			}
		}
	}
	ans2 = Dinic();
	/************Q3************/
	build(st,1,INF);
	build(1,b(1),INF);
	if(dp[n]==ans1&&n!=1){
		build(b(n),ed,INF);
		build(n,b(n),INF);
	}
	ans3 = ans2+Dinic();
	/************A************/
	printf("%d\n%d\n%d\n",ans1,ans2,ans3);
	return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值