飞行员配对方案问题
原题链接:洛谷P2756
——题意——
现在有
n
(
<
100
)
n(<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
<
=
)
k
(
<
=
20
)
(2<=)k(<=20)
(2<=)k(<=20)种题型和
(
k
<
=
)
n
(
<
=
1000
)
(k<=)n(<=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
(
<
=
100
)
m(<=100)
m(<=100)行
n
(
<
=
100
)
n(<=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
(
<
=
150
)
n(<=150)
n(<=150),边数为
m
(
<
=
6000
)
m(<=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
(
<
=
55
)
n(<=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
(
<
=
500
)
n(<=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
>
j
且
d
p
[
i
]
=
=
d
p
[
j
]
+
1
且
x
[
i
]
>
=
x
[
j
]
i>j且dp[i]==dp[j]+1且x[i]>=x[j]
i>j且dp[i]==dp[j]+1且x[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
1、5、8、2、6为例建图:
(图中所有边从左指向右)
——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;
}