网络流
Part 1:前置知识
一个网络
设
-
容量限制:
-
斜对称:
-
流量守恒:
则称
Part 2:最大流
对于一个给定的网络,使得整个网络流量最大的流函数被称为网络的最大流,此时的流量被称为网络的最大流量
Ford–Fulkerson 增广(FF 增广)
我们将
在残量网络中,我们将一条从源点
由此,最大流的求解可以被视为若干次增广分别得到的流的叠加。值得注意的是,根据流的斜对称性,我们在增广时需要记得退流,即
,
这个退流操作的意义就是可以让我们在之后“反悔“”以前的增广操作,使得我们无需担心按照错误的顺序选择了增广路
容易发现,只要
这样增广的复杂度上界是
Edmonds–Karp 算法
贺一下 oi-wiki 上的算法流程:
-
如果在
上我们可以从 出发 到 ,则我们找到了新的增广路。 -
对于增广路
,我们计算出 经过的边的剩余容量的最小值 。我们给 上的每条边都加上 流量,并给它们的反向边都退掉 流量,令最大流增加了 。 -
因为我们修改了流量,所以我们得到新的
,我们在新的 上重复上述过程,直至增广路不存在,则流量不再增加。
在具体实现时,可以使用“成对储存”的技巧去存储每一条边及其反向边
时间复杂度
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N=210,M=5010,INF=(1<<30);
int n,m,s,t,incf[2*M],pre[N];
int head[N],ver[2*M],nxt[2*M],edge[2*M],tot=1;
LL maxflow;
bool v[N];
void add(int x,int y,int z)
{
ver[++tot]=y; edge[tot]=z; nxt[tot]=head[x]; head[x]=tot;
ver[++tot]=x; edge[tot]=0; nxt[tot]=head[y]; head[y]=tot;
}
bool bfs()
{
memset(v,0,sizeof(v));
queue <int> q;
q.push(s); v[s]=1;
incf[s]=INF;
while(q.size())
{
int x=q.front(); q.pop();
for(int i=head[x]; i; i=nxt[i])
{
if(edge[i])
{
int y=ver[i];
if(v[y])
continue;
incf[y]=min(incf[x],edge[i]);
pre[y]=i;
q.push(y); v[y]=1;
if(y==t)
return 1;
}
}
}
return 0;
}
void update()
{
int x=t;
while(x!=s)
{
int i=pre[x];
edge[i]-=incf[t];
edge[i^1]+=incf[t];
x=ver[i^1];
}
maxflow+=incf[t];
}
LL EK()
{
while(bfs())
update();
return maxflow;
}
int main()
{
scanf("%d%d%d%d",&n,&m,&s,&t);
for(int i=1; i<=m; i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
}
printf("%lld",EK());
return 0;
}
Dinic 算法
设
形式化地,我们称
如果我们在层次图
定义层次图和阻塞流后,
-
在
上 出层次图 。 -
在
上 出阻塞流 。 -
将
并到原先的流 中,即 。 -
重复以上过程直到不存在从
到 的路径。
此时的
当前弧优化
在
当前弧优化是必要的,否则复杂度很容易被卡到上界
时间复杂度的上界是
特别地,
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N=210,M=5010,INF=(1<<30);
int n,m,s,t,d[N];
int head[N],ver[2*M],nxt[2*M],edge[2*M],now[N],tot=1;
LL maxflow;
void add(int x,int y,int z)
{
ver[++tot]=y; edge[tot]=z; nxt[tot]=head[x]; head[x]=tot;
ver[++tot]=x; edge[tot]=0; nxt[tot]=head[y]; head[y]=tot;
}
bool bfs()
{
memset(d,0,sizeof(d));
queue <int> q;
q.push(s); d[s]=1;
now[s]=head[s];
while(q.size())
{
int x=q.front(); q.pop();
for(int i=head[x]; i; i=nxt[i])
{
if(edge[i] && !d[ver[i]])
{
int y=ver[i];
q.push(y);
now[y]=head[y];
d[y]=d[x]+1;
if(y==t)
return 1;
}
}
}
return 0;
}
int dinic(int x,int flow)
{
if(x==t)
return flow;
int rest=flow,k,i;
for(i=now[x]; i && rest; i=nxt[i])
{
if(edge[i] && d[ver[i]]==d[x]+1)
{
int y=ver[i];
k=dinic(y,min(rest,edge[i]));
if(!k)
d[y]=0;
edge[i]-=k;
edge[i^1]+=k;
rest-=k;
if(rest<=0)
break;
}
}
now[x]=i;
return flow-rest;
}
LL Dinic()
{
LL flow=0;
while(bfs())
while(flow=(LL)dinic(s,INF))
maxflow+=flow;
return maxflow;
}
int main()
{
scanf("%d%d%d%d",&n,&m,&s,&t);
for(int i=1; i<=m; i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
}
printf("%lld",Dinic());
return 0;
}
常见技巧与模型
拆点:P1231 教辅的组成
为了保证每本书只被用一次,需要将同本书拆成
点边转化:P2472 [SCOI2007] 蜥蜴
将原图抽象成无向图,再进行点边转化,具体操作见下文
Part 3:最小割
对于一个给定的网络
最大流最小割定理
常见技巧与模型
能用最小割解决的题目往往有如下特征:
-
每个物体只有两种状态,要么选,要么不选;
-
给定所有物体被选择的状态,则问题能够得到唯一的答案。
理清逻辑关系,贡献与损失间的转化,在最小割的题目中尤为重要
点边转化: UVA1660 Cable TV Network
无向图的点转边分为三步:
-
将每个点
拆成 和 两个点 -
对于
,连有向边 ,容量为 (其实也可以说是点权) -
对于原无向图的每条边
,在网络中连有向边 ,容量为 (防止割断)
这样,
求补集:P2774 方格取数问题
由于最小割的特殊性,它往往可以用于求一些满足“答案
集合划分模型:P2057 [SHOI2007] 善意的投票 / [JLOI2010] 冠军调查
有
个物品和两个集合 。如果将一个物品放入 会产生 的花费,放入 中会产生 的花费。还有若干形如 的限制,表示当 两点不在同一个集合时,就会产生 的额外花费。求总花费最小值
对于每个点
当
因此,最小割即为最小花费
集合划分模型变式:P1646 [国家集训队] happiness
前两种情况建边方式同上。后四种情况,先考虑文科,对于
最小割的可行边与必须边:P4126 [AHOI2009] 最小割
首先求最大流,那么最小割的可行边和必须边都必须是满流
-
可行边
:残量网络中不存在 到 的路径 -
必须边
:残量网络中 能到 且 能到
具体实现时先跑一遍
最大权闭合子图
若有向图
使得子图中点权和最大的闭合子图被称为
构图方式
建立源点
定理
-
最大权闭合子图的点权和等于所有正点权和减去最小割
-
上述图的最小割上述图的最小割包含
到不在最大权闭合图内的正权节点的边和在最大权闭合图内的负权节点到 的边。
最大权闭合子图方案
在残量网络中
[模板] Petya and Graph / P4174 [NOI2006] 最大获利
把所有点当作负权点,所有边当作正权点。
考虑一条边
建模完毕后用上述方法计算最大权闭合子图点权和即可
最小割树
对于一张带容量的无向图
维护一个树形结构
容易发现,我们会得到一课树,这棵树被称为最小割树
一些引理
-
对于
,总有 -
任取三点
, 之中,最小值至少出现两次 -
由这些引理我们可以推出下面的定理
最小割树定理
【模板】最小割树(Gomory-Hu Tree)
#include<bits/stdc++.h>
#define mp make_pair
using namespace std;
const int N=510,M=4010,INF=1e9;
int n,m,q,node[N],t1[N],t2[N],dep[N],f[N][15],ans[N][15];
int s,t,maxflow,d[N];
int head[N],ver[2*M],nxt[2*M],edge[2*M],now[N],tot=1;
vector < pair<int,int> > g[N];
void add(int x,int y,int z)
{
ver[++tot]=y; edge[tot]=z; nxt[tot]=head[x]; head[x]=tot;
ver[++tot]=x; edge[tot]=0; nxt[tot]=head[y]; head[y]=tot;
}
void init()
{
maxflow=0;
for(int i=2; i<=tot; i+=2)
edge[i]+=edge[i^1],edge[i^1]=0;
}
bool bfs()
{
memset(d,0,sizeof(d));
queue <int> q;
d[s]=1; now[s]=head[s];
q.push(s);
while(q.size())
{
int x=q.front(); q.pop();
for(int i=head[x]; i; i=nxt[i])
{
if(edge[i] && !d[ver[i]])
{
int y=ver[i];
d[y]=d[x]+1;
now[y]=head[y];
q.push(y);
if(y==t)
return 1;
}
}
}
return 0;
}
int dinic(int x,int flow)
{
if(x==t)
return flow;
int rest=flow,k;
for(int &i=now[x]; i && rest; i=nxt[i])
{
if(edge[i] && d[ver[i]]==d[x]+1)
{
int y=ver[i];
k=dinic(y,min(rest,edge[i]));
if(!k)
d[y]=0;
edge[i]-=k;
edge[i^1]+=k;
rest-=k;
if(rest<=0)
break;
}
}
return flow-rest;
}
int Dinic()
{
init();
int flow=0;
while(bfs())
while(flow=dinic(s,INF))
maxflow+=flow;
return maxflow;
}
void build(int l,int r)
{
if(l>=r)
return;
s=node[l]; t=node[l+1];
int cut=Dinic();
g[s].push_back(mp(t,cut));
int cnt1=0,cnt2=0;
for(int i=l; i<=r; i++)
{
if(d[node[i]])
t1[++cnt1]=node[i];
else
t2[++cnt2]=node[i];
}
for(int i=l; i<=l+cnt1-1; i++)
node[i]=t1[i-l+1];
for(int i=l+cnt1; i<=r; i++)
node[i]=t2[i-l-cnt1+1];
build(l,l+cnt1-1); build(l+cnt1,r);
}
void dfs(int x,int fa)
{
dep[x]=dep[fa]+1;
f[x][0]=fa;
for(int i=1; i<=10; i++)
{
f[x][i]=f[f[x][i-1]][i-1];
ans[x][i]=min(ans[x][i-1],ans[f[x][i-1]][i-1]);
}
for(int i=0; i<g[x].size(); i++)
{
int y=g[x][i].first,z=g[x][i].second;
if(y==fa)
continue;
ans[y][0]=z;
dfs(y,x);
}
}
int query(int x,int y)
{
if(dep[x]<dep[y])
swap(x,y);
int res=INF;
for(int i=10; i>=0; i--)
if(dep[f[x][i]]>=dep[y])
res=min(res,ans[x][i]),x=f[x][i];
if(x==y)
return res;
for(int i=10; i>=0; i--)
if(f[x][i]!=f[y][i])
res=min(res,min(ans[x][i],ans[y][i])),x=f[x][i],y=f[y][i];
res=min(res,min(ans[x][0],ans[y][0]));
return res;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1; i<=m; i++)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z); add(y,x,z);
}
for(int i=1; i<=n; i++)
node[i]=i;
build(1,n);
dfs(1,0);
scanf("%d",&q);
for(int i=1; i<=q; i++)
{
int x,y;
scanf("%d%d",&x,&y);
printf("%d\n",query(x,y));
}
return 0;
}
Part 4:费用流
给定一个网络
注意:费用流的前提是最大流,然后才考虑费用的最值。
SSP 算法
在
#include<bits/stdc++.h>
using namespace std;
const int N=5010,M=50010,INF=0x3f3f3f3f;
int n,m,s,t,maxflow,ans;
int d[N],incf[N],pre[N],v[N];
int head[N],ver[2*M],nxt[2*M],edge[2*M],cost[2*M],tot=1;
void add(int x,int y,int z,int c)
{
ver[++tot]=y; edge[tot]=z; cost[tot]=c;
nxt[tot]=head[x]; head[x]=tot;
ver[++tot]=x; edge[tot]=0; cost[tot]=-c;
nxt[tot]=head[y]; head[y]=tot;
}
bool spfa()
{
memset(d,0x3f,sizeof(d));
queue <int> q;
q.push(s); d[s]=0; v[s]=1;
incf[s]=INF;
while(q.size())
{
int x=q.front(); q.pop();
v[x]=0;
for(int i=head[x]; i; i=nxt[i])
{
if(!edge[i])
continue;
int y=ver[i];
if(d[y]>d[x]+cost[i])
{
d[y]=d[x]+cost[i];
incf[y]=min(incf[x],edge[i]);
pre[y]=i;
if(!v[y])
v[y]=1,q.push(y);
}
}
}
if(d[t]==INF)
return 0;
return 1;
}
void update()
{
int x=t;
while(x!=s)
{
int i=pre[x];
edge[i]-=incf[t];
edge[i^1]+=incf[t];
x=ver[i^1];
}
maxflow+=incf[t];
ans+=d[t]*incf[t];
}
void SSP()
{
while(spfa())
update();
}
int main()
{
scanf("%d%d%d%d",&n,&m,&s,&t);
for(int i=1; i<=m; i++)
{
int x,y,z,c;
scanf("%d%d%d%d",&x,&y,&z,&c);
add(x,y,z,c);
}
SSP();
printf("%d %d",maxflow,ans);
return 0;
}
常见技巧与模型
费用提前计算:P2053 [SCOI2007] 修车
由于每个人的等待时间受到前面人的影响,导致无法直接建模。但是我们发现,每个人对后面人的贡献是可以计算的,设第
所以我们可以将每一个工人拆成
最后跑
动态开点:P2050 [NOI2012] 美食节
上一题的加强版
像上一题那样建图规模太大,无法通过此题。稍加思考可发现这样建图会有许多浪费的点,所以考虑动态开点,每增广一次再新开一个节点,这样可大大减少图的规模
费用流自行求解最短路/最小环覆盖:P6061 [加油武汉] 疫情调查
最小环覆盖,用二分图带权最大匹配解决
将每个点拆成
但是这样做边数达到
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?