听课笔记 - 2026 重庆 - 04 网络流建模【Dinic】【Dilworth 定理】【上下界网络流】

· · 算法·理论

膜拜 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 分治。考虑使用费用流卡常。