【学习笔记】网络流常见模型(一):有限制的图上最短(长)路
\mathcal{My} \mathcal{Blog}
网络瘤,网络瘤,网络建模最毒瘤
【大前言】
网络流的大部分题都在考问题分析和转换,以及对状态维护的考虑(DP ? 嗯哼 ?),综合起来就是考问题建模。
只要建立出了合适的模型,只需要背一遍模板就 ok 了,有些题目甚至都不需要跑网络流,直接上最短路,bfs或者dfs(只要有耐心去思考,dp 也能解决一部分题目),由于网络流算法过程的本质是贪心,所以在特定的情况下还可以直接贪心 + 模拟。
作为史上第一届提高组 CSPer,先在这里大胆地立个 flag: (NOIP) CSP 不考网络流!
关于网络流中更高深的东西还没有作深入研究,主要还是学一下基础建模思想。
网络流基础知识康这里:【学习笔记】网络流算法简单入门
【进入正题】
有时候我们可能会遇到这样一种题目:一眼看过去瞬间就想到最短(长)路,仔细想想后又被它鬼畜的限制条件给吓住,可能我们会想到状压,但状压并不是万能的,数据过大时就只能另想办法了。这时候可以将费用流作为一种待选思路。
其实这种带限制的图上最短(长)路应该是网络流建模中最简单的类型了,涉及到的方法都比较基础,所以下文在分类讨论时不考虑复杂的建模方法。
一:【建模思路】
建模是最难的部分,但要相信,一些类型的题目也是有套路可寻的。
1.【确定建模方法】
首先我们要明确这鬼畜的限制条件都有些什么。
例如:
找出 K 条最短路。
找出两条不相交的最短路。
每个点、边最多只能经过 K 次。
特定的点、边最多只能经过 K 次。
...
常见的可以分为两类:
(1). 【局部相同】
特征: 一大类(或者几大类)的点、边都有着相同的限制条件。
方法: 分层图。每一层就表示限制条件中的一种状态,这种状态有时比较明显,有时需要用状压将其简化。
如果有天数的概念,并且到达指定天数后就结束或者花费 XXX 重置天数,诸如此类的问题可以按照天数进行分层,在图上的某个位置 x 的某一层 k 代表在 x 这个位置,时间为第 k 天的状态。
(2). 【局部不同】
特征: 每个点、边都会给出不同的限制条件(可能有少数相同,看给出的数据如何)。
方法: 拆点。拆点是最玄学的方法,一道莫名其妙的题,可能一波神仙操作后就变成了最大流。
但在图上面要求似乎没那么高。最简单的是拆成入点和出点,如果题目给出了点权或者点的最大允许经过次数,就在拆出的入点和出点之间连一条流量为经过次数,费用为点权的边,这样就可以把点权变成边权,同时实现了点的经过次数限制(如果某些边有经过次数限制的话,也可以将其作为边流量)。
(3). 【全局相同】
特征: 整体限制。
方法: 超级源点和超级汇点。这种方法非常好用,凡是看到网络流的题,都可以先建个超源和超汇,不仅对答案没有影响,还能帮助我们思考问题,利用它去完成很多不太好解决的限制条件。
超源和超汇可以轻松解决存在多个起点和终点的情况。
在求非完备匹配二分图的可行边、必须边时,超汇还有构造增广路的作用,这里不做讨论。
如果题目要求找出 K 条路径使得经过的边权总和最小,那么可以让超源和起点连流量为 K 的边,超汇同理,对于图里面的点则按具体要求连边。
为什么这样做就是对的呢?利用费用流的特性,即最大流为前提,在此基础上求最小(大)花费,因此路径数可以人为设定,只要有解,最后一定会跑出最大流(即人为设定的路径数)。
2.【思考建图时的状态转移】
就像 dp 一样,需要做到不重不漏(有时候“重”似乎并不会造成影响,但会降低算法效率)
所谓的“状态转移”就是计算从一个状态到另一个状态需要消耗的费用,流量等等。
这里的“状态”有分层后的层数,拆点后的入点和出点,还有单纯的从一个位置(坐标)到达另一个位置(坐标),这些都是需要考虑的东西。
3.【认真计算空间复杂度】
最好是把空间抠死(抠到最小空间 +10 这种程度),因为开太小会 RE,开太大又可能 TLE。
有时候算好了总边数,结果总点数算错。还有拆点时为了好写导致中间空了编号很多没有使用,于是在跑 $SPFA$ 前的初始化 $dis$ 时,巨大的点数狠狠地将一个个 $TLE$ 拍到了我的脸上。
一些可能需要注意的地方:
$(1).$ 如果分出 $[1,K]$ 层后还使用了第 $0$ 层,那么点数应该是 $n*(K+1)$ 。
$(2).$ 计算点个数时别忘了还有**超源**和**超汇**。
$(3).$ 算出的总边数还要乘以 $2$(因为要建反边供算法中的“反悔”操作)。
$(4).$ 与**超源**和**超汇**相连的边也要计算。
------
### **4.【跑图】**
建好了图就是愉快的套模板时刻啦,如果网络流中的所有边**流量**都为 $1$,可以发现和最短路算法写出来没什么区别($EK$ 跑费用流毕竟是自带 $SPFA$ 的)。
------
## **二:【例题】**
前面说的都比较抽象,现在来看几道例题。
($1,2,3$ 为散乱的图,$4,5,6$ 为方格图)
注:下文中说到连“**流量**”为 $XX$ “**费用**”为 $XX$ 的边,表达的意思是**残留流量**和**单位费用**。
------
### **1.【运输问题】**
传送门:[运输问题 $[P4015]$](https://www.luogu.org/problem/P4015) [$[Loj6011]$](https://loj.ac/problem/6011)
**(难度:简单)**
#### **【分析】**
只需要建个**超源和超汇**就解决啦$QAQ$。
$(1).$ **超源**与 $i \in [1,n]$ 连**流量为** $a[i]$ **费用为** $0$ 的边。
$(2).$ $j \in [1,m]$ 与**超汇**连**流量为** $b[j]$ **费用为** $0$ 的边。
(注意连边的方向,是**超源**指向起点,终点指向**超汇**,之后不再提醒)
$(3).$ 然后 $i \in [1,n]$ 与 $j \in [1,m]$ 连**流量为** $inf$ **费用为** $cost[i][j]$ 的边。
至于最大费用,清空数组后把 $cost$ 全变为负数再跑一遍即可。
#### **【Code】**
```cpp
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<queue>
#define LL long long
#define Re register int
using namespace std;
const int N=205,M=10205,inf=2e9;//点数: 100+100+2=202, 边数: 100*100+100*2=10200
int o=1,n,m,h,t,st,ed,cyf[N],pan[N],pre[N],dis[N],head[N],Flow[N][2],Cost[N][N];LL mincost,maxflow;
struct QAQ{int w,to,next,flow;}a[M<<1];queue<int>Q;
inline void in(Re &x){
int f=0;x=0;char c=getchar();
while(c<'0'||c>'9')f|=c=='-',c=getchar();
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
x=f?-x:x;
}
inline void add(Re x,Re y,Re z,Re w){a[++o].flow=z,a[o].w=w,a[o].to=y,a[o].next=head[x],head[x]=o;}
inline void add_(Re a,Re b,Re flow,Re w){add(a,b,flow,w),add(b,a,0,-w);}
inline int SPFA(Re st,Re ed){
for(Re i=0;i<=ed;++i)dis[i]=inf,pan[i]=0;
Q.push(st),pan[st]=1,dis[st]=0,cyf[st]=inf;
while(!Q.empty()){
Re x=Q.front();Q.pop();pan[x]=0;
for(Re i=head[x],to;i;i=a[i].next)
if(a[i].flow&&dis[to=a[i].to]>dis[x]+a[i].w){//最小费用
dis[to]=dis[x]+a[i].w,pre[to]=i;
cyf[to]=min(cyf[x],a[i].flow);
if(!pan[to])pan[to]=1,Q.push(to);
}
}
return dis[ed]!=inf;
}
inline void EK(Re st,Re ed){
while(SPFA(st,ed)){
Re x=ed;maxflow+=cyf[ed],mincost+=(LL)cyf[ed]*dis[ed];
while(x!=st){
Re i=pre[x];
a[i].flow-=cyf[ed];
a[i^1].flow+=cyf[ed];
x=a[i^1].to;
}
}
}
int main(){
in(n),in(m),st=n+m+1,ed=st+1;
for(Re i=1;i<=n;++i)in(Flow[i][0]),add_(st,i,Flow[i][0],0);//超源->仓库
for(Re i=1;i<=m;++i)in(Flow[i][1]),add_(n+i,ed,Flow[i][1],0);//商店->超汇
//防止代表仓库i和商店j的数字混乱,用n+j表示商店j
for(Re i=1;i<=n;++i)
for(Re j=1;j<=m;++j)
in(Cost[i][j]),add_(i,n+j,inf,Cost[i][j]);//仓库->商店
EK(st,ed);
printf("%lld\n",mincost);
memset(head,0,sizeof(head));
memset(cyf,0,sizeof(cyf));
memset(pre,0,sizeof(pre));
memset(a,0,sizeof(a));
o=1;maxflow=mincost=0;
for(Re i=1;i<=n;++i)add_(st,i,Flow[i][0],0);
for(Re i=1;i<=m;++i)add_(n+i,ed,Flow[i][1],0);
for(Re i=1;i<=n;++i)
for(Re j=1;j<=m;++j)
add_(i,n+j,inf,-Cost[i][j]);//变成负数后直接求得最大花费
EK(st,ed);
printf("%lld\n",-mincost);
}
```
------
### **2.【数字梯形问题】**
传送门:[数字梯形问题 $[P4013]$](https://www.luogu.org/problem/P4013) [$[Loj6010]$](https://loj.ac/problem/6010)
**(难度:简单)**
#### **【分析】**
三种不同拆点的实现。
将**最大流**人为设成 $m$,最后求一个最大流为 $m$ 的最大花费。
**【$Task$ $1$】**
每个点、每条边都只能经过一次。
$(1).$ **超源**与 $j \in [1,m]$ 连**流量为** $1$ **费用为** $0$ 的边。
$(2).$ $j \in [1,m+n-1]$ 与**超汇**连**流量为** $1$ **费用为** $0$ 的边。
$(3).$ 将点拆为**入点**和**出点**,连**流量为** $1$ **费用为数值**的边。
$(4).$ $i \in [1,n-1]$,$j\in [1,m+i-1]$ 与 $[i+1,j],[i+1,j+1]$ 都连流量为 $1$ 费用为 $0$ 的边。
**【$Task$ $2$】**
每个点可经过无数次,每条边只能经过一次。
在【$Task$ $1$】的基础上稍加修改即可。
$(1).$ $j \in [1,m+n-1]$ 与**超汇**连边**流量改为** $inf$ 。
$(2).$ 将点拆为**入点**和**出点**,连边**流量改为** $inf$ 。
**【$Task$ $3$】**
每个点、每条边都能经过无数次。
在【$Task$ $1$】的基础上稍加修改即可。
除了**超源**与 $j \in [1,m]$ 连的边,其他的**流量全部改为** $inf$ 。
#### **【Code】**
```cpp
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<queue>
#define LL long long
#define Re register int
using namespace std;
const int N=1200,M=1865,inf=2e9;//点数:(2m+n-1)*n/2*2+2=(3n-1)*n+2=1182 边数:1.5*n*n*3+m+m+n-1=4.5*n*n+3n=1860
int o=1,n,m,h,t,st,ed,O,Poi[23][43],A[23][43],cyf[N],pan[N],pre[N],dis[N],head[N];LL maxcost,maxflow;
struct QAQ{int w,to,next,flow;}a[M<<1];queue<int>Q;
inline void in(Re &x){
int f=0;x=0;char c=getchar();
while(c<'0'||c>'9')f|=c=='-',c=getchar();
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
x=f?-x:x;
}
inline void add(Re x,Re y,Re z,Re w){a[++o].flow=z,a[o].w=w,a[o].to=y,a[o].next=head[x],head[x]=o;}
inline void add_(Re a,Re b,Re flow,Re w){add(a,b,flow,w),add(b,a,0,-w);}
inline int SPFA(Re st,Re ed){
for(Re i=0;i<=ed;++i)dis[i]=-inf,pan[i]=0;
Q.push(st),pan[st]=1,dis[st]=0,cyf[st]=inf;
while(!Q.empty()){
Re x=Q.front();Q.pop();pan[x]=0;
for(Re i=head[x],to;i;i=a[i].next)
if(a[i].flow&&dis[to=a[i].to]<dis[x]+a[i].w){//求最大花费
dis[to]=dis[x]+a[i].w,pre[to]=i;
cyf[to]=min(cyf[x],a[i].flow);
if(!pan[to])pan[to]=1,Q.push(to);
}
}
return dis[ed]!=-inf;
}
inline void EK(Re st,Re ed){
while(SPFA(st,ed)){
Re x=ed;maxflow+=cyf[ed],maxcost+=(LL)cyf[ed]*dis[ed];
while(x!=st){
Re i=pre[x];
a[i].flow-=cyf[ed];
a[i^1].flow+=cyf[ed];
x=a[i^1].to;
}
}
}
inline void TAT1(){
for(Re i=1;i<=m;++i)add_(st,Poi[1][i],1,0);
for(Re i=1;i<=m+n-1;++i)add_(Poi[n][i]+O,ed,1,0);
for(Re i=1;i<=n;++i)
for(Re j=1;j<=m+i-1;++j){
add_(Poi[i][j],Poi[i][j]+O,1,A[i][j]);
if(i<n)add_(Poi[i][j]+O,Poi[i+1][j],1,0),
add_(Poi[i][j]+O,Poi[i+1][j+1],1,0);
}
EK(st,ed);
printf("%lld\n",maxcost);
}
inline void TAT2(){
for(Re i=1;i<=m;++i)add_(st,Poi[1][i],1,0);
for(Re i=1;i<=m+n-1;++i)add_(Poi[n][i]+O,ed,inf,0);//点数开放后,一个终点可以经过无数次
for(Re i=1;i<=n;++i)
for(Re j=1;j<=m+i-1;++j){
add_(Poi[i][j],Poi[i][j]+O,inf,A[i][j]);
if(i<n)add_(Poi[i][j]+O,Poi[i+1][j],1,0),
add_(Poi[i][j]+O,Poi[i+1][j+1],1,0);
}
EK(st,ed);
printf("%lld\n",maxcost);
}
inline void TAT3(){
for(Re i=1;i<=m;++i)add_(st,Poi[1][i],1,0);//全部开放后,起点仍然只能经过一次
for(Re i=1;i<=m+n-1;++i)add_(Poi[n][i]+O,ed,inf,0);
for(Re i=1;i<=n;++i)
for(Re j=1;j<=m+i-1;++j){
add_(Poi[i][j],Poi[i][j]+O,inf,A[i][j]);
if(i<n)add_(Poi[i][j]+O,Poi[i+1][j],inf,0),
add_(Poi[i][j]+O,Poi[i+1][j+1],inf,0);
}
EK(st,ed);
printf("%lld\n",maxcost);
}
inline void CL(){
memset(head,0,sizeof(head));
memset(cyf,0,sizeof(cyf));
memset(pre,0,sizeof(pre));
memset(a,0,sizeof(a));
maxflow=maxcost=0,o=1;
}
int main(){
in(m),in(n);
for(Re i=1;i<=n;++i)
for(Re j=1;j<=m+i-1;++j)
in(A[i][j]),Poi[i][j]=++O;
st=O<<1|1,ed=st+1;
TAT1(),CL(),TAT2(),CL(),TAT3();
}
```
------
### **3.【航空路线问题】**
传送门:[航空路线问题 $[P2770]$](https://www.luogu.org/problem/P2770) [$[Loj6122]$](https://loj.ac/problem/6122)
**(难度:中等)**
#### **【分析】**
题意:求从 $1$ 到 $n$ 两条互不相交的路径,使得经过的点数最大。
人为设定最大流为 $2$,节点数设为费用,最后求一个最大流为 $2$ 的最大花费。
$(1).$ 将点拆为**入点**和**出点**,连**流量为** $1$ **费用为** $1$ 的边。起点和终点特殊处理,连**流量为** $2$ **费用为** $1$ 的边。
$(2).$ 对于题目给定的边 $(x,y)$,将 $x$ 的**出点**与 $y$ 的**入点**相连。
完整题解:[【题解】【网络流24题】航空路线问题 $[P2770]$ $[Loj6122]$](https://www.luogu.org/blog/ChenXingLing/solution-p2770)
#### **【Code】**
```cpp
#include<algorithm>
#include<iostream>
#include<string>
#include<cstdio>
#include<queue>
#include<map>
#define LL long long
#define Re register int
using namespace std;
const int N=103,M=40000,inf=2e9;
int x,y,o=1,n,m,h,t,st,ed,flag,cyf[N],pan[N],pre[N],dis[N],head[N];LL maxcost,maxflow;
struct QAQ{int w,to,next,flow;}a[M<<1];queue<int>Q;string CH,ch[N];map<string,int>ip;
inline void add(Re x,Re y,Re z,Re w){a[++o].flow=z,a[o].w=w,a[o].to=y,a[o].next=head[x],head[x]=o;}
inline void add_(Re a,Re b,Re flow,Re w){add(a,b,flow,w),add(b,a,0,-w);}
inline int SPFA(Re st,Re ed){
for(Re i=0;i<=ed;++i)dis[i]=-inf,pan[i]=0;
Q.push(st),pan[st]=1,dis[st]=0,cyf[st]=inf;
while(!Q.empty()){
Re x=Q.front();Q.pop();pan[x]=0;
for(Re i=head[x],to;i;i=a[i].next)
if(a[i].flow&&dis[to=a[i].to]<dis[x]+a[i].w){//最大花费
dis[to]=dis[x]+a[i].w,pre[to]=i;
cyf[to]=min(cyf[x],a[i].flow);//最小残余网络
if(!pan[to])pan[to]=1,Q.push(to);
}
}
return dis[ed]!=-inf;
}
inline void EK(Re st,Re ed){
while(SPFA(st,ed)){
Re x=ed;maxflow+=cyf[ed],maxcost+=(LL)cyf[ed]*dis[ed];
while(x!=st){
Re i=pre[x];
a[i].flow-=cyf[ed];
a[i^1].flow+=cyf[ed];
x=a[i^1].to;
}
}
}
inline void dfs1(Re x){
pan[x]=1;//记录一下第一次选的点,第二次就不选它们了
cout<<ch[x-n]<<endl;//第一次正序输出。记得减n
for(Re i=head[x],to;i;i=a[i].next)
if((to=a[i].to)<=n&&!a[i].flow){dfs1(to+n);break;}//出点x>n到入点to<=n,再从to的出点搜下去
}
inline void dfs2(Re x){
for(Re i=head[x],to;i;i=a[i].next)
if((to=a[i].to)<=n&&!a[i].flow&&!pan[to+n])dfs2(to+n);//出点x>n到入点to<=n,再从to的出点搜下去
cout<<ch[x-n]<<endl;//第二次倒序输出。记得减n
}
int main(){
cin>>n>>m;st=1,ed=n<<1;
for(Re i=1;i<=n;++i)cin>>ch[i],ip[ch[i]]=i;
for(Re i=2;i<n;++i)add_(i,n+i,1,1);//1~n表示入点,n+1~2n表示出点
add_(1,1+n,2,1),add_(n,n+n,2,1);//起点和中点可以经过两次
while(m--){
cin>>CH;x=ip[CH];
cin>>CH;y=ip[CH];
if(x>y)swap(x,y);
flag|=x==1&&y==n;
add_(x+n,y,1,0);//刚从x的出点出来,接下来连到y的入点
}
EK(st,ed);
if(maxflow==2)printf("%d\n",maxcost-2);//找到了两条路
else if(maxflow==1&&flag){//只有一条从1直通n的边
printf("2\n");
cout<<ch[1]<<endl<<ch[n]<<endl<<ch[1]<<endl;//这里要输出三个
return 0;
}
else return !printf("No Solution!\n");
for(Re i=1;i<=n+2;++i)pan[i+n]=0;
dfs1(1+n),dfs2(1+n);//根据边的残余网络来判断是否选了该边,所以从出点开始搜,这里害我调了一个多小时
}
```
------
### **4.【深海机器人问题】**
传送门:[深海机器人问题 $[P4012]$](https://www.luogu.org/problem/P4012) [$[Loj6224]$](https://loj.ac/problem/6224)
**(难度:简单)**
#### **【分析】**
先把 $n,m$ 都加个 $1$ 。
有多个起点和终点,所以建立**超源和超汇**。
每条边可经过多次,但边权(费用)只会获得一次,用边流量来表示这个限制条件。
(另外,题目中说“*输入时横纵坐标要反过来*”是骗人的)
$(1).$ 对于每一个未到边界的坐标 $(i,j)$ 与 $[i+1,j],[i,j+1]$ 连一条**流量为** $1$ **费用为读入边权**的边。
$(2).$ 对于每一个未到边界的坐标 $(i,j)$ 与 $[i+1,j],[i,j+1]$ 连一条**流量为** $inf$ **费用为** $0$ 的边。
$(2).$ **超源**与给定坐标 $(x,y)$ 连**流量为读入当前坐标机器人数费用为** $0$ 的边。
$(3).$ 给定坐标 $(x,y)$ 与**超汇**连**流量为读入当前坐标机器人数费用为** $0$ 的边。
#### **【Code】**
```cpp
#include<algorithm>
#include<cstdio>
#include<queue>
#define LL long long
#define Re register int
using namespace std;
const int N=260,M=1540,inf=2e9;//点数: 16*16+2=258, 边数: 16*16*(4+2)=1536
int x,y,z,o=1,n,m,h,t,Q1,Q2,st,ed,cyf[N],pan[N],pre[N],dis[N],head[N];LL maxcost,maxflow;
struct QAQ{int w,to,next,flow;}a[M<<1];queue<int>Q;
inline void in(Re &x){
int f=0;x=0;char c=getchar();
while(c<'0'||c>'9')f|=c=='-',c=getchar();
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
x=f?-x:x;
}
inline void add(Re x,Re y,Re z,Re w){a[++o].flow=z,a[o].w=w,a[o].to=y,a[o].next=head[x],head[x]=o;}
inline void add_(Re a,Re b,Re flow,Re w){add(a,b,flow,w),add(b,a,0,-w);}
inline int SPFA(Re st,Re ed){
for(Re i=0;i<=ed;++i)dis[i]=-inf,pan[i]=0;
Q.push(st),pan[st]=1,dis[st]=0,cyf[st]=inf;
while(!Q.empty()){
Re x=Q.front();Q.pop();pan[x]=0;
for(Re i=head[x],to;i;i=a[i].next)
if(a[i].flow&&dis[to=a[i].to]<dis[x]+a[i].w){//最大花费
dis[to]=dis[x]+a[i].w,pre[to]=i;
cyf[to]=min(cyf[x],a[i].flow);
if(!pan[to])pan[to]=1,Q.push(to);
}
}
return dis[ed]!=-inf;
}
inline void EK(Re st,Re ed){
while(SPFA(st,ed)){
Re x=ed;maxflow+=cyf[ed],maxcost+=(LL)cyf[ed]*dis[ed];
while(x!=st){
Re i=pre[x];
a[i].flow-=cyf[ed];
a[i^1].flow+=cyf[ed];
x=a[i^1].to;
}
}
}
inline int Poi(Re x,Re y){return (x-1)*m+y;}
int main(){
in(Q1),in(Q2),in(n),in(m),++n,++m,st=n*m+1,ed=st+1;
for(Re i=1;i<=n;++i)
for(Re j=1;j<m;++j)
in(x),add_(Poi(i,j),Poi(i,j+1),1,x),add_(Poi(i,j),Poi(i,j+1),inf,0);
for(Re j=1;j<=m;++j)
for(Re i=1;i<n;++i)
in(x),add_(Poi(i,j),Poi(i+1,j),1,x),add_(Poi(i,j),Poi(i+1,j),inf,0);
while(Q1--)in(z),in(x),in(y),add_(st,Poi(x+1,y+1),z,0);//x,y要+1
while(Q2--)in(z),in(x),in(y),add_(Poi(x+1,y+1),ed,z,0);//x,y要+1
EK(st,ed);
printf("%lld",maxcost);
}
```
------
### **5.【汽车加油行驶问题】**
传送门:[汽车加油行驶 $[P4009]$](https://www.luogu.org/problem/P4009) [$[Loj6223]$](https://loj.ac/problem/6223)
**(难度:中等)**
#### **【分析】**
限制条件的转换:一份油可供汽车走一个单位长度,油箱最多可装 $K$ 份油。
贪心可知:
$(1).$ 每个坐标只会经过一次(虽然不会证明,但一直没有举出反例,而且这样子做能 $AC$ 姑且认为它是对的吧)。
$(2).$ 汽车只会在油用光时才自建加油站(这个是可以证明的,自己在草稿上举个例子看看就懂了)。
然后再说建模,可以用拆点实现**强制加油**,也可以不用,直接用**分层**所提供的天然优势进行**强制**操作即可。
$(1).$ 把每个坐标分为 $K+1$ 层,用第 $0$ 层表示满油状态,第 $1$ 层表示用掉了 $1$ 份油,第 $2$ 层表示用掉了 $2$ 份油 $...$ 第 $K$ 层表示油被用光的状态。
$(2).$ **超源**与第 $0$ 层的 $(1,1)$ 连**流量为** $1$ **费用为** $0$ 的边。
$(3).$ 将第 $k \in [1,K]$ 层的 $(n,n)$ 与**超汇**分别连**流量为** $1$ **费用为** $0$ 的边。
$(4).$ 对于有加油站的坐标 $(i,j)$,将第 $k \in [1,K]$ 层的 $(i,j)$ 与第 $0$ 层的 $(i,j)$ 连**流量为** $1$ **费用为** $A$ 的边,再第 $0$ 层的 $(i,j)$ 与上下左右四个方向坐标的第 $1$ 层分别连**流量为** $1$ 的边,当**向左**、**向上**时**费用为** $B$ ,反之**费用为** $0$ 。
$(4).$ 对于没有加油站的坐标 $(i,j)$,将第 $K$ 层的 $(i,j)$ 与 第 $0$ 层的 $(i,j)$ 连流量为 $1$ 费用为 $A+C$ 的边。将第 $k \in [0,K-1]$ 层的 $(i,j)$ 与上下左右四个方向坐标的第 $k+1$ 层分别连**流量为** $1$ 的边,**费用**同上。
$(5).$ 图中流量全为 $1$,可以直接跑最短路。
完整题解:[【题解】【网络流24题】汽车加油行驶问题 $[P4009]$ $[Loj6223]$](https://www.luogu.org/blog/ChenXingLing/solution-p4009)
#### **【Code】**
```cpp
#include<algorithm>
#include<cstdio>
#include<queue>
#define LL long long
#define Re register int
using namespace std;
const int N=11e4+5,M=6e5+5,inf=2e9;
int x,y,z,w,o=1,n,m,h,t,A,B,C,K,st,ed,cyf[N],pan[N],pre[N],dis[N],head[N];LL mincost,maxflow;
struct QAQ{int w,to,next,flow;}a[M<<1];queue<int>Q;
inline void in(Re &x){
int f=0;x=0;char c=getchar();
while(c<'0'||c>'9')f|=c=='-',c=getchar();
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
x=f?-x:x;
}
inline void add(Re x,Re y,Re z,Re w){a[++o].flow=z,a[o].w=w,a[o].to=y,a[o].next=head[x],head[x]=o;}
inline void add_(Re a,Re b,Re flow,Re w){add(a,b,flow,w),add(b,a,0,-w);}
inline int SPFA(Re st,Re ed){
for(Re i=0;i<=ed;++i)dis[i]=inf,pan[i]=0;
Q.push(st),pan[st]=1,dis[st]=0,cyf[st]=inf;
while(!Q.empty()){
Re x=Q.front();Q.pop();pan[x]=0;
for(Re i=head[x],to;i;i=a[i].next)
if(a[i].flow&&dis[to=a[i].to]>dis[x]+a[i].w){
dis[to]=dis[x]+a[i].w,pre[to]=i;
cyf[to]=min(cyf[x],a[i].flow);
if(!pan[to])pan[to]=1,Q.push(to);
}
}
return dis[ed]!=inf;
}
inline void EK(Re st,Re ed){
while(SPFA(st,ed)){
Re x=ed;maxflow+=cyf[ed],mincost+=(LL)cyf[ed]*dis[ed];
while(x!=st){
Re i=pre[x];
a[i].flow-=cyf[ed];
a[i^1].flow+=cyf[ed];
x=a[i^1].to;
}
}
}
inline int P(Re x,Re y,Re k){return (x-1)*n+y+k*n*n;}
int main(){
in(n),in(K),in(A),in(B),in(C),st=(K+1)*n*n+1,ed=st+1;//一共有(K+1)层
add_(st,P(1,1,0),1,0);//超级源点连到满油的起点
for(Re k=1;k<=K;++k)add_(P(n,n,k),ed,1,0);
//把每一层的终点连到超级汇点,所以第0层可以不连
for(Re i=1;i<=n;++i)
for(Re j=1;j<=n;++j){
in(x);
if(x){//已有加油站
for(Re k=1;k<=K;++k)add_(P(i,j,k),P(i,j,0),1,A);
//所有状态都必须花费A加油加到满,但由于不可能满油到达某一点,所以满油的第0层可以不加(连)
//加满油之后状态可以由满油状态到达K-1油的上下左右四个方向
if(i<n)add_(P(i,j,0),P(i+1,j,1),1,0);//横坐标+1,费用为0
if(j<n)add_(P(i,j,0),P(i,j+1,1),1,0);//纵坐标+1,费用为0
if(i>1)add_(P(i,j,0),P(i-1,j,1),1,B);//横坐标-1,费用为B
if(j>1)add_(P(i,j,0),P(i,j-1,1),1,B);//纵坐标-1,费用为B
}
else{//无加油站
for(Re k=0;k<K;++k){//从有油的状态到达下一层的四个方向
if(i<n)add_(P(i,j,k),P(i+1,j,k+1),1,0);//横坐标+1,费用为0
if(j<n)add_(P(i,j,k),P(i,j+1,k+1),1,0);//纵坐标+1,费用为0
if(i>1)add_(P(i,j,k),P(i-1,j,k+1),1,B);//横坐标-1,费用为B
if(j>1)add_(P(i,j,k),P(i,j-1,k+1),1,B);//纵坐标-1,费用为B
}
add_(P(i,j,K),P(i,j,0),1,A+C);//没有加油站的地方可以自给自足
}
}
EK(st,ed);
printf("%lld",mincost);
}
```
------
### **6.【孤岛营救问题】**
传送门:[孤岛营救问题 $[P4011]$](https://www.luogu.org/problem/P4011) [$[Loj6121]$](https://loj.ac/problem/6121)
**(难度:困难)**
#### **【分析】**
~~这是一道膜你题~~($huaji$)
**坑点一:** **一个坐标可以有多把钥匙(这个是真的毒瘤)。**
**坑点二:** **每种钥匙都可以无限使用(玩魔塔玩多了会自然而然的以为一把钥匙只能用一次)。**
钥匙的拥有情况和门的存在都是限制条件,由于钥匙种类 $P$ 较小,可用二进制数压缩一下表示 $P$ 种钥匙的拥有情况。同 [$5.$ 汽车加油行驶](https://www.luogu.org/problem/P4009),用 **分层**提供的不同状态来实现单点处获取钥匙。
$(1).$ **永远不可穿过**的墙所拦截的两点之间**不连边**。
$(2).$ 有钥匙的坐标就将**所有不包含该钥匙的状态**与**包含该钥匙的状态**连**流量为** $inf$ **费用为** $0$ 的边,然后将有钥匙的状态连出去,**流量为** $1$ **费用为 **$1$。
$(3).$ 可打开的门就找**包含了对应钥匙的状态**连边出去,**流量为** $inf$ **费用为** $1$ 的边。
最后,可以跑最大流为 $1$ 的最小费用,如果无法达到这个最大流就说明无解,也可以直接跑最短路。
#### **【Code】**
```cpp
#include<algorithm>
#include<cstdio>
#include<vector>
#include<queue>
#define LL long long
#define Re register int
using namespace std;
const int N=2e5,M=2e6+5,inf=2e9;
int T,x,y,z,w,G,o=1,p,n,m,h,t,st,ed,maxp,cyf[N],pan[N],pre[N],dis[N],head[N],Map[11][11][11][11];LL mincost,maxflow;
struct QAQ{int w,to,next,flow;}a[M<<1];queue<int>Q;vector<int>key[11][11];
inline void in(Re &x){
int f=0;x=0;char c=getchar();
while(c<'0'||c>'9')f|=c=='-',c=getchar();
while(c>='0'&&c<='9')x=(x<<1)+(x<<3)+(c^48),c=getchar();
x=f?-x:x;
}
inline void add(Re x,Re y,Re z,Re w){a[++o].flow=z,a[o].w=w,a[o].to=y,a[o].next=head[x],head[x]=o;}
inline void add_(Re a,Re b,Re flow,Re w){add(a,b,flow,w),add(b,a,0,-w);}
inline int SPFA(Re st,Re ed){
for(Re i=0;i<=ed;++i)dis[i]=inf,pan[i]=0;
Q.push(st),pan[st]=1,dis[st]=0,cyf[st]=inf;
while(!Q.empty()){
Re x=Q.front();Q.pop();pan[x]=0;
for(Re i=head[x],to;i;i=a[i].next)
if(a[i].flow&&dis[to=a[i].to]>dis[x]+a[i].w){
dis[to]=dis[x]+a[i].w,pre[to]=i;
cyf[to]=min(cyf[x],a[i].flow);
if(!pan[to])pan[to]=1,Q.push(to);
}
}
return dis[ed]!=inf;
}
inline void EK(Re st,Re ed){
while(SPFA(st,ed)){
Re x=ed;maxflow+=cyf[ed],mincost+=(LL)cyf[ed]*dis[ed];
while(x!=st){
Re i=pre[x];
a[i].flow-=cyf[ed];
a[i^1].flow+=cyf[ed];
x=a[i^1].to;
}
}
}
inline int P(Re x,Re y,Re k){return (x-1)*m+y+k*n*m;}
int main(){
in(n),in(m),in(p),maxp=(1<<p)-1;st=(maxp+1)*n*m+1,ed=st+1;
in(T);while(T--)in(x),in(y),in(z),in(w),in(G),Map[x][y][z][w]=Map[z][w][x][y]=G?G:-1;
in(T);while(T--)in(x),in(y),in(G),key[x][y].push_back(G);
add_(st,P(1,1,0),1,0);
for(Re k=0;k<=maxp;++k)add_(P(n,m,k),ed,1,0);
for(Re i=1;i<=n;++i)
for(Re j=1;j<=m;++j)
if(!key[i][j].empty())//有钥匙
for(Re O=0;O<key[i][j].size();++O){
Re KEY=key[i][j][O];
for(Re k=0;k<=maxp;++k){
if(!((k>>KEY-1)&1))add_(P(i,j,k),P(i,j,k|(1<<KEY-1)),inf,0);//这个状态没有这个钥匙
else{//这个状态有这个钥匙
if(i<n&&Map[i][j][i+1][j]!=-1){
if(!Map[i][j][i+1][j])add_(P(i,j,k),P(i+1,j,k),inf,1);
else if((k>>Map[i][j][i+1][j]-1)&1)add_(P(i,j,k),P(i+1,j,k),inf,1);
}
if(j<m&&Map[i][j][i][j+1]!=-1){
if(!Map[i][j][i][j+1])add_(P(i,j,k),P(i,j+1,k),inf,1);
else if((k>>Map[i][j][i][j+1]-1)&1)add_(P(i,j,k),P(i,j+1,k),inf,1);
}
if(i>1&&Map[i][j][i-1][j]!=-1){
if(!Map[i][j][i-1][j])add_(P(i,j,k),P(i-1,j,k),inf,1);
else if((k>>Map[i][j][i-1][j]-1)&1)add_(P(i,j,k),P(i-1,j,k),inf,1);
}
if(j>1&&Map[i][j][i][j-1]!=-1){
if(!Map[i][j][i][j-1])add_(P(i,j,k),P(i,j-1,k),inf,1);
else if((k>>Map[i][j][i][j-1]-1)&1)add_(P(i,j,k),P(i,j-1,k),inf,1);
}
}
}
}
else//无钥匙
for(Re k=0;k<=maxp;++k){
if(i<n&&Map[i][j][i+1][j]!=-1){
if(!Map[i][j][i+1][j])add_(P(i,j,k),P(i+1,j,k),inf,1);
else if((k>>Map[i][j][i+1][j]-1)&1)add_(P(i,j,k),P(i+1,j,k),inf,1);
}
if(j<m&&Map[i][j][i][j+1]!=-1){
if(!Map[i][j][i][j+1])add_(P(i,j,k),P(i,j+1,k),inf,1);
else if((k>>Map[i][j][i][j+1]-1)&1)add_(P(i,j,k),P(i,j+1,k),inf,1);
}
if(i>1&&Map[i][j][i-1][j]!=-1){
if(!Map[i][j][i-1][j])add_(P(i,j,k),P(i-1,j,k),inf,1);
else if((k>>Map[i][j][i-1][j]-1)&1)add_(P(i,j,k),P(i-1,j,k),inf,1);
}
if(j>1&&Map[i][j][i][j-1]!=-1){
if(!Map[i][j][i][j-1])add_(P(i,j,k),P(i,j-1,k),inf,1);
else if((k>>Map[i][j][i][j-1]-1)&1)add_(P(i,j,k),P(i,j-1,k),inf,1);
}
}
EK(st,ed);
if(maxflow)printf("%lld",mincost);
else printf("-1");
}
```
------
## **三:【参考文献】**
- [最详细(也可能现在不是了)网络流建模基础](https://www.cnblogs.com/victorique/p/8560656.html) (近乎完整的全套问题建模方法)
- [网络流 $24$ 题 题解](https://ksmeow.moe/graph_flow_24prob_sol/#toc-9)(经典网络流 $24$ 题全解)
------