网络流学习笔记

网络流学习笔记

引入+概念

网络

网络是指一个有向图 G=(V,E)

每条边 (u,v)E 都有一个权值 c(u,v),称之为容量,当 (u,v)E 时有 c(u,v)=0

其中有两个特殊的点:源点 s 和汇点 t,其中 s,tV,并且 st

f(u,v) 定义在二元组 (uV,vV) 上的实数函数且满足:

1.容量限制:对于每条边,流经该边的流量不得超过该边的容量,即 f(u,v)c(u,v)

2.斜对称性:每条边的流量与其相反边的流量之和为 0,即 f(u,v)=f(v,u)

3.流守恒性:从源点流出的流量等于汇点流入的流量。

那么称 f 为网络 G 的流函数。对于 (u,v)Ef(u,v) 称为边的 流量c(u,v)f(u,v) 成为边的剩余容量。整个网络的流量为 (s,v)Ef(s,v),即 从源点发出的所有流量之和

(以上内容摘自 OI-Wiki)

关于网络流问题,常见的有最大流,最小割,费用流等。实际上,对于网络和流的概念我们目前并不需要深入研究,理解即可。在 OI 中对网络流的考察,更偏向于抽象建模能力而非算法本身。

网络最大流

求解网络最大流的基本思路就是寻找增广路。我们将 G 上一条从 st 的路径称为增广路,而对于一条增广路,我们给每一条边 (u,v) 都加上等量的流量,以使整个网络的流量增加,这一过程称为增广。

这里有三种算法求解网络最大流—— EK(Edmond-Karp) 算法, Dinic 算法,还有 ISAP 算法。

Edmond-Karp 算法

算法思想

利用 bfs 不断寻找增广路,直到无法增广为止。

算法过程

  1. s 开始 bfs,如果可以到 t 则我们找到了新的增广路;
  2. 对于增广路 p,我们计算出 p 经过的边的剩余容量的最小值 Δ=min(u,v)pc(u,v)。我们给 p 上每条边都加上 Δ 流量,并给它们的反向边都退掉 Δ 流量,令最大流增加了 Δ
  3. 修改原图 G,在新的图 G 上重复上述过程,直到无法找到新的增广路。

复杂度

上界 O(nm2),一般跑不满。(本蒟蒻不会证qwq)。

代码

#include<bits/stdc++.h>
using namespace std;
int n, m, s, t;
const int N = 205, M = 5050;
const long long inf = 1111111111111111111;
int head[N], tot = 1, pre[N];
long long incf[N]; bool vis[N];
struct node
{
int nxt, to, w;
}edge[M<<1];
void add(int u, int v, int w)
{
edge[++tot].nxt = head[u];
edge[tot].to = v;
edge[tot].w = w;
head[u] = tot;
edge[++tot].nxt = head[v];
edge[tot].to = u;
edge[tot].w = 0;
head[v] = tot;
}//利用链式前向星连续加边的性质来处理正向边与反向边,注意从 2 开始。
bool bfs()
{
memset(vis, 0, sizeof(vis));//标记已经经过的点。
queue<int> q;
q.push(s); vis[s] = 1;
incf[s] = inf;
while(q.size())
{
int x = q.front(); q.pop();
for(int i = head[x]; i; i = edge[i].nxt)
{
if(edge[i].w!=0)
{
int y = edge[i].to;
if(vis[y]) continue;
incf[y] = min(incf[x], 1ll*edge[i].w);
pre[y] = i;
q.push(y); vis[y] = 1;
if(y == t) return 1;
}
}
}
return 0;
}
long long maxflow = 0;
void update()
{
int x = t;
while(x != s)
{
int i = pre[x];
edge[i].w -= incf[t];
edge[i^1].w+=incf[t];
x = edge[i^1].to;
}
maxflow += incf[t];
}
int main()
{
scanf("%d%d%d%d", &n, &m, &s, &t);
for(int i = 1; i<=m; i++)
{
int u, v, c;
scanf("%d%d%d", &u, &v, &c);
add(u, v, c);
}
while(bfs()) update();
printf("%lld", maxflow);
return 0;
}

Dinic 算法

其实我们更常用的是 Dinic 算法,它复杂度要比 EK 算法优秀(理论上界 n2m,本蒟蒻还是不会证xwx)。

算法思想

在增广前先对 G 进行 bfs 分层,即按照点 us 的距离分为若干层,使每次只能使 u 的流量向下一层流去。

算法过程

  1. s 开始 bfs 图 G,求出每个点的层次;
  2. dfs 全图,同时对各边的容量进行减少或退回;
  3. 对新的图 G,重复上述过程,直到无法 bfs 到 t

代码

注:Dinic 算法不进行当前弧优化复杂度是不对的,所以下面代码不保证复杂度正确

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 205, M = 5050;
const ll INF = 0x3f3f3f3f3f3f3f3f;
int s, t;
int tot = 1;
int head[N];
struct node{
int nxt, to;
ll w;
}edge[M<<1];
void add(int u, int v, ll w){
edge[++tot].nxt = head[u];
edge[tot].to = v;
edge[tot].w = w;
head[u] = tot;
}
void Add(int u, int v, int w){
add(u, v, w);
add(v, u, 0);
}
int dep[N];
bool bfs(){
queue<int> q;
memset(dep, 0, sizeof(dep));
dep[s] = 1;
q.push(s);
while(!q.empty()){
int u = q.front();
q.pop();
for(int i = head[u]; i; i = edge[i].nxt){
int v = edge[i].to;
if((edge[i].w>0)&&(!dep[v])){
dep[v] = dep[u]+1;
q.push(v);
}
}
}
if(dep[t]){
return 1;
}
return 0;
}
ll dfs(int u, ll flow){
if(u == t) return flow;
for(int i = head[u]; i; i = edge[i].nxt){
int v = edge[i].to;
if(dep[v] == dep[u]+1&&(edge[i].w)){
ll fl = dfs(v, min(flow, edge[i].w));
if(fl>0){
edge[i].w -=fl;
edge[i^1].w+=fl;
return fl;
}
}
}
return 0;
}
ll ans;
int n, m;
int main(){
scanf("%d%d%d%d", &n, &m, &s, &t);
for(int i = 1; i<=m; ++i){
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
Add(u, v, w);
}
while(bfs()){
ll d = dfs(s, INF);
while(d){
ans+=d;
d = dfs(s, INF);
}
}
printf("%lld\n", ans);
return 0;
}

当前弧优化

刚才的过程中,由于我们每次都需要遍历 u 的每一条出边,导致局部复杂度可能达到 O(|E|2)。我们可以记录一下之前 u 循环到哪条边,以此来加速。

代码

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 205, M = 5050;
const ll INF = 0x3f3f3f3f3f3f3f3f;
int s, t;
int tot = 1;
int head[N];
struct node{
int nxt, to;
ll w;
}edge[M<<1];
void add(int u, int v, ll w){
edge[++tot].nxt = head[u];
edge[tot].to = v;
edge[tot].w = w;
head[u] = tot;
}
void Add(int u, int v, int w){
add(u, v, w);
add(v, u, 0);
}
int dep[N], cur[N];//cur用来记录当前到了哪条边,防止重复遍历
bool bfs(){
queue<int> q;
memset(dep, 0, sizeof(dep));
dep[s] = 1;//必须初始化为 1 !警钟长鸣!
q.push(s);
while(!q.empty()){
int u = q.front();
q.pop();
for(int i = head[u]; i; i = edge[i].nxt){
int v = edge[i].to;
if((edge[i].w>0)&&(!dep[v])){
dep[v] = dep[u]+1;
q.push(v);
}
}
}
if(dep[t]){
return 1;
}
return 0;
}
ll dfs(int u, ll flow){
if(u == t) return flow;
for(int &i = cur[u]; i; i = edge[i].nxt){//利用取址符每次修改当前弧
int v = edge[i].to;
if(dep[v] == dep[u]+1&&(edge[i].w)){
ll fl = dfs(v, min(flow, edge[i].w));
if(fl>0){
edge[i].w -=fl;
edge[i^1].w+=fl;
return fl;
}
}
}
return 0;
}
ll ans;
int n, m;
int main(){
scanf("%d%d%d%d", &n, &m, &s, &t);
for(int i = 1; i<=m; ++i){
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
Add(u, v, w);
}
while(bfs()){
for(int i = 1; i<=n; ++i){
cur[i] = head[i];//记得每次bfs后初始化
}
ll d = dfs(s, INF);
while(d){
ans+=d;
d = dfs(s, INF);
}
}
printf("%lld\n", ans);
return 0;
}

ISAP 算法

该算法是基于 Dinic 的。在 Dinic 算法中,我们每次求完增广路都需要跑 bfs 来分层;而 ISAP 可以通过反向分层来达到更高的效率。

ISAP 中也存在当前弧优化,同时,它也有另一个优化:GAP 优化。具体来讲,就是我们记录一个 num 数组表示每层内的点数,若更新后某一层 num 变成 0,则说明出现断层,这样一定是无法找到增广路,直接退出算法即可。

代码

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 205, M = 5050;
const ll INF = 0x3f3f3f3f3f3f3f3f;
int head[N], tot = 1;
struct node{
int from, nxt, to;ll w;
}edge[M<<1];
int s, t;
int n, m;
ll ans;
int path[N];//记录增广路。
int cur[N], num[N]/*每层节点数,用于GAP优化*/,dep[N];
bool vis[N];
void add(int u, int v, int w){
edge[++tot].nxt = head[u];
edge[tot].from = u;
edge[tot].to = v;
edge[tot].w = w;
head[u] = tot;
}
void adde(int u, int v, int w){
add(u, v, w);
add(v, u, 0);
}
bool bfs(){
memset(vis, 0, sizeof(vis));
queue<int> q;
q.push(t);//跑反图分层
vis[t] = 1;
dep[t] = 0;
while(!q.empty()){
int v = q.front();
q.pop();
for(int i = head[v]; i; i = edge[i].nxt){
node e = edge[i^1];//反向边才是原图的边。
int u = e.from;
if(!vis[u] && e.w){
vis[u] = 1;
dep[u] = dep[v]+1;
q.push(u);
}
}
}
return vis[s];
}
ll augment(){
int u = t;
ll imp = INF;
while(u ^ s){
node e = edge[path[u]];
imp = min(imp, e.w);
u = e.from;
}
u = t;
while(u ^ s){
edge[path[u]].w -= imp;
edge[path[u] ^ 1].w += imp;
u = edge[path[u]].from;
}
return imp;
}
ll maxFlow(){
ll ret = 0;
bfs();// ISAP 在dfs中动态修改分层,所以一边bfs预处理即可。
for(int i = 1; i<=n; ++i){
num[dep[i]]++;//统计每层节点数目
}
int u = s;
for(int i = 1; i<=n; ++i) cur[i] = head[i];//当前弧优化初始化
while(dep[s] < n){
if(u == t){// 如果找到汇点,开始增广。
ret += augment();
u = s;
}
bool ok = false;
for(int &i = cur[u]; i; i = edge[i].nxt){
node e = edge[i];
int v = e.to;
if(dep[u] == dep[v]+1 && e.w){//找到一条增广路
ok = true;
path[v] = i;
u = v;
break;
}
}
if(!ok){//如果没找到,说明正向边残量全为0, 反向边残量大于0,更新层数。
//其实这等于你把head[u]遍历完了,所以增广已经全部完成,接下来的遍历只会找到反向边。
//需要注意,这里的层数是对于反图来讲的。
//或者说,这一步动态维护了原来需要bfs求出来的信息。
int mind = n;
for(int i = head[u]; i; i = edge[i].nxt){
node e = edge[i];
int v = e.to;
if(e.w) mind = min(mind, dep[v]);//找到最小层数(其实就是最短路)
}
if(--num[dep[u]] == 0) break;//GAP优化
dep[u] = mind+1;//层数改变
num[dep[u]]++;//修改层内点数
cur[u] = head[u];//当前弧重置
if(u ^ s) u = edge[path[u]].from;//向前回溯一个节点。
}
}
return ret;
}
int main(){
scanf("%d%d%d%d", &n, &m, &s, &t);
for(int i = 1; i<=m; ++i){
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
adde(u, v, w);
}
printf("%lld\n", maxFlow());
return 0;
}

最小费用最大流

对于一些问题,我们的网络中的每条边 (u,v),不仅有容量限制,还有一个权值(费用),我们要做的,就是保证整个网络流最大的前提下,让权值和最小。需要注意的是,这里的费用是单位流量的费用。

其实,我们只需要对原算法进行改造即可。我们令正向边边权为正,逆向边边权为负。整个过程其实在寻找增广路的时候去找最短路,也就是,我们把 bfs 改成最短路算法即可。这里我们以 spfa 为例(用于处理负环)。需要注意,flowdst 特别容易搞混,所以尽量不要简写防止晕菜。

代码:

#include<bits/stdc++.h>
#define ll long long
#define int long long
using namespace std;
const int N = 5050, M = 50050;
inline int read(){
int x = 0; char ch = getchar();
while(ch<'0' || ch>'9') ch = getchar();
while(ch>='0'&&ch<='9'){
x = x*10+ch-48; ch = getchar();
}
return x;
}
ll dst[N];int s, t;
int head[N], tot = 1;
struct node{
int nxt, to;ll fl, c;
}edge[M<<1];
void add(int u, int v, int w, int c){
edge[++tot].nxt = head[u];
edge[tot].to = v;
edge[tot].fl= w;
edge[tot].c = c;
head[u] = tot;
}
void adde(int u, int v, int w, int c){
add(u, v, w, c);
add(v, u, 0, -c);
}
int n, m;
int pre[N], last[M];
ll flow[N];
bool vis[N];
queue<int> q;
bool spfa(){
memset(dst, 0x3f, sizeof(dst));
memset(flow, 0x3f, sizeof(flow));
memset(vis, 0, sizeof(vis));
q.push(s);
vis[s] = 1;
dst[s] = 0;
pre[t] = -1;
while(!q.empty()){
int u = q.front();
q.pop();
vis[u] = 0;
for(int i = head[u]; i; i = edge[i].nxt){
int v = edge[i].to;
if(edge[i].fl>0&&dst[v]>dst[u]+edge[i].c){
dst[v] = dst[u]+edge[i].c;
pre[v] = u;
last[v] = i;
flow[v] = min(flow[u], 1ll*edge[i].fl);
if(!vis[v]){
vis[v] = 1;
q.push(v);
}
}
}
}
return pre[t]!=-1;
}
ll maxFlow, minCost;
void MCMF(){
while(spfa()){
int u = t;
maxFlow+=flow[t];
minCost+=flow[t]*dst[t];
while(u^s){
edge[last[u]].fl-=flow[t];
edge[last[u]^1].fl+=flow[t];
u = pre[u];
}
}
}
signed main(){
n = read(), m = read();
s = read(), t = read();
for(int i = 1; i<=m; ++i){
int u = read(), v = read(), w = read(), c = read();
adde(u, v, w, c);
}
MCMF();
printf("%d %d\n", maxFlow, minCost);
return 0;
}

一些注意事项

deps 一定要初始化为 1

posted @   霜木_Atomic  阅读(39)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示