二分图最大权匹配(KM算法) 学习笔记

二分图最大权匹配(KM算法) 学习笔记

学习资料

匈牙利与KM算法

二分图最大权匹配——OI Wiki

题解 P6577【模板】二分图最大权完美匹配

<https://www.cnblogs.com/zhltao/p/12549489.html >

KM算法

P6577 【模板】二分图最大权完美匹配

KM算法用来求一类特殊二分图的最大权完美匹配。这个特殊指:

  1. 每对 左右部点间都有边
  2. 左右部的节点数量相同。

其实没有关系。我们只要这样做,就可以推至普适情况:

  1. 原来没有边的左右部点间连权值为 0 / − inf ⁡ -\inf inf的边(通常直接用邻接矩阵实现。连 0 还是连 − inf ⁡ -\inf inf 取决于是否要求一定要完美匹配)。
  2. 往少的那一部补一些点使之相等。

所以我们说,KM算法适用于普遍的二分图的最大权匹配

定义

顶标

我们给每个节点设置一个 “顶标”。为了方便,左部节点的顶标叫做 l x i lx_i lxi,右部节点的顶标叫做 l y i ly_i lyi

顶标的性质:

任意时刻,对于图中任意一条边 (u,v,w),有 l x u + l y v ≥ w lx_u+ly_v\ge w lxu+lyvw

相等子图

相等子图 包括原二分图中的所有节点(即一个生成子图),而边集却是原图中满足 l x u + l y v = w lx_u+ly_v=w lxu+lyv=w 的边 (u,v,w) 构成的。也就是把那些 l x u + l y v = w lx_u+ly_v=w lxu+lyv=w 的边搞出来的一个生成子图。

相等子图的性质:

  1. 相等子图若存在完美匹配,则完美匹配的权和就是顶标和。
  2. 相等子图若存在完美匹配,则同时也是原图的最大权完美匹配。

于是我们只要增广一个相等子图,使之具有完美匹配即可。

算法流程

  1. 赋予一个顶标初始值,比如 l x u = max ⁡ { w ∣ ( u , v , w ) } , l y i = 0 lx_u=\max\{w|(u,v,w)\},ly_i=0 lxu=max{w(u,v,w)},lyi=0
  2. 选一个未匹配点,从它那里开始在 相等子图 中增广。
  3. 调整顶标,给在交错树中的左部的节点减去一个值,给在交错树中的右部的节点加上一个值。这是为了让更多的节点加进相等子图。

我们详细地说说“调整顶标”。

调整顶标

比如我们要进行调整的变化值为 a a a,即让所有在交错树中的点的 l x u lx_u lxu a a a l y v ly_v lyv a a a

于是:

若假设 u , v u,v u,v 分别是交错树上任意的两个左、右部点, u ′ , v ′ u',v' u,v 分别是交错树外任意的两个左、右节点。

  1. ( u , v ) (u, v) (u,v) 仍在交错树上。
  2. ( u ′ , v ′ ) (u',v') (u,v) 仍不在交错树上。
  3. ( u , v ′ ) (u,v') (u,v) 由于顶标和减少,有可能 进入相等子图。
  4. ( u ′ , v ) (u',v) (u,v) 顶标和增加,更不可能进入相等子图。

所以我们要增广,关键看第3种情况。

为了维护顶标的性质,并且让第3种情况的边加入, a a a 值应选择为:

a = min ⁡ { ( l x u + l y v ′ − w ) ∣ ∀ ( u , v ′ , w ) } a=\min\{(lx_u+ly_{v'}-w)|\forall(u,v',w)\} a=min{(lxu+lyvw)(u,v,w)}

于是,我们可以先写出个dfs版本的KM算法。可惜的是,它的时间复杂度可能卡到 O ( n 4 ) O(n^4) O(n4),无法通过模板题。

代码(Dfs)

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
char In[1 << 20], *ss = In, *tt = In;
#define getchar() (ss == tt && (tt = (ss = In) + fread(In, 1, 1 << 20, stdin), ss == tt) ? EOF : *ss++)
ll read() {
	ll x = 0, f = 1; char ch = getchar();
	for(; ch < '0' || ch > '9'; ch = getchar()) if(ch == '-') f = -1;
	for(; ch >= '0' && ch <= '9'; ch = getchar()) x = x * 10 + int(ch - '0');
	return x * f;
}
const int MAXN = 505;
const ll INF = 0x3f3f3f3f3f3f3f3f;
int n, m, match[MAXN], vx[MAXN], vy[MAXN];
ll e[MAXN][MAXN], slack[MAXN], lx[MAXN], ly[MAXN];
bool dfs(int u) {
	vx[u] = 1;
	for(int v = 1; v <= n; v++) if(!vy[v]) {
		if(lx[u] + ly[v] == e[u][v]) {
			vy[v] = 1;
			if(!match[v] || dfs(match[v])) {
				match[v] = u;
				return 1;
			}
		} else slack[v] = min(slack[v], lx[u] + ly[v] - e[u][v]);
	}
	return 0;
}
void KM() {
	for(int i = 1; i <= n; i++) lx[i] = -INF, ly[i] = 0;
	for(int i = 1; i <= n; i++) for(int j = 1; j <= n; j++) lx[i] = max(lx[i], e[i][j]);
	for(int i = 1; i <= n; i++)
		while(1) {
			for(int j = 1; j <= n; j++) vx[j] = vy[j] = 0, slack[j] = INF;
			if(dfs(i)) break;
			ll d = INF;
			for(int j = 1; j <= n; j++) if(!vy[j]) d = min(d, slack[j]);
			for(int j = 1; j <= n; j++) {
				if(vx[j]) lx[j] -= d;
				if(vy[j]) ly[j] += d;
			}
		}
}
int main() {
	n = read(), m = read();
	for(int i = 1; i <= n; i++) for(int j = 1; j <= n; j++) e[i][j] = -INF;
	for(int i = 1; i <= m; i++) {
		int u = read(), v = read(); ll w = read();
		e[u][v] = max(e[u][v], w);
	}
	KM();
	ll ans = 0;
	for(int i = 1; i <= n; i++) ans += lx[i] + ly[i];
	printf("%lld\n", ans);
	for(int i = 1; i <= n; i++) printf("%d ", match[i]);
	return 0;
}

那么怎么办呢?只要换用bfs写法就好啦。这样就不会每次从头增广。

时间复杂度 O ( n 3 ) O(n^3) O(n3)

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
typedef long long ll;
char In[1 << 20], *ss = In, *tt = In;
#define getchar() (ss == tt && (tt = (ss = In) + fread(In, 1, 1 << 20, stdin), ss == tt) ? EOF : *ss++)
ll read() {
	ll x = 0, f = 1; char ch = getchar();
	for(; ch < '0' || ch > '9'; ch = getchar()) if(ch == '-') f = -1;
	for(; ch >= '0' && ch <= '9'; ch = getchar()) x = x * 10 + int(ch - '0');
	return x * f;
}
const int MAXN = 505;
const ll INF = 0x3f3f3f3f3f3f3f3fll;
int n, m, vx[MAXN], vy[MAXN], px[MAXN], py[MAXN], pre[MAXN];
ll e[MAXN][MAXN], lx[MAXN], ly[MAXN], slack[MAXN];
queue<int> que;
void aug(int v) {
	while(v) {
		int t = px[pre[v]];
		px[pre[v]] = v;
		py[v] = pre[v];
		v = t;
	}
}
void bfs(int s) {
	for(int i = 1; i <= n; i++) vx[i] = vy[i] = 0, slack[i] = INF;
	que = queue<int>();
	que.push(s);
	while(1) {
		while(que.size()) {
			int u = que.front(); que.pop();
			vx[u] = 1;
			for(int v = 1; v <= n; v++) if(!vy[v]) {
				if(lx[u] + ly[v] - e[u][v] < slack[v]) {
					slack[v] = lx[u] + ly[v] - e[u][v];
					pre[v] = u;
					if(slack[v] == 0) {
						vy[v] = 1;
						if(!py[v]) {aug(v); return ;}
						else que.push(py[v]);
					}
				}
			}
		}
		ll d = INF;
		for(int i = 1; i <= n; i++) if(!vy[i]) d = min(d, slack[i]);
		for(int i = 1; i <= n; i++) {
			if(vx[i]) lx[i] -= d;
			if(vy[i]) ly[i] += d;
			else slack[i] -= d;
		}
		for(int i = 1; i <= n; i++) if(!vy[i]) {
			if(slack[i] == 0) {
				vy[i] = 1;
				if(!py[i]) {aug(i); return ;}
				else que.push(py[i]);
			}
		}
	}
}
void KM() {
	for(int i = 1; i <= n; i++) lx[i] = -INF, ly[i] = 0;
	for(int i = 1; i <= n; i++) for(int j = 1; j <= n; j++) lx[i] = max(lx[i], e[i][j]);
	for(int i = 1; i <= n; i++) bfs(i);
}
int main() {
	n = read(); m = read();
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= n; j++) e[i][j] = -INF;
	for(int i = 1; i <= m; i++) {
		int u = read(), v = read(); ll w = read();
		e[u][v] = max(e[u][v], w);
	}
	KM();
	ll ans = 0;
	for(int i = 1; i <= n; i++) ans += lx[i] + ly[i];
	printf("%lld\n", ans);
	for(int i = 1; i <= n; i++) printf("%d ", py[i]);
	return 0;
}
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
二分大权匹配是指在一个二分中,找到一种匹配方式,使得匹配的边的权重之和最大。 首先,二分是指一个中的所有节点可以被分为两个不相交的集合,并且中的每条边都连接着一个集合中的节点和另一个集合中的节点。 二分大权匹配可以用多种算法来求解,包括匈牙利算法KM算法等等。其中,匈牙利算法是一种经典的求解二分最大匹配问题的算法。 以下是匈牙利算法的基本思想和步骤: 1. 初始化:将每个节点都标记为未匹配状态。 2. 对于二分中的每个节点,依次进行匹配。 3. 对于每个未匹配的节点,尝试找到它可以匹配的节点。具体地,对于一个未匹配的节点,从它所在的集合中选择一个节点,然后尝试将它们匹配起来。如果匹配成功,则将两个节点标记为已匹配状态。 4. 如果一个节点无法匹配,则尝试将它和其他未匹配的节点匹配。如果仍然无法匹配,则返回失败。 5. 当所有节点都被匹配完毕时,算法结束。 在匈牙利算法的实现中,可以使用增广路径来优化匹配过程。增广路径是指一条从未匹配的节点出发,经过一系列已匹配的节点,最终到达另一个未匹配的节点的路径。 具体地,增广路径的求解步骤如下: 1. 从一个未匹配的节点开始,沿着未匹配的节点尝试匹配。 2. 如果找到了一个匹配节点,则从该匹配节点开始,继续沿着未匹配的节点尝试匹配。 3. 如果最终找到了一个未匹配的节点,则说明找到了一条增广路径。 在匈牙利算法中,每次找到一条增广路径时,可以将该路径上的匹配状态进行调整,使得当前的匹配数量增加一。由于增广路径的搜索过程可以通过 DFS 或 BFS 进行,因此匈牙利算法的时间复杂度为 $O(NM)$,其中 $N$ 和 $M$ 分别表示二分的两个集合中的节点数。 需要注意的是,虽然匈牙利算法的实现比较简单,但是对于大规模的来说,它的时间复杂度可能较高,而且可能会存在一些性能问题。因此,在实际应用中,可能需要使用一些更加高效的算法来求解二分大权匹配问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

日居月诸Rijuyuezhu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值