二分图问题——匈牙利算法+链式前向星 二分图性质 KM算法

17 篇文章 0 订阅

匈牙利算法+链式前向星

时间复杂度为O(NM)

这算法是用来求二分图的最大匹配数,二分图就是可以把点集分成两部分,点集内部互不相连的图。

原理网上有很多,比如https://zhuanlan.zhihu.com/p/96229700之类的形象化讲述,严谨的讲数学可以参考https://www.bilibili.com/video/BV1LZ4y1T7Nt

例题https://acm.hdu.edu.cn/showproblem.php?pid=2063

模板

#include <iostream>//求的是二分图的最大匹配 
#include <cstring>
#include <algorithm>
const int N=1e3+10;
using namespace std;
int n,m;//n表示左侧的节点数,m表示右侧的节点数,我们要匹配n次 
struct node {//链式前向星 建立从左侧到右侧的有向图 
	int to,nxt;
} edge[N*N];
int cnt;//链式前向星边的下标 
int head[N];//链式前向星边的头节点 
int vis[N];//dfs中右侧点是否访问  在getans中初始化 
int match[N];//dfs中右侧点配对者是左侧的谁 -1说明未配对 
void init() {
	memset(head,-1,sizeof(head));
	memset(match,-1,sizeof(match));
	cnt=0;
}
void addedge(int from,int to) {//添加链式前向星的边 
	edge[cnt].to=to;
	edge[cnt].nxt=head[from];
	head[from]=cnt++;
}
bool dfs(int now) {//配对 
	for(int i=head[now]; i!=-1; i=edge[i].nxt) {
		int to=edge[i].to;
		if(vis[to]) continue;//不寻找这一轮中接触过的点 
		vis[to]=1;
		if(match[to]<0||dfs(match[to])) {//match[to]==0说明目标没有被匹配 或者被匹配的点能找到新的匹配 
			match[to]=now;//把他俩匹配 
			return 1;
		}
	}
	return 0;
}
void getans() {//求最大匹配 
	int ans=0;
	for(int i=1; i<=n; i++) {//左侧有n个点,右侧有m个点,我们要匹配n次 
		memset(vis,0,sizeof(vis));
		if(dfs(i))
			ans++;
	}
	printf("%d\n",ans);
}
int main() {
	//https://acm.hdu.edu.cn/showproblem.php?pid=2063板子题 
	int k;
	while(scanf("%d%d%d",&k,&n,&m),k){
		init();
		for(int i=1;i<=k;i++){
			int a,b;
			scanf("%d%d",&a,&b);//输入女生编号,男生编号
			b+=n;//为了避免编号重复,男生编号一律+n
			addedge(a,b);//建立女生->男生有向边 
		}
		getans();
	}
	return 0;
}

二分图性质

二分图中成立,其他图不一定 

最小点覆盖=最大匹配

最小边覆盖=|V|-最大匹配

最大独立集=|V|-最大匹配

以上均指数量而不是集合!

点覆盖,在图论中点覆盖的概念定义如下:对于图G=(V,E)中的一个点覆盖是一个集合S⊆V使得每一条边至少有一个端点在S中。

就是说一个点集,每个边都至少有一个端点属于该集合

边覆盖是一类覆盖,指一类边子集。具体地说,图的一个边子集,使该图上每一节点都与这个边子集中的一条边关联,只有含孤立点的图没有边覆盖,边覆盖也称为边覆盖集,图G的最小边覆盖就是指边数最少的覆盖,图G的最小边覆盖的边数称为G的边覆盖数,常记为β′(G)。 [1]

就是说一个边集,他们的端点包含图中所有点

独立集就是两两不相连的点组成的集合

所以最小点覆盖就是最大匹配喽,匹配边抽一个点就是了。

最小边覆盖除了最大匹配的边之外,孤立点算作一个环,也是一条边,所以是最大匹配+孤立点,孤立点=V-最大匹配*2,所以是V-最大匹配。

最大独立集更简单了,比如左边四个点右边三个点,最大匹配的边是三个,那左边四个点内部互不相连,它们不就组成最大独立集了么。最大匹配要是两个,说明右边有个孤立点,,那么左边四个加上右边的一个就是了,最大匹配每少于点集少的一侧的点数,孤立点就多一个,所以是V-最大匹配。

二分图简单应用

 行和列作为顶点那一行是这么理解的,比如小行星坐标(2,3),他就相当于是一条连接了第二行和第三列的边,化坐标为边。而行和列两个集合本身不可能相连(你不可能同时在两行或者两列),所以是个二分图,消灭小行星就是删除一条边,每次删除一行或者一列,那么删除所有边就转化为求最小点覆盖。最小点覆盖=V-最大匹配,求最大匹配就完事了,套板子

 

 如图所示

KM算法

时间复杂度为O(N^3)

https://www.cnblogs.com/wenruo/p/5264235.html

这个学姐写的特别形象,虽然我个人觉得例子的思想有点emmm,不过能理解算法就行

严谨数学推导请参考https://www.bilibili.com/video/BV1LZ4y1T7Nt

下面贴三张来自上述视频的图

 

 

交错树有点难理解,不想理解可以不理解,也可以用DFS

这个要用邻接矩阵实现,因为S和T即使题目给的不相等,你也要给他添加成相等(虚拟点所连的边权值为0),即使S和T中的点有些没有两两相连,你也得给他加成两两相连(权值为0),那么就会形成下面的格局

S                T

1      ↘→   1

2      ↗ →  2        

那么,邻接矩阵map[i][j]就是点集S中的i指向点集T中的j的有向边,其他存储方式有比邻接矩阵更简单的么?没有!有问题么?没有问题!

下面贴一下上面学姐博客写的板子

#include <iostream>
#include <cstring>
#include <cstdio>
using namespace std;
const int MAXN = 305;
const int INF = 0x3f3f3f3f;
int love[MAXN][MAXN];   // 记录每个妹子和每个男生的好感度
int ex_girl[MAXN];      // 每个妹子的期望值
int ex_boy[MAXN];       // 每个男生的期望值
bool vis_girl[MAXN];    // 记录每一轮匹配匹配过的女生
bool vis_boy[MAXN];     // 记录每一轮匹配匹配过的男生
int match[MAXN];        // 记录每个男生匹配到的妹子 如果没有则为-1
int slack[MAXN];        // 记录每个汉子如果能被妹子倾心最少还需要多少期望值
int N;
bool dfs(int girl)
{
    vis_girl[girl] = true;
    for (int boy = 0; boy < N; ++boy) {
        if (vis_boy[boy]) continue; // 每一轮匹配 每个男生只尝试一次
        int gap = ex_girl[girl] + ex_boy[boy] - love[girl][boy];
        if (gap == 0) {  // 如果符合要求
            vis_boy[boy] = true;
            if (match[boy] == -1 || dfs( match[boy] )) {    // 找到一个没有匹配的男生 或者该男生的妹子可以找到其他人
                match[boy] = girl;
                return true;
            }
        } else {
            slack[boy] = min(slack[boy], gap);  // slack 可以理解为该男生要得到女生的倾心 还需多少期望值 取最小值 备胎的样子【捂脸
        }
    }
    return false;
}
int KM()
{
    memset(match, -1, sizeof match);    // 初始每个男生都没有匹配的女生
    memset(ex_boy, 0, sizeof ex_boy);   // 初始每个男生的期望值为0
    // 每个女生的初始期望值是与她相连的男生最大的好感度
    for (int i = 0; i < N; ++i) {
        ex_girl[i] = love[i][0];
        for (int j = 1; j < N; ++j) {
            ex_girl[i] = max(ex_girl[i], love[i][j]);
        }
    }
    // 尝试为每一个女生解决归宿问题
    for (int i = 0; i < N; ++i) {
        fill(slack, slack + N, INF);    // 因为要取最小值 初始化为无穷大
        while (1) {
            // 为每个女生解决归宿问题的方法是 :如果找不到就降低期望值,直到找到为止
            // 记录每轮匹配中男生女生是否被尝试匹配过
            memset(vis_girl, false, sizeof vis_girl);
            memset(vis_boy, false, sizeof vis_boy);
            if (dfs(i)) break;  // 找到归宿 退出
            // 如果不能找到 就降低期望值
            // 最小可降低的期望值
            int d = INF;
            for (int j = 0; j < N; ++j)
                if (!vis_boy[j]) d = min(d, slack[j]);
            for (int j = 0; j < N; ++j) {
                // 所有访问过的女生降低期望值
                if (vis_girl[j]) ex_girl[j] -= d;

                // 所有访问过的男生增加期望值
                if (vis_boy[j]) ex_boy[j] += d;
                // 没有访问过的boy 因为girl们的期望值降低,距离得到女生倾心又进了一步!
                else slack[j] -= d;
            }
        }
    }
    // 匹配完成 求出所有配对的好感度的和
    int res = 0;
    for (int i = 0; i < N; ++i)
    	if(match[i]>=0)
     	   res += love[ match[i] ][i];
    return res;
}
int main()
{
    while (~scanf("%d", &N)) {
        for (int i = 0; i < N; ++i)
            for (int j = 0; j < N; ++j)
                scanf("%d", &love[i][j]);
        printf("%d\n", KM());
    }
    return 0;
}        

改变一下变量名和细节,使模板符合上面那个B站视频的讲述和数组开头为1的习惯就是这样的

https://acm.hdu.edu.cn/showproblem.php?pid=2255

也是本题的AC代码

#include<iostream>//求的是二分图最大权值匹配 O(N^3) 因为这玩意要求点集S和T数量相同且都得相连,就算不连权值也得设置为0
#include<cstring>//所以用邻接矩阵一点也不浪费空间,他就可以表示成有向图。邻接表和链式前向星反而麻烦且多余  因此模板就不改了 
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 6e2+10;
const int INF = 0x3f3f3f3f;
int map[N][N];   // 记录每个S和每个T的权值 第一维为S 第二维为T 
int ex_S[N];      // 每个S的期望值(或者可行标杆,以下统称期望值) 
int ex_T[N];       // 每个T的期望值
bool vis_S[N];    // 记录每一轮匹配匹配过的S
bool vis_T[N];     // 记录每一轮匹配匹配过的T
int match[N];        // 记录每个T匹配到的S 如果没有则为-1
int slack[N];        // 记录每个T如果能被S倾心最少还需要多少期望值  这个每个S匹配都会被初始化,然后动态变化,根据这个减少S期望,增加T期望 
int n;
bool dfs(int S) {
	vis_S[S] = true;
	for (int T = 1; T <= n; ++T) {
		if (vis_T[T]) {
			continue;
		}// 每一轮匹配 每个T只尝试一次
		int gap = ex_S[S] + ex_T[T] - map[S][T];
		if (gap == 0) {  // 如果符合要求
			vis_T[T] = true;
			if (match[T] == -1 || dfs( match[T] )) {    // 找到一个没有匹配的T 或者匹配到该T的S可以找到其他T
				match[T] = S;
				return true;
			}
		} else {
			slack[T] = min(slack[T], gap);  // slack 可以理解为该T要得到S的完美匹配 还需多少期望值 取最小值 
		}
	}
	return false;
}
int KM() {
	memset(match, -1, sizeof match);    // 初始每个T都没有匹配的S
	memset(ex_T, 0, sizeof ex_T);   // 初始每个T的期望值为0
	// 每个S的初始期望值是与她相连的T最大的权值
	for (int i = 1; i <= n; ++i) {
		ex_S[i] = map[i][1];
		for (int j = 2; j <= n; ++j) {
			ex_S[i] = max(ex_S[i], map[i][j]);
		}
	}
	// 尝试为每一个S解决匹配问题
	for (int i = 1; i <= n; ++i) {
		memset(slack,0x3f,sizeof(slack));   // 因为要取最小值 初始化为无穷大
		while (1) {
			// 为每个S解决匹配问题的方法是 :如果找不到就降低期望值,直到找到为止
			memset(vis_S, false, sizeof vis_S);// 记录每轮匹配中T、S是否被尝试匹配过
			memset(vis_T, false, sizeof vis_T);
			if (dfs(i)) break;  // 找到匹配 退出
			// 如果不能找到 就降低期望值 直到找到为止
			int d = INF;// 最小可降低的期望值
			for (int j = 1 ; j <= n; ++j)
				if (!vis_T[j]) d = min(d, slack[j]);
			for (int j = 1; j <= n; ++j) {
				if (vis_S[j]) {// 所有访问过的S降低期望值
					ex_S[j] -= d;
				}
				if (vis_T[j]) {// 所有访问过的T增加期望值
					ex_T[j] += d;
				}
				else {// 没有访问过的T 因为S的期望值降低,距离得到S完美匹配又进了一步!
					slack[j] -= d;
				}
			}
		}
	}
	int res = 0;	// 匹配完成 求出所有配对的权值的和
	for (int i = 1; i <= n; ++i)
			res += map[ match[i] ][i];
	return res;
}
int main() {
	while (~scanf("%d", &n)) {
		for (int i = 1; i <= n; i++) {//i为点集S 
			for (int j = 1; j <= n; j++) {//j为点集T 
				scanf("%d", &map[i][j]);
			}
		}
		printf("%d\n", KM());
	}
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值