(OI wiki、洛谷)KM算法模板及详细解释

该算法相关资料的搜集和证明(甚至有部分还得自己推)都比较难,这里提供知乎一位大佬的文章,写得非常直白,建议看完这个再看百度百科或者蓝书也有,然后细心推敲,再看模板以及解释。

首先声明代码模板从OI wiki中获取的,但是OIwiki中并没有添加代码解释,这里我基本上把每一句该解释的都解释了!代码如下:

//这个里面的点都是0开始的,但其实一般还是1开始的
template <typename T>
struct hungarian {  // km
	int n;
	vector<int> matchx;  // 左集合对应的匹配点
	vector<int> matchy;  // 右集合对应的匹配点
	vector<int> pre;     // 连接右集合的左点
	vector<bool> visx;   // 拜访数组 左
	vector<bool> visy;   // 拜访数组 右
	vector<T> lx;
	vector<T> ly;
	vector<vector<T> > g;
	vector<T> slack;
	T inf;
	T res;
	queue<int> q;
	int org_n;
	int org_m;

	hungarian(int _n, int _m) {
		org_n = _n;
		org_m = _m;
		n = max(_n, _m);
		inf = numeric_limits<T>::max();
		res = 0;
		g = vector<vector<T> >(n, vector<T>(n));//由于KM算法最优也是O(n^3)的,所以n肯定很小(比如两三百),正常来说直接搞邻接矩阵就能存储
		matchx = vector<int>(n, -1);
		matchy = vector<int>(n, -1);
		pre = vector<int>(n);
		visx = vector<bool>(n);
		visy = vector<bool>(n);
		lx = vector<T>(n, -inf);//lx则要该点连接的所有边中权值最大的,得下面solve函数才开始更新
		ly = vector<T>(n);//右边的初始全为零
		slack = vector<T>(n);
	}

	void addEdge(int u, int v, int w) {
		g[u][v] = max(w, 0);  // 负值还不如不匹配 因此设为0不影响(由于我们的目的是找最大边权和匹配)
	}

	bool check(int v) {//要每找一个点就调用该函数,由于这方法比较特殊,边也是在找的过程中再加入的
		visy[v] = true;
		if (matchy[v] != -1) {
			q.push(matchy[v]);
			visx[matchy[v]] = true;  // in S
			return false;
		}
		//想要证明这样做的合理性,首先看第一个被遍历的结点u,他找到的第一条lx+ly=w的边就刚好连接到一个未盖点v
		//那么此时的pre[v]肯定是u,因此matchy[v]就被赋为u,然后让v=(先前)matchx[u](-1),而让matchx[u]=(先前)v
		//即实现了matchy[v]=u,matchx[u]=v并return 1,而bfs函数找到增广路后也将return

		//其次就是其他情况,比如经过三条边的增广路,那么后面一节肯定就可以做到了(因为是基础情况),关键只看v值转化
		//而假设原匹配到的为 x1,y1 ,现在发现需要 x2,y1 和 x1,y2 ,那么此时v为y2,pre[v]为x1,第一轮循环就是match[y2]=x1
		//然后matchx[x1]=y2,而v则变为原x1的匹配y1。如何保证pre[y1]就是当前的x2呢?因为在开始遍历x2的时候,在这次假设
		//的情况下就是整个bfs从x2开始找起来,那么所有y点都是没有被访问过的,现在发现x2和y1之间有右边便调用次函数,把y1
		//设置为访问过的了,因此pre[y1]就会保持x2的值了,不再被其他点更改,至于多个点的情况也是这样,反正既然从它那里
		//走过的了,它的pre值就不会变化了,就是那个可能找到增广路之后要匹配的新边

		//至于为什么下面的不break,而是所有delta为0的都遍历,毕竟函数名就叫做BFS嘛,反正就是要所有地方都找一找,防止有
		//某条路走不通了就难搞了,而这种情况也是很好假设出来的

		// 找到新的未匹配点 更新匹配点 pre 数组记录着"非匹配边"上与之相连的点
		while (v != -1) {
			matchy[v] = pre[v];
			swap(v, matchx[pre[v]]);//一直循环,v就一直等于matchx[pre[v]]
		}
		return true;
	}

	void bfs(int i) {
		while (!q.empty()) {//每次调用bfs都要来一个全新的队列
			q.pop();
		}
		q.push(i);
		visx[i] = true;//路过的x都标记
		while (true) {
			while (!q.empty()) {
				int u = q.front();
				q.pop();
				for (int v = 0; v < n; v++) {//注意到对于每个x都会遍历所有的v,更新slack[v],因此slack[v]就是根据S来的
					if (!visy[v]) {//要没访问过,毕竟是在找增广路的过程
						T delta = lx[u] + ly[v] - g[u][v];
						if (slack[v] >= delta) {
							//小于的都是不用考虑的,比如假设lx[1]=5,lx[2]=0,而中间连接有w=1的边,且slack[2]已经更新到1(slack就是要找最小的),也就是这条边即使要加入也不是最近的事
							pre[v] = u;
							if (delta) {
								//更新slack当然是要选那些不是在相等子图中的边,也就是lx+ly-w(亦即delta)大于0的
								//而还没加入相等子图的同时也就意味着这条路目前不可以走,和下面形成if else关系
								slack[v] = delta;
							}
							else if (check(v)) {  // delta=0 代表有机会加入相等子图 找增广路
												 // 找到就return 重建交错树
								//上面那个有机会是OI wiki给的,但其实delta=0本来就是代表在相等子图里面了。。
								return;
							}
						}
					}
				}
			}//是把整个队列都走完才走下面的,也就是找不到增广路
			// 没有增广路 修改顶标
			T a = inf;
			for (int j = 0; j < n; j++) {
				if (!visy[j]) {//就是没有访问过的T'区域的,才是决定a值大小的,这个在知乎那个看的很清楚,因为要从T'中加入新边
					a = min(a, slack[j]);
				}
			}
			for (int j = 0; j < n; j++) {
				if (visx[j]) {  // S  明确凡是有遍历到的就是和T在相等子图中有连接的
					//而S和T区的划定其实就是在相等子图中,目前找不到增广路的那个点所
					//有可以走“增广路”(这里指的是所谓交错子树)经过的点,也就是
					//上面的那个while(!q.empty())所有经过的点
					lx[j] -= a;
				}
				if (visy[j]) {  // T
					ly[j] += a;
				}
				else {  // T'
					slack[j] -= a;
				}
			}
			for (int j = 0; j < n; j++) {
				if (!visy[j] && slack[j] == 0 && check(j)) {
					//简单情况当然一目了然,对于复杂一点的举个例子,比如T'中两个点,
					//S'中1个,且S新增连接T'的点只有那个已盖点,那么全部不符合,继续循环
					//而继续循环注意到队列中就是S集合中新增的结点(就是在check函数中加入的)
					return;
				}
			}
		}
	}

	void solve() {
		// 初始顶标
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < n; j++) {
				lx[i] = max(lx[i], g[i][j]);//更新lx
			}
		}

		for (int i = 0; i < n; i++) {//匈牙利算法,每次要记得更新vis,这里还有slack。。
			//算小学到一点新知识,像是memset,但是在vector用不了,毕竟sizeof都是不正常的(似乎是开在堆区)
			fill(slack.begin(), slack.end(), inf);
			fill(visx.begin(), visx.end(), false);
			fill(visy.begin(), visy.end(), false);
			bfs(i);
		}

		// custom
		for (int i = 0; i < n; i++) {
			if (g[i][matchx[i]] > 0) {//为0的就不用匹配了,一方面是可能是没有边的,一方面是有边可能是负数或者0都没必要加上
				//根据原理,每个i最后都能找到匹配,因此下标matchx[i]肯定不是-1
				//但是如果n原本就是大于m的呢?!!要怎么找最大权匹配?这个情况应该不会出现吧。。。
				res += g[i][matchx[i]];
			}
			else {
				matchx[i] = -1;
			}
		}
		cout << res << "\n";
		for (int i = 0; i < org_n; i++) {
			cout << matchx[i] + 1 << " ";//相当于答案要的是x中第几个结点匹配到了y中第几个结点(而且下标从1开始),如果不匹配的就输出0。。
		}
		cout << "\n";
	}
};

下面就是根据上面的模板在洛谷相关的模板题中提交通过的代码(由于要求不同,稍作改动,改动处都有标记)

题目描述

给定一张二分图,左右部均有 nn 个点,共有 mm 条带权边,且保证有完美匹配。

求一种完美匹配的方案,使得最终匹配边的边权之和最大。

输入格式

第一行两个整数 n,mn,m,含义见题目描述。

第 2\sim m+12∼m+1 行,每行三个整数 y,c,hy,c,h 描述了图中的一条从左部的 yy 号结点到右部的 cc 号节点,边权为 hh 的边。

输出格式

本题存在 Special Judge

第一行一个整数 ansans 表示答案。

第二行共 nn 个整数 a_1,a_2,a_3\cdots a_na1​,a2​,a3​⋯an​,其中 a_iai​ 表示完美匹配下与右部第 ii 个点相匹配的左部点的编号。如果存在多种方案,请输出任意一种。

输入输出样例

输入 #1复制

5 7
5 1 19980600
4 2 19980587
1 3 19980635
3 4 19980559
2 5 19980626
1 2 -15484297
4 5 -17558732

输出 #1复制

99903007
5 4 1 3 2 

说明/提示

数据规模与约定

  • 对于 10\%10% 的数据,满足 n\leq 10n≤10。
  • 对于 30\%30% 的数据,满足 n\leq 100n≤100。
  • 对于 60\%60% 的数据,满足 n\leq 500n≤500,且保证数据随机 。
  • 对于 100\%100% 的数据,满足 1\leq n\leq 5001≤n≤500,1\leq m\leq n^21≤m≤n2,-19980731\leq h \leq 19980731−19980731≤h≤19980731 。且保证没有重边。

数据由善于出锅的出题人耗时好久制造完成。

善良的杨村花提醒你,别忘了仔细观察一下边权范围哦~

善良的杨村花又提醒你,你的复杂度可能只是「看起来」很对哦~

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
template <typename T>
struct hungarian {
	int n;
	vector<int> matchx;
	vector<int> matchy;
	vector<int> pre;
	vector<bool> visx;
	vector<bool> visy;
	vector<T> lx;
	vector<T> ly;
	vector<vector<T> > g;
	vector<T> slack;
	T inf;
	T res;
	queue<int> q;
	int org_n;
	int org_m;
	hungarian(int _n, int _m) {
		org_n = _n;
		org_m = _m;
		n = max(_n, _m);
		inf = numeric_limits<T>::max();
		res = 0;
		g = vector<vector<T> >(n, vector<T>(n,-inf));//表示无边 
		matchx = vector<int>(n, -1);
		matchy = vector<int>(n, -1);
		pre = vector<int>(n);
		visx = vector<bool>(n);
		visy = vector<bool>(n);
		lx = vector<T>(n, -inf);
		ly = vector<T>(n);
		slack = vector<T>(n);
	}
	void addEdge(int u, int v, int w) {
		g[u][v] = w;//要按照真实值存储 
	}
	bool check(int v) {
		visy[v] = true;
		if (matchy[v] != -1) {
			q.push(matchy[v]);
			visx[matchy[v]] = true;
			return false;
		}
		while (v != -1) {
			matchy[v] = pre[v];
			swap(v, matchx[pre[v]]);
		}
		return true;
	}
	void bfs(int i) {
		while (!q.empty()) {
			q.pop();
		}
		q.push(i);
		visx[i] = true;
		while (true) {
			while (!q.empty()) {
				int u = q.front();
				q.pop();
				for (int v = 0; v < n; v++) {
					if (!visy[v]&&g[u][v]!=-inf) {//这里除了没访问过还要判断是否有路 
						T delta = lx[u] + ly[v] - g[u][v];
						if (slack[v] >= delta) {
							pre[v] = u;
							if (delta) {
								slack[v] = delta;
							}
							else if (check(v)) {
								return;
							}
						}
					}
				}
			}
			T a = inf;
			for (int j = 0; j < n; j++) {
				if (!visy[j]) {
					a = min(a, slack[j]);
				}
			}
			for (int j = 0; j < n; j++) {
				if (visx[j]) {
					lx[j] -= a;
				}
				if (visy[j]) {
					ly[j] += a;
				}
				else {
					slack[j] -= a;
				}
			}
			for (int j = 0; j < n; j++) {
				if (!visy[j] && slack[j] == 0 && check(j)) {
					return;
				}
			}
		}
	}
	void solve() {
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < n; j++) {
				lx[i] = max(lx[i], g[i][j]);
			}
		}
		for (int i = 0; i < n; i++) {
			fill(slack.begin(), slack.end(), inf);
			fill(visx.begin(), visx.end(), false);
			fill(visy.begin(), visy.end(), false);
			bfs(i);
		}
		for (int i = 0; i < n; i++) {
			res += g[i][matchx[i]];//这里是要全部加上的 
			//根据原理(相等子图不断扩大,最终需要的话一定能把T'最少逼到剩下一个点并加入T,毕竟完备匹配是在的,S肯定和T'有联系) 
			//每个x都能匹配到一个y 
		}
		cout << res << "\n";
		for (int i = 0; i < org_m; i++) {
			cout << matchy[i] + 1 << " ";//注意这道题要输出的是y中对应的x的,因为这个还错了一次 
		}
	}
};
int main()
{
	//本题要求一定要完备匹配(负权值也要上) 
	ios_base::sync_with_stdio(false),cin.tie(nullptr);
	int n,m;
	cin>>n>>m;
	hungarian<ll>h(n,n);
	for(int i=0;i<m;i++)
	{
		int u,v,w;
		cin>>u>>v>>w;
		u--;
		v--;
		h.addEdge(u,v,w);
	}
	h.solve();
	return 0;
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值