「学习笔记」Dinic算法

网络瘤
时间复杂度:On2m,在稀疏图上和 EK 算法相当,在稠密图上的效率比 EK 算法更高
Dinic 算法步骤

  1. bfs 分层
  2. dfs 找增广路
  3. 重复以上两个步骤

bfs 分层#

从源点出发,给每一个点设置一个层数,像这样
image
1 号节点是第 1 层,23 号节点是第 2 层,45 号节点是第 3 层,6 号节点是第 4 层。
image
在这个图中,选一个最短路径,你不会选 134567 这个路径
这里还要注意,我们分层是根据点的深度和是否还有流量来分的,如果最后分层时汇点已经不在分层中了,那就退出即可
整完分层,我们就可以找增广路了
先上代码:

int bfs() {
	memset(dep, 127, sizeof dep); // dep 分层的深度
	memset(inque, 0, sizeof inque); // inque 是否在队列内
	while (!q.empty()) {
		q.pop();
	}
	q.push(s);
	dep[s] = 0;
	while (!q.empty()) {
		int u = q.front();
		q.pop();
		inque[u] = 0; // 标志出队
		for (int i = h[u]; i; i = e[i].nxt) {
			int v = e[i].v;
			if (dep[v] > dep[u] + 1 && e[i].w) { // 这条边还有流量
				dep[v] = dep[u] + 1;
				if (!inque[v]) {
					q.push(v); // 入队,从这个点再更新其他的点
					inque[v] = 1; // 标志入队
				}
				if (v == t)	return true; // 可以到达汇点
			}
		}
	}
	return false;
}

dfs 找增广路#

dfs 向下搜索,只能往下一层的节点搜,不能返回或跳层,一直搜到汇点,返回最后到达汇点的流量,每条路径的流量再减去最后返回的流量
上代码:

ll dfs(int u, ll flow) {
	if (u == t)	return flow; // 到达汇点
	ll rlow = 0;
	for (int i = h[u]; i; i = e[i].nxt) {
		int v = e[i].v;
		if (e[i].w && dep[v] == dep[u] + 1) { // 只流入下一层,还要有流量
			if ((rlow = dfs(v, min(flow, e[i].w)))) {
				e[i].w -= rlow; // 减去流过去的流量
				e[i ^ 1].w += rlow; // 反向边,以便于反悔
				return rlow;
			}
		}
	}
	return 0;
}

初始版本的 Dinic#

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

inline ll read() {
	ll x = 0;
	int fg = 0;
	char ch = getchar();
	while (ch < '0' || ch > '9') {
		fg |= (ch == '-');
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = (x << 3) + (x << 1) + (ch ^ 48);
		ch = getchar();
	}
	return fg ? ~x + 1 : x;
}

const int N = 210;
const int M = 5010;
const ll inf = 1e18;

int n, m, s, t, cnt;
int h[N], dep[N], inque[N];
queue<int> q;

struct edge {
	int v, nxt;
	ll w;
} e[M << 1];

void add(int u, int v, ll w) {
	e[++ cnt].v = v;
	e[cnt].nxt = h[u];
	h[u] = cnt;
	e[cnt].w = w;
}

int bfs() {
	memset(dep, 127, sizeof dep); // dep 分层的深度
	memset(inque, 0, sizeof inque); // inque 是否在队列内
	while (!q.empty()) {
		q.pop();
	}
	q.push(s);
	dep[s] = 0;
	while (!q.empty()) {
		int u = q.front();
		q.pop();
		inque[u] = 0;
		for (int i = h[u]; i; i = e[i].nxt) {
			int v = e[i].v;
			if (dep[v] > dep[u] + 1 && e[i].w) { // 这条边还有流量
				dep[v] = dep[u] + 1;
				if (!inque[v]) {
					q.push(v);
					inque[v] = 1;
				}
				if (v == t)	return true; // 可以到达汇点
			}
		}
	}
	return false;
}

ll dfs(int u, ll flow) {
	if (u == t)	return flow; // 到达汇点
	ll rlow = 0;
	for (int i = h[u]; i; i = e[i].nxt) {
		int v = e[i].v;
		if (e[i].w && dep[v] == dep[u] + 1) { // 只流入下一层
			if ((rlow = dfs(v, min(flow, e[i].w)))) {
				e[i].w -= rlow; // 减去流过去的流量
				e[i ^ 1].w += rlow; // 反向边反悔
				return rlow;
			}
		}
	}
	return 0;
}

int main() {
	n = read(), m = read(), s = read(), t = read();
	cnt = 1;
	for (int i = 1; i <= m; ++ i) {
		int u = read(), v = read();
		ll w = read();
		add(u, v, w);
		add(v, u, 0);
	}
	ll lowflow, ans = 0;
	while (bfs()) {
		while ((lowflow = dfs(s, inf))) {
			ans += lowflow;
		}
	}
	printf("%lld\n", ans);
	return 0;
}

当然,如果说上面是初始版本,那就说明这个东西是有优化的

小优化#

上面的代码中,我们每次 dfs,搜到汇点就直接返回,而再一次开始搜则是从原点重新开始搜,这样的话效率就很低下,跑得慢,那我们为什么不继续搜下去找到最后用了多少流量,然后一次返回呢?
先上代码:

ll dfs(int u, ll flow) {
	if (u == t) {
		vis = 1;
		maxflow += flow;
		return flow; // 到达汇点
	}
	ll rlow = 0, used = 0;
	for (int i = h[u]; i; i = e[i].nxt) {
		int v = e[i].v;
		if (e[i].w && dep[v] == dep[u] + 1) { // 只流入下一层
			if ((rlow = dfs(v, min(flow - used, e[i].w)))) {
				used += rlow;
				e[i].w -= rlow; // 减去流过去的流量
				e[i ^ 1].w += rlow; // 反向边反悔
				if (used == flow)	break;
			}
		}
	}
	return used;
}
while (bfs()) {
		vis = 1;
		while (vis == 1) {
			vis = 0;
			dfs(s, inf);
		}
	}

这段 dfs 代码中,used 表示已经流过多少流量了,flow 表示流入这条路径的流量是多少,所以我们用剩余的流量来继续往下搜,知道没有增广路或流量用完了为止

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

inline ll read() {
	ll x = 0;
	int fg = 0;
	char ch = getchar();
	while (ch < '0' || ch > '9') {
		fg |= (ch == '-');
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = (x << 3) + (x << 1) + (ch ^ 48);
		ch = getchar();
	}
	return fg ? ~x + 1 : x;
}

const int N = 210;
const int M = 5010;
const ll inf = 1e18;

int n, m, s, t, cnt, vis;
ll maxflow;
int h[N], dep[N], inque[N], cur[N];
queue<int> q;

struct edge {
	int v, nxt;
	ll w;
} e[M << 1];

void add(int u, int v, ll w) {
	e[++ cnt].v = v;
	e[cnt].nxt = h[u];
	h[u] = cnt;
	e[cnt].w = w;
}

int bfs() {
	memset(dep, 127, sizeof dep); // dep 分层的深度
	memset(inque, 0, sizeof inque); // inque 是否在队列内
	while (!q.empty()) {
		q.pop();
	}
	q.push(s);
	dep[s] = 0;
	while (!q.empty()) {
		int u = q.front();
		q.pop();
		inque[u] = 0;
		for (int i = h[u]; i; i = e[i].nxt) {
			int v = e[i].v;
			if (dep[v] > dep[u] + 1 && e[i].w) { // 这条边还有流量
				dep[v] = dep[u] + 1;
				if (!inque[v]) {
					q.push(v);
					inque[v] = 1;
				}
				if (v == t)	return true; // 可以到达汇点
			}
		}
	}
	return false;
}

ll dfs(int u, ll flow) {
	if (u == t) {
		vis = 1;
		maxflow += flow;
		return flow; // 到达汇点
	}
	ll rlow = 0, used = 0;
	for (int i = h[u]; i; i = e[i].nxt) {
		int v = e[i].v;
		if (e[i].w && dep[v] == dep[u] + 1) { // 只流入下一层
			if ((rlow = dfs(v, min(flow - used, e[i].w)))) {
				used += rlow;
				e[i].w -= rlow; // 减去流过去的流量
				e[i ^ 1].w += rlow; // 反向边反悔
				if (used == flow)	break;
			}
		}
	}
	return used;
}

int main() {
	n = read(), m = read(), s = read(), t = read();
	cnt = 1;
	for (int i = 1; i <= m; ++ i) {
		int u = read(), v = read();
		ll w = read();
		add(u, v, w);
		add(v, u, 0);
	}
	while (bfs()) {
		vis = 1;
		while (vis == 1) {
			vis = 0;
			dfs(s, inf);
		}
	}
	printf("%lld\n", maxflow);
	return 0;
}

传说中的当前弧优化#

当前弧优化,可以把弧看作边,就是从当前的路径继续往下搜,什么意思呢?我们 h 数组中存的,是从这个点出发的最后一条边,但是我们 dfs 时,从最后一条边开始,每一条边都进行了增广,而且增广得很彻底,所以从最后一条边到当前我们所在的边中间所有的边对我们而言,都已经是“废边”了,那就直接从当前边开始继续往下搜,设置一个 cur 数组来存储当前边,这个 cur 会随着循环的进行而改变,具体看代码

int bfs() {
	for (int i = 1; i <= n; ++ i) {
		dep[i] = 1e9; // dep 分层的深度
		inque[i] = 0; // inque 是否在队列内
		cur[i] = h[i]; // 当前弧优化
	}
	while (!q.empty()) {
		q.pop();
	}
	q.push(s);
	dep[s] = 0;
	while (!q.empty()) {
		int u = q.front();
		q.pop();
		inque[u] = 0;
		for (int i = h[u]; i; i = e[i].nxt) {
			int v = e[i].v;
			if (dep[v] > dep[u] + 1 && e[i].w) { // 这条边还有流量
				dep[v] = dep[u] + 1;
				if (!inque[v]) {
					q.push(v);
					inque[v] = 1;
				}
				if (v == t)	return true; // 可以到达汇点
			}
		}
	}
	return false;
}

ll dfs(int u, ll flow) {
	if (u == t) {
		maxflow += flow;
		return flow; // 到达汇点
	}
	ll rlow = 0, used = 0;
	for (int &i = cur[u]; i; i = e[i].nxt) { // 当前弧优化
		int v = e[i].v;
		if (e[i].w && dep[v] == dep[u] + 1) { // 只流入下一层
			if ((rlow = dfs(v, min(flow - used, e[i].w)))) {
				used += rlow;
				e[i].w -= rlow; // 减去流过去的流量
				e[i ^ 1].w += rlow; // 反向边反悔
				if (used == flow)	break;
			}
		}
	}
	return used;
}

下面就是最终版的 Dinic 算法!

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

inline ll read() {
	ll x = 0;
	int fg = 0;
	char ch = getchar();
	while (ch < '0' || ch > '9') {
		fg |= (ch == '-');
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = (x << 3) + (x << 1) + (ch ^ 48);
		ch = getchar();
	}
	return fg ? ~x + 1 : x;
}

const int N = 210;
const int M = 5010;
const ll inf = 1e18;

int n, m, s, t, cnt;
ll maxflow;
int h[N], dep[N], inque[N], cur[N];
queue<int> q;

struct edge {
	int v, nxt;
	ll w;
} e[M << 1];

void add(int u, int v, ll w) {
	e[++ cnt].v = v;
	e[cnt].nxt = h[u];
	h[u] = cnt;
	e[cnt].w = w;
}

int bfs() {
	for (int i = 1; i <= n; ++ i) {
		dep[i] = 1e9; // dep 分层的深度
		inque[i] = 0; // inque 是否在队列内
		cur[i] = h[i]; // 当前弧优化
	}
	while (!q.empty()) {
		q.pop();
	}
	q.push(s);
	dep[s] = 0;
	while (!q.empty()) {
		int u = q.front();
		q.pop();
		inque[u] = 0;
		for (int i = h[u]; i; i = e[i].nxt) {
			int v = e[i].v;
			if (dep[v] > dep[u] + 1 && e[i].w) { // 这条边还有流量
				dep[v] = dep[u] + 1;
				if (!inque[v]) {
					q.push(v);
					inque[v] = 1;
				}
				if (v == t)	return true; // 可以到达汇点
			}
		}
	}
	return false;
}

ll dfs(int u, ll flow) {
	if (u == t) {
		maxflow += flow;
		return flow; // 到达汇点
	}
	ll rlow = 0, used = 0;
	for (int &i = cur[u]; i; i = e[i].nxt) { // 当前弧优化
		int v = e[i].v;
		if (e[i].w && dep[v] == dep[u] + 1) { // 只流入下一层
			if ((rlow = dfs(v, min(flow - used, e[i].w)))) {
				used += rlow;
				e[i].w -= rlow; // 减去流过去的流量
				e[i ^ 1].w += rlow; // 反向边反悔
				if (used == flow)	break;
			}
		}
	}
	return used;
}

int main() {
	n = read(), m = read(), s = read(), t = read();
	cnt = 1;
	for (int i = 1; i <= m; ++ i) {
		int u = read(), v = read();
		ll w = read();
		add(u, v, w);
		add(v, u, 0);
	}
	while (bfs()) {
		dfs(s, inf);
	}
	printf("%lld\n", maxflow);
	return 0;
}

作者:yifan0305

出处:https://www.cnblogs.com/yifan0305/p/17024884.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

转载时还请标明出处哟!

posted @   yi_fan0305  阅读(76)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示