• 二分图最大权匹配——KM算法


    前言

    • 这东西虽然我早就学过了,但是最近才发现我以前学的是假的,心中感慨万千(雾),故作此篇。

    简介

    • 带权二分图:每条边都有权值的二分图
    • 最大权匹配:使所选边权和最大的匹配
    • KM算法,全称Kuhn-Munkres算法,是用于解决最大权匹配的一种算法。
    • 根据我的理解,该算法算是一种基于贪心的松弛算法,它通过设置顶标将原问题转化为求一个完备匹配(完备匹配:匹配数=min(左部点数,右部点数))。

    流程

    • 设左部中点(x)的顶标(wx_x)、右部中点(y)的顶标(wy_y)。初始时(wx_u=max{w_{u,v}})(wy_v=0)
    • 我们扫一遍左部,每扫到一个(x)点,尝试增广,我们只能走满足条件(wx_u+wy_v=w_{u,v})的边;这种边构成了原图的相等子图(不要问我为什么,它就叫这个名字)。我们增广失败,将访问过的点(包括增广失败的点)形成的树称为交错树,该树显然所有叶子都是(x)点。
    • 接下来即是算法关键:我们为扩大相等子图(使当前的(x)尽量匹配上),修改所有交错树中的点的顶标,即将其中的(x)点顶标(-d)(y)点顶标(+d)。为保速度,(d=min{wx_u+wy_v-w_{u,v}})(u)在交错树中,(v)不在交错树中)。
    • 由于我们要尝试为左部(n)个点匹配,每次匹配最多增广(n)次(即最多要修改(n)次顶标,因为无法保证修改完一次顶标后就能扩大相等子图),每次增广是(O(n+m))的,故此做法的复杂度应为(O(n^2(n+m)))

    某个优化

    • 给每个(y)顶点一个“松弛量”函数(slack),每次开始找增广路时初始化为无穷大。在寻找增广路的过程中,检查边<i,j>时,如果它不在相等子图中,则让(slack[j])变成原值与(w_{i,j})的较小值。这样,在修改顶标时,取所有不在交错树中的(y)顶点的(slack)值中的最小值作为(d)值即可。但还要注意一点:修改顶标后,要把所有的不在交错树中的(y)顶点的(slack)值都减去(d)
    • 这个优化似乎是很有用,但并不能把KM优化到(O(n^3))。这其实和原算法差不多,还是要为左部(n)个点匹配,每次匹配还是最多要增广(n)次,每次增广还是(O(n+m))。如果是完全图,并且出题人稍微构造一下数据,依然是(O(n^4))

    Code

    bool dfs(int k) {
    	visx[k] = 1;
    	F(i, 1, n) {
    		if (!visy[i]) {
    			int t = A[k] + B[i] - Edge[k][i];
    			if (!t) {
    				visy[i] = 1;
    				if (!link[i] || dfs(link[i])) return link[i] = k;
    			} else slack[i] = min(slack[i], t);
    		}
    	}
    	return 0;
    }
    
    int KM() {
    	mem(link, 0);
    	F(i, 1, n) {
    		A[i] = -1e18, B[i] = 0;
    		F(j, 1, n)
    			A[i] = max(A[i], Edge[i][j]);
    	}
    	F(v, 1, n) {
    		int cnt = 0;
    		F(i, 1, n) slack[i] = 1e18;
    		while (1) {
    			mem(visx, 0), mem(visy, 0);
    			if (dfs(v)) break;
    			int d = 1e18;
    			F(i, 1, n) if (!visy[i]) d = min(d, slack[i]);
    			F(i, 1, n) if (visx[i]) A[i] -= d;
    			F(i, 1, n) if (visy[i]) B[i] += d; else slack[i] -= d;
    		}
    	}
    	Ans = 0;
    	F(i, 1, n) Ans += A[i] + B[i];
    	return Ans;
    }
    
    

    再次优化

    • 先前的算法中,我们把大量时间浪费在 修改顶标-尝试增广 的操作上了。每次修改完顶标后,我们都要花至多(O(n^2))的时间走先前已经走过的路。
    • 但实际上,每次修改顶标后,我们可以确定一个(y)点可以被增广:那就是迫使我们修改顶标的那个(y)点。我们可以记录下它,并且下次增广就直接从它已连的(x)点增广(当然,如果它没有连(x)点,那就增广结束)。
    • 这样,我们就把(dfs)的增广改为了一个类似(bfs)的东西。并且对于每个(x)点而言,每次修改顶标后不需要清空(vis)数组、增广时每个点、每条边至多被经过一次,故时间复杂度成功优化至(O(n^2+nm))

    Code

    int n,w[N][N],my[N],wx[N],wy[N],slack[N],pre[N];
    bool vis[N];
    void augment(int s)
    {
    	fo(y,1,n) vis[y]=uy[y],slack[y]=inf;
    	int y0,nxt=0,tm;
    	for(my[0]=s; vis[y0=nxt]=1,my[y0];)
    	{
    		int x=my[y0],d=inf;
    		fo(y,1,n) if(!vis[y])
    		{
    			if((tm=wx[x]+wy[y]-w[x][y])<slack[y]) slack[y]=tm,pre[y]=y0;
    			if(slack[y]<d) d=slack[y],nxt=y;
    		}
    		if(d<inf) fo(y,0,n) vis[y]?wx[my[y]]-=d,(y?wy[y]+=d:0):slack[y]-=d;
    	}
    	for(int y; y0; y0=pre[y=y0],my[y]=my[y0]);
    }
    int KM()
    {
    	fo(i,1,n) wx[i]=wy[i]=my[i]=pre[i]=0;
    	fo(i,1,n) fo(j,1,n)
    	{
    		if(wx[i]<w[i][j]) wx[i]=w[i][j];
      		if(wy[j]<w[i][j]) wy[j]=w[i][j];
    	}
      	fo(i,1,n) augment(i);
    	int s=0;
    	fo(i,1,n) s+=wx[i]+wy[i];
    	return s;
    }
    

    小结

    • 学算法时要带着脑子,莫被网上的博客骗了。
  • 相关阅读:
    [纯C#实现]基于BP神经网络的中文手写识别算法
    【转载】Jedis对管道、事务以及Watch的操作详细解析
    redis 缓存用户账单策略
    redis 通配符 批量删除key
    explain分析sql效率
    mysql 常用命令大全
    【转载】实战mysql分区(PARTITION)
    mysql表名忽略大小写配置
    【转】微服务架构的分布式事务解决方案
    【转载】Mysql中的Btree与Hash索引比较
  • 原文地址:https://www.cnblogs.com/Iking123/p/11300885.html
Copyright © 2020-2023  润新知