网络流学习笔记
网络流
1.关于网络的一些定义:
(1)网络:
网络(又称流网络
每条边
其中有两个特殊点:分别是源点
(2)流
设
1). 容量限制:对于每条边,流经该边的流量不得超过该边的容量,即
2).斜对称性:每条边流量与其相反边流量之和为零即
3).流守恒性:从源点流出的流量等于汇点流入的流量,即
那么
一般而言也可以把网络流理解为整个图的流量,流量均满足上述性质.
2.网络流的常见问题
1).最大流
我们有一张图,要求从源点向汇点的最大流量(可以有很多条路到达汇点),就是最大流问题.
2).最小费用最大流
最小费用最大流问题:每条边都有一个费用,代表单位流量经过这条边的开销.我们要在求出最大流的同时,要求花费费用最小.
3).最小割
割其实就是删边的意思,当然最小割是删掉
----------------------------------------------(分割线)
以下是算法部分
2.最大流
本页面主要介绍最大流问题相关的算法知识
(1) 增广
概述
网络
我们将
我们将
Tips:在最大流算法的代码实现中,我们往往需要支持快速访问反向边的操作。在邻接矩阵中,这一操作是 trivial(琐碎的)的(
初次接触可能会遇到违反直觉的情形--反向边的流量可能是个负值.实际上我们可以注意到,在增广过程中真正有意义的是剩余容量
假设
由上可知,退流带来的
容易发现,只要
上最大流.这是因为单轮增广时间复杂度为
增广的实现:
(1). 算法
思想
· 如果在
· 对于增广路
· 因为我们修改流量,所以我们得到形
时间复杂度:
code:
#define maxn 250
#define INF 0x3f3f3f3f
struct Edge {
int from, to, cap, flow;
Edge(int u, int v, int c, int f) : from(u), to(v), cap(c), flow(f) {}
};
struct EK {
int n, m; // n:点数,m:边数
vector<Edge> edges; // edges:所有边的集合
vector<int> G[maxn]; // G:点 x -> x 的所有边在 edges 中的下标
int a[maxn], p[maxn]; // a:点 x -> BFS 过程中最近接近点 x 的边给它的最大流
// p:点 x -> BFS 过程中最近接近点 x 的边
void init(int n) {
for (int i = 0; i < n; i++) G[i].clear();
edges.clear();
}
void AddEdge(int from, int to, int cap) {
edges.push_back(Edge(from, to, cap, 0));
edges.push_back(Edge(to, from, 0, 0));
m = edges.size();
G[from].push_back(m - 2);
G[to].push_back(m - 1);
}
int Maxflow(int s, int t) {
int flow = 0;
for (;;) {
memset(a, 0, sizeof(a));
queue<int> Q;
Q.push(s);
a[s] = INF;
while (!Q.empty()) {
int x = Q.front();
Q.pop();
for (int i = 0; i < G[x].size(); i++) { // 遍历以 x 作为起点的边
Edge& e = edges[G[x][i]];
if (!a[e.to] && e.cap > e.flow) {
p[e.to] = G[x][i]; // G[x][i] 是最近接近点 e.to 的边
a[e.to] =
min(a[x], e.cap - e.flow); // 最近接近点 e.to 的边赋给它的流
Q.push(e.to);
}
}
if (a[t]) break; // 如果汇点接受到了流,就退出 BFS
}
if (!a[t])
break; // 如果汇点没有接受到流,说明源点和汇点不在同一个连通分量上
for (int u = t; u != s;
u = edges[p[u]].from) { // 通过 u 追寻 BFS 过程中 s -> t 的路径
edges[p[u]].flow += a[t]; // 增加路径上边的 flow 值
edges[p[u] ^ 1].flow -= a[t]; // 减小反向路径的 flow 值
}
flow += a[t];
}
return flow;
}
};
(2). 算法
思想
考虑在增广前先对
如果我们在层次图
注意:尽管上文中我们仅在单挑增广路上定义了增广/增广流,广义地, 增广 一词不仅可以用于单条路径上的增广流,也可以用于若干增广流的并--后者才是我们定义阻塞流时使用的意义.
定义层次图和阻塞流后,
1).在
2).在
3).将
4).重复以上过程直到不存在从
此时
弧优化:
注意到
多路增广
多路增广是
则接下来我们未必需要重新从
实现:
#include <bits/stdc++.h>
using namespace std;
const long long inf=2005020600;
int n,m,s,t,u,v;
long long w,ans,dis[520010];
int tot=1,now[520010],head[520010];
struct node {
int to,net;
long long val;
} e[520010];
inline void add(int u,int v,long long w) {
e[++tot].to=v;//如果使用e[++tot]这样形式来加边,请将tot初值赋为1
e[tot].val=w;
e[tot].net=head[u];
head[u]=tot;
e[++tot].to=u;
e[tot].val=0;
e[tot].net=head[v];
head[v]=tot;
}
inline int bfs() { //在残量网络中构造分层图
for(register int i=1;i<=n;i++) dis[i]=inf;
queue<int> q;
q.push(s);
dis[s]=0;
now[s]=head[s];
while(!q.empty()) {
int x=q.front();
q.pop();
for(register int i=head[x];i;i=e[i].net) {
int v=e[i].to;
if(e[i].val>0&&dis[v]==inf) {
q.push(v);
now[v]=head[v];
dis[v]=dis[x]+1;
if(v==t) return 1;
}
}
}
return 0;
}
inline int dfs(int x,long long sum) { //sum是整条增广路对最大流的贡献
if(x==t) return sum;
long long k,res=0; //k是当前最小的剩余容量
for(register int i=now[x];i&∑i=e[i].net) {
now[x]=i; //当前弧优化
int v=e[i].to;
if(e[i].val>0&&(dis[v]==dis[x]+1)) {
k=dfs(v,min(sum,e[i].val));
if(k==0) dis[v]=inf; //剪枝,去掉增广完毕的点
e[i].val-=k;
e[i^1].val+=k;
res+=k; //res表示经过该点的所有流量和(相当于流出的总量)
sum-=k; //sum表示经过该点的剩余流量
}
}
return res;
}
int main() {
scanf("%d%d%d%d",&n,&m,&s,&t);
for(register int i=1;i<=m;i++) {
scanf("%d%d%lld",&u,&v,&w);
add(u,v,w);
}
while(bfs()) {
ans+=dfs(s,inf); //流量守恒(流入=流出)
}
printf("%lld",ans);
return 0;
}
//code by @Eleven谦
算法
大家都不学那我就不写了(’∇’)シ┳━┳
Step:
1).和
2).执行完1).后,我们通过DFS找最短路.
3).增广过程和
然后与
具体说,设
容易发现,当
与
而
预流推进算法
该方法在求解过程中忽略流守恒性,并对每个节点更新信息,以求解最大流
通用的预流退进算法
首先我们介绍预流推进算法的主要思想,以及一个可行的暴力实现算法.
预流推进算法通过对单个节点的更新操作,直到没有节点需要更新来求解最大流.
算法过程维护的流函数不一定保持流守恒形,对于一个节点,我们允许进入节点的流超过流出节点的流,
超过的部分被称为节点
若
预留推进算法维护每个结点的高度
只能向高度小于
高度函数
准确地说,预留推进维护以下的一个映射
·
·
称
引理1:设
算法只会在
推送(Push)
适用条件:结点
于是,我们尽可能将超额流从
如果
重贴标签(Relabel)
适用条件:如果节点
则将
初始化
上述将
算法
最高标号预流推进算法(
1.初始化(基于预留推进算法);
2.选择溢出结点中高度最高的结点
3.如果
4.如果没有溢出的节点,算法结束.
BFS优化
在
GAP优化
我们可以使用
(持续更新中其实就是从oiwiki爬下来当笔记的,一个一个手敲非粘贴,代码是在学习过程中在题解中搞下来的代码)
3.最小割
概念
割
对于一个网络流图
割的容量
我们定义割
最小割
最小割就是求得一个割
最大流最小割定理
定理 :
code(其实就是最大流的
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>
const int N = 1e4 + 5, M = 2e5 + 5;
int n, m, s, t, tot = 1, lnk[N], ter[M], nxt[M], val[M], dep[N], cur[N];
void add(int u, int v, int w) {
ter[++tot] = v, nxt[tot] = lnk[u], lnk[u] = tot, val[tot] = w;
}
void addedge(int u, int v, int w) { add(u, v, w), add(v, u, 0); }
int bfs(int s, int t) {
memset(dep, 0, sizeof(dep));
memcpy(cur, lnk, sizeof(lnk));
std::queue<int> q;
q.push(s), dep[s] = 1;
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = lnk[u]; i; i = nxt[i]) {
int v = ter[i];
if (val[i] && !dep[v]) q.push(v), dep[v] = dep[u] + 1;
}
}
return dep[t];
}
int dfs(int u, int t, int flow) {
if (u == t) return flow;
int ans = 0;
for (int &i = cur[u]; i && ans < flow; i = nxt[i]) {
int v = ter[i];
if (val[i] && dep[v] == dep[u] + 1) {
int x = dfs(v, t, std::min(val[i], flow - ans));
if (x) val[i] -= x, val[i ^ 1] += x, ans += x;
}
}
if (ans < flow) dep[u] = -1;
return ans;
}
int dinic(int s, int t) {
int ans = 0;
while (bfs(s, t)) {
int x;
while ((x = dfs(s, t, 1 << 30))) ans += x;
}
return ans;
}
int main() {
scanf("%d%d%d%d", &n, &m, &s, &t);
while (m--) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
addedge(u, v, w);
}
printf("%d\n", dinic(s, t));
return 0;
}
ps:由于最大流=最小割,所以我们可以直接用
求出所有
割边数量
如果需要再最小割的前提喜爱最小化割边数量,那么先求出最小割,把没有满流的边改成
问题模型1
有n个物品和两个集合
这时一个景点 二者选其一 的最小割题目.我们对于每个集合设置源点
注意到当源点和汇点不相连的时候,代表这些点都选择其中一个集合.如果将连向
最小割即最小花费.
问题模型
最大权值闭合图,即给定一张有向图,每个点都有一个权值(可以为正或负或0),你需要选择一个权值和最大子图,使得子图中每个点的后继都在子图中.
做法:建立超级源点
若节点权值为负,则由
费用流
给定一个网络
当
则该网络中总花费最小的最大流称为最小费用最大流,即在最大化
SSP算法
如果图上存在单位费用为负的圈,SSP算法无法正确求出该网络的最小费用最大流.
此时,需要先用消圈算法消去图上的负圈.
实现(基于
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>
const int N = 5e3 + 5, M = 1e5 + 5;
const int INF = 0x3f3f3f3f;
int n, m, tot = 1, lnk[N], cur[N], ter[M], nxt[M], cap[M], cost[M], dis[N], ret;
bool vis[N];
void add(int u, int v, int w, int c) {
ter[++tot] = v, nxt[tot] = lnk[u], lnk[u] = tot, cap[tot] = w, cost[tot] = c;
}
void addedge(int u, int v, int w, int c) { add(u, v, w, c), add(v, u, 0, -c); }
bool spfa(int s, int t) {
memset(dis, 0x3f, sizeof(dis));
memcpy(cur, lnk, sizeof(lnk));
std::queue<int> q;
q.push(s), dis[s] = 0, vis[s] = 1;
while (!q.empty()) {
int u = q.front();
q.pop(), vis[u] = 0;
for (int i = lnk[u]; i; i = nxt[i]) {
int v = ter[i];
if (cap[i] && dis[v] > dis[u] + cost[i]) {
dis[v] = dis[u] + cost[i];
if (!vis[v]) q.push(v), vis[v] = 1;
}
}
}
return dis[t] != INF;
}
int dfs(int u, int t, int flow) {
if (u == t) return flow;
vis[u] = 1;
int ans = 0;
for (int &i = cur[u]; i && ans < flow; i = nxt[i]) {
int v = ter[i];
if (!vis[v] && cap[i] && dis[v] == dis[u] + cost[i]) {
int x = dfs(v, t, std::min(cap[i], flow - ans));
if (x) ret += x * cost[i], cap[i] -= x, cap[i ^ 1] += x, ans += x;
}
}
vis[u] = 0;
return ans;
}
int mcmf(int s, int t) {
int ans = 0;
while (spfa(s, t)) {
int x;
while ((x = dfs(s, t, INF))) ans += x;
}
return ans;
}
int main() {
int s, t;
scanf("%d%d%d%d", &n, &m, &s, &t);
while (m--) {
int u, v, w, c;
scanf("%d%d%d%d", &u, &v, &w, &c);
addedge(u, v, w, c);
}
int ans = mcmf(s, t);
printf("%d %d\n", ans, ret);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)