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