网络流算法与建模
网络流简介
网络流其实是图论问题。
对于一张有向图,每条边 \((u,v)\) 设一个容量限制 \(c(u,v)\)。再设源点 \(s\),汇点 \(t\)。
对于每一条边 \((u,v)\),设流量 \(f(u,v)\),满足 \(0\leq f(u,v)\leq c(u,v)\)。
定义点 \(u\) 的净流量 \(\displaystyle f(u)=\sum_{v\in V}f(u,v)-\sum_{v\in V}f(v,u)\)。显然,除 \(s,t\) 外,所有点的净流量均为 \(0\)。
那么,网络流就可以处理一些流量的问题。
常见问题有:
- 最大流。
- 最小割。
- 费用流。
网络流算法
最大流
给出一个 \(n\) 个点 \(m\) 条边的网络图,以及其源点 \(s\) 和汇点 \(t\),求出其网络最大流。
\(1\leq n\leq200,1\leq m\leq5000,0\leq w<2^{31}\)。
Ford–Fulkerson 增广
设剩余容量 \(c_f(u,v)=c(u,v)-f(u,v)\),将所有剩余容量大于 \(0\) 的边构成的子图称为残量网络 \(G_f\)。
如果 \(G_f\) 上存在一条 \(s\) 至 \(t\) 的路径,且路径上所有的剩余容量都大于 \(0\),那么就称这条路径为增广路。
一种显然的算法是不断找增广路,并且将增广路上的每一条边都增加等量流量,这个过程称为增广。
但是考虑到直接增广可能不是最优,因此引入反悔操作。对于每一条边 \((u,v)\) 建反边 \((v,u)\),钦定 \(f(v,u)=-f(u,v)\) 恒成立,更新 \(f(u,v)\) 同时更新 \(f(v,u)\)。
引入反边后,实际上增广时可以通过反边来进行「退流」。

例如上图中所有边的容量限制均为 \(1\)。左图 \(u\rightarrow v\) 直接增广,没有利用 \(p,q\),最大流为 \(1\);但是中图 \(u\rightarrow q,p\rightarrow v\) 增广最大流为 \(2\)。
本质上是因为左图没有利用 \(p,q\) 增广,而中图充分利用了。引入退流后,最右图 \(p\rightarrow v\rightarrow u\rightarrow q,u\rightarrow v\) 增广完全等价于 \(u\rightarrow q,p\rightarrow v\),最大流为 \(2\)。
重复增广直到增广路不存在,即可得到最大流。
Ford-Fulkerson 增广有很多不同的实现,主流有 EK 和 Dinic。
正确性 & 时间复杂度
最大流最小割定理指出,当 \(G_f\) 上不再存在 \(s\) 至 \(t\) 的路径时,取到最大流。即一直增广直到增广路不存在。
单轮增广时间复杂度为 \(\mathcal O(m)\),则 Ford-Fulkerson 增广的时间复杂度上界为 \(\mathcal O(m\vert f\vert)\)。当然,这只是一个基于答案分析得到的非常宽松的上界,实际上 EK、Dinic 并不会达到。
EK 算法
Edmonds-Karp 算法。
显然主要在于寻找 \(s\rightarrow t\) 的增广路,考虑在 \(G_f\) 上从 \(s\) 开始 BFS 即可。
对于找到的路径 \(p\),将 \(p\) 中的每一条边的流量和最大流增加 \(\displaystyle\min_{(u,v)\in p}c_f(u,v)\),并且同时退流。
直到不存在增广路,即得到了最大流。
时间复杂度 \(\mathcal O\left(nm^2\right)\)。
实际实现上,可以直接维护每条边的剩余容量。找反边可以利用邻接表。
需要注意的是,即便初始时剩余容量在 int 范围内,实际运行过程中由于退流操作的存在,也可能会加至爆 int。需要注意数据范围。
参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
typedef long long ll;
constexpr const int N=200,M=5000;
constexpr const ll inf=0x3f3f3f3f3f3f3f3f;
struct graph{
struct edge{
int v,r;
ll w;
}a[(M<<1)+1+1];
int size=1,h[N+1];
inline void create(int u,int v,int w){
a[++size]={v,h[u],w};
h[u]=size;
}
edge& operator [](int x){
return a[x];
}
}g;
int n,s,t,pre[N+1];
ll dis[N+1];
bool bfs(){
static bool vis[N+1];
memset(vis,0,sizeof(vis));
queue<int>q;
q.push(s);
vis[s]=true;
dis[s]=inf;
while(q.size()){
int x=q.front();q.pop();
for(int i=g.h[x];i;i=g[i].r){
auto [v,r,w]=g[i];
if(w&&!vis[v]){
q.push(v);
pre[v]=i;
dis[v]=min(dis[x],w);
vis[v]=true;
if(v==t){
return true;
}
}
}
}
return false;
}
ll EK(){
ll ans=0;
while(bfs()){
int x=t;
while(x!=s){
g[pre[x]].w-=dis[t];
g[pre[x]^1].w+=dis[t];
x=g[pre[x]^1].v;
}
ans+=dis[t];
}
return ans;
}
int main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int m;
cin>>n>>m>>s>>t;
for(int i=1;i<=m;i++){
int u,v,w;
cin>>u>>v>>w;
g.create(u,v,w);
g.create(v,u,0);
}
cout<<EK()<<'\n';
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
Dinic 算法
对 \(G_f\) 进行 BFS,按照深度分层。只保留每一个点到下一层的边,得到层次图 \(G_L\)。
定义阻塞流 \(f_b\) 为一个不能继续增大的流。
Dinic 算法流程即重复执行:
- 在 \(G_f\) 上 BFS,得到 \(G_L\)。
- 在 \(G_L\) 上 DFS,得到阻塞流 \(f_b\)。
- 合并 \(f,f_b\),更新最大流。
- 重复执行,直到 \(G_L\) 上不存在 \(s\rightarrow t\) 的路径。
BFS 分层是简单的,考虑 DFS 求阻塞流 \(f_b\) 如何实现。
只需要在 DFS 到 \(x\) 的过程中维护 \(\textit{Min}\),表示 \(s\rightarrow x\) 的路径上最小的剩余容量为 \(\textit{Min}\)。DFS 结束 \(x\) 后,返回 \(\textit{ans}\) 表示流的增大量。
更新这条边的剩余容量和反边即可。
当前弧优化
考虑同一轮 BFS 后,$x$ 的下一层节点 $v_1,v_2,v_3,\cdots$。
如果上一次 DFS 遍历到 $v_1,v_2,\cdots,v_k$ 的时候,已经有 $\textit{Min}=0$,则下一次 DFS 到 $x$ 时,可以直接从 $v_k$ 开始遍历处理子节点。正确性是显然的,因为 $v_1\sim v_{k-1}$ 都已经在 $\textit{Min}>0$ 的时候处理掉了,利用了所有的剩余容量。
当前弧优化其实是 Dinic 时间复杂度的保证,不加当前弧优化的 Dinic 复杂度是错的。
Dinic 时间复杂度是 \(\mathcal O\left(n^2m\right)\),同时:
-
在所有边的容量均为 \(1\) 的网络上,Dinic 复杂度为 \(\mathcal O\left(m\min\left(m^{\frac12},n^{\frac23}\right)\right)\)。
-
在所有边的容量均为 \(1\),且除 \(s,t\) 外均有入度为 \(1\) 或出度为 \(1\),则 Dinic 的时间复杂度为 \(\mathcal O\left(m\sqrt n\right)\)。
这可以应用到求解二分图最大匹配的 Hopcroft-Karp 算法上。
参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
using ll=long long;
constexpr const int N=200,M=5000;
constexpr const ll inf=0x3f3f3f3f3f3f3f3f;
struct graph{
struct edge{
int v,r;
ll w;
}a[(M<<1)+1+1];
int size=1,h[N+1];
inline void create(int u,int v,int w){
a[++size]={v,h[u],w};
h[u]=size;
}
edge& operator [](int x){
return a[x];
}
}g;
int n,s,t,cur[N+1];
ll dis[N+1];
bool bfs(){
memset(dis,0x3f,sizeof(dis));
for(int i=1;i<=n;i++){
cur[i]=g.h[i];
}
queue<int>q;
q.push(s);
dis[s]=0;
while(q.size()){
int x=q.front();q.pop();
for(int i=g.h[x];i;i=g[i].r){
auto [v,r,w]=g[i];
if(w>0&&dis[v]==inf){
q.push(v);
dis[v]=dis[x]+1;
if(v==t){
return true;
}
}
}
}
return false;
}
ll dfs(int x,ll Min){
if(x==t){
return Min;
}
ll ans=0;
for(int i=cur[x];i&&Min;i=g[i].r){
cur[x]=i;
auto [v,r,w]=g[i];
if(w&&dis[v]==dis[x]+1){
ll pl=dfs(v,min(Min,w));
if(!pl){
dis[v]=inf;
}
g[i].w-=pl;
g[i^1].w+=pl;
ans+=pl;
Min-=pl;
}
}
return ans;
}
ll Dinic(){
ll ans=0;
while(bfs()){
ans+=dfs(s,inf);
}
return ans;
}
int main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int m;
cin>>n>>m>>s>>t;
for(int i=1;i<=m;i++){
int u,v,w;
cin>>u>>v>>w;
g.create(u,v,w);
g.create(v,u,0);
}
cout<<Dinic()<<'\n';
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
最小割
对于一张网络 \(G=(V,E)\),定义割 \(\set{S,T}\) 为一种点的划分方式,满足 \(S\cap T=\varnothing,S\cup T=V\),且 \(s\in S,t\in T\)。
定义割 \(\set{S,T}\) 的容量为 \(\vert\vert S,T\vert\vert=\displaystyle\sum_{u\in S}\sum_{v\in T}c(u,v)\)。
最小割就是求得 \(\set{S,T}\),使得 \(\vert\vert S,T\vert\vert\) 最小。
Kőnig 定理是最大流最小割定理的特殊情形。
最大流最小割定理
最大流最小割定理指出,最大流 \(f\) 和最小割 \(\set{S,T}\) 总是满足 \(\vert f\vert=\vert\vert S,T\vert\vert\)。
也就是说,最大流等于最小割。
证明
先考虑一个引理:对于网络 \(G=(V,E)\),\(\vert f\vert\leq\vert\vert S,T\vert\vert\),且当且仅当 \(\set{(u,v)\mid u\in S,v\in T}\) 满流,\(\set{(u,v)\mid u\in T,v\in S}\) 空流时取等。
当 \(\set{(u,v)\mid u\in T,v\in S}\) 空流时,取到 \(\displaystyle\vert f\vert=\sum_{u\in S}\sum_{v\in T}f(u,v)\)。
当 \(\set{(u,v)\mid u\in S,v\in T}\) 满流时,取到 \(\displaystyle\vert f\vert=\sum_{u\in S}\sum_{v\in S}c(u,v)=\vert\vert S,T\vert\vert\) 最大。
假设 \(G=(V,E)\) 的某一轮增广后得到了流 \(f\),使得 \(G_f\) 上不存在增广路。记 \(s\) 出发可以到达的所有点构成的点集为 \(S\),\(T=V\setminus S\)。
则 \(\set{S,T}\) 是 \(G_f\) 的割,且 \(\displaystyle\vert\vert S,T\vert\vert=\sum_{u\in S}\sum_{v\in T}c_f(u,v)=0\)。(因为 \(\set{S,T}\) 是 \(G_f\) 而不是 \(G\) 的割,因此统计的是 \(c_f\) 而不是 \(c\)。)
对于 \(u\in S,v\in T\) 的边 \((u,v)\) 进行分类讨论:
- \((u,v)\in E\):\(c_f(u,v)=0\),则 \(f(u,v)=c(u,v)\),即 \(\set{(u,v)\mid u\in S,v\in T}\) 满流。
- \((v,u)\in E\):\(c_f(u,v)=c(u,v)-f(u,v)=0-f(u,v)=0\),\(f(u,v)=0\),即 \(\set{(u,v)\mid u\in T,v\in S}\) 空流。
因此,总有 \(\set{(u,v)\mid u\in S,v\in T}\) 且 \(\set{(u,v)\mid u\in T,v\in S}\) 空流时,有最大流 \(\vert f\vert\) 和最小割 \(\vert\vert S,T\vert\vert\) 满足 \(\vert f\vert=\vert\vert S,T\vert\vert\)。
方案构造
求解最小割的大小是简单的,直接跑最大流即可。
那么 \(\set{S,T}\) 的具体构造,其实也就是最大流最小割定理中的 \(s\) 可以到达的所有点的点集 \(S\) 和 \(T=V\setminus S\)。
给定一张无向图,每条边有断边代价,断开一些边使得 \(1,2\) 不连通,求代价和最小的方案。
考虑 \(1\) 为源点,\(2\) 为汇点,那么原问题等价于最小割,对于 \(G=(V,E)\) 的最小割 \(\set{S,T}\),\(\set{(u,v)\in E\mid u\in S,v\in T}\) 即为所有断开的边。
可能特别需要考虑一下原图为无向图,那么将无向边转化为两条有向边,容量限制均为原来无向边的限制即可。
参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
typedef long long ll;
constexpr const int N=50,M=500;
constexpr const ll inf=0x3f3f3f3f3f3f3f3f;
struct graph{
struct edge{
int v,r;
ll w;
}g[M<<2|1];
int size=1,h[N+1];
void create(int u,int v,int w){
g[++size]={v,h[u],w};
h[u]=size;
}
edge& operator [](int x){
return g[x];
}
void clear(){
size=1;
memset(h,0,sizeof(h));
}
}g;
int n,s=1,t=2,cur[N+1];
ll dis[N+1];
bool bfs(){
memset(dis,0x3f,sizeof(dis));
for(int i=1;i<=n;i++){
cur[i]=g.h[i];
}
queue<int>q;
q.push(s);
dis[s]=0;
while(q.size()){
int x=q.front();q.pop();
for(int i=g.h[x];i;i=g[i].r){
auto [v,r,w]=g[i];
if(w>0&&dis[v]==inf){
q.push(v);
dis[v]=dis[x]+1;
if(v==t){
return true;
}
}
}
}
return false;
}
ll dfs(int x,ll Min){
if(x==t){
return Min;
}
ll ans=0;
for(int i=cur[x];i&&Min;i=g[i].r){
cur[x]=i;
auto [v,r,w]=g[i];
if(w&&dis[v]==dis[x]+1){
ll pl=dfs(v,min(Min,w));
if(!pl){
dis[v]=inf;
}
g[i].w-=pl;
g[i^1].w+=pl;
ans+=pl;
Min-=pl;
}
}
return ans;
}
ll Dinic(){
ll ans=0;
while(bfs()){
ans+=dfs(s,inf);
}
return ans;
};
bool inS[N+1];
void dfs1(int x){
if(inS[x]){
return;
}
inS[x]=true;
for(int i=g.h[x];i;i=g[i].r){
auto [v,r,w]=g[i];
if(w){
dfs1(v);
}
}
}
int main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
while(true){
int m;
cin>>n>>m;
if(!n&&!m){
break;
}
g.clear();
while(m--){
int u,v,w;
cin>>u>>v>>w;
g.create(u,v,w);
g.create(v,u,w);
}
Dinic();
memset(inS,0,sizeof(inS));
dfs1(s);
for(int u=1;u<=n;u++){
for(int i=g.h[u];i;i=g[i].r){
auto [v,r,w]=g[i];
if(inS[u]&&!inS[v]){
cout<<u<<' '<<v<<'\n';
}
}
}
cout<<'\n';
cout.flush();
}
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
费用流
对于网络 \(G=(V,E)\),每条边除了容量限制 \(c(u,v)\),还有单位流量的费用 \(w(u,v)\)。
流量为 \(f(u,v)\) 时,总费用为 \(f(u,v)\cdot w(u,v)\)。
\(w\) 满足斜对称性,即 \(w(u,v)=-w(v,u)\)。
求总花费最小的最大流称为最小费用最大流,即在最大化 \(\displaystyle\sum_{(u,v)\in E}f(u,v)\) 的情况下,最小化 \(\displaystyle\sum_{(u,v)\in E}f(u,v)\cdot w(u,v)\)。
SSP 算法
每次寻找单位费用最小的增广路进行增广,直到图上不存在增广路为止。
实际上也属于 Ford-Fulkerson 增广,用 Bellman-Ford 求最短路,时间复杂度为 \(\mathcal O(nm\vert f\vert)\)。
但是 Bellman-Ford 会跑满 \(\mathcal O(nm)\),一般写的都是 SPFA。
对于具体实现,以 Dinic 实现为例,SSP 就是把 BFS 分层,换成 SPFA 分层,每个点的 dis 从到 \(s\) 的距离,改为了到 \(s\) 的链上的单位费用和的最小值。
特别地,考虑单位费用可能为负,因此 Dinic 的 dfs 实现的时候需要记录 vis。不然可能会重复访问,然后无限递归。
给定一张网络的点数 \(n\),边数 \(m\),源点 \(s\),汇点 \(t\),和 \(m\) 条 \(u\rightarrow v\) 的费用为 \(w\)、容量为 \(c\) 的有向边。
求最小费用最大流。
\(1\leq n\leq5\times10^3,1\leq m\leq5\times10^4\),\(0\leq w,c\leq10^3\),最大流、最小费用不超过 \(2^{31}-1\),数据随机生成。
数据随机生成,可以跑 SPFA,总时间复杂度 \(\mathcal O(m\vert f\vert)\)。
参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
constexpr const int N=5e3,M=5e4,inf=0x3f3f3f3f;
struct graph{
struct edge{
int v,r,w,c;
}g[M<<2|1];
int h[N+1],size=1;
void create(int u,int v,int w,int c){
g[++size]={v,h[u],w,c};
h[u]=size;
}
edge& operator [](int x){
return g[x];
}
}g;
int n,s,t,dis[N+1],cur[N+1];
bool SPFA(){
static bool in[N+1];
memset(in,0,sizeof(in));
memset(dis,0x3f,sizeof(dis));
for(int i=1;i<=n;i++){
cur[i]=g.h[i];
}
queue<int>q;
q.push(s);
dis[s]=0;
in[s]=true;
while(q.size()){
int x=q.front();q.pop();
in[x]=false;
for(int i=g.h[x];i;i=g[i].r){
auto [v,r,w,c]=g[i];
if(w&&dis[v]>dis[x]+c){
dis[v]=dis[x]+c;
if(!in[v]){
q.push(v);
in[v]=true;
}
}
}
}
return dis[t]!=inf;
}
bool vis[N+1];
pair<int,int> dfs(int x,int Min){
if(x==t){
return {Min,0};
}
vis[x]=true;
int ansW=0,ansC=0;
for(int i=cur[x];i&&Min;i=g[i].r){
cur[x]=i;
auto [v,r,w,c]=g[i];
if(!vis[v]&&w&&dis[v]==dis[x]+c){
auto [plW,plC]=dfs(v,min(w,Min));
if(!plW){
dis[v]=inf;
}
g[i].w-=plW;
g[i^1].w+=plW;
Min-=plW;
ansW+=plW;
ansC+=plW*c+plC;
}
}
vis[x]=false;
return {ansW,ansC};
}
pair<int,int> Dinic(){
pair<int,int>ans;
while(SPFA()){
auto pl=dfs(s,inf);
ans.first+=pl.first;
ans.second+=pl.second;
}
return ans;
}
int main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int m;
cin>>n>>m>>s>>t;
while(m--){
int u,v,w,c;
cin>>u>>v>>w>>c;
g.create(u,v,w,c);
g.create(v,u,0,-c);
}
auto [w,c]=Dinic();
cout<<w<<' '<<c<<'\n';
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
网络流建模
最小割除数值上与最大流相等外,和最大流的性质没有任何关系。
因此,最小割问题中边的容量,其实可以直接理解为普通权值。
二元选择
其实可以视为切糕模型的特殊形式。
\(n\) 个物品和两个集合 \(A,B\),如果物品 \(i\) 未放入 \(A\) 花费 \(a_i\),未放入 \(B\) 花费 \(b_i\)。
同时给定 \(u_i,v_i,w_i\),若 \(u_i,v_i\) 不在同一集合内,花费 \(w_i\)。
一个物品只能放入一个集合,求最小代价和。
二者选其一,考虑网络流建模。
设置源点 \(s\),代表集合 \(A\);设置汇点 \(t\) 代表集合 \(B\)。
对于边 \(i\rightarrow j\),表示 \(i\) 要求 \(j\) 和 \(i\) 处于一个集合。其容量 \(c(i,j)\) 表示 \(i,j\) 不在同一集合时的花费。
具体而言,连边 \(s\rightarrow i\) 容量为 \(a_i\),\(i\rightarrow t\) 容量为 \(b_i\),\(u_i\rightarrow v_i,v_i\rightarrow u_i\) 容量均为 \(w_i\)。
对于冲突,考虑花费最小,因此割掉的边的容量之和要最小,即最小割。
求最小割即可。由最大流最小割定理,求最大流即可。
luogu P2057 [SHOI2007] 善意的投票 / [JLOI2010] 冠军调查
\(n\) 个人,放入 \(A,B\) 两个集合,\(A\) 表示反对,\(B\) 表示赞成。
设意愿 \(c_1,c_2,\cdots,c_n\),对于 \(i\),未放入 \(A\) 花费 \([c_i=0]\),未放入 \(B\) 花费 \([c_i=1]\)。
给定 \(m\) 组 \((i,j)\),如果 \(i,j\) 不在同一集合内,花费 \(1\)。
\(2\leq n\leq300,1\leq m\leq\dfrac{n(n-1)}2\)。
记 \(s=n+1,t=n+2\)。
按照上述方式连边,求解即可。
参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
constexpr const int N=300+2,M=N*(N-1)>>1,inf=0x3f3f3f3f;
struct graph{
struct edge{
int v,r,w;
}g[M+N*2<<1|1];
int h[N+1],size=1;
void create(int u,int v,int w){
g[++size]={v,h[u],w};
h[u]=size;
}
edge& operator [](int x){
return g[x];
}
}g;
int n,s,t,c[N+1],cur[N+1],dis[N+1];
bool bfs(){
memset(dis,0x3f,sizeof(dis));
for(int i=1;i<=n;i++){
cur[i]=g.h[i];
}
queue<int>q;
q.push(s);
dis[s]=0;
while(q.size()){
int x=q.front();q.pop();
for(int i=g.h[x];i;i=g[i].r){
auto [v,r,w]=g[i];
if(w>0&&dis[v]==inf){
q.push(v);
dis[v]=dis[x]+1;
if(v==t){
return true;
}
}
}
}
return false;
}
int dfs(int x,int Min){
if(x==t){
return Min;
}
int ans=0;
for(int i=cur[x];i&&Min;i=g[i].r){
cur[x]=i;
auto [v,r,w]=g[i];
if(w&&dis[v]==dis[x]+1){
int pl=dfs(v,min(Min,w));
if(!pl){
dis[v]=inf;
}
g[i].w-=pl;
g[i^1].w+=pl;
ans+=pl;
Min-=pl;
}
}
return ans;
}
int Dinic(){
int ans=0;
while(bfs()){
ans+=dfs(s,inf);
}
return ans;
}
int main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int m;
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>c[i];
}
s=n+1,t=n+2;
for(int i=1;i<=n;i++){
g.create(s,i,c[i]==0);
g.create(i,s,0);
g.create(i,t,c[i]==1);
g.create(t,i,0);
}
n+=2;
while(m--){
int u,v;
cin>>u>>v;
g.create(u,v,1);
g.create(v,u,1);
}
cout<<Dinic()<<'\n';
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
最大权闭合子图
即给定一张有向图,每个点 \(i\) 都有一个权值 \(a_i\),你需要选择一个权值和最大的子图,使得子图中每个点在原图中的后继(有出边的点)都在子图中。
建立源点 \(s\) 和汇点 \(t\)。若 \(a_i>0\),连容量为 \(a_i\) 的边 \(s\rightarrow i\);若 \(a_i<0\),连容量为 \(-a_i\) 的边 \(i\rightarrow t\)。
原图上所有边的容量为 \(+\infty\)。
最大权值和 \(=\) 正权和 \(-\) 最小割。
对于我们选择的子图,权值和 \(=\) 所有正权值之和 \(-\) 未选择的正权点的权值和 \(+\) 选择的负权点的权值和。
考虑一个割对应一个子图。因为一个割把图分成两部分,有 \(s\) 的那一部分没有边指向另一部分,有 \(t\) 的部分是闭合子图;而且钦定原图中的边容量为 \(+\infty\),求最小割的时候不会割掉原图的边,破坏原图性质。
钦定不选择一个正权点 \(i\) 时,断开 \(s\rightarrow i\);选择一个负权点 \(i\) 时,断开 \(i\rightarrow t\)。
断开边的权值和即为割的容量。
那么,这个割对应的子图的权值和 \(=\) 正权和 \(-\) 割的容量,则最大权值和 \(=\) 正权和 \(-\) 最小割。
对于最终方案, \(s\) 能到达的正权点是被选择的,不能到达 \(t\) 的负权点是被选择的。而不能到达 \(t\) 的负权点,就可以从 \(s\) 出发到达。
因此最终选择的点即从 \(s\) 出发的所有点。特别地,用 Dinic 求解时,可以直接判断求 \(G_L\) 之后有没有最后一次广搜得到的层数。
\(n\) 个仪器,\(m\) 个实验,每个实验需要一些仪器,仪器可重复使用。
配置仪器 \(i\) 的成本为 \(c_i\),实验 \(j\) 的收益为 \(p_j\)。
求净收益的最大值。
\(1\leq n,m\leq50,1\leq c,p<2^{31}\)。
实验对应 \(1\sim m\),仪器对应 \(m+1\sim m+n\)。
一个实验向其需要的仪器连有向边,表示该实验需要这些仪器。
之后就是选择一个闭合子图:仪器没有后继,而实验的后继必须全部选择。
净收益 \(=\) 实验收益 \(-\) 仪器成本。
以 \(p_j\) 为正权点,\(-c_i\) 为负权点,建图,即最大权闭合子图问题。钦定源点 \(s=m+n+1\),汇点 \(t=m+n+2\)。
按照上述建图求解即可。
参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
typedef long long ll;
constexpr const int N=102,M=N*N+1,inf=0x3f3f3f3f;
constexpr const ll lnf=0x3f3f3f3f3f3f3f3f;
struct graph{
struct edge{
int v,r;
ll w;
}g[M<<1|1];
int h[N+1],size=1;
void create(int u,int v,int w){
g[++size]={v,h[u],w};
h[u]=size;
}
edge& operator [](int x){
return g[x];
}
}g;
int n,s,t,dis[N+1];
int cur[N+1];
bool bfs(){
memset(dis,0x3f,sizeof(dis));
for(int i=1;i<=n;i++){
cur[i]=g.h[i];
}
queue<int>q;
dis[s]=0;
q.push(s);
while(q.size()){
int x=q.front();q.pop();
for(int i=g.h[x];i;i=g[i].r){
auto [v,r,w]=g[i];
if(w>0&&dis[v]==inf){
dis[v]=dis[x]+1;
q.push(v);
if(v==t){
return true;
}
}
}
}
return false;
}
ll dfs(int x,ll Min){
if(x==t){
return Min;
}
ll ans=0;
for(int i=cur[x];i&&Min;i=g[i].r){
cur[x]=i;
auto [v,r,w]=g[i];
if(w&&dis[v]==dis[x]+1){
ll pl=dfs(v,min(Min,w));
if(!pl){
dis[v]=inf;
}
g[i].w-=pl;
g[i^1].w+=pl;
ans+=pl;
Min-=pl;
}
}
return ans;
}
ll Dinic(){
ll ans=0;
while(bfs()){
ans+=dfs(s,lnf);
}
return ans;
}
int main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int n,m;
cin>>m>>n;
s=n+m+1,t=n+m+2;
::n=t;
ll ans=0;
for(int i=1;i<=m;i++){
int p;
cin>>p;
g.create(s,i,p);
g.create(i,s,0);
ans+=p;
string str;
getline(cin,str);
stringstream ss(str);
int x;
while(ss>>x){
g.create(i,x+m,inf);
g.create(x+m,i,0);
}
}
for(int i=1;i<=n;i++){
int c;
cin>>c;
g.create(m+i,t,c);
g.create(t,m+i,0);
}
ans-=Dinic();
for(int i=1;i<=m;i++){
if(dis[i]!=inf){
cout<<i<<' ';
}
}
cout<<'\n';
for(int i=1;i<=n;i++){
if(dis[i+m]!=inf){
cout<<i<<' ';
}
}
cout<<'\n'<<ans<<'\n';
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
切糕模型
给定代价函数 \(v(x,y,z),x\in[1,P],y\in[1,Q],z\in[1,R]\)。
构造函数 \(f(x,y),x\in[1,P],y\in[1,Q]\),满足 \(f(x,y)\in[1,R]\),且 \(\forall1\leq x,x'\leq P,1\leq y,y'\leq Q\),满足 \(\vert x-x'\vert+\vert y-y'\vert=1\),有 \(\vert f(x,y)-f(x',y')\vert\leq D\)。
求 \(\displaystyle\sum_{x=1}^P\sum_{y=1}^Qv(x,y,f(x,y))\) 最小值。
\(1\leq P,Q,R\leq40\),\(0\leq D\leq R\)。
考虑对于确定的 \(f(x_0,y_0)\),其需要从 \(1,2,3,\cdots,R\) 中选择一个值,等价于从长度为 \(z\) 的链上选择至少一条边删除。
具体而言,定义节点 \(V_{x_0,y_0,z}\),\(z=0,1,\cdots,R\)。钦定 \(s=V_{x_0,y_0,0}\) 为源点,\(t=V_{x_0,y_0,R}\) 为汇点。特别地,\(\forall x,y\),\(s=V_{x,y,0}\) 是同一个点,\(t=V_{x,y,R}\) 也是同一个点。
对于 \(i=0,1,\cdots,R-1\),构造一条 \(V_{x_0,y_0,i}\rightarrow V_{x_0,y_0,i+1}\) 的容量为 \(v(x_0,y_0,i+1)\) 的边。
此时选择 \(f(x_0,y_0)\),等价于割掉 \(V_{x_0,y_0,f(x_0,y_0)-1}\rightarrow V_{x_0,y_0,f(x_0,y_0)}\) 的边,割的容量增大 \(v(x_0,y_0,f(x_0,y_0))\)。
最终得到的就是一个图的割。
如果不考虑限制 \(\vert f(x,y)-f(x',y')\vert\leq D\),只需要求最小割即可。
现在考虑限制 \(\vert f(x,y)-f(x',y')\vert\leq D\),根据对称性可以拆成 \(f(x,y)-f(x',y')\leq D\)。
即 \(f(x,y)\leq f(x',y')+D\),令 \(z\leq f(x,y)\),则有 \(f(x',y')\geq z-D\)。因此当 \(f(x,y)\geq z\) 且 \(f(x',y')<z-D\) 时,选择不成立。
以 \(R=5,D=2\) 为例(省略了其他支路):

取 \(z=5\),对于 \(f(x,y)>5\) 的部分,应在 \(f(x',y')<3\) 时不成立。也就是说,\(f(x,y)\geq5\) 且 \(f(x',y')\leq2\) 的割是无效的。
为了让这个割不是最小割,可以有:

此时,所有 \(f(x,y)\geq 5\) 且 \(f(x',y')\leq 2\) 的割不再是割。如果割掉红边,需要 \(+\infty\) 的代价,一定不为最小割。这样,求最小割的过程中就自然避免了这种不合法的方案。

连出所有红边后,跑最小割即可。
具体而言,我们对于每一个 \(z\),连容量为 \(+\infty\) 的边 \(V_{x,y,z-1}\rightarrow V_{x',y',z-D-1}\),代表 \(f(x,y)\geq z,f(x',y')\leq z-D-1\) 不能同时成立。关于 \(z\) 的范围,因为 \(1\leq z-1,z-D-1\leq R-1\),所以 \(D+2\leq z\leq R\)。
参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
typedef long long ll;
constexpr const int P=40,Q=40,R=40,D=40,N=P*Q*R,M=100*N+1,inf=0x3f3f3f3f;
constexpr const int f[4][2]={{-1,0},{0,-1},{1,0},{0,1}};
constexpr const ll lnf=0x3f3f3f3f3f3f3f3f;
int p,q,r,d,s,t,v[P+1][Q+1][R+1];
struct graph{
struct edge{
int v,r;
ll w;
}g[M<<1|1];
int h[N+1],size=1;
void create(int u,int v,int w){
g[++size]={v,h[u],w};
h[u]=size;
}
edge& operator [](int x){
return g[x];
}
}g;
int V(int x,int y,int z){
if(z==0){
return s;
}else if(z==r){
return t;
}else{
return ((x-1)*q+y-1)*(r-1)+z;
}
}
int cur[N+1],dis[N+1];
bool bfs(){
memset(dis,0x3f,sizeof(dis));
for(int i=0;i<=t;i++){
cur[i]=g.h[i];
}
queue<int>q;
dis[s]=0;
q.push(s);
while(q.size()){
int x=q.front();q.pop();
for(int i=g.h[x];i;i=g[i].r){
auto [v,r,w]=g[i];
if(w&&dis[v]==inf){
dis[v]=dis[x]+1;
q.push(v);
if(v==t){
return true;
}
}
}
}
return false;
}
ll dfs(int x,ll Min){
if(x==t){
return Min;
}
ll ans=0;
for(int i=cur[x];i&&Min;i=g[i].r){
cur[x]=i;
auto [v,r,w]=g[i];
if(w&&dis[v]==dis[x]+1){
ll pl=dfs(v,min(w,Min));
if(!pl){
dis[v]=inf;
}
g[i].w-=pl;
g[i^1].w+=pl;
ans+=pl;
Min-=pl;
}
}
return ans;
}
ll Dinic(){
ll ans=0;
while(bfs()){
ans+=dfs(s,lnf);
}
return ans;
}
int main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>p>>q>>r>>d;
for(int z=1;z<=r;z++){
for(int i=1;i<=p;i++){
for(int j=1;j<=q;j++){
cin>>v[i][j][z];
}
}
}
s=0;
t=p*q*(r-1)+1;
for(int x=1;x<=p;x++){
for(int y=1;y<=q;y++){
for(int i=0;i<r;i++){
g.create(V(x,y,i),V(x,y,i+1),v[x][y][i+1]);
g.create(V(x,y,i+1),V(x,y,i),0);
}
}
}
for(int x=1;x<=p;x++){
for(int y=1;y<=q;y++){
for(int i=0;i<4;i++){
int xx=x+f[i][0],yy=y+f[i][1];
if(1<=xx&&xx<=p&&1<=yy&&yy<=q){
for(int z=d+2;z<=r;z++){
g.create(V(x,y,z-1),V(xx,yy,z-d-1),inf);
g.create(V(xx,yy,z-d-1),V(x,y,z-1),0);
}
}
}
}
}
cout<<Dinic()<<'\n';
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
/*
2 2 2
0
5 1
5 1
2 5
2 5
*/
有很多网络流问题都可以化归成这题的模型,即构造函数 \(f:S\rightarrow[1,R]\),例如比较经典的 \(0,1\) 二元选择问题。
由于「切糕」这个描述十分形象,所以这一类问题就被称为切糕模型。
即将对于函数 \(f\) 的值的选择变成对于链上的割的选择,将额外的代价/限制变成点之间的连边。 如果存在收益,那么将其提前加入答案中,然后变成不满足条件的代价。
网络流与线性规划 24 题
24 年 NOIP 集训时,某个人推荐给我的题单。虽然我现在才开始学网络流。
网络流与线性规划 24 题其实是一个中文互联网上古早的网络流题单。
餐巾计划问题
记原题面中的 \(N,p,m,f,n,s\) 分别为 \(n,p,a,\textit{ca},b,\textit{cb}\)。
这还是非常早期的一次模拟赛的第四题,应该是我第一次遇到网络流问题。
首先考虑如果第 \(i\) 天的可用餐巾的数量大于 \(r_i\),一定不优。因为延期送洗没有额外花费,而买了多的新的餐巾也可以之后再买。因此钦定第 \(i\) 天的餐巾为 \(r_i\) 条。
考虑把一天拆成早晚,因为早上会得到可用餐巾,晚上会得到脏餐巾并处理。
定义节点 \(V_{i,0/1}\) 分别表示第 \(i\) 天的早上、晚上对应的节点,\(s\) 为源点,\(t\) 为汇点。
考虑把「餐巾的操作」(购买、清洗、使用)视为「流」,在网络里流动起来,然后求最小费用最大流。
因而可以得到以下操作建模:
- 送到快洗部:连容量为 \(+\infty\)、费用为 \(\textit{ca}\) 的边 \(V_{i,1}\rightarrow V_{i+a,0}\)。
- 送到慢洗部:连容量为 \(+\infty\)、费用为 \(\textit{cb}\) 的边 \(V_{i,1}\rightarrow V_{i+b,0}\)。
- 延期送洗:连容量为 \(+\infty\)、费用为 \(0\) 的边 \(V_{i,1}\rightarrow V_{i+1,1}\)。
我们还需要的操作就是购买餐巾,和限制每天早晚用到的餐巾为 \(r_i\) 条。
<<1145141919810>>
限制 \(V_{i,1}\) 流出的餐巾为 \(r_i\) 条。
[CTSC1999] 家园 / 星际转移问题
飞行员配对方案问题
给定一个二分图,左部 \(1\sim m\) 编号,右部 \(m+1\sim n\) 编号,和左部至右部的若干条有向边,求其最大匹配并输出方案。
其实是二分图最大匹配问题。建源点、汇点,跑 Dinic 即可。时间复杂度 \(\mathcal O\left(m\sqrt n\right)\)。
输出方案就判断都是 \(1\sim n\) 的点,且流量为 \(1\)(剩余容量为 \(0\))。
参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
constexpr const int N=100,M=(N+2)*(N+2);
constexpr const int inf=0x3f3f3f3f;
struct graph{
struct edge{
int v,r,w;
}g[M<<1|1];
int h[N+1],size=1;
void create(int u,int v,int w){
g[++size]={v,h[u],w};
h[u]=size;
}
edge& operator [](int x){
return g[x];
}
}g;
int m,n,s,t,dis[N+1],cur[N+1];
bool bfs(){
memset(dis,0x3f,sizeof(dis));
for(int i=1;i<=n+2;i++){
cur[i]=g.h[i];
}
queue<int>q;
q.push(s);
dis[s]=0;
while(q.size()){
int x=q.front();q.pop();
for(int i=g.h[x];i;i=g[i].r){
auto [v,r,w]=g[i];
if(w>0&&dis[v]==inf){
dis[v]=dis[x]+1;
q.push(v);
if(v==t){
return true;
}
}
}
}
return false;
}
int dfs(int x,int Min){
if(x==t){
return Min;
}
int ans=0;
for(int i=cur[x];i&&Min;i=g[i].r){
cur[x]=i;
auto [v,r,w]=g[i];
if(w&&dis[v]==dis[x]+1){
int pl=dfs(v,min(Min,w));
if(!pl){
dis[v]=inf;
}
g[i].w-=pl;
g[i^1].w+=pl;
ans+=pl;
Min-=pl;
}
}
return ans;
}
int Dinic(){
int ans=0;
while(bfs()){
ans+=dfs(s,inf);
}
return ans;
}
int main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>m>>n;
s=n+1,t=n+2;
while(true){
int u,v;
cin>>u>>v;
if(u==-1){
break;
}
g.create(u,v,1);
g.create(v,u,0);
}
for(int i=1;i<=m;i++){
g.create(s,i,1);
g.create(i,s,0);
}
for(int i=m+1;i<=n;i++){
g.create(i,t,1);
g.create(t,i,0);
}
cout<<Dinic()<<'\n';
for(int u=1;u<=m;u++){
for(int i=g.h[u];i;i=g[i].r){
auto [v,r,w]=g[i];
if(v!=s&&v!=t&&!w){
cout<<u<<' '<<v<<'\n';
}
}
}
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
软件补丁问题
太空飞行计划问题
实验对应 \(1\sim m\),仪器对应 \(m+1\sim m+n\)。
一个实验向其需要的仪器连有向边,表示该实验需要这些仪器。
之后就是选择一个闭合子图:仪器没有后继,而实验的后继必须全部选择。
净收益 \(=\) 实验收益 \(-\) 仪器成本。
以 \(p_j\) 为正权点,\(-c_i\) 为负权点,建图,即最大权闭合子图问题。钦定源点 \(s=m+n+1\),汇点 \(t=m+n+2\)。
按照上述建图求解即可。
参考代码
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
typedef long long ll;
constexpr const int N=102,M=N*N+1,inf=0x3f3f3f3f;
constexpr const ll lnf=0x3f3f3f3f3f3f3f3f;
struct graph{
struct edge{
int v,r;
ll w;
}g[M<<1|1];
int h[N+1],size=1;
void create(int u,int v,int w){
g[++size]={v,h[u],w};
h[u]=size;
}
edge& operator [](int x){
return g[x];
}
}g;
int n,s,t,dis[N+1];
int cur[N+1];
bool bfs(){
memset(dis,0x3f,sizeof(dis));
for(int i=1;i<=n;i++){
cur[i]=g.h[i];
}
queue<int>q;
dis[s]=0;
q.push(s);
while(q.size()){
int x=q.front();q.pop();
for(int i=g.h[x];i;i=g[i].r){
auto [v,r,w]=g[i];
if(w>0&&dis[v]==inf){
dis[v]=dis[x]+1;
q.push(v);
if(v==t){
return true;
}
}
}
}
return false;
}
ll dfs(int x,ll Min){
if(x==t){
return Min;
}
ll ans=0;
for(int i=cur[x];i&&Min;i=g[i].r){
cur[x]=i;
auto [v,r,w]=g[i];
if(w&&dis[v]==dis[x]+1){
ll pl=dfs(v,min(Min,w));
if(!pl){
dis[v]=inf;
}
g[i].w-=pl;
g[i^1].w+=pl;
ans+=pl;
Min-=pl;
}
}
return ans;
}
ll Dinic(){
ll ans=0;
while(bfs()){
ans+=dfs(s,lnf);
}
return ans;
}
int main(){
/*freopen("test.in","r",stdin);
freopen("test.out","w",stdout);*/
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int n,m;
cin>>m>>n;
s=n+m+1,t=n+m+2;
::n=t;
ll ans=0;
for(int i=1;i<=m;i++){
int p;
cin>>p;
g.create(s,i,p);
g.create(i,s,0);
ans+=p;
string str;
getline(cin,str);
stringstream ss(str);
int x;
while(ss>>x){
g.create(i,x+m,inf);
g.create(x+m,i,0);
}
}
for(int i=1;i<=n;i++){
int c;
cin>>c;
g.create(m+i,t,c);
g.create(t,m+i,0);
}
ans-=Dinic();
for(int i=1;i<=m;i++){
if(dis[i]!=inf){
cout<<i<<' ';
}
}
cout<<'\n';
for(int i=1;i<=n;i++){
if(dis[i+m]!=inf){
cout<<i<<' ';
}
}
cout<<'\n'<<ans<<'\n';
cout.flush();
/*fclose(stdin);
fclose(stdout);*/
return 0;
}
试题库问题
最小路径覆盖问题
魔术球问题
最长不下降子序列问题
航空路线问题
方格取数问题
机器人路径规划问题(疑似错题)
圆桌问题
骑士共存问题
火星探险问题
最长k可重线段集问题
最长 k 可重区间集问题
孤岛营救问题
深海机器人问题
数字梯形问题
分配问题
运输问题
负载平衡问题
备忘录
在最大流的代码中,w 表示剩余流量,对应 \(c_f\)。
在费用流的代码中,w 表示剩余流量,c 表示单位费用,与 \(w,c_f\) 相反。

浙公网安备 33010602011771号