• [学习笔记] KM算法


    前言

    这个东西学了我挺久了,我先奉劝各位一定要先搞清楚匈牙利算法。感谢 ( t jzm) 巨佬对我耐心的讲解,因为我太弱了所以卡了很久都不懂。如果你有任何问题请在本篇博客下面留言,我会尽力解答的。

    ( t KM) 算法主要用来解决最大权完美匹配,因其稳定的 (O(n^3)) 可以吊打玄学费用流,所以出匹配的题时无脑写费用流可能被卡。但是它也只能解决匹配问题,费用流的应用却极其广泛。

    我会着重讲解代码,因为我觉得 ( t KM) 的代码才是最难懂的(而且网上的代码很多都有点问题)

    思想

    神奇的思想,关键是 顶标 这个设计。所谓顶标也就是二分图上的点权,我们把边权转化成点权,设 (X) 部的点的顶标为 (vx[i])(Y) 部的点的顶标为 (vy[i]) ,记图上的边为 (a[i][j]) ,那么对于任意的 (i,j) ,我们要一直保证:

    [vx[i]+vy[j]geq a[i][j] ]

    那么对于 (a[i][j]=vx[i]+vy[j]) 的边,贪心地选他是最优的。我们成这样的边为 相等边 ,称相等边连接而成的子图为 相等子图 ,那么当相等子图的最大匹配是完美匹配时,说明我们找到了答案,答案是顶标之和。

    ( t KM) 算法和匈牙利算法类似,每次都会进行 增广 操作,如果可以直接用相等边增广的话,自然是极好的。如果不能进行增广操作呢?就说明现在的相等子图不行了,我们要 扩大相等子图

    怎么扩大相等子图?其实就是修改顶标,顶标是可以按照我们的愿望修改的,但是注意要一直满足 (vx[i]+vy[j]geq a[i][j]) ,设我们现在增广到的集合是 (S)(既有 (X) 部也有 (Y) 部),记 (i)(X) 部的点,(j)(Y) 部的点,那么对于 (iin S) ,我们使他的顶标减少 (d) ,对于 (jin S) ,我们使他的顶标增加 (d) ,考虑这样做的影响:

    • 对于 (xin S,yin S)(vx[x]+vy[y]) 不变,所以还是满足(vx[x]+vy[x]geq a[x][y])
    • 对于 (xin S,y otin S) ,这条边 (vx[x]+vy[y]) 会减少 (d) ,这条边可能就会成为新的相等边。
    • 对于 (x otin S,yin S) ,这条边 (vx[x]+vy[y]) 会增加 (d) ,这条边依然不是相等边,但是我们用不到他。
    • 对于 (x otin S,y otin S) ,无影响,不用管。

    综上所诉,我们可以知道 (d) 应该这样取值:

    [d=min_{xin S,y otin S} vx[x]+vy[y]-a[x][y] ]

    当我们修改顶标之后获得新的相等边之后就可以继续增广了,而且那个限制条件还是一直满足的,挺不错。注意一开始为了满足条件我们应该让 (vx[i]=max_j a[i][j])(vy[i]=0)

    代码

    就算懂了思想之后可能还是难以敲出代码,先给出一些数组的定义:

    • (vx[i],vy[i]) ,顶标数组,如上所述。
    • (xtoy[i],ytox[i]) ,表示和 (X/Y) 部的点匹配的是哪个点,如果没有匹配点就是 (0)
    • (visx[i],visy[i]) ,表示在当前增广过程中这个点是否被访问到。
    • (pre[i]) 表示 (Y) 部的点想匹配其的 (X) 部的点 ,理解这个数组很重要,就是匈牙利算法的思想,(X) 部的点 (a) 虽然想匹配他,但是碍于这个点已经有了一个匹配的 (X) 部的点 (b) ,所以我们先记录下这个数组,再去增广 (b) ,因为写法是 ( t bfs) 的,所以用这个数组来达到 ( t dfs) 的功能。
    • (sy[i]) 表示还没有访问的 (Y) 部的点的 (min_{x_in S}vx[x]+vy[y]-a[x][y]) ,这个数组方便算 (d) ,而且维护起来也不是很麻烦,在每次 (X) 部的点加入 (S) 集合时修改一下它就行了。

    请先完全理解上面的定义之后再读我带注释的代码,就是这道题【模板】二分图最大权完美匹配

    如果你读懂了代码,那么不难知道复杂度是 (O(n^3+nm)) ,因为均摊下来花费 (O(n)) 的时间可以使一条边变成相等边,最差情况当所有边变成相等边时一定可以得到完美匹配,(O(n^3)) 就是每次枚举增广哪个点,二分图的每个点又要扩展一次,扩展的消耗时 (O(n)) ,所以总共时 (O(n^3)) 的。

    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <queue>
    using namespace std;
    #define int long long
    const int inf = 1e18;
    const int M = 505;
    int read()
    {
    	int x=0,f=1;char c;
    	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
    	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
    	return x*f;
    }
    int n,m,vx[M],vy[M],xtoy[M],ytox[M];
    int visx[M],visy[M],pre[M],sy[M],a[M][M];
    void aug(int y)
    //也就是y这个点是空闲的,可以把以前记录的匹配修改了,可以增加一个匹配 
    {
    	for(int nxt,x;y;y=nxt)//我觉得这个函数很像"链式反应" 
    	{
    		nxt=xtoy[x=pre[y]];
    		//nxt表示x的原配y,由于x已经找到了新的y所以原配就被解放出来了 
    		ytox[y]=x;xtoy[x]=y;//修改匹配关系 
    	}
    }
    void jzm(int x)//每次都只解决x的问题,就和匈牙利很类似啊 
    {
    	queue<int> q;
    	memset(visx,0,sizeof visx);//一定要清空 
    	memset(visy,0,sizeof visy);//一定要情况 
    	for(int i=1;i<=n;i++) sy[i]=inf;//一开始都是最大值,注意一下定义哦 
    	q.push(x);
    	while(1)//直到找到了x的匹配再离开 
    	{
    		while(!q.empty())
    		{
    			int u=q.front();q.pop();
    			visx[u]=1;
    			for(int i=1;i<=n;i++)
    			{
    				if(visy[i] || a[u][i]==-inf) continue;
    				//如果已经在S中或者边没有,不能访问这个y 
    				int d=vx[u]+vy[i]-a[u][i];//算d 
    				if(d==0)//直接就是相等边了 
    				{
    					visy[i]=1;pre[i]=u;//这个点就被增广过了,点u是想匹配i的,所以修改pre 
    					if(!ytox[i]) {aug(i);return ;}//这个点空闲,可以直接达到目的,做完之后就走了 
    					else q.push(ytox[i]);//否则继续从它的原配开始找 
    				}
    				else if(sy[i]>d)//不是相等边,所以可以修改sy 
    					sy[i]=d,pre[i]=u;//如果这个相等边被激活了,那么u就想匹配i,多打了标记也没关系 
    			}
    		}
    		int del=inf;//求那个min(....),就是修改量 
    		for(int i=1;i<=n;i++)
    			if(!visy[i]) del=min(del,sy[i]);//只能在没有访问的y里面找 
    		for(int i=1;i<=n;i++)//这里就是按照定义修改顶标 
    		{
    			if(visx[i]) vx[i]-=del; 
    			if(visy[i]) vy[i]+=del;
    			else if(sy[i]!=inf) sy[i]-=del;
    			//如果sy有意义,那么他一定被哪个x访问过,那么由于vx的减少,他就一定会减小 
    		}
    		for(int i=1;i<=n;i++)
    			if(!visy[i] && !sy[i])//再修改顶标之后成为了相等边 
    			{
    				visy[i]=1;//和上面类似的操作,扩展 
    				if(!ytox[i]) {aug(i);return ;}
    				else q.push(ytox[i]);
    			}
    	}
    }
    int KM()
    {
    	int ans=0;
    	for(int i=1;i<=n;i++)
    		for(int j=1;j<=n;j++)
    			vx[i]=max(vx[i],a[i][j]);//初始的vx 
    	for(int i=1;i<=n;i++)//每次就增广一个点 
    		jzm(i);
    	for(int i=1;i<=n;i++)
    		ans+=vx[i]+vy[i];//最后的答案一定是这个 
    	return ans;
    }
    signed main()
    {
    	n=read();m=read();
    	for(int i=1;i<=n;i++)
    		for(int j=1;j<=n;j++)
    			a[i][j]=-inf;//注意要初始化 
    	for(int i=1;i<=m;i++)
    	{
    		int u=read(),v=read(),c=read();
    		a[u][v]=max(a[u][v],c);//选最大的边 
    	}
    	printf("%lld
    ",KM());
    	for(int i=1;i<=n;i++)
    		printf("%lld ",ytox[i]);
    }
    

    应用

    注意我再 前言 中说它可以解决匹配问题,所以不只能解决最大权完美匹配问题哦,可以看看 oneindark 巨佬博客的最后一部分,然后如果我做到了更多关于 ( t KM) 的题是会补充到这里的:

  • 相关阅读:
    第一次面试准备
    第一次面试复盘(谐云)
    段子第二弹(乐趣就是把一件丢人的事儿不点破,让观众自己打开包袱,但是又不能太难理解,最好1秒半能反应过来)
    mac上安装配置allure(必定成功)
    Django和Python的版本对应关系
    http请求默认值
    mac安装brew
    一种解决方法 Error: No such file or directory @ rb_sysopen
    git提交代码的正确步骤
    C#DirectoryHelper文件夹IO
  • 原文地址:https://www.cnblogs.com/C202044zxy/p/14375248.html
Copyright © 2020-2023  润新知