网络流24题 - 3
题目顺序按照洛谷“\(\color{#13C2C2}{网络流24题}\)”标签按难度排序。
题目的字体颜色为洛谷此题难度的颜色。
本人的题单: 网络流24题
P4015 \(\color{#9D3DCF}{运输问题}\)
题目大意
有\(n\)间商店和\(m\)间仓库。仓库分别有\(a_1,a_2,…,a_m\)的存货,商店分别需要\(b_1,b_2,…,b_n\)的货物,满足总存货量等于总需求量,即\(\sum\limits_{i=1}^{m}{a_i}=\sum\limits_{j=1}^{n}{b_j}\)。从仓库\(i\)运\(1\)个单位的货到商店\(j\)需花费\(c_{i,j}\)的费用,求把存货全部运至商店且均满足需货量的最小费用与最大费用。
思路
有了前\(8\)题的经验,这题一看就是要跑最小&最大费用最大流。我们建超级源点\(s\)和超级汇点\(t\),\(s\)连向所有的仓库且对于仓库\(i\),\(f=a_i,c=0\),即提供\(a_i\)的流量,代表存货(因为是存货所以显然是免费的);所有的商店连向\(t\)且对于商店\(i\),\(f=b_i,c=0\),即这个商店要被提供且最多被提供\(b_i\)的货量(实际上只能有\(b_i\),因为如果这里少了,其他地方也不能多,故少不了);对于仓库\(i\)与商店\(j\)之间,连接\(f=\infty,c=c_{i,j}\)的边,因为可以不限量(在存货范围内)供货且收费\(c_{i,j}\)。剩下的就是跑板子了,要跑两遍,和上次的\(P4014\)很像。
细节
- 没啥细节,板子别错就行
代码
点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define maxn 5005
#define maxm 50005
#define ll long long
#define inf 0x3fffffff
using namespace std;
ll n,m,x,s,t,cost;
ll ina[maxn],inb[maxn],inc[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;
a[++tt].to=from;a[tt].dis=0;a[tt].cost=-cost;a[tt].nex=head[to];head[to]=tt;
}
bool vis[maxn];
ll 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()){
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){
while(spfa()){
do{
memset(vis,0,sizeof(vis));
dfs(s,inf);
}while(vis[t]);
}
printf("%lld\n",anscost*flag);
}
void set(){
memset(head,0,sizeof(head));
memset(a,0,sizeof(a));
tt=1;
ans=anscost=0;
}
int main(){
scanf("%lld%lld",&m,&n);
s=n+m+1;
t=s+1;
for(int i=1;i<=m;i++){
scanf("%lld",&ina[i]);
add(s,i,ina[i],0);
}
for(int i=1;i<=n;i++){
scanf("%lld",&inb[i]);
add(i+m,t,inb[i],0);
}
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
scanf("%lld",&inc[i][j]);
add(i,j+m,inf,inc[i][j]);
}
}
dinic(1);
set();
for(int i=1;i<=m;i++){
add(s,i,ina[i],0);
}
for(int i=1;i<=n;i++){
add(i+m,t,inb[i],0);
}
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
add(i,j+m,inf,-inc[i][j]);
}
}
dinic(-1);
return 0;
}
P2770 \(\color{#9D3DCF}{航空路线问题}\)
题目大意
\(\color{red}{自西至东}\)给出一些城市[1],并给出它们之间的航线(无向,图中有线连接代表有航线)。
现要从\(\color{red}{最西边的城市}\)出发,严格\(\color{red}{自西向东}\)沿航线游玩某些城市,最后到达\(\color{red}{最东边的城市}\)。再从\(\color{red}{最东边的城市}\)出发,严格\(\color{red}{自东向西}\)沿航线游玩若干城市,最后回到\(\color{red}{最西边的城市}\),且两条路线上\(\color{red}{不能有重复的城市}\)(除两端点)。求最多能游玩到的城市数,无解输出\(“No\ Solution!”\)。如图答案为\(7\),路线为(反过来也行):$$Vancouver\rightarrow Edmonton\rightarrow Montreal\rightarrow Halifax\rightarrow Toronto\rightarrow Winnipeg\rightarrow Calgary\rightarrow Vancouver$$
思路
注意到题目里的几个关键词,既然“两条路线上不能有重复的城市”,那么我们不妨把题目看作是要找两条互不相交的从\(s\)到\(t\)的路径。又因为“每个城市只能经过一次”,但是,;网络流中并没有“点权”这种东西,那么我们可以把一个点拆成两个,一个是“入点”,一个是“出点”,两点之间连\(f=1,c=1\)的边,这样就限制了每条边最多流过一次,也就是某城市最多只经过一次。至于费用等于\(1\),是因为我们最后用费用表示经过了多少城市,即经过一个城市就要付\(1\)的费用。对于点\(i\),我们不妨设点\(i\)为其“入点”,\(i+n\)为其“出点”。把点\(1\),即最西边的城市看做源点\(s\),把点\(n\),即最东边的城市作为汇点\(t\)。对于\(s\)和\(t\),入点和出点的流量为\(2\),因为要找两条路。对于有航线的两城市\(i,j\)(假设\(i\)在\(j\)西边),则应该把\(i+n\)和\(j\)连边,即把\(i\)的出点与\(j\)的入点相连,\(f=1,c=0\),因为只能有\(1\)的流量,而没有经过城市,故\(c=0\)。
最后,跑完最大费用最大流后,我们会得到结果。若最大流为\(2\),则说明有至少两条路可以从\(s\)到\(t\),则\(DFS\)找\(f\)被清零的路线输出即可。若小于\(2\)则无解。但是,若源点直接和汇点相连,此时是有一条路(\(s\rightarrow t\rightarrow s\))满足条件的,应该要特判。
细节
- 要拆点
- 字符串开始可以用\(map\)映射成数
- 要跑最大费用最大流
,除了我没人会打成最小费用还调了半小时吧(\(\color{rgb(231,76,60)}{WA}\ \ On\ \ Test4\))
代码
点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#include<map>
#define maxn 500005
#define maxm 5000005
#define ll long long
#define inf 0x3fffffff
using namespace std;
ll n,m,s,t;
string x,y;
map<string,ll> mp;//城市映射到数
map<ll,string> mp2;//记录数对应的城市
bool flag=0;
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;
a[++tt].to=from;a[tt].dis=0;a[tt].cost=-cost;a[tt].nex=head[to];head[to]=tt;
}
bool vis[maxn];
ll 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()){
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(){
while(spfa()){
do{
memset(vis,0,sizeof(vis));
dfs(s,inf);
}while(vis[t]);
}
anscost*=-1;
}
ll ansvis[maxn];
void dfs1(ll x){//输出第一条路
cout<<mp2[x-n]<<endl;//因为是正序输出,故先输出,记得减n
ansvis[x]=1;//记录搜过的点,防止下次再搜
for(ll i=head[x];i;i=a[i].nex){//寻找与之相连的边
if(a[i].to<=n&&!a[i].dis){
dfs1(a[i].to+n);
break;
}
}
}
void dfs2(ll x){//输出第二条路(从最东边回来)
for(ll i=head[x];i;i=a[i].nex){//寻找与之相连的边
if(a[i].to<=n&&!a[i].dis&&!ansvis[a[i].to+n]){//还要判断第一次是否没搜过
dfs2(a[i].to+n);
}
}
cout<<mp2[x-n]<<endl;//因为是倒序输出,故后输出
}
int main(){
scanf("%lld%lld",&n,&m);
s=1;
t=2*n;//源/汇点
add(1,1+n,2,-1);//源/汇点的连边要特殊处理,因为流量为2
add(n,2*n,2,-1);
for(ll i=1;i<=n;i++){
cin>>x;
mp[x]=i;//映射
mp2[i]=x;
if(i==1||i==n){
continue;
}
add(i,i+n,1,-1);//入点和出点连边
}
for(ll i=1;i<=m;i++){
cin>>x>>y;
if(min(mp[x],mp[y])==1&&max(mp[x],mp[y])==n){//起点和终点有直达的边
flag=1;
}
add(min(mp[x],mp[y])+n,max(mp[x],mp[y]),1,0);//防止数据给的顺序是东连向西,故加上min和max
}
dinic();//板子
if(ans==2){//有两条路
printf("%lld\n",anscost-2);//最大城市数(减2是因为起点和终点算了两次)
dfs1(1+n);
dfs2(1+n);
}else if(ans==1&&flag){//只有起点直达终点的一条路
printf("2\n");
cout<<mp2[1]<<endl<<mp2[n]<<endl<<mp2[1];
}else{//只有一条路或根本没有路则无解
printf("No Solution!");
}
return 0;
}
P2754 \(\color{#9D3DCF}{[CTSC1999]家园 / 星际转移问题}\)
题目大意
\(2177\)年,由于资源破环严重,所有人们被迫移民月球。
现有\(k\)人要从地球前往月球,在地球与月球间有\(n\)个能容纳\(\infty\)人的太空站,有\(m\)艘飞船往返在若干个太空站之间。具体地,第\(i\)艘飞船最多容纳\(h_i\)人,往返于\(r_i\)个太空站\(s_1,s_2,…,s_{r_i}\)之间,如若有一飞船往返于第\(1,3,4\)个太空站之间,那么它将周期性地停靠太空站\(1\;3\;4\;1\;3\;4\;1\;3\;4…\)。特殊地,地球为\(0\),月球为\(-1\)。飞船在两太空站之间消耗的时间均为\(1\),求所有人都到月球上的最短时间(人可以在任意太空站停留任意时间),无解输出\(0\)。
思路
首先考虑无解的判断。显然,若不管如何乘坐飞船都无法从地球到月球则无解。那么,我们很容易能想到使用并查集维护能互相到达的太空站/地球/月球,这样就能判断是否有解。
在确定有解的情况下,考虑如何找到最小时间。因为飞船每天停靠的位置不一样,所以这是一幅动态图,考虑在动态图上跑网络流。最难的就是建图部分,考虑到每一天飞船的停靠点不同,我们按照时间把每天的地球\(\&\)月球\(\&\)空间站全部建一个点,如图,每一天(时间的单位不一定是天,为方便以下都用天代替)建\(4\)个节点(地球\(1\)个\(+\)月球\(1\)个\(+\)本数据中太空站\(2\)个),因为地球和太空站可以留任意时间的任意人数,故上一天的地球\(\&\)太空站都向这天的连一条\(f=\infty\)的边,又由于初始的月球连接着汇点,所以每天的月球向上一天的月球连\(f=\infty\)的边,对于每个太空船\(i\),上一天所在的位置向当天所在位置连一条\(f=h_i\)的边,因为只能容纳\(h_i\)人。此时,每一天跑一边最大流,得到结果\(people_i\)就是这一天能增加运往月球的人数(因为前几天跑最大流的时候把有些边的\(f\)设为\(0\)了,故后面的天数都不会向这些边跑),答案就是最小的\(day\)使得\(\sum\limits_{i=1}^{day}{people_i}\ge k\)。
细节
- 因为不知道有多少天,而且每一天要新建节点,故可以把\(s\)和\(t\)的编号设为\(maxn-1\)和\(maxn-2\)。此时更新时要循环到\(s\)而不是\(t\),因为\(t\)的下标比\(s\)小。(\(\color{rgb(231,76,60)}{WA}\ \ On\ \ Test1\))
- 初始要设\(n+2\)个点表示初始状况,也为了第一天的时候能把边连过来。
- 每天有\(n+2\)个节点,其中地球为第\(1\)个 ,第\(i\)个空间站为第\(i+1\)个,月球为\(n+2\)个,故连边时注意是\((day-1)*(n+2)+1\)是当天的地球,空间站和月球同理(根据代码可变)。
- 上一天飞船\(i\)的位置是\(orb_{i,((day-1)\mod r_i)}\)(\(orb\)记录飞船的运动周期,\(1\)为地球,第\(i\)个空间站为\(i+1\),月球\(n+2\))。
代码
点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define maxn 20005
#define maxm 500005
#define ll long long
#define inf 0x3fffffff
using namespace std;
int n,m,k,s,t;
ll totpeo=0;//当前天数能最大运到月球的人数
int h[maxn],r[maxn],orb[maxn][maxn];
int head[maxn],tt=1;
int f[maxn];
int find(int x){//并查集-查询
if(f[x]==x){
return x;
}
return f[x]=find(f[x]);
}
void unionn(int u,int v){//并查集-合并
int r1=find(u),r2=find(v);
if(r1!=r2){
f[r2]=r1;
}
return;
}
struct node{
int to,dis,nex;
}a[maxm*2];
void add(int from,int to,int 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];
int dep[maxn],cur[maxn];
bool bfs(){
for(int i=0;i<=s;i++){//更新到s,因为我的代码中t比s的下标小
vis[i]=0;
dep[i]=inf;
cur[i]=head[i];
}
queue<int> q;
vis[s]=1;
q.push(s);
dep[s]=0;
while(!q.empty()){
int top=q.front();
q.pop();
for(int 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);
}
}
}
}
return dep[t]!=dep[0];
}
ll ans=0;
int dfs(int x,int minn){
if(x==t){
ans+=minn;
return minn;
}
int use=0;
for(int i=cur[x];i;i=a[i].nex){
cur[x]=i;
if(dep[a[i].to]==dep[x]+1&&a[i].dis){
int 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;
}
ll dinic(){
ans=0;
while(bfs()){
dfs(s,inf);
}
return ans;
}
int main(){
scanf("%d%d%d",&n,&m,&k);
s=maxn-2;
t=maxn-3;
for(int i=1;i<=t;i++) f[i]=i;//并查集
for(int i=1;i<=m;i++){
scanf("%d%d",&h[i],&r[i]);
for(int j=0;j<r[i];j++){//因为mod r[i]的结果为0~r[i]-1,所以下标从0开始
scanf("%d",&orb[i][j]);
orb[i][j]++;
if(orb[i][j]==0) orb[i][j]=n+2;//月球编号为n+2
if(j!=0) unionn(orb[i][j],orb[i][j-1]);//并查集-合并
}
}
if(find(1)!=find(n+2)){//无法到达
printf("0");
return 0;
}
add(s,1,inf);
add(n+2,t,inf);//源点&汇点连到初始的地球&月球
for(int day=1;;day++){
for(int i=1;i<=n+1;i++){
add((day-1)*(n+2)+i,day*(n+2)+i,inf);//地球/空间站的上一天向当天连边
}
add(day*(n+2)+n+2,(day-1)*(n+2)+n+2,inf);//月球的当天向上一天连边
for(int i=1;i<=m;i++){
int x=(day-1+r[i])%r[i],y=day%r[i];
add((day-1)*(n+2)+orb[i][x],day*(n+2)+orb[i][y],h[i]);
//飞船的当天所在位置向上一天所在位置连边
}
totpeo+=dinic();
if(totpeo>=k){//能运送的最大人数超过总人数即输出答案
printf("%d",day);
break;
}
}
return 0;
}
P2762 \(\color{#9D3DCF}{太空飞行计划问题}\)
题目大意
有\(m\)项实验和\(n\)种仪器,每项实验需要用到\(n\)种仪器其中的若干种。完成某项实验\(i\)会得到\(p_i\)美元,购置某种仪器\(i\)需花费\(c_i\)美元,求能得到的最大利润。
思路
建超级源点\(s\)和超级汇点\(t\)。\(s\)向每一项实验\(i\)连权值为\(p_i\)的边;每一种仪器\(i\)向\(t\)连权值为\(c_i\)的边;每项实验向其所需仪器连权值为\(\infty\)的边。我们让\(s\)与实验\(i\)之间的边割掉表示要做此实验,仪器向\(t\)的边割掉表示要选此仪器,此时跑最小割得到结果(因为最小割后图中没有从\(s\)到\(t\)的路径,所以选实验\(i\)后,其需要的仪器到\(t\)的边一定会被割掉)。
现在证明结论正确性:令选的实验的下标集合为\(P\),选的仪器的下标集合为\(C\),那么:
\(ans=\max(\sum\limits_{i\in P}{p_i}-\sum\limits_{i\in C}{c_i})=\sum\limits_{i=1}^{m}{p_i}-\min(\sum\limits_{1\le i\le m且i\notin P}{p_i}+\sum\limits_{i\in C}{c_i})=\sum\limits_{i=1}^{m}{p_i}-最小割\)。
其实就是:
\(ans=\max(\)要选的实验的收益和\(-\)要选的仪器的花费和\()=\)全部的实验收益\(-min(\)不选的实验收益和\(+\)要选的仪器的花费和\()=\)全部的试验收益\(-\)最小割。(因为不选的实验和要选的仪器我们将他们割掉了)
细节
- 此题的读入特殊,除了用题中给的读入,还可以用以下改进的快读(题解中找到的):
点击查看代码
inline int read(){
char c;int r=0;
while (c<'0' || c>'9') c=getchar();
while (c>='0' && c<='9')
{
r=r*10+c-'0';
c=getchar();
}
if (c=='\n') flag=1;
return r;
}
代码
点击查看代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define maxn 205
#define maxm 5005
#define ll long long
#define inf 0x3fffffff
using namespace std;
bool flag=0;
inline int read(){
char c;int r=0;
while (c<'0' || c>'9') c=getchar();
while (c>='0' && c<='9')
{
r=r*10+c-'0';
c=getchar();
}
if (c=='\n') flag=1;
return r;
}
int n,m,s,t,fy,xx,totf;
int head[maxn],tt=1;
struct node{
int to,dis,nex;
}a[maxm*2];
void add(int from,int to,int 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];
int dep[maxn],cur[maxn];
bool bfs(){
for(int i=0;i<=t;i++){
vis[i]=0;
dep[i]=inf;
cur[i]=head[i];
}
queue<int> q;
vis[s]=1;
q.push(s);
dep[s]=0;
while(!q.empty()){
int top=q.front();
q.pop();
for(int 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);
}
}
}
}
return dep[t]!=dep[0];
}
ll ans=0;
int dfs(int x,int minn){
if(x==t){
ans+=minn;
return minn;
}
int use=0;
for(int i=cur[x];i;i=a[i].nex){
cur[x]=i;
if(dep[a[i].to]==dep[x]+1&&a[i].dis){
int 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);
}
}
void output(){
for(int i=head[s];i;i=a[i].nex){
if(dep[a[i].to]!=inf){
printf("%d ",a[i].to);
}
}
printf("\n");
for(int i=head[t];i;i=a[i].nex){
if(dep[a[i].to]!=inf){
printf("%d ",a[i].to-m);
}
}
printf("\n%d",totf-ans);
}
int main(){
scanf("%d%d",&m,&n);
s=n+m+1;
t=s+1;
for(int i=1;i<=m;i++){
scanf("%d",&fy);
totf+=fy;
add(s,i,fy);
flag=0;
while(!flag){
xx=read();
add(i,m+xx,inf);
}
}
for(int i=1;i<=n;i++){
scanf("%d",&xx);
add(m+i,t,xx);
}
dinic();
output();
return 0;
}
/*
2 2
10 1
20 2
20 10
*/