二分图最大权完美匹配

问题

给定一个二分图,左部有 (n) 个点,右部有 (m) 个点,边 ((u_i, v_j)) 的边权为 (A_{i,j})。求该二分图的最大权完美匹配。

转化

问题可以写成线性规划的形式,设 (f_{i, j}) 表示匹配中是否有边 ((u_i, v_j)),求

[begin{gather*} text{maximize} quad & sum_{i=1}^n sum_{j=1}^m f_{i, j} times A_{i, j} \ text{subject to} quad & sum_{i=1}^n f_{i, j} = 1 quad forall j in [1, m] \ & sum_{j=1}^m f_{i,j}=1quadforall i in [1, n] \ & f_{i,j} ge 0 quadforall i, j end{gather*} ]

转为对偶问题:

[begin{gather*} text{minimize} quad & sum_{i=1}^n hu_i + sum_{j=1}^m hv_i \ text{subject to} quad & hu_i + hv_i ge A_{i,j} quadforall i, j end{gather*} ]

在这个问题中,(hu)(hv) 又称作“顶标”。

分析

根据互补松弛定理,如果 (f_{i,j}=1),则有 (hu_i+hv_j=A_{i,j})。这给出了判定一组顶标是否最优的方式:

一组顶标 (hu, hv) 最优,当且仅当子图 (H = left{(i, j) mid hu_i + hv_j = A_{i,j}right}) 存在完美匹配。

做法

首先给出一个满足 (hu_i+hv_j ge A_{i,j}) 的顶标(不一定最优)(例如,令 (hu_i = hv_j = +infty)),并维护对应的匹配。然后尝试在不破坏条件的情况下修改顶标,使得匹配可以被增广。

具体而言,遍历 (i)(1)(n),每次尝试将 (i) 加入到匹配中(类似于求二分图最大匹配的匈牙利算法)。我们可以求出(i) 为根的交错树,如果已经存在增广路,那么直接增广便是,否则我们需要修改顶标来使交错树生长。设交错树中的左部点集为 (S),右部点集为 (T),那么必有 (|S| = |T| + 1)(交错树的性质)。令 (Delta = min{hu_i + hv_j - A_{i,j} mid i in S land j notin T}),那么将 (S) 中的点顶标减去 (Delta),将 (T) 中的点顶标加上 (Delta),可以验证得到的新的顶标依然满足 (hu_i+hv_j ge A_{i,j}) 的限制,且原图中的匹配一定包含于对应的新图 (H') 中,此外,新图中至少增加了一条边,这使得我们的交错树可以继续生长,直到找到增广路为止。

代码(洛谷 P6577

#define DEBUG 0 #include <cstdio> #include <cassert> #include <vector> template <class T> using Arr = std::vector<T>; #define int long long const int INF = 1e8;  signed main() { 	int n, m; 	scanf("%lld%lld", &n, &m); 	struct edge_t { 		int v, w; 	}; 	Arr<Arr<edge_t>> e(2 * n); 	for (int i = 0; i < m; ++i) { 		int l, r, w; 		scanf("%lld%lld%lld", &l, &r, &w); 		--l; r += n - 1; 		e[l].push_back({r, w}); 		e[r].push_back({l, w}); 	}  	Arr<int> match(2 * n, -1), h(2 * n, INF); 	for (int i = 0; i < n; ++i) { 		Arr<int> vis(2 * n, false);    // 是否在当前求出的交错树中,即 $S cup T$  		Arr<int> upd(2 * n, n * INF);  // 最小的 Δ 值                              		Arr<int> from(2 * n, -1);      // 维护增广路所用,即交错树上的父亲         		int p = i; 		vis[p] = true; 		int d, dp;  // Δ 及其对应的 j 		while (true) { 			d = n * INF, dp = -1;  			// 求出 Δ 			for (auto [to, w] : e[p]) 				if (!vis[to]) { 					int delta = h[p] + h[to] - w; 					if (delta < upd[to]) 						upd[to] = delta, from[to] = p; 				} 			for (int j = n; j < 2 * n; ++j) 				if (!vis[j] && upd[j] < d && from[j] != -1) 					d = upd[j], dp = j; 			assert(~dp);   			// 修改顶标 			h[i] -= d; 			for (int j = n; j < 2 * n; ++j) 				if (vis[j]) 					h[j] += d, h[match[j]] -= d; 				else 					upd[j] -= d;  			// 找到增广路 			if (match[dp] == -1) 				break;  			// 生长交错树 			vis[dp] = true; 			vis[match[dp]] = true; 			p = match[dp]; 		}  		// 增广 		while (~dp) { 			match[dp] = from[dp]; 			int tmp = match[from[dp]]; 			match[from[dp]] = dp; 			dp = tmp; 		} 	}  	long long ans = 0; 	for (int i = 0; i < 2 * n; ++i) 		ans += h[i]; 	printf("%lldn", ans); 	for (int i = n; i < 2 * n; ++i) 		printf("%lld ", match[i] + 1); 	puts(""); 	return 0; } 

参考资料

发表评论

相关文章