最大二分匹配问题——“相亲巧妙解匈牙利算法”

4 篇文章 1 订阅
3 篇文章 1 订阅

二分图

1,什么是二分图

二分图又称作二部图,是图论中的一种特殊模型。
设G=(V, E)是一个无向图。如果顶点集 V可分割为两个互不相交的子集X和Y,并且图中每条边连接的两个顶点一个在 X中,另一个在 Y中,则称图G为二分图。
在这里插入图片描述

2,二分图相关性质

定理:当且仅当无向图G的每一个环
的结点数均是偶数时,图G才是一个二分图。如果无环,也视为二分图。

3,二分图的判定

如果一个图是连通的,可以用如下的染色法判定是否二分图:
我们把X部的结点颜色设为0,Y部的颜色设为1。
从某个未染色的结点u开始,做BFS或者DFS 。把u染为0,枚举u的儿子v。如果v未染色,就染为与u相反的颜色,如果已染色,则判断u与v的颜色是否相同,相同则不是二分图。
如果一个图不连通,则在每个连通块中作判定。如图所示
在这里插入图片描述

二分图的判定结合这道模板题看下:
给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环。

请你判断这个图是否是二分图。

输入格式
第一行包含两个整数 n 和 m。

接下来 m 行,每行包含两个整数 u 和 v,表示点 u 和点 v 之间存在一条边。

输出格式
如果给定图是二分图,则输出 Yes,否则输出 No。

数据范围
1≤n,m≤105
输入样例:
4 4
1 3
1 4
2 3
2 4
输出样例:
Yes
题目来源:染色法判断二分图
推荐 这道题的题中相关解释是根据我后面将的“男女相亲”来陈述的,可以先去看看男女相亲巧解匈牙利

#include<cstring>
#include<iostream>
using namespace std;
const int N = 100010, M = 200010;
int n, m;
int h[N], e[M], ne[M], idex;//因为邻接表存的是无向图,不仅要存ab也要存ba所以M的范围是N的两倍
int st[N];//判断点是否染色以及染的颜色是否相同
void add(int x, int y)
{
    //表示的是x->y的边
    e[idex] = y;
    ne[idex] = h[x];
    h[x] = idex++;

}
bool dfs(int u, int c)//表示将点u染成颜色c
{
    st[u] = c;
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j])//如果未染色
        {
            if (!dfs(j, 3 - c))
                return false;
        }
        else if (st[j] == c) return false;//如果染色但是染色相同那么也是没用的


    }
    return true;




}
int main()
{

    cin >> n >> m;
    memset(h, -1, sizeof h);
    while (m--)
    {
        int u, v;
        cin >> u >> v;
        add(u, v), add(v, u);
    }
    bool flag = false;
    for (int i = 1; i <= n; i++)//之所以循环n次是因为并非所有图都是连通的
    {
        if (!st[i])
        {
            if (!dfs(i, 1))//每次dfs都是将与i点连通的点染色,不连通的点不会染色
            {
                flag = true;
                break;
            }
        }
    }
    if (flag) printf("No\n");
    else printf("Yes\n");

    return 0;
}


二分图的匹配

在这里插入图片描述

匹配:这个图的一个边的集合,集合中任意两条边都没有公共的顶点,则称这个边集为一个匹配。我们称匹配中的边为匹配边,边中的点为匹配点;未在匹配中的边为非匹配边,边中的点为未匹配点。
二分图的最大匹配:一个图中所有匹配中,所含匹配边数最大的匹配称为最大匹配。
完美匹配:
  如果一个图的某个匹配中,图的所有顶点都是匹配点,那么这个匹配就是完美匹配。很显然,完美匹配一定是最大匹配,但是并不是所有的图都存在完美匹配。

Fig.2是某个二分图。Fig.3是该二分图中的某个匹配,匹配中的边用红色标明了。Fig.4是该二分图的某个最大匹配,同时也是完美匹配。

最小点覆盖 = 最大匹配问题证明(koning定理)证明!一定看得懂!

König定理的内容是,一个二分图中的最大匹配数等于这个图中的最小点覆盖数。假如已知最大匹配M,由最大匹配的定义可知,二分图K中的两个集合A,B中已经取出了最多个匹配点,即取出了最多个不共用一个点的路径。如果K中除了M中的点以外再没有其他任何点,那么显然K中所有路径已经被覆盖。如果K中除了M中的点还有其他点时,我们把这些点组成的集合定为L。对于任意x,y∈L,如果x∈A,y∈B,x,y间不可能存在路径,因为x,y∉M,x,y不能匹配。所以任意x∈L,如果x与其它点z连通,则z一定属于M。所以路径x–z也已经被z覆盖。由此可见,M中的点覆盖了所有存在的边,即最小点覆盖=最大匹配。

男女相亲巧解匈牙利!

匈牙利算法:
看了很多博客,都用什么交替路,增广路,我觉得好绕口,我下面用一组“男女相亲”表示一下匈牙利算法具体过程。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

oj题

1,小行星

先看一道板子题目:
小行星
直接给出代码:

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 510, M = 10010;
int h[N], e[M], ne[M], idex;
bool st[N];//判断女生是否已经被男生选择
int match[N];//表示的是匹配。如果左边的点i和右边的点j相匹配,那么match[j] = i
int n, k;
int res;//表示的是最大匹配数
void add(int a, int b)
{
	e[idex] = b;
	ne[idex] = h[a];
	h[a] = idex++;
}
bool find(int x)
{
	for (int i = h[x]; i != -1; i = ne[i])
	{
		int j = e[i];
		if (!st[j])
		{
			st[j] = true;//检查完毕一定要记得将st[j] = true;
			if (match[j] == 0 || find(match[j]))
			{
				match[j] = x;
				return true;

			}
		}
	}

	return false;

}
int main()
{
	memset(h, -1, sizeof h);
	cin >> n >> k;
	int a[N];
	memset(a, 0, sizeof a);
	int q = 0;
	while (k--)
	{
		int r, c;
		cin >> r >> c;
		a[q++] = r;
		add(r, c);
	}
	sort(a, a + q);
	int j = 1,sp = 0;
   while(j<q)
	{
		if (a[j] != a[sp])
		{
			a[++sp] = a[j++];
		}
		else
			j++;


	}
   j--;
	for (int i = 0; i < j; i++)
	{
		memset(st, false, sizeof st);//将所有女孩子清空,表示都还未被选择过

		if (find(a[i]))
			res++;
	}
	cout << res << endl;
	return 0;
}

2,泥泞的领域

题目描述:
雨打湿了奶牛场,一个由 R 行和 C 列组成的矩形网格 (1 <= R <= 50, 1 <= C <= 50)。虽然对草有好处,但雨水使一些裸露的土地变得很泥泞。奶牛是一丝不苟的食草动物,它们不想在吃东西的时候弄脏它们的蹄子。

为了防止那些泥泞的蹄子,农夫约翰将在奶牛场的泥泞部分放置一些木板。每块板的宽度为 1 个单位,长度可以是任意长度。每块板必须平行于场地的一侧对齐。

Farmer John 希望尽量减少覆盖泥泞点所需的木板数量,其中一些可能需要不止一块木板才能覆盖。这些板可能不会覆盖任何草并剥夺奶牛的放牧区,但它们可以相互重叠。

计算 FJ 覆盖现场所有泥土所需的最少板数。
输入

  • 第 1 行:两个以空格分隔的整数:R 和 C

  • 第 2…R+1 行:每行包含一串 C 字符,‘*’ 代表泥泞的补丁,‘.’ 代表一片草地。不存在空格。
    输出

  • 第 1 行:代表 FJ 需要的板数的单个整数。
    样本输入
    4 4
    ..
    .***
    **.
    .
    样本输出
    4
    暗示
    输出细节:

板 1、2、3 和 4 放置如下:
1.2。
.333
444.
…2.
板 2 与板 3 和 4 重叠。

试题来源:POJ2226
在线测试:泥泞的领域
试题分析:
图论的题目其实敲代码是不难的,难的是建模,我乍一看这道题的时候,我以为直接是行列建模,就是题中有很明显的行列表示,那么肯定可以用二分图表示,二分图本身就是两个不相交的点集,然后两个点集之间有边,刚开始我将泥泞的横坐标看作是左半部分点集合,泥泞的右半部分看作是右半部分的集合,然后根据这个二分图直接求最小点覆盖,但我完全就是凭感觉,毫无依据,不出意外的wa了。
正规分析一下,由题意可以知道,我们的木板有两种,横着的木板和竖着的木板,我们把行里面连着的坑洼连起来视为一个点,即是一块横木板,编上序号,那么此时转化为

1 0 2 0
0 3 3 3
4 4 4 0
0 0 5 0
板子数为r
竖着也是一样操作
1 0 4 0
0 3 4 5
2 3 4 0
0 0 4 0
所有横向板子即第一个点集,所有竖向板子就是第二个点集,然后再确定边关系,也就是哪些点之间有边,该边就代表了一个洼点,也就是横着的木板和竖着的木板重合的时候。然后将对应的点连起来。建完图就二分图最大匹配就行了。可以把这个二分图里的点理解为是板子,然后边理解为是洼地,要求用最少的板子,也就是求最少用最少的点来覆盖所有边。
程序清单:

#include<iostream>
#include<cstring>
using namespace std;
const int N = 2501,M = 51;
int g[N][N];//构造图g[i][j]代表的是i->j之间有边
int a[M][M];//构造男孩点集
int b[M][M];//构造女孩点集合
int st[N];//判断是否检查女孩
int r, c;//行,列
char s[M][M];
int n1;//代表的男孩点集的个数
int n2;//女孩点集个数
int res;//代表的是最大匹配数
int match[N];//如果男孩i匹配的是女孩j 那么match[j] = i
void build_map()
{
	//构造男孩图
	for (int i = 1; i <= r; i++)
	{
		for (int j = 1; j <= c; j++)
		{
			if (s[i][j] == '*')
			{
				if (s[i][j - 1] == '*') a[i][j] = a[i][j - 1];
				else
					a[i][j] = ++n1;
			}
		}
	}
	//构造女孩图
	for (int j = 1; j <= c; j++)
	{
		for (int i = 1; i <= r; i++)
		{
			if (s[i][j] == '*')
			{
				if (s[i - 1][j] == '*') b[i][j] = b[i - 1][j];
				else
					b[i][j] = ++n2;
				g[a[i][j]][b[i][j]] = 1;//表明此时男孩和女孩之间有一个边
			}
		}
	}

}
bool find(int x)
{
	for (int i = 1; i <= n2; i++)
	{
		if (g[x][i] && !st[i])//如果男女之间有变并且女生还未被查看
		{
			st[i] = true;
			if (match[i] == 0 || find(match[i]))//match[i] = 0代表此时女生没对象,后者代表的是女生现任对象有备胎
			{
				match[i] = x;
				return true;
			}
		}
	}
	return false;
}
int main()
{
	memset(match, 0, sizeof match);
	memset(g, 0, sizeof g);
	cin >> r >> c;
	for (int i = 1; i <=r; i++)
		cin >> s[i]+1;//此时行和列的下标都是从1开始的,为了防止建图操作时的内存越界
	build_map();
	//建造完图就是匈牙利算法算最大匹配数了
	
	for (int i = 1; i <= n1; i++)
	{
		memset(st, false, sizeof st);//每次寻找女孩之前都得重新赋值为false
		if (find(i))
			res++;
	}
	cout << res << endl;
	return 0;
}
评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

每天少点debug

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值