Loading

「学习笔记」Dinic算法

网络瘤
时间复杂度:\(O_{n^2m}\),在稀疏图上和 EK 算法相当,在稠密图上的效率比 EK 算法更高
Dinic 算法步骤

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

bfs 分层

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

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;
}
posted @ 2023-01-04 17:06  yi_fan0305  阅读(70)  评论(0编辑  收藏  举报