2023.3.28 【图论】KM算法 | 二分图最大权完美匹配
2023.3.28 【模板】KM算法 | 二分图最大权完美匹配
题目概述
给定一张二分图,左右部均有
求一种完美匹配的方案,使得最终匹配边的边权之和最大。
数据规模与约定
- 对于
的数据,满足 , , 。且保证没有重边。
我们看到,在一般情况下,这道题可以用普通的最大费用最大流解决,但是dinic的上限复杂度是毒瘤出题人就会将其卡成
km算法(Kuhn-Munkres算法),是一种在二分图上求解最大权完美匹配的算法,用邻接矩阵存图即可。相比费用流较高的复杂度,km算法有着更为优秀的效率,但局限性在于只能做匹配,而不像费用流那样灵活可变。
前置芝士:匈牙利算法
我们记一个点的顶标为
所有点和所有相等边所组成的子图称为相等子图。
核心算法:贪心地将増广所需的边中,边权最大的那些边变成相等边,即逐渐扩大相等子图。
核心性质:扩大相等子图至其刚好有完美匹配时,该匹配即为原图的最大权完美匹配(很好理解,因为扩大相等子图的过程是贪心的)
因为所有点都要被匹配,所以最终的顶标和满足
所以当
参考Luogu上第一篇题解的定义,设:
为什么
可以简单地理解为,当无法匹配的时候,我们要适当修改顶标,让其匹配,修改量就应该是所有点可以修改的量中最小的,而为了满足顶标的定义,即
所以当
为了满足要求,初始的左部点顶标
算法流程
首先,对于每个点,我们都尝试用匈牙利算法来将其匹配一次,为了减小时间复杂度,我们使用bfs搜索(详见Luogu P6577 题解第一篇)。队列里的点即试图匹配的点,我们将当前待匹配点s推进去。
对于当前队头x,我们试图在它上面扩展相等子图,顺便匹配。对于一个右部点i,如果它在当前轮没有被搜索过,即
更新。然后退出当前轮匹配,不然就让x与i匹配,让原来匹配的左部点(
以上过程更新结束过后,代表没有找到一个增广路,需要修改顶标,记录一个变量
找好d以后,对于每一条边(u,v):
1.若
2.若
3.若
4.若
以上四条可以巧妙地用以下四行解决
for(int i = 1;i <= n;i++)
{
if(vx[i]) lx[i] -= d;
if(vy[i]) ly[i] += d; else slack[i] -= d;
}
更新以后,再扩大一遍相等子图即可。
所有的扩大相等子图操作都在
因为每扩大一次相等子图就要找一次在相等子图上找一次增广路,所以以上操作是循环的,通过函数实现,找到增广路并更新后直接return即可。
关于更新操作,我们在之前记了一个
完结撒花
Code
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N = 505,inf = 0x3f3f3f3f3f3f3f3f;
ll e[N][N],vx[N],vy[N],px[N],py[N],lx[N],ly[N],slack[N],d,pre[N];
int n,m;
queue <int> q;
inline void upd(int x)
{
int t;
while(x)
{
t = px[pre[x]];
px[pre[x]] = x;
py[x] = pre[x];
x = t;
}
}
inline void bfs(int s)
{
memset(vx,0,sizeof(vx));
memset(vy,0,sizeof(vy));
memset(slack,0x3f,sizeof(slack));
while(!q.empty()) q.pop();
q.push(s);
while(1)
{
while(!q.empty())
{
int x = q.front();
q.pop();
vx[x] = 1;
for(int i = 1;i <= n;i++)//右部点
{
if(!vy[i])
{
if(lx[x] + ly[i] - e[x][i] < slack[i])
{
slack[i] = lx[x] + ly[i] - e[x][i];
pre[i] = x;//pre[i]代表“可能成为增广路的预选值”
if(slack[i] == 0)
{
vy[i] = 1;
if(!py[i])
{
upd(i);
return;
}
else q.push(py[i]);
}
}
}
}
}
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])
{
upd(i);
return;
}
else q.push(py[i]);
}
}
}
int main()
{
ll x,y,z;
cin>>n>>m;
for(int i = 1;i <= n;i++)
for(int j = 1;j <= n;j++)
e[i][j] = -inf;
memset(ly,0,sizeof(ly));
for(int i = 1;i <= m;i++)
{
cin>>x>>y>>z;
e[x][y] = z;
lx[x] = max(lx[x],e[x][y]);
}
for(int i = 1;i <= n;i++) bfs(i);//左部点
ll ans = 0;
for(int i = 1;i <= n;i++) ans += e[py[i]][i];
cout<<ans<<endl;
for(int i = 1;i <= n;i++)
cout<<py[i]<<" ";
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!