0. 前言
网络流算是在OI中一个博大精深的问题了。使用它解题的关键就是知道如何建图。网络流24题就是其中建图比较典型的题目了。下面我将按照我刷题的顺序写下每道题的解题报告。
注:部分题目在LOJ上题面和输入格式与洛谷上的有出路,此处以洛谷题面为准。
0.5 一些约定
( u , v , f ) (u,v,f) (u,v,f) 一条从 u u u 到 v v v,流量为 f f f 的边。
( u , v , f , c ) (u,v,f,c) (u,v,f,c) 一条从 u u u 到 v v v,流量为 f f f,费用为 c c c 的边。
1. 飞行员配对问题
题目链接:洛谷P2756 或 LOJ 6000
题意:有 m + n m+n m+n 个飞行员,其中 m m m 个为英国皇家飞行员, n n n 个为外籍飞行员。已知有若干对皇家飞行员和外籍飞行员可以配对。求最多可以配成多少对,并输出方案。 1 ≤ n , m ≤ 100 1 \leq n,m \leq 100 1≤n,m≤100。
题解:二分图最大匹配模板。但是由于是网络流24题,所以我这里写的是网络流的方法。
超级源点 S S S 与 1 1 1 至 m m m 连流量为 1 1 1 的边, m + 1 m+1 m+1 至 n n n 与超级汇点 T T T 连流量为 1 1 1 的边,中间连题目中所给的边,流量为 + ∞ +\infty +∞。
至于输出方案,只要检查对于中读入的边,流过去的流量是否非零,即反向边的容量非零即可。
代码:
/*
数据不清空,爆零两行泪。
多测不读完,爆零两行泪。
边界不特判,爆零两行泪。
贪心不证明,爆零两行泪。
D P 顺序错,爆零两行泪。
大小少等号,爆零两行泪。
变量不统一,爆零两行泪。
越界不判断,爆零两行泪。
调试不注释,爆零两行泪。
溢出不 l l,爆零两行泪。
*/
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define fz(i,a,b) for(int i=a;i<=b;i++)
#define fd(i,a,b) for(int i=a;i>=b;i--)
#define put(x) putchar(x)
#define eoln put('\n')
#define space put(' ')
inline int read(){
int x=0,neg=1;char c=getchar();
while(!isdigit(c)){
if(c=='-') neg=-1;
c=getchar();
}
while(isdigit(c)) x=x*10+c-'0',c=getchar();
return x*neg;
}
inline void print(int x){
if(x<0){
putchar('-');
print(abs(x));
return;
}
if(x<=9) putchar(x+'0');
else{
print(x/10);
putchar(x%10+'0');
}
}
int n,m;
int ecnt=1,head[100005],dep[100005];
struct edge{
int to,next,cap;
} e[100005];
bool b[100005];
inline void addedge(int u,int v,int f){
e[++ecnt].to=v;e[ecnt].cap=f;e[ecnt].next=head[u];head[u]=ecnt;
}
inline bool bfs(int s,int t){
queue<int> q;
memset(dep,-1,sizeof(dep));
q.push(s);dep[s]=0;
while(!q.empty()){
int now=q.front();q.pop();
for(int i=head[now];i;i=e[i].next){
int v=e[i].to;
if(dep[v]==-1&&e[i].cap){
dep[v]=dep[now]+1;
q.push(v);
}
}
}
if(dep[t]!=-1) return 1;
return 0;
}
inline int dfs(int x,int t,int f){
if(x==t) return f;
int w,ret=0;
for(int i=head[x];i;i=e[i].next){
int v=e[i].to;
if(dep[v]==dep[x]+1&&e[i].cap){
w=dfs(v,t,min(f-ret,e[i].cap));
e[i].cap-=w;
e[i^1].cap+=w;
ret+=w;
if(ret==f) return f;
}
}
if(!ret) dep[x]=-1;
return ret;
}
int dinic(int s,int t){
int tot=0;
while(bfs(s,t))
tot+=dfs(s,t,0x3f3f3f3f);
return tot;
}
int main(){
//源点0,汇点n+1
scanf("%d%d",&m,&n);
while(1){
int x,y;scanf("%d%d",&x,&y);
if(x==-1&&y==-1) break;
addedge(x,y,0x3f3f3f3f);//建中间的边
addedge(y,x,0);
}
for(int i=1;i<=m;i++) addedge(0,i,1),addedge(i,0,0);//建源点连向1~m的边
for(int i=m+1;i<=n;i++) addedge(i,n+1,1),addedge(n+1,i,0);//建m+1~n连向汇点的边
int tot=dinic(0,n+1);
if(tot==0){
puts("No Solution!");
return 0;
}
printf("%d\n",tot);
for(int i=2;i<=ecnt;i=i+2){
if(e[i].to!=n+1&&e[i^1].to!=0)
if(e[i].to!=n+1&&e[i^1].to!=n+1)//判断是否为中间的边
if(e[i^1].cap!=0){
//如果反向边容量不为0
printf("%d %d\n",e[i^1].to,e[i].to);
}
}
return 0;
}
2. 软件补丁问题
题目链接:洛谷P2761 或 LOJ 6009
题意:有一个公司要应对软件中的 n n n 个 b u g bug bug ,有 m m m 个修复 b u g bug bug 的程序,对于每个程序有四个集合 b i , 1 , b i , 2 , f i , 1 , f i , 2 b_{i,1},b_{i,2},f_{i,1},f_{i,2} bi,1,bi,2,fi,1,fi,2,如果当前 b u g bug bug 的集合包含 b i , 1 b_{i,1} bi,1 中的所有 b u g bug bug 并且不包含 b i , 2 b_{i,2} bi,2 中的所有 b u g bug bug 的时候,使用这个程序可以花费 t i t_i ti 的时间修复 f i , 1 f_{i,1} fi,1 中的所有 b u g bug bug,而又会新增添 f 2 , i f_{2,i} f2,i 中的所有 b u g bug bug。求修复所有 b u g bug bug 需要的最小时间。 1 ≤ n ≤ 20 1 \leq n \leq 20 1≤n≤20, 1 ≤ m ≤ 100 1 \leq m \leq 100 1≤m≤100。
题解:这道题不是网络流啊!
状压 d p dp dp, d p i dp_i dpi 表示当 b u g bug bug 集合为 i i i 的时候所需要的时间,最短路转移,就可以过了。
/*
数据不清空,爆零两行泪。
多测不读完,爆零两行泪。
边界不特判,爆零两行泪。
贪心不证明,爆零两行泪。
D P 顺序错,爆零两行泪。
大小少等号,爆零两行泪。
变量不统一,爆零两行泪。
越界不判断,爆零两行泪。
调试不注释,爆零两行泪。
溢出不 l l,爆零两行泪。
*/
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define fz(i,a,b) for(int i=a;i<=b;i++)
#define fd(i,a,b) for(int i=a;i>=b;i--)
#define put(x) putchar(x)
#define eoln put('\n')
#define space put(' ')
inline int read(){
int x=0,neg=1;char c=getchar();
while(!isdigit(c)){
if(c=='-') neg=-1;
c=getchar();
}
while(isdigit(c)) x=x*10+c-'0',c=getchar();
return x*neg;
}
inline void print(int x){
if(x<0){
putchar('-');
print(abs(x));
return;
}
if(x<=9) putchar(x+'0');
else{
print(x/10);
putchar(x%10+'0');
}
}
int n,m,a[105],dp[1<<21],b1[105],b2[105],f1[105],f2[105];
bool legal(int s,int i){
//判断状态为s的时候是否可以使用程序i
if((b1[i]|s)==s&&!(b2[i]&s)) return true;
return false;
}
void spfa(){
queue<int> q;
q.push((1<<n)-1);
dp[(1<<n)-1]=0;
while(!q.empty()){
int s=q.front();
q.pop();
for(int i=1;i<=m;i++)
if(legal(s,i)){
int t=(s&(~f1[i]))|f2[i];
if(dp[s]+a[i]<dp[t]){
dp[t]=dp[s]+a[i];//最短路转移
q.push(t);
}
}
}
}
int main(){
memset(dp,1,sizeof(dp));
cin>>n>>m;
for(int i=1;i<=m&&cin>>a[i];i++){
char ch;
for(int j=1;j<=n;j++){
char ch;cin>>ch;
if(ch=='+')
b1[i]+=1<<j-1;
else if(ch=='-')
b2[i]+=1<<(j-1);
}
for(int j=1;j<=n;j++){
char ch;cin>>ch;
if(ch=='-')
f1[i]+=1<<(j-1);
else if(ch=='+')
f2[i]+=1<<(j-1);
}
}
spfa();
cout<<(dp[0]==0x01010101?0:dp[0])<<endl;
return 0;
}
3. 餐巾计划问题
题目链接:洛谷P1251 或 LOJ 6008
题意:有一个餐厅在 n n n 天中第 i i i 天要用 r i r_i ri 块餐巾。餐厅可以购买新的餐巾,每块餐巾的费用为 p p p 分,或者把用过的餐巾送到快洗部,洗一块需 m m m 天,费用为 f f f 分,或者送到慢洗部,洗一块需 l l l 天,费用为 s s s 分。 1 ≤ n ≤ 2000 1 \leq n \leq 2000 1≤n≤2000。
题解:费用流的经典题,建图较为恶心。
看了洛谷的题解
我们将每一天拆成早上和晚上两个点,每天晚上会受到用过的餐巾,每天早上又会受到干净的餐巾。
按以下方式建图:
- 从源点 S S S 向每一天晚上表示的节点 j j j 连一条边 ( S , j , r i , 0 ) (S,j,r_i,0) (S,j,ri,0),表示每天夜晚获得 r i r_i ri 条用过的餐巾。
- 从每一天早上表示的节点 j j j 向汇点 T T T 连一条边 ( j , T , r i , 0 ) (j,T,r_i,0) (j,T,ri,0) 表示向汇点提供 r i r_i ri 条干净的餐巾,流满时表示第 i i i 天的餐巾够用。
- 从每一天晚上 j j j 向第二天晚上 k k k 连一条边 ( j , k , + ∞ , 0 ) (j,k,+\infty,0) (j,k,+∞,0),表示每天晚上可以将用过的餐巾留到第二天晚上。
- 从每一天晚上 j j j 向这一天过了快洗所用天数 m m m 的那一天早上 k k k 连一条边 ( j , k , + ∞ , f ) (j,k,+\infty,f) (j,k,+∞,f),表示每天晚上可以将用过的餐巾送去快洗部,在第 i + m i+m i+m 天早上收到干净的餐巾。
- 同理,从每一天晚上 j j j 向这一天过了慢洗所用天数 l l l 的那一天早上 k k k 连一条边 ( j , k , + ∞ , s ) (j,k,+\infty,s) (j,k,+∞,s),意义同上。
- 从源点 S S S 向每一天早上 j j j 连一条边 ( S , j , + ∞ , p ) (S,j,+\infty,p) (S,j,+∞,p),表示每天早上可以购买干净的餐巾。
然后跑最小费用最大流。
这里用到了一个最小(大)费用最大流的一个重要思想,最大流保证构造满足题目条件(如本题中每一天早上向汇点连边,流满表示够用,如果不流满就不合法了),最小(大)费用保证代价最小。
/*
数据不清空,爆零两行泪。
多测不读完,爆零两行泪。
边界不特判,爆零两行泪。
贪心不证明,爆零两行泪。
D P 顺序错,爆零两行泪。
大小少等号,爆零两行泪。
变量不统一,爆零两行泪。
越界不判断,爆零两行泪。
调试不注释,爆零两行泪。
溢出不 l l,爆零两行泪。
*/
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define fz(i,a,b) for(int i=a;i<=b;i++)
#define fd(i,a,b) for(int i=a;i>=b;i--)
#define put(x) putchar(x)
#define eoln put('\n')
#define space put(' ')
#define int long long
inline int read(){
int x=0,neg=1;char c=getchar();
while(!isdigit(c)){
if(c=='-') neg=-1;
c=getchar();
}
while(isdigit(c)) x=x*10+c-'0',c=getchar();
return x*neg;
}
inline void print(int x){
if(x<0){
putchar('-');
print(abs(x));
return;
}
if(x<=9) putchar(x+'0');
else{
print(x/10);
putchar(x%10+'0');
}
}
int ecnt=1,head[100005];
struct edge{
int to,nxt,cap,cost;
} e[100005];
inline void addedge(int u,int v,int f,int c){
e[++ecnt].to=v;e[ecnt].cap=f;e[ecnt].cost=c;e[ecnt].nxt=head[u];head[u]=ecnt;
}
bool vis[100005];
int dist[100005],flow[100005],pre[100005],pos[100005];
inline bool spfa(int s,int t){
memset(vis,1,sizeof(vis));
memset(dist,63,sizeof(dist));
queue<int> q;
vis[s]=0;
dist[s]=0;
flow[s]=0x3f3f3f3f;
q.push(s);
while(!q.empty()){
int x=q.front();
// cout<<x<<endl;
vis[x]=true;
for(int i=head[x];i;i=e[i].nxt){
int y=e[i].to;
// cout<<y<<endl;
if(e[i].cap>0&&dist[y]>dist[x]+e[i].cost){
dist[y]=dist[x]+e[i].cost;pos[y]=x;pre[y]=i;
flow[y]=min(flow[x],e[i].cap);
// cout<<flow[y]<<endl;
if(vis[y]){
q.push(y);
vis[y]=false;
}
}
}
q.pop();
}
// cout<<flow[t]<<endl;
return dist[t]<0x3f3f3f3f3f3f3f3fll;
}
inline int Dinic(int s,int t){
int sum=0;
while(spfa(s,t)){
sum+=flow[t]*dist[t];
for(int i=t;i!=s;i=pos[i]){
e[pre[i]].cap-=flow[t];
e[pre[i]^1].cap+=flow[t];
}
}
return sum;
}
int n,m,t1,t2,m1,m2;
signed main(){
//源点0,汇点2n+1
scanf("%lld",&n);
int st=0,ed=2*n+1;
for(int i=1;i<=n;i++){
int x;scanf("%lld",&x);
addedge(st,i,x,0);addedge(i,st,0,0);//条件1
addedge(i+n,ed,x,0);addedge(ed,i+n,0,0);//条件2
}
scanf("%lld%lld%lld%lld%lld",&m,&t1,&m1,&t2,&m2);
for(int i=1;i<=n;i++){
if(i+1<=n) addedge(i,i+1,1e9,0),addedge(i+1,i,0,0); //条件3
if(i+t1<=n) addedge(i,i+n+t1,1e9,m1),addedge(i+n+t1,i,0,-m1);//条件4
if(i+t2<=n) addedge(i,i+n+t2,1e9,m2),addedge(i+n+t2,i,0,-m2);//条件5
addedge(st,i+n,1e9,m);addedge(i+n,st,0,-m);//条件6
}
cout<<Dinic(st,ed)<<endl;
return 0;
}
4. 深海机器人问题
题目链接:洛谷P4012 或 LOJ 6224
题意:有一张网格图,左下角 ( 0 , 0 ) (0,0) (0,0),右上角 ( Q , P ) (Q,P) (Q,P),有 a a a 个出发位置,对于每一个位置给出三个数 k , x , y k,x,y k,x,y,表示有 k k k 个机器人从 ( x , y ) (x,y) (x,y) 出发。有 b b b 个终点,也给出三个数 k , x , y k,x,y k,x,y,表示有 k k k 个机器人要到 ( x , y ) (x,y) (x,y)。每个机器人可以向上或向右走到终点。每条向右或向上的路径上都有一个生物标本,采集一个生物标本可以获得一些价值。一条路上如果生物标本已被采集过就没有了。求总价值的最大值。
题解:费用流的气息很明显。
建出图来,从源点连向每一个起点连一条边 ( S , i , k , 0 ) (S,i,k,0) (S,i,k,0),再从每一个终点连向汇点连一条边 ( i , T , k , 0 ) (i,T,k,0) (i,T,k,0)。对于每一条网格图中的道路 ( x , y , z ) (x,y,z) (x,y,z),连两条边 ( x , y , 1 , z ) (x,y,1,z) (x,y,1,z) 与 ( x , y , ∞ , 0 ) (x,y,\infty,0) (x,y,∞,0),因为道路可以通过多次,但标本只能收集一次。然后跑最大费用最大流。这里有一个技巧,将每条边的价值都取相反数,这样就变成了一个最小费用最大流,然后答案再取相反数就行了。
/*
数据不清空,爆零两行泪。
多测不读完,爆零两行泪。
边界不特判,爆零两行泪。
贪心不证明,爆零两行泪。
D P 顺序错,爆零两行泪。
大小少等号,爆零两行泪。
变量不统一,爆零两行泪。
越界不判断,爆零两行泪。
调试不注释,爆零两行泪。
溢出不 l l,爆零两行泪。
*/
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define fz(i,a,b) for(int i=a;i<=b;i++)
#define fd(i,a,b) for(int i=a;i>=b;i--)
#define put(x) putchar(x)
#define eoln put('\n')
#define space put(' ')
#define int long long//开long long
inline int read(){
int x=0,neg=1;char c=getchar();
while(!isdigit(c)){
if(c=='-') neg=-1;
c=getchar();
}
while(isdigit(c)) x=x*10+c-'0',c=getchar();
return x*neg;
}
inline void print(int x){
if(x<0){
putchar('-');
print(abs(x));
return;
}
if(x<=9) putchar(x+'0');
else{
print(x/10);
putchar(x%10+'0');
}
}
int ecnt=1,head[100005];
struct edge{
int to,nxt,cap,cost;
} e[100005];
inline void addedge(int u,int v,int f,int c){
e[++ecnt].to=v;e[ecnt].cap=f;e[ecnt].cost=c;e[ecnt].nxt=head[u];head[u]=ecnt;
e[++ecnt].to=u;e[ecnt].cap=0;e[ecnt].cost=-c;e[ecnt].nxt=head[v];head[v]=ecnt;
}
bool vis[100005];
int dist[100005],flow[100005],pre[100005],pos[100005];
inline bool spfa(int s,int t){
memset(vis,1,sizeof(vis));
memset(dist,63,sizeof(dist));
queue<int> q;
vis[s]=0;
dist[s]=0;
flow[s]=0x3f3f3f3f;
q.push(s);
while(!q.empty()){
int x=q.front();
// cout<<x<<endl;
vis[x]=true;
for(int i=head[x];i;i=e[i].nxt){
int y=e[i].to;
// cout<<y<<endl;
if(e[i].cap>0&&dist[y]>dist[x]+e[i].cost){
dist[y]=dist[x]+e[i].cost;pos[y]=x;pre[y]=i;
flow[y]=min(flow[x],e[i].cap);
// cout<<flow[y]<<endl;
if(vis[y]){
q.push(y);
vis[y]=false;
}
}
}
q.pop();
}
// cout<<flow[t]<<endl;
return dist[t]<0x3f3f3f3f3f3f3f3fll;
}
inline int Dinic(int s,int t){
int sum=0;
while(spfa(s,t)){
sum+=flow[t]*dist[t];
for(int i=t;i!=s;i=pos[i]){
e[pre[i]].cap-=flow[t];
e[pre[i]^1].cap+=flow[t];
}
}
return sum;
}
int a=read(),b=read(),n=read(),m=read();
inline int id(int x,int y){
return (m+1)*x+y+1;
}
signed main(){
//源点1234,汇点5678(不要问我为什么)
fz(i,0,n){
fz(j,0,m-1){
int num=read();
addedge(id(i,j),id(i,j+1),1,-num);
addedge(id(i,j),id(i,j+1),0x3f3f3f3f,0);//向每个点东边节点连边
}
}
fz(i,0,m){
fz(j,0,n-1){
int num=read();
addedge(id(j,i),id(j+1,i),1,-num);
addedge(id(j,i),id(j+1,i),0x3f3f3f3f,0);//向每个点南边节点连边
}
}
fz(i,1,a){
int k=read(),x=read(),y=read();
addedge(1234,id(x,y),k,0);//从源点向每个起点连边
}
fz(i,1,b){
int k=read(),x=read(),y=read();
addedge(id(x,y),5678,k,0);//从每个终点向汇点连边
}
cout<<-Dinic(1234,5678)<<endl;
return 0;
}
5. 方格取数问题
题目链接:洛谷P2774 或 LOJ 6007
题意:有一个 n × m n \times m n×m 的矩阵,你可以从中选取一些元素使得任意两个数都不相邻,求和的最大值。
解法:网络流之最小割
我们考虑先选中所有方格,再想办法删去权值和尽量小的一批方格。
我们可以建一张图,节点表示矩阵中每一个元素,两节点之间有一条边表示这两个点相邻。我们不难发现,当把矩阵进行黑白间隔染色,相邻两点一定是一黑一白,因此我们构造出的矩阵一定是一个二分图。
那么思路就出来了。我们建一个虚拟源点 S S S,和虚拟汇点 T T T,我们对于所有白色格子上的点,连一条从 S S S 到这个点的边,流量为点权,删掉这条边,就表明不选这个点。同理,对于所有黑色格子上的点,连一条从这个点到 T T T 的边,流量为点权,删掉这条边也表明不选这个点。而二分图内部连着互斥的点,边权为 i n f inf inf (即删掉这条边没有意义),那么我们要求的就是这张图的最小割,根据最大流 = = = 最小割可以通过跑一次最大流求出答案。
/*
数据不清空,爆零两行泪。
多测不读完,爆零两行泪。
边界不特判,爆零两行泪。
贪心不证明,爆零两行泪。
D P 顺序错,爆零两行泪。
大小少等号,爆零两行泪。
变量不统一,爆零两行泪。
越界不判断,爆零两行泪。
调试不注释,爆零两行泪。
溢出不 l l,爆零两行泪。
*/
#include <bits/stdc++.h>
using namespace std;
#define fi first
#define se second
#define fz(i,a,b) for(int i=a;i<=b;i++)
#define fd(i,a,b) for(int i=a;i>=b;i--)
#define put(x) putchar(x)
#define eoln put('\n')
#define space put(' ')
inline int read(){
int x=0,neg=1;char c=getchar();
while(!isdigit(c)){
if(c=='-') neg=-1;
c=getchar();
}
while(isdigit(c)) x=x*10+c-'0',c=getchar();
return x*neg;
}
inline void print(int x){
if(x<0){
putchar('-');
print(abs(x));
return;
}
if(x<=9) putchar(x+'0');
else{
print(x/10);
putchar(x%10+'0');
}
}
int n=read(),m=read();
int dx[]={
1,0,-1,0};
int dy[]={
0,1,0,-1};
inline int id(int x,int y){
return (x-1)*m+y;
}
int head[100005];
struct edge{
int to,nxt,cap;
} e[100005];
int ecnt=1;
inline void addedge(int u,int v,int f){
e[++ecnt].to=v;e[ecnt].cap=f;e[ecnt].nxt=head[u];head[u]=ecnt;
}
int dep[100005];
inline bool bfs(int s,int t){
queue<int> q;
memset(dep,-1,sizeof(dep));
q.push(s);dep[s]=0;
while(!q.empty()){
int cur=q.front();q.pop();
for(int i=head[cur];i;i=e[i].nxt){
int to=e[i].to;
if(dep[to]==-1&&e[i].cap){
dep[to]=dep[cur]+1;
q.push(to);
}
}
}
if(dep[t]!=-1) return 1;
return 0;
}
inline int dfs(int x,int t,int f){
if(x==t) return f;
int ret=0;
for(int i=head[x];i;i=e[i].nxt){
int y=e[i].to;
if(dep[y]==dep[x]+1&&e[i].cap){
int w=dfs(y,t,min(f-ret,e[i].cap));
e[i].cap-=w;
e[i^1].cap+=w;
ret+=w;
if(ret==f) return f;
}
}
return ret;
}
int sum=0;
inline int Dinic(int s,int t){
int tot=0;
while(bfs(s,t)) tot+=dfs(s,t,0x3f3f3f3f);
return tot;
}
int main(){
//源点0,汇点nm+1
fz(i,1,n) fz(j,1,m){
int val=read();
sum+=val;
if((i+j)%2){
for(int k=0;k<4;k++){