网络流24题 - 2
题目顺序按照洛谷“\(\color{#13C2C2}{网络流24题}\)”标签按难度排序。
题目的字体颜色为洛谷此题难度的颜色。
本人的题单: 网络流24题
P4014 \(\color{#9D3DCF}{分配问题}\)
题目大意
有\(n\)件工作要分配给\(n\)个人做。第\(i\)个人做第\(j\)件工作产生的效益为\(c_{i,j}\),求产生总效益的最值。
思路
显然是二分图最大/小权完美匹配的模板题,由于我不会\(KM\),故使用两次费用流解决。
建超级源点和超级汇点,分别免费连接至左边点集和右边点集。
第一次跑最小效益,就用输入的值跑最小费用最大流。
第二次跑最大效益,用输入值的相反数跑最小费用最大流,结果取相反数,即跑了一遍最大费用最大流。
细节
- 题目使用邻接矩阵读入,相当于要建\(n^2+2n\)条边。
模板不要错
代码
点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define maxn 5005
#define maxm 500005
#define ll long long
#define inf 0x3fffffff
using namespace std;
ll n,xx,s,t,u,v,cost;
ll num[maxn][maxn];
ll head[maxn],tt=1;
struct node{
ll to,dis,cost,nex;
}a[maxm*2];
void add(ll from,ll to,ll dis,ll cost){
a[++tt].to=to;
a[tt].dis=dis;
a[tt].cost=cost;
a[tt].nex=head[from];
head[from]=tt;
}
bool vis[maxn];
ll costs[maxn];
bool spfa(){
memset(vis,0,sizeof(vis));
memset(costs,0x3f,sizeof(costs));
queue<ll> q;
vis[s]=1;
q.push(s);
costs[s]=0;
while(!q.empty()){
ll top=q.front();
q.pop();
vis[top]=0;
for(ll i=head[top];i;i=a[i].nex){
if(costs[top]+a[i].cost<costs[a[i].to]&&a[i].dis){
costs[a[i].to]=costs[top]+a[i].cost;
if(!vis[a[i].to]){
vis[a[i].to]=1;
q.push(a[i].to);
}
}
}
}
if(costs[t]==costs[0]){
return 0;
}
return 1;
}
ll ans=0,anscost=0;
ll dfs(ll x,ll minn){
if(x==t){
vis[t]=1;
ans+=minn;
return minn;
}
ll use=0;
vis[x]=1;
for(ll i=head[x];i;i=a[i].nex){
if((!vis[a[i].to]||a[i].to==t)&&costs[a[i].to]==costs[x]+a[i].cost&&a[i].dis){
ll search=dfs(a[i].to,min(minn-use,a[i].dis));
if(search>0){
use+=search;
anscost+=(a[i].cost*search);
a[i].dis-=search;a[i^1].dis+=search;
if(use==minn){
break;
}
}
}
}
return use;
}
void dinic(int flag){//flag记录跑的是最小费用还是最大费用
while(spfa()){
vis[t]=1;
while(vis[t]){
memset(vis,0,sizeof(vis));
dfs(s,inf);
}
}
printf("%lld\n",anscost*flag);
}
int main(){
scanf("%lld",&n);
s=n*2+1;t=n*2+2;
for(int i=1;i<=n;i++){
add(s,i,1,0);add(i,s,0,0);
add(i+n,t,1,0);add(t,i+n,0,0);
for(int j=1;j<=n;j++){
scanf("%lld",&num[i][j]);
add(i,j+n,1,num[i][j]);add(j+n,i,0,-num[i][j]);
}
}
dinic(1);//板子
memset(head,0,sizeof(head));
memset(a,0,sizeof(a));
tt=1;
ans=anscost=0;
for(int i=1;i<=n;i++){
add(s,i,1,0);add(i,s,0,0);
add(i+n,t,1,0);add(t,i+n,0,0);
for(int j=1;j<=n;j++){
add(i,j+n,1,-num[i][j]);add(j+n,i,0,num[i][j]);
}
}
dinic(-1);
return 0;
}
P3358 \(\color{#9D3DCF}{最长k可重区间集问题}\)
题目大意
给定数轴上\(n\)个开区间和正整数\(k\),要求从这些区间中选择若干个,使得数轴上的任意一点都不被大于\(k\)个区间覆盖,求选出区间的长度和的最大值(此处开区间\((l,r)\)的长度定义为\(r-l\))(如图\(4\),答案为\(15\),\(4\)个区间都选即可)。
思路
换句话说,题目实际上是找若干区间,使任意\(k\)个都不交于一点。那么,对于两个不相交的区间,它们就可以同时选;对于两个有交集的区间,得看情况能不能同时选。考虑到用网络流的流量来限制这些条件,用水是否流过表示是否选此区间。若两区间不交,则中间可以连\(f=\infty,c=0\)的边,表示两边不相关,选哪个对另一个没有影响,多少水流过都没关系;若两区间相交,则之间没边(没看懂的可以先看后面再回来理解)。之后,建立超级源点\(s\)连接数轴上的\(1\)位置(这里把所有区间的右端点的最大值作为汇点,也可以另建超级汇点),\(f=k,c=0\),限制有\(k\)的水流流出。随后把数轴上的每个点\(i\)和\(i+1\)连接,\(f=k,c=0\),表示没选区间的水可以从这里免费流向汇点。对于每一个区间\((l,r)\),用一条边连接\(l\)和\(r\),\(f=1,c=-(r-l)\)(因为是最大费用,所以要取相反数再跑最小费用最大流的模板),表示可以有\(1\)的水流流过,费用为\(r-l\),最后的答案即为最大费用。现在分析正确性:因为到每个点\(i\)的流量最大只有\(k\)(因为源点只能输出\(k\)的水流),所以就算有超过\(k\)个区间以\(i\)为左端点,也不可能全部有水流过,最多只有\(k\)个,满足不能有超过\(k\)个区间交于一点。那么可不可能之前选了\(k\)个区间,在这些区间未结束时又选了其他区间呢?显然是不可能的,因为假设在节点\(i\)所有\(k\)的流量全部往区间流,在结束前不可能有多余的流量往其他区间流了。
但是,会多余很多没用的点\(i\)只和\(i-1\)和\(i+1\)相连,所以我们可以先离散化以后再执行算法。
细节
- \(i\)与\(i+1\)的点的容量应该为\(k\)而不是\(1\),连接区间两端点的边的容量应该是\(1\)而不是\(k\)。(\(\color{rgb(231,76,60)}{WA}\ \ On\ \ Test2\))
- 要离散化
且离散化不要写错。(\(\color{rgb(231,76,60)}{WA}\ \ On\ \ Test6\)) - 可能会有重复的区间。
代码
点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#include<algorithm>
#define maxn 5005
#define maxm 50005
#define ll long long
#define inf 0x3fffffff
using namespace std;
int n,k,s,t;
int l[maxn],r[maxn],num[maxn],cnt=0;
int head[maxn],tt=1;
struct node{
int to,dis,cost,nex;
}a[maxm*2];
void add(int from,int to,int dis,int cost){
a[++tt].to=to;
a[tt].dis=dis;
a[tt].cost=cost;
a[tt].nex=head[from];
head[from]=tt;
}
bool vis[maxn];
int costs[maxn];
bool spfa(){
memset(vis,0,sizeof(vis));
memset(costs,0x3f,sizeof(costs));
queue<int> q;
vis[s]=1;
q.push(s);
costs[s]=0;
while(!q.empty()){
int top=q.front();
q.pop();
vis[top]=0;
for(int i=head[top];i;i=a[i].nex){
if(costs[top]+a[i].cost<costs[a[i].to]&&a[i].dis){
costs[a[i].to]=costs[top]+a[i].cost;
if(!vis[a[i].to]){
vis[a[i].to]=1;
q.push(a[i].to);
}
}
}
}
if(costs[t]==costs[0]){
return 0;
}
return 1;
}
ll ans=0,anscost=0;
int dfs(int x,int minn){
if(x==t){
vis[t]=1;
ans+=minn;
return minn;
}
int use=0;
vis[x]=1;
for(int i=head[x];i;i=a[i].nex){
if((!vis[a[i].to]||a[i].to==t)&&costs[a[i].to]==costs[x]+a[i].cost&&a[i].dis){
int search=dfs(a[i].to,min(minn-use,a[i].dis));
if(search>0){
use+=search;
anscost+=(a[i].cost*search);
a[i].dis-=search;
a[i^1].dis+=search;
if(use==minn){
break;
}
}
}
}
return use;
}
void dinic(){
while(spfa()){
vis[t]=1;
while(vis[t]){
memset(vis,0,sizeof(vis));
dfs(s,inf);
}
}
printf("%lld",anscost*-1);
}
int main(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++){
scanf("%d%d",&l[i],&r[i]);
num[++cnt]=l[i];//读入
num[++cnt]=r[i];
}
sort(num+1,num+1+cnt);//离散化
int len=unique(num+1,num+1+cnt)-num-1;
s=len+1;
t=len;
add(s,1,k,0);add(1,s,0,0);
for(int i=1;i<len;i++){
add(i,i+1,k,0);add(i+1,i,0,0);
}
for(int i=1;i<=n;i++){
add(lower_bound(num+1,num+1+len,l[i])-num,lower_bound(num+1,num+1+len,r[i])-num,1,-(r[i]-l[i]));//加边
add(lower_bound(num+1,num+1+len,l[i])-num,lower_bound(num+1,num+1+len,r[i])-num,1,(r[i]-l[i]));
}
dinic();//板子
return 0;
}
/*
4 2
1 4
2 3
2 3
5 6
*/
P2774 \(\color{#9D3DCF}{方格取数问题}\)
题目大意
有一个\(m\times n\)的网格,每格中有数,现在要取出一些数,使得取出的数互不相邻,求能取出的数的和的最大值。
思路
因为取出一个数能影响其周围的四个格子,想到对格子进行黑白染色,即将\((i+j)\ mod\ 2==1\)的点\((i,j)\)都染成黑色,其它的点染成白色。此时发现直接做不太好做,于是想到取出的最大值=总值-最小值,于是考虑如何求最小值。
我们把刚才染成两种颜色的点划分为两个点集,这样就成了一个二分图。我们建立超级源点连向某一个点集(这里连了黑色的点集),另一个点集的点连向超级汇点,边权均为点的价值(即数字),用\(\color{red}{删掉这个点与源/汇点的边表示不选这个点}\)。对于两个点集之间,每一个点连向与其相邻的点,边权为\(\infty\)(原因底下会讲),此时求出最小割就能求出最小取值了。
正确性分析:由于求的是最小割,中间的\(inf\)边一定不会被删(删了就不一定是最小的了),删掉的一定是连接源/汇点的边,因此删掉的边一定能表示删去某个格子。再分析为什么不会留下两个相邻的边,假设有两个相邻的点\(i\)和\(j\)且\(i\)和源点\(s\)连,\(j\)和\(t\)连,若两个点都被取,即\((s,i)(j,t)\)两条边都没被删,又因为边\((i,j)\)不会被删,则存在一条路径\(s\rightarrow i\rightarrow j\rightarrow t\),这与割的定义不符(删去一些边得到两点集,且两点集不连通),所以假设不成立。
然后,根据最大流等于最小割就可以求出最小割了。
细节
- 因为是矩阵,点\((i,j)\)在图中的编号应为\((i-1)\times n+j\)(看个人偏好)。
- 记得建反向边(
除了我应该没有人会犯这种错误吧)。(\(\color{rgb(231,76,60)}{WA}\ \ On\ \ Test3\)) - 读入的依次是\(m,n\),不要弄错。(\(\color{rgb(231,76,60)}{WA}\ \ On\ \ Test3\))
代码
点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define maxn 505
#define maxm 50005
#define ll long long
#define inf 0x3fffffff
using namespace std;
ll n,m,s,t;
ll num[maxn][maxn],tot;
ll head[maxn],tt=1;
struct node{
ll to,dis,nex;
}a[maxm*2];
void add(ll from,ll to,ll dis){
a[++tt].to=to,a[tt].dis=dis,a[tt].nex=head[from],head[from]=tt;
a[++tt].to=from,a[tt].dis=0,a[tt].nex=head[to],head[to]=tt;//建反向边
}
bool vis[maxn];
ll dep[maxn],cur[maxn];
bool bfs(){
for(ll i=0;i<=t;i++){
vis[i]=0;
dep[i]=inf;
cur[i]=head[i];
}
queue<ll> q;
vis[s]=1;
q.push(s);
dep[s]=0;
while(!q.empty()){
ll top=q.front();
q.pop();
for(ll i=head[top];i;i=a[i].nex){
if(dep[top]+1<dep[a[i].to]&&a[i].dis){
dep[a[i].to]=dep[top]+1;
if(!vis[a[i].to]){
vis[a[i].to]=1;
q.push(a[i].to);
}
}
}
}
if(dep[t]==dep[0]){
return 0;
}
return 1;
}
ll ans=0;
ll dfs(ll x,ll minn){
if(x==t){
ans+=minn;
return minn;
}
ll use=0;
for(ll i=cur[x];i;i=a[i].nex){
cur[x]=i;
if(dep[a[i].to]==dep[x]+1&&a[i].dis){
ll search=dfs(a[i].to,min(minn-use,a[i].dis));
if(search>0){
use+=search;
a[i].dis-=search;
a[i^1].dis+=search;
if(use==minn){
break;
}
}
}
}
return use;
}
void dinic(){
while(bfs()){
dfs(s,inf);
}
printf("%lld",tot-ans);
}
int main(){
scanf("%d%d",&m,&n);
s=m*n+1;
t=m*n+2;
for(ll i=1;i<=m;i++){
for(ll j=1;j<=n;j++){
scanf("%d",&num[i][j]);
tot+=num[i][j];
if((i+j)%2){
add(s,(i-1)*n+j,num[i][j]);
if(i!=1) add((i-1)*n+j,(i-2)*n+j,inf);//相邻的四个
if(i!=m) add((i-1)*n+j,i*n+j,inf);
if(j!=1) add((i-1)*n+j,(i-1)*n+j-1,inf);
if(j!=n) add((i-1)*n+j,(i-1)*n+j+1,inf);
}else{
add((i-1)*n+j,t,num[i][j]);
}
}
}
dinic();//Dinic最大流
return 0;
}
P4009 \(\color{#9D3DCF}{汽车加油行驶问题}\)
题目大意
如图\(5\),有一\(n*n\)的方形网格,\(X\)轴向右为正,\(Y\)轴向下为正。一汽车从左上角\((1,1)\)为起点,要走到右下角\((n,n)\)。油箱的容量为\(K\)个单位,每走一步(上下左右到相邻的网格交点)消耗\(1\)单位的油量。油箱没有不能前进,每到一个加油站就必须加满油并花\(A\)的费用;向右/下走不用付费,向左/上走要付\(B\)的费用;若某时刻没油还不在加油站,可以花\(C\)的费用建立加油站(还要花\(A\)的费用加满油)。求到终点的最小费用(如图为样例,答案为\(12\),路线和花的费用已标注在图中)。
思路
以下为网络流做法(其实一样的思路直接\(DP\)也行)
这道题目写错基本不能靠调试出来,样例的量很大(个人观点)
若没有油量限制,显然可以用费用流的模板过:每个点向右/下的连\(f=1,c=0\)的边,向左/上连\(f=1,c=B\)的边,跑最小费用最大流即可。
但是,考虑到油量限制,我们可以给每个点加一层状态:行走的距离数,即把它变成了“分层图”,\(0\)表示满油,\(1\)表示走了一步(还有\(K-1\)的油),…,\(K\)表示油没了。但是,参数太多了,此时我们考虑“状压”,因为状态最大只有\(n\),我们用\(n\)进制存状态,即用\((\overline{kij})_n=(k\times n^2+i\times n+j)_{10}\)表示点\((i,j)\)的\(k\)状态(用\(ys\)函数状压)。
然后考虑建图。首先肯定还是要建立超级源点和超级汇点,超级源点与第\(0\)层的\((1,1)\)相连,因为一开始油箱是满的;而\((n,n)\)的所有状态都要与汇点相连,因为到终点没有限制油量。对于其他所有点\((i,j)\):
- 若为加油站,则所有状态和\(ys(0,i,j)\)相连,\(ys(0,i,j)\)再和周围的点相连,表示强制加油后才能走。
- 若不是加油站,则可以生成加油站并加满油(花\(A+C\)的费用),也可以直接向周围的点连边。
边权均为\(1\),费用为要花费的费用。
最后跑最小费用最大流即可。
细节
- 建边的时候别建错!
- 进制转换不要错。(\(\color{rgb(231,76,60)}{WA}\ \ On\ \ ALL\ Tests\))
代码
点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define ll long long
#define maxn 10000050
#define maxm 1000000050
#define inf 1e8
using namespace std;
ll n,K,A,B,C,s,t,x;
ll ys(int x,int y,int k){
return (x-1)*n+y+k*n*n;
}//状压
ll head[maxn],cnt=1;
struct node{
ll to,dis,cost,nex;
}g[maxn];
void add(int from,int to,int dis,int cost){//正向边反向边一起建
g[++cnt].to=to;g[cnt].dis=dis;g[cnt].cost=cost;g[cnt].nex=head[from];head[from]=cnt;
g[++cnt].to=from;g[cnt].dis=0;g[cnt].cost=-cost;g[cnt].nex=head[to];head[to]=cnt;
}
ll vis[maxn],costs[maxn];
bool spfa(){
memset(vis,0,sizeof(vis));
memset(costs,0x3f,sizeof(costs));
queue<ll> q;
vis[s]=1;
costs[s]=0;
q.push(s);
while(!q.empty()){
ll top=q.front();
q.pop();
vis[top]=0;
for(ll i=head[top];i;i=g[i].nex){
if(costs[g[i].to]>costs[top]+g[i].cost&&g[i].dis){
costs[g[i].to]=costs[top]+g[i].cost;
if(!vis[g[i].to]){
vis[g[i].to]=1;
q.push(g[i].to);
}
}
}
}
if(costs[t]==costs[0]){
return 0;
}
return 1;
}
ll ans=0,anscost=0;
ll dfs(ll x,ll mmin){
if(x==t){
vis[t]=1;
ans+=mmin;
return mmin;
}
ll use=0;
vis[x]=1;
for(int i=head[x];i;i=g[i].nex){
if((!vis[g[i].to]||g[i].to==t)&&costs[g[i].to]==costs[x]+g[i].cost&&g[i].dis){
ll search=dfs(g[i].to,min(mmin-use,g[i].dis));
if(search){
use+=search;
anscost+=(g[i].cost*search);
g[i].dis-=search;
g[i^1].dis+=search;
if(use==mmin){
break;
}
}
}
}
return use;
}
void dinic(){
while(spfa()){
do{
memset(vis,0,sizeof(vis));
dfs(s,inf);
}while(vis[t]);
}
printf("%lld",anscost);
}
int main(){
scanf("%lld%lld%lld%lld%lld",&n,&K,&A,&B,&C);
s=(K+1)*n*n+1;
t=(K+1)*n*n+2;
add(s,ys(1,1,0),1,0);
for(ll k=1;k<=K;++k) add(ys(n,n,k),t,1,0);
for(ll i=1;i<=n;i++){
for(ll j=1;j<=n;j++){
scanf("%d",&x);
if(x){//有加油站
for(ll k=1;k<=K;k++) add(ys(i,j,k),ys(i,j,0),1,A);//强制加油
if(i!=1) add(ys(i,j,0),ys(i-1,j,1),1,B);
if(i!=n) add(ys(i,j,0),ys(i+1,j,1),1,0);
if(j!=1) add(ys(i,j,0),ys(i,j-1,1),1,B);
if(j!=n) add(ys(i,j,0),ys(i,j+1,1),1,0);
}else{//没有加油站
for(int k=0;k<K;k++){
if(i!=1) add(ys(i,j,k),ys(i-1,j,k+1),1,B);
if(i!=n) add(ys(i,j,k),ys(i+1,j,k+1),1,0);
if(j!=1) add(ys(i,j,k),ys(i,j-1,k+1),1,B);
if(j!=n) add(ys(i,j,k),ys(i,j+1,k+1),1,0);
}
add(ys(i,j,K),ys(i,j,0),1,A+C);//新建加油站
}
}
}
dinic();//板子
return 0;
}