听课笔记 - 2026 重庆 - 04 网络流建模【Dinic】【Dilworth 定理】【上下界网络流】
ExFish
·
·
算法·理论
膜拜 orzGEOTCBRL。
定义见我自己的学习笔记。
增广路径与残量网络
考虑 FF 算法是每一次 dfs 在残量网络(建造反向边来返回)中找一条增广路流 1 流量复杂度为 \mathcal O(mf)。然后我们改良为 \mathcal O(n^2m) 的 EK,也就是每一次增广 \min 的容量,最多 n 次增广就结束,复杂度容易证明。
Dinic 复杂度分析
然后 Dinic 就是按照层次增广。也就是首先 bfs 按照 dep 分层。这个过程就是 \mathcal O(n) 的。然后是当前弧优化,你就发现一个点最多就走出边次,也就是一次增广就只有 \mathcal O(m),总复杂度为 \mathcal O(nm)。但是注意有 n 层分层所以总复杂度为 \mathcal O(n^2m) 的。
虽然这看起来跟 EK 是一样的,但是你根本卡不满,所以一般可以当做为 n\sqrt{m} 之类的超级小时间复杂度呢。
最大流最小割定理
这个我在学习笔记里写过,但是直接是搬的 OI-wiki。也就是删除边让源点汇点不连通。你发现这个东西就相当于最大流。然后最小割是容易转化的。
P2057 善意的投票
考虑网络流建模。考虑建模二分图,赞成左侧,反对右侧。S 是连赞成,T 连反对,朋友连双向的容量为 1 的边。你发现割 S\to u 就是让 u 取反,v\to T 就是让 v 取反,u\to v 就是让两人叛变。所以这样就变成最小割模版。
::::success[code]
警示:开大点。注意 ll。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=305,M=9e4+10;
int tot=1, hd[N];
int n,m,a[N];
struct edges{ int to,nxt; ll w; } e[2*M];
void addedge(int u,int v,ll w){
++tot; e[tot]={v,hd[u],w};
hd[u]=tot;
}
const ll inf=1e12;
ll now[N],dep[N];
queue<int>q;
int s,t;
bool bfs(){
for(int i=0;i<=n+1;i++) dep[i]=inf;
dep[s]=0; now[s]=hd[s];
while(q.size()) q.pop();
q.push(s);
while(q.size()){
int p=q.front(); q.pop();
for(int i=hd[p];i>0;i=e[i].nxt){
int v=e[i].to;
if(e[i].w>0&&dep[v]==inf){
q.push(v);
now[v]=hd[v];
dep[v]=dep[p]+1;
if(v==t) return 1;
}
}
}
return 0;
}
ll dfs(int p,ll flow){
if(p==t) return flow;
ll nowf=0,cost;
for(int i=now[p];i>0&&flow>0;i=e[i].nxt){
int v=e[i].to; now[p]=i;
if(e[i].w>0&&(dep[v]==dep[p]+1)){
cost=dfs(v,min(flow,e[i].w));
if(!cost) dep[v]=inf;
e[i].w-=cost, e[i^1].w+=cost;
nowf +=cost, flow-=cost;
}
}
return nowf;
}
ll maxflow(){
ll ans=0;
while(bfs()) ans+=dfs(s,inf);
return ans;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
addedge(u,v,1);
addedge(v,u,0);
addedge(v,u,1);
addedge(u,v,0);
}
s=0, t=n+1;
for(int i=1;i<=n;i++)
if(a[i]) addedge(s,i,1), addedge(i,s,0);
else addedge(i,t,1), addedge(t,i,0);
cout<<maxflow();
return 0;
}
::::
P1345 奶牛的电信
如何求最小割点?考虑转化一个点为一个入点和出点,所有原图上的边就没有容量限制,然后这个点的容量就是 1,这个就是最小割的板子了。
::::success[code]
注意边数数组大小。
#include<bits/stdc++.h>
using namespace std;
const int N=505,M=1e5+10;
#define ll long long
int n,m,s,t;
int tot=1,hd[N];
struct edges{ int to,nxt; ll w; }e[2*M];
void addedge(int u,int v,ll w){ ++tot; e[tot]={v,hd[u],w}; hd[u]=tot; }
const ll inf=1e12;
ll now[N],dep[N];
queue<int>q;
bool bfs(){
for(int i=0;i<500;i++) dep[i]=inf;
dep[s]=0; now[s]=hd[s];
while(q.size()) q.pop();
q.push(s);
while(q.size()){
int p=q.front(); q.pop();
for(int i=hd[p];i>0;i=e[i].nxt){
int v=e[i].to;
if(e[i].w>0&&dep[v]==inf){
q.push(v);
now[v]=hd[v];
dep[v]=dep[p]+1;
if(v==t) return 1;
}
}
}
return 0;
}
ll dfs(int p,ll flow){
if(p==t) return flow;
ll cost,nowf=0;
for(int i=now[p];i>0&&flow>0;i=e[i].nxt){
int v=e[i].to; now[p]=i;
if(e[i].w>0&&(dep[v]==dep[p]+1)){
cost=dfs(v,min(flow,e[i].w));
if(!cost) dep[v]=inf;
e[i].w-=cost, e[i^1].w+=cost;
nowf+=cost, flow-=cost;
}
}
return nowf;
}
ll maxflow(){
ll ans=0;
while(bfs()) ans+=dfs(s,inf);
return ans;
}
int main(){
cin>>n>>m>>s>>t;
for(int i=1;i<=n;i++)
addedge(i,n+i,1),
addedge(n+i,i,0);
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
addedge(u+n,v,inf);
addedge(v,u+n,0);
addedge(v+n,u,inf);
addedge(u,v+n,0);
}
s+=n;
cout<<maxflow();
return 0;
}
::::
最大权闭合子图
给一个点集,给一些选择约束关系,要让选择总和最大。假设我们已经获得所有正权点,那么我们就可以 S 往所有正权点连边,容量为权值绝对值,原图保持不变,容量为 \infty ,同理负权点连向 T,这样就把边权转移走了,如图。
::::info[图片]
::::
你发现这个东西相当于找了正权和 - 不选正权和 + 选择负权和。而目前找到的最小割是不选正权 + 要选负权值的绝对值。用正权和减去计算出的值才正确。
最大密度子图
让选择的 \frac{|E'|}{|V'|} 最大。考虑二分答案,转移不等式就是 |E'|-mid|V'|\ge 0,具体的,这就相当于一个点要付出 mid 的代价,一条边有 1 的收益,就选择边也要选两点,那么就是一个把边当做点后的最大权值闭合子图。
参考 UVA1389。
最小路径覆盖
题目:给你一个 dag,你要用若干条不交路径覆盖所有点,要求最小化路径条数。假设我们直接用 n 条单个点的路径覆盖了所有点,现在我们试图合并路径,使得路径数最小。当存在以 u 为终点,以 v 为起点的路径,这条边就能将它们合并。
问题转化,变为每个点一开始都各有一条为起点和终点的路径,边可以合并,最大化合并次数。拆点,每个点变为起点和终点,二分图最大匹配。
网络流求二分图最大匹配
### 最小链覆盖
同上最小路径覆盖,只是路径可以有交集了。考虑维护一个传递闭包,这个时候如果 $u$ 可达 $v$ 就连边,本质一致。
### Dilworth 定理
定义:反链为彼此无法走到的点集。那么有最小链覆盖等于最长反链。最小反链覆盖等于最长链。
证明:因为最长反链都不能被同一条路径覆盖,那么最小链覆盖 $\ge$ 最长反链。然后已经覆盖了最宽的部分,则最小链覆盖 $\le$ 最长反链。~~虽然不是很严谨。~~
### 混合图欧拉回路
考虑任意定向,然后反向一条边就是把一个点的度流给另一个点。那么就有 $S$ 连所有 $d_u>0$ 连容量为 $d_u$,$d_u<0$ 则连 $T$ 容量 $-d_u$。这个时候跑最大流如果满流就说明多余的度都已经分配出去了。
构造方案:看边的流动情况,如果满流就换,如果未流就是没换。构造是容易的。
### P3511 [POI] MOS-Bridges
考虑直接二分,如果有一条边都 $>mid$ 就 $>mid$,如果不合法就要调整,也就变成了混合图欧拉回路。
::::success[code]
注意内存的位置以及当前弧的存储。
```cpp
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=5e3+10,M=1e6+10;
int n,m;
const ll inf=1e12;
struct max_flow{
int tot=1,hd[N];
int s,t;
struct edges{ int to,nxt; ll w; } e[2*M];
void add(int u,int v,ll w){ ++tot; e[tot]={v,hd[u],w}; hd[u]=tot; }
ll now[N],dep[N];
queue<int>q;
bool bfs(){
for(int i=0;i<N;i++) dep[i]=inf;
dep[s]=0, now[s]=hd[s];
while(q.size()) q.pop();
q.push(s);
while(q.size()){
int p=q.front(); q.pop();
for(int i=hd[p];i>0;i=e[i].nxt){
int v=e[i].to;
if(e[i].w>0&&dep[v]==inf){
q.push(v); now[v]=hd[v];
dep[v]=dep[p]+1;
if(v==t) return 1;
}
}
}
return 0;
}
ll dfs(int p,ll flow){
if(p==t) return flow;
ll nowf=0, cost;
for(int i=now[p];i>0&&flow>0;i=e[i].nxt){
int v=e[i].to; now[p]=i;
if(e[i].w>0&&(dep[v]==dep[p]+1)){
cost=dfs(v,min(flow,e[i].w));
if(!cost) dep[v]=inf;
e[i].w-=cost, e[i^1].w+=cost;
nowf+=cost, flow-=cost;
}
}
return nowf;
}
ll dinic_maxflow(){
ll ans=0;
while(bfs()) ans+=dfs(s,inf);
return ans;
}
} ori, bin;
vector<pair<int,int>>e[N];
int eu[N],ev[N],el[N],ep[N],deg[N];
bool check(int mid){
bin=ori;
for(int i=1;i<=m;i++){
if(el[i]>mid&&ep[i]>mid)
return 0;
else{
if(el[i]<=mid)
bin.add(n+i,eu[i],1),
bin.add(eu[i],n+i,0);
if(ep[i]<=mid)
bin.add(n+i,ev[i],1),
bin.add(ev[i],n+i,0);
}
}
return bin.dinic_maxflow()==m;
}
vector<int>ans;
int hd[N];
void dfs(int p){
for(int &i=hd[p];i<e[p].size();){
auto v=e[p][i++];
dfs(v.first); ans.push_back(v.second);
}
return;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
ori.s=0, ori.t=N-1;
for(int i=1;i<=m;i++){
cin>>eu[i]>>ev[i]>>el[i]>>ep[i];
deg[eu[i]]++, deg[ev[i]]++;
ori.add(ori.s,n+i,1), ori.add(n+i,ori.s,0);
}
for(int i=1;i<=n;i++)
if(deg[i]&1) return cout<<"NIE",0;
else ori.add(i,ori.t,deg[i]>>1),
ori.add(ori.t,i,0);
int l=0,r=1e3+1;
while(l+1<r){
int mid=(l+r)/2;
if(check(mid)) r=mid;
else l=mid;
}
cout<<r<<"\n"; check(r); // do flow [r]
for(int i=1;i<=m;i++){
for(int d=bin.hd[n+i];d>0;d=bin.e[d].nxt){
int v=bin.e[d].to;
if(v==eu[i]&&!bin.e[d].w)
e[eu[i]].push_back({ev[i],i});
if(v==ev[i]&&!bin.e[d].w)
e[ev[i]].push_back({eu[i],i});
}
}
dfs(1);
reverse(ans.begin(),ans.end());
for(auto i:ans) cout<<i<<" ";
return 0;
}
```
::::
### P2754 家园
判断无解就是走不到 $-1$。若有解应该也不会很大。考虑最坏情况,假设只有 $1$ 人,那么从 $0$ 走到 $-1$ 要走 $n+1$ 步。那么也就可以按照步数分层($945$ 层)来跑。具体的,我们按照时间拆点,$(t,i)$ 向 $(t+1,i)$ 连无限制边,也就是不动。对于太空船,那么我们设移动为 $u\to v$,这些 $(t,u)\to(t+1,v)$ 的移动就是 $h$ 的容量。做一个限制就是 $S\to(0,0)$ 限制为 $k$,这就限制了流量。判断是否可以移动,则 $(0,-1),\dots$ 向 $T$ 连边,走完就结束了。这样的建模是完美的。
::::success[code]
```cpp
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1e6+10,M=5e4+10,K=105;
int n,m,k,nodes;
int tot=1,hd[N];
struct edges{ int to,nxt; ll w; }e[M<<1];
void add(int u,int v,ll w){
++tot; e[tot]={v,hd[u],w}; hd[u]=tot;
++tot; e[tot]={u,hd[v],0}; hd[v]=tot;
return;
}
const ll inf=1e12;
int now[N]; ll dep[N];
queue<int>q; int s,t;
bool bfs(){
for(int i=0;i<=nodes;i++) dep[i]=inf;
dep[t]=inf;
dep[s]=0; now[s]=hd[s]; while(q.size()) q.pop();
q.push(s);
while(q.size()){
int p=q.front(); q.pop();
for(int i=hd[p];i>0;i=e[i].nxt){
int v=e[i].to;
if(e[i].w>0&&dep[v]==inf){
q.push(v); now[v]=hd[v];
dep[v]=dep[p]+1;
if(v==t) return 1;
}
}
}
return 0;
}
ll dfs(int p,ll flow){
if(p==t) return flow;
ll cost, nowf=0;
for(int i=now[p];i>0&&flow>0;i=e[i].nxt){
int v=e[i].to; now[p]=i;
if(e[i].w>0&&(dep[v]==dep[p]+1)){
cost=dfs(v,min(flow,e[i].w));
if(!cost) dep[v]=inf;
e[i].w-=cost, e[i^1].w+=cost;
nowf+=cost, flow-=cost;
}
}
return nowf;
}
ll dinic(){
ll ans=0;
while(bfs()) ans+=dfs(s,inf);
return ans;
}
int h[K],r[K],l[K][K],fa[K];
int find(int x){ return x==fa[x]?x:fa[x]=find(fa[x]); }
void join(int u,int v){
u=find(u), v=find(v);
if(u!=v) fa[u]=v;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m>>k;
s=0, t=N-1;
for(int i=0;i<=n+2;i++)
fa[i]=i;
for(int i=1;i<=m;i++){
cin>>h[i]>>r[i];
for(int j=1;j<=r[i];j++){
cin>>l[i][j];
if(l[i][j]==-1) l[i][j]=n+2;
else if(!l[i][j]) l[i][j]=n+1;
if(j>1) join(l[i][j],l[i][j-1]);
}
}
if(find(n+2)!=find(n+1)) return cout<<0,0;
ll res=0;
for(int ti=1;;ti++){
add(s,n+1+(ti-1)*(n+1),inf);
nodes=ti*(n+1)+n+1;
for(int i=1;i<=m;i++){
int nxt=(ti-1)%r[i]+1,nww=ti%r[i]+1;
int u=l[i][nxt],v=l[i][nww];
if(u==n+2) u=t; else u=(ti-1)*(n+1)+u;
if(v==n+2) v=t; else v=ti*(n+1)+v;
add(u,v,h[i]);
}
res+=dinic();
if(res>=k) return cout<<ti,0;
for(int i=1;i<=n+1;i++)
add((ti-1)*(n+1)+i,ti*(n+1)+i,inf);
}
return 0;
}
```
::::
### 费用流
多一个代价流函数而已。但是费用流的复杂度与 $f$ 有关。这个东西是在费用图上找最短路增广。复杂度 $\mathcal O(nmf)$,伪多项式复杂度,这伪人吧。
费用流出现负权边,要用 SPFA 来增广(相当于 BFS 增广时多了一个边权),一般是没有负环的图。但是如果有要用教练不会的消圈算法(?)。
### 网络扩容
见此博客下的同名题解。
### 网格图上网络流建模
相邻格子互相排斥:黑白染色为二分图(好像做过一些增广路算法(匈牙利)的是这个 trick)
行列互斥:行为左侧点,列为右侧点建模。
### P2774 方格取数
是一个相邻互斥的网格图,那么黑白染色直接最小割。建图比较有趣。()()()
### 新生舞会
式子一眼分数规划,那么直接二分图完美匹配,费用流。具体源点走到左部点 / 右部点到汇点都是容量 $1$ 费用 $0$ 之间的就是要取式子。
这下知道为什么叫网络流与线性规划 24 题了。
### P2825 HEOI TJOI 2016 游戏
如果没有硬石头,那么每行每列只能选一个,直接行列建模就是对的。只需要被硬石头分开的每一段分别建点即可,因为可以一起选择了。这是就是二分图最大匹配的板子了。
### 按照状态拆点
网格图的状态可能有很多,所以拆点。
### P1251 餐巾计划
这状态真多吧。餐巾复用太难处理了,那么考虑每天要提供 $r_i$ 同时儿子会送 $r_i$ 块脏餐巾。拆点,考虑 $i_x,i_y$,那么就有 $s\to i_x (r_i,0)$(容量 $r_i$ 费用 $0$),同理有 $i_y\to t(r_i,0)$。然后买餐巾是 $s\to i_y(\infty,p)$,换餐巾就比较容易了。
### P2469 星际竞速
首先最小路径覆盖,然后把每个点拆成起点终点 $u_x,u_y$。每个点都可以作为路径起点终点,$S\to u_x(1,0)$,$u_y\to T(1,0)$。对于 $(u,v)$,$u_x\to v_y(1,w)$,表示合并代价为 $w$。对于 $v$,$S\to v_y(1,a_v)$ 也是同理。
### 上下界网络流
增加了流量上下界,一条边记录为 $(u,v,[l,r],w)$ 也就是点,上下界,流量。
### 无源汇上下界可行流
首先我们先考虑把所有的下界跑满。建造源点和汇点 $S'$ 和 $T'$,连边 $(u,v,[l,r],c)\to(S',v,l,0),(u,T',l,0),(u,v,r-l,c)$。让费用加上 $lc$。如果这个**新的网络没有满流**,也就是 $(S',v)$、$(u,T')$ 没有流满,那么说明怎么样都**没办法让下界满足**,否则这条边的流量加上下界是一种可行流。
还有可能使边得重边。这样就设 $d_u=$ 入边下界减去出边下界,当 $d_u>0$ 只连 $(u,T',d_u,0)$,否则只连 $(S',u,-d_u,0)$。
::::success[code]
要开大边空间!要记住清掉 Flow!
```cpp
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1e6+10,M=1e6+10;
int n,m,s,t;
int tot=1,hd[N];
struct edges{ int to,nxt; ll w,f; } e[2*M];
void addedge(int u,int v,ll w){ ++tot; e[tot]={v,hd[u],w,0}; hd[u]=tot; }
const ll inf=1e12;
ll dep[N];
int now[N],id[N];
int deg[N],le[N];
queue<int>q;
bool bfs(){
for(int i=0;i<=n+2;i++) dep[i]=inf;
dep[s]=0; now[s]=hd[s];
while(q.size()) q.pop();
q.push(s);
while(q.size()){
int p=q.front(); q.pop();
for(int i=hd[p];i>0;i=e[i].nxt){
int v=e[i].to;
if(e[i].w>e[i].f&&dep[v]==inf){
q.push(v);
dep[v]=dep[p]+1;
}
}
}
return dep[t]<inf;
}
bool vis[N];
ll dfs(int p,ll flow){
if(p==t) return flow;
ll nowf=0, cost;
vis[p]=1;
for(int i=now[p];i>0&&flow>nowf;i=e[i].nxt){
int v=e[i].to; now[p]=i;
if(!vis[v]&&e[i].w>e[i].f&&(dep[v]==dep[p]+1)){
cost=dfs(v,min(flow-nowf,e[i].w-e[i].f));
e[i^1].f-=cost, e[i].f+=cost;
nowf+=cost;
}
}
vis[p]=0;
return nowf;
}
ll maxflow(){
ll ans=0;
while(bfs()){
memcpy(now,hd,sizeof(now));
ans+=dfs(s,inf);
}
return ans;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
s=n+1, t=n+2;
for(int i=1;i<=m;i++){
int u,v,l,r;
cin>>u>>v>>l>>r; le[i]=l;
addedge(u,v,r-l);
id[i]=tot;
addedge(v,u,0);
deg[u]+=l, deg[v]-=l;
}
ll fullflow=0;
for(int i=1;i<=n;i++){
if(deg[i]>0)
addedge(i,t,deg[i]),
addedge(t,i,0),
fullflow+=deg[i];
if(deg[i]<0)
addedge(s,i,-deg[i]),
addedge(i,s,0);
}
if(maxflow()!=fullflow) return cout<<"No",0;
cout<<"Yes\n";
for(int i=1;i<=m;i++)
cout<<e[id[i]].f+le[i]<<"\n";
return 0;
}
```
::::
### 有源汇上下界最大 / 小流
这个时候连边 $(T,S,\infty,0)$ 变为无源汇图。之后直接跑出可行流。设旧的流量为 $ans$,删除旧边,此时因为有新边,所以 $S$ 到 $T$ 流了 $ans$ 的流量是合法的,在这个残量网络上跑最大流也一定是合法的,这样求最大流。那么要求最小流则可以**反着跑**,从 $T$ 到 $S$ 跑,本质相当于给之前的跑法做反向反悔,就求出最小流了。
::::success[max flow code]
```cpp
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1e6+10,M=1e6+10;
int n,m,s,t;
int tot=1,hd[N];
struct edges{ int to,nxt; ll w,f; } e[2*M];
void addedge(int u,int v,ll w){ ++tot; e[tot]={v,hd[u],w,0}; hd[u]=tot; }
const ll inf=1e12;
ll dep[N];
int now[N],id[N];
int deg[N],le[N];
queue<int>q;
bool bfs(){
for(int i=0;i<=n+2;i++) dep[i]=inf;
dep[s]=0; now[s]=hd[s];
while(q.size()) q.pop();
q.push(s);
while(q.size()){
int p=q.front(); q.pop();
for(int i=hd[p];i>0;i=e[i].nxt){
int v=e[i].to;
if(e[i].w>e[i].f&&dep[v]==inf){
q.push(v);
dep[v]=dep[p]+1;
}
}
}
return dep[t]<inf;
}
bool vis[N];
ll dfs(int p,ll flow){
if(p==t) return flow;
ll nowf=0, cost;
vis[p]=1;
for(int i=now[p];i>0&&flow>nowf;i=e[i].nxt){
int v=e[i].to; now[p]=i;
if(!vis[v]&&e[i].w>e[i].f&&(dep[v]==dep[p]+1)){
cost=dfs(v,min(flow-nowf,e[i].w-e[i].f));
e[i^1].f-=cost, e[i].f+=cost;
nowf+=cost;
}
}
vis[p]=0;
return nowf;
}
ll maxflow(){
ll ans=0;
while(bfs()){
memcpy(now,hd,sizeof(now));
ans+=dfs(s,inf);
}
return ans;
}
int os,ot; // Original S/T
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>m>>os>>ot;
s=n+1, t=n+2;
for(int i=1;i<=m;i++){
int u,v,l,r;
cin>>u>>v>>l>>r; le[i]=l;
addedge(u,v,r-l);
addedge(v,u,0);
deg[u]+=l, deg[v]-=l;
}
addedge(ot,os,inf);
int spe=tot;
addedge(os,ot,0);
ll fullflow=0;
for(int i=1;i<=n;i++){
if(deg[i]>0)
addedge(i,t,deg[i]),
addedge(t,i,0),
fullflow+=deg[i];
if(deg[i]<0)
addedge(s,i,-deg[i]),
addedge(i,s,0);
}
if(maxflow()!=fullflow) return cout<<"N",0;
ll fl=e[spe].f; // Max Re-flow flow.
e[spe].f=e[spe^1].f=e[spe].w=0;
s=os, t=ot;
fl+=maxflow();
cout<<fl; // Add extra flow.
return 0;
}
```
::::
### P4043 支线剧情
考虑覆盖问题使用网络流,有每条边至少走一次,加入上下界就是 $(u,v,[1,\infty],w)$ 的边,要从 $1$ 出发走若干路径,那么就可以建立源点汇点 $(S,1,[0,m],0),(i,T,[0,m],0)$。最后答案就变成最小费用有源汇上下界可行流。
### P2050 美食节
建分层图,把厨师按照菜的数量拆点,考虑如何连边,菜 $i$ 厨师 $j$ 制作 $w$ 道菜的代价 $(j,w)\to i(1,wt_{i,j})$,然后源点以及汇点就 $S\to (j,w)(1,0)$,因为 $i$ 要求 $p_i$ 道所以有 $i\to T(p_i,0)$。然后跑费用流,时间复杂度为 $\mathcal O(nmp)$(卡满,因为可以让一个厨师一直做菜)。
然后你就考虑有一大堆点都是没用的,所以可以一边 Dinic 一边加边,考虑维护那些被用过的边,然后维护当前厨师要做到多少道菜,动态加这个菜即可,其实不是很难。
### P5331 通信
当时上课时这题没听到,但是难度主要在 CDQ 分治。考虑使用费用流卡常。