二分图最大匹配问题

article/2025/9/17 10:21:04

最近在做的打车项目中,涉及到了用户叫单后,将所有出单司机和所有订单匹配的问题,借此来学习一下二分图的匹配算法。

一、无权二分图最大匹配

首先要区分一下各个概念:

  1. 匹配:图G的一个匹配是由一组没有公共端点的不是环的边构成的集合。
    匹配有两个重点:1. 匹配是边的集合;2. 在该集合中,任意两条边不能有共同的顶点。
    注意:二分图无奇圈。

  2. 完美匹配:考虑部集为X={x1 ,x2, …}和Y={y1, y2, …}的二分图,一个完美匹配就是定义从X-Y的一个双射,依次为x1, x2, … xn找到配对的顶点,最后能够得到 n!个完美匹配。

  3. 最大匹配:使得所有匹配中总边数最大的匹配。
    注意:完美匹配一定是最大匹配,而最大匹配不一定是完美匹配。

  4. 交错路径:给定图G的一个匹配M,如果一条路径的边交替出现在M中和不出现在M中,我们称之为一条M-交错路径。

  5. 增广路径:如果一条M-交错路径,它的两个端点都不与M中的边关联,我们称这条路径叫做M-增广路径。

    如图,这里两条加粗的边2、4是一个匹配M,而1-2-3-4-5构成一条增广路,在这条路径上,存在一个比M更大的匹配(1、3、5)

    因此,我们寻找最大匹配的任务就相当于我们不断地在已经确定的匹配下,不断找到新的增广路径,因为出现一条增广路径,就意味着目前的匹配中增加一条边!
    当匹配中不再存在增广路径,即说明得到了最大匹配

下面我们讨论下匈牙利算法的内容:

  1. 给定一个图:
    在这里插入图片描述
    为了得到最大匹配,我们的目标是尽可能给x中最多的点找到配对。

刚开始,一个匹配都没有,我们随意选取一条边,(x1, y1)这条边,构建最初的匹配出来,结果如下,已经配对的边用粗线标出:
在这里插入图片描述
2. 我们给x2添加一个匹配,如下图的(x2, y2)边。
在这里插入图片描述

  1. 我们现在想给x3匹配一条边,发现它的另一端y1已经被x1占用了,那x3就不高兴了,它就去找y1游说,让y1离开x1。

    即将被迫分手的x1很委屈,好在它还有其他的选择,于是 x1 妥协了,准备去找自己看中的y2

    但很快,x1发现 y2 被x2 看中了,它就想啊,y1 抛弃了我,那我就让 y2 主动离开 x2 (很明显,这是个递归的过程)

    x2 该怎么办呢?好在天无绝人之路,它去找了y5。谢天谢地,y5 还没有名花有主,终于皆大欢喜。

    匹配如下:
    在这里插入图片描述
    (x3, y1, x1, y2, x2, y5),很明显,这是一条路径P。
    而在第二步中,我们已经形成了匹配M,而P原来是M的一条增广路径!

    上文已经说过,发现一条增广路径,就意味着一个更大匹配的出现,于是,我们将M中的配对点拆分开,重新组合,得到了一个更大匹配,M1, 其拥有(x3, y1),(x1, y2), (x2, y5)三条边。

    而这,就是匈牙利算法的精髓:
    匈牙利算法寻找最大匹配,就是通过不断寻找原有匹配M的增广路径,因为找到一条M匹配的增广路径,就意味着一个更大的匹配M’ , 其恰好比M 多一条边。

    4.将x4 , x5 按顺序加入进来,最终会得到本图的最大匹配。
    在这里插入图片描述

#include <iostream>
#include <stdio.h>
#include <cstring>
#include <vector>
#include<algorithm>using namespace std;const int maxn=10;
int map[maxn][maxn]; //邻接矩阵存图
int nx,ny;
bool vis[maxn];
int link[maxn]; // link[j]=i 表示x方i号点匹配了y方j号点int Hungarian()
{int cnt = 0;for (int i = 0; i < nx; i++){memset(vis, 0, sizeof(vis)); //每一轮都要重置vis数组if (match(i))cnt++;}return cnt;
}bool match(int i)
{for (int j = 0; j < ny; j++)if (map[i][j] && !vis[j]) //有边且未访问{vis[j] = true;                 //记录状态为访问过if (link[j] == 0 || match(link[j])) //如果暂无匹配,或者原来匹配的左侧元素可以找到新的匹配{link[j] = i;    //当前左侧元素成为当前右侧元素的新匹配return true; //返回匹配成功}}return false; //循环结束,仍未找到匹配,返回匹配失败
}int main(){int k;cin>>nx>>ny>>k;for(int i=0;i<k;i++){int x,y;cin>>x>>y;map[x][y]=1;}cout<<Hungarian();system("pause");return 0;
}

二、有权二分图最大匹配

首先要明确一下各个概念:

  1. 点标号:每一个点都有一个标号,记lx[i]为X方点i的标号,ly[j]为Y方点j的标号。

  2. 可行点标与可行边:如果对于图中的任意边(i, j, W)都有lx[i]+ly[j]>=W,则这一组点标是可行的。特别地,对于lx[i]+ly[j]=W的边(i, j, W),称为可行边

  3. 定理:若由二分图中所有满足A[i]+B[j]=w[i,j]的边(i,j)构成的子图(称做相等子图)有完备匹配,那么这个完备匹配就是二分图的最大权匹配。

下面介绍下KM算法的内容:
KM算法的核心思想就是通过修改某些点的标号(但要满足点标始终是可行的),不断增加图中的可行边总数(可行边就是两点标号之和恰为二者边权的边),直到图中存在仅由可行边组成的完全匹配为止,此时这个匹配一定是最佳的。

  1. 求出每个点的初始标号:lx[i]=max{e.W|e.x=i}(即每个X方点的初始标号为与这个X方点相关联的权值最大的边的权值),ly[j]=0(即每个Y方点的初始标号为0)。

    这个初始点标显然是可行的,并且与任意一个X方点关联的边中至少有一条可行边。

  2. 从每个X方点开始DFS增广。DFS增广的过程与最大匹配的Hungary算法基本相同,只是要注意两点:一是只找可行边,二是要把搜索过程中遍历到的X方点全部记下来(可以用vst搞一下),以进行后面的修改

  3. 增广的结果有两种:若成功(找到了增广路),则该点增广完成,进入下一个点的增广。

    若失败(没有找到增广路),则需要改变一些点的标号,使得图中可行边的数量增加。方法为:在增广过程中遍历到的X方点的标号全部减去一个常数d,所有遍历到的Y方点的标号全部加上一个常数d,经过这一步后,图中至少会增加一条可行边。

    d = min(lx[i]+ly[j]-W),其中i是参与此次增广过程的X点,j是未参与此次增广过程的Y点

  4. 修改后,继续对这个X方点DFS增广,若还失败则继续修改,直到成功为止

以上就是KM算法的基本思路。但是朴素的实现方法,时间复杂度为O(n4)——需要找O(n)次增广路,每次增广最多需要修改O(n)次顶标,每次修改顶标时由于要枚举边来求d值,复杂度为O(n2)。

实际上KM算法的复杂度是可以做到**O(n3)**的。我们给每个Y顶点一个“松弛量”函数slack,每次开始找增广路时初始化为无穷大。

在寻找增广路的过程中,检查边(i,j)时,如果它不在相等子图中,则让slack[j]变成原值与lx[i]+ly[j]-w[i,j]的较小值。

这样,在修改顶标时,取所有不在交错树中的Y顶点的slack值中的最小值作为d值即可。

但还要注意一点:修改顶标后,要把所有的slack值都减去d。

注意以下几点:
1,匹配两边节点不需要相等;但是需要nx<=ny
2,求最小权的时候只需要将权值取负,在求最大权即可;//十分有效
3,不存在的边权初始化为负无穷大;

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define inf 100000000
using namespace std;const int maxn=100;
int map[maxn][maxn];
int nx,ny;
int link[maxn];
int visx[maxn],visy[maxn];
int lx[maxn],ly[maxn];
int slack[maxn];int KM();int main(){cin>>nx>>ny;for(int i=0;i<nx;i++)for(int j=0;j<ny;j++){cin>>map[i][j];}printf("%d\n", KM());system("pause");return 0;
}int KM() {memset(link, -1, sizeof(link));// lx 和 ly的初始化memset(ly, 0, sizeof(ly));for (int i=0; i<nx; i++) {lx[i] = -inf;for (int j=0; j<ny; j++) {lx[i] = max(lx[i], map[i][j]);}}// 从x集合的每个点寻找增广路for (int x=0; x<nx; x++) {for (int i=0; i<ny; i++) //slack 的初始化对当前点寻找增广路的左右过程中有效{ slack[i] = inf;}while(1) {memset(visx, 0, sizeof(visx));  //每次寻找增广路之前初始化memset(visy, 0, sizeof(visy));if (dfs(x)) break; //该点增广完成// 否则修改可行顶标int d = inf;for (int i=0; i<ny; i++) {if (!visy[i])d = min(d, slack[i]);}for (int i=0; i<nx; i++) {if (visx[i]) lx[i] -= d;}for (int i=0; i<ny; i++) {if (visy[i]) ly[i] += d;else slack[i] -= d;}}}int res = 0;for (int i=0; i<ny; i++) { //必定已经找到一个所有相等子图的点导出的完美匹配if (link[i] != -1)res += map[link[i]][i]; // 右集合i的匹配点link[i] 这里不像无向图那样,需要注意顺序---------}return res;
}bool dfs(int x) {visx[x] = 1;  // 左集合访问到的点一定都在交错树中for (int i=0; i<ny; i++) {if (visy[i]) continue;int w = lx[x] + ly[i] - map[x][i];if (w == 0) //可行边{visy[i] = 1;  // 右集合的点在相等子图中 就一定在交错树中if (link[i] == -1 || dfs(link[i])) {link[i] = x;return 1;}}else slack[i] = min(slack[i], w); // 修改不在相等子图中的点的slack值}return 0;
}

http://chatgpt.dhexx.cn/article/Z6eAkCKN.shtml

相关文章

二分图最大匹配——匈牙利算法

二分图最大匹配 &#xff08;一&#xff09;、二分图的介绍1、定义2、充要条件 &#xff08;二&#xff09;、二分图的匹配1、二分图的最大匹配2、增广路径3、匈牙利算法&#xff08;1&#xff09;、复杂度&#xff08;2&#xff09;、算法思路&#xff08;3&#xff09;、代码…

B树最小高度和最大高度的推导

B树最小高度和最大高度的推导 对任意一棵包含n个关键字&#xff0c;高度为h&#xff0c;阶数为m的B树&#xff0c;其最小高度和最大高度的推导过程如下&#xff1a;

算法篇:树之树的高度

算法&#xff1a; 这一类题目很简单&#xff0c;不过却是树的最基本操作之一&#xff0c;引申为判断树是不是平衡二叉树。 一般做法是&#xff0c;计算二叉树的左右子树的高度1&#xff0c;然后取它们的最大值或者最小值。 题目1: https://leetcode-cn.com/problems/balanced-b…

【二叉树】最小高度树

0x00 题目 给定一个 有序 整数数组 元素各不相同且按 升序 排列 编写一个算法&#xff0c;创建一棵 高度最小 的二叉 搜索 树 给定有序数组: [-10,-3,0,5,9] 一个可能的答案是&#xff1a;[0,-3,9,-10,null,5] 它可以表示下面这个高度平衡二叉搜索树&#xff1a; 0 / \ -3 …

常见数据结构详细图解、树的高度、深度、层数、跳表、二叉搜索树、平衡二叉树、红黑树、B树、B+树

常见数据结构 常用的数据结构知识。 1.1 跳表 上图是一个有序链表&#xff0c;我们要检索一个数据就挨个遍历。如果想要再提升查询效率&#xff0c;可以变种为以下结构&#xff1a; 现在&#xff0c;我们要查询11&#xff0c;可以跳着来查询&#xff0c;从而加快查询速度。 …

树的高度 递归法和非递归法

递归法思路&#xff1a; 树的高度即节点子树的高度1&#xff08;节点子树的高度即左子树高度&#xff0c;右子树高度的最大值&#xff09; 代码如下&#xff1a; // Height_Recursive 递归法求树的高度 int Height_Recursive(TreeNode* pTree) {if (pTree NULL) {return 0;…

获取树高度的两种方法与完全二叉树的判断

树的高度 树的高度是节点高度的最大值。 每一层节点的高度如图所示。 方法一&#xff1a;递归 根节点的高度显然就是二叉树的高度。 /** * 获取树的高度* return*/ public int height(){return height2(root); }/*** 使用递归方法获取树的高度* param node* return*/ priv…

红黑树

一.简介 红黑树作为一种二叉搜索树的一种实现&#xff0c;红黑树的左右子树高度差可能大于 1。所以红黑树不是严格意义上的平衡二叉树&#xff08;AVL&#xff09;&#xff0c;但红黑树是黑色节点完美平衡&#xff0c; 其平均统计性能要强于 AVL 。 红黑树是每个节点都带有颜…

B树最大高度推导

文章目录 B树最大高度推导推导B树的最小高度推导最大高度 B树&#xff1a;MySQL数据库索引是如何实现的&#xff1f;1. 遇到问题2. 尝试用学过的数据结构解决这个问题3. 改造二叉查找树4. 索引的弊端 B树最大高度推导 【声明几个重要概念】 B树的关键字就是要查找的东西 B树…

红黑树详解

1&#xff0c;红黑树特点 每一个结点都有颜色&#xff0c;要么红色&#xff0c;要么黑色。根结点必须是黑色的。红色结点的子结点必须是黑色的。任何一个结点&#xff0c;到它所有叶子结点&#xff0c;经过相同个数的黑色结点。&#xff08;红黑树的平衡含义&#xff0c;左右高…

MYSQL的B+Tree索引树高度如何计算

我们使用MySQL数据库的时候&#xff0c;绝大部分的情况下在使用InnoDB存储引擎&#xff0c;偶尔会使用MyISAM存储引擎&#xff0c;至于其他存储引擎&#xff0c;我相信大家都很少接触到&#xff0c;甚至可能都没有听说过。所以本文只讲解InnoDB和MyISAM两个存储引擎的索引&…

红黑树高度上限的证明(通俗易懂)

先把结论放上&#xff0c;设红黑树的高度为h&#xff0c;总结点数为n&#xff0c;那么h与n的关系就是 下面开始证明过程 首先&#xff0c;从任意节点出发&#xff0c;到其子树的叶子节点的路径中黑色节点的数量称为该节点的黑高&#xff0c;即 bh 我们设根节点为T,那么根节点…

数据结构与算法(一): 树的高度和深度的区别

1.高度 对于高度的理解&#xff0c;我们不管他数据结构什么什么知识&#xff0c;就拿楼房来说&#xff0c;假如一个人提问&#xff1a;楼房的高度有好高&#xff1f;我们会下意识的从底层开始往上数&#xff0c;假如楼有6层&#xff0c;则我们会说&#xff0c;这个楼有6层楼那…

经典PID算法

首先感谢黄工&#xff0c;公众号strongerHuang&#xff0c;以下为三篇文章整合而成。 文章链接&#xff1a; https://mp.weixin.qq.com/s/6Ew431m4nXhScpNVp8mGxQ https://mp.weixin.qq.com/s/JYWnu67HEx2uMrntcUhggQ https://mp.weixin.qq.com/s/IrTehHvTofXWP_BapoN1NQ 在工…

PID控制原理

PID控制原理 在模拟控制系统中,控制器最常用的控制规律是PID控制。 PID控制器是一种线性控制器,它根据给定值与实际输出值构成控制偏差。 PID控制规律为 数学表达形式为: 进行拉普拉斯变换,写出传递函数的形式: kp为比例系数,T1为积分时间常数,Td为微分时间常数。…

SMART PLC PID算法基本解析(附公式)

在稳态运行中,控制器调节输出值,使偏差 (e) 为零。偏差是设定值(目标值)与过程变量(实际值、反馈值)之差。输出 = 比例项 + 积分项 + 微分项 离散化的PID公式基本框架几乎一样,不同的厂家描述符号,变量名称定义可能不太一样, 从公式中可以看出,积分项是从第一次采样到…

PID算法-理论分析

连续PID算法 典型PID算法框图 r(t)&#xff1a;设定状态量y(t)&#xff1a;实际状态量e(t)&#xff1a;当前误差u(t)&#xff1a;控制 器输出 P-比例环节 u p ( t ) K p ∗ e ( t ) K p [ r ( t ) − y ( t ) ] u_{p}(t)Kp*e(t)Kp[r(t)-y(t)] up​(t)Kp∗e(t)Kp[r(t)−y(t)…

PID详解

PID在控制领域应该是应用最为广泛的算法了&#xff0c;在工业控制&#xff0c;汽车电子等诸多领域中运用 下面我用一个例子和算法过程来讲解PID的概念 PID&#xff1a; P比例控制&#xff1a;基本作用就是控制对象以线性的方式增加&#xff0c;在一个常量比例下&#xff0c;动态…

模糊PID算法

在讲解模糊PID前&#xff0c;我们先要了解PID控制器的原理&#xff08;本文主要介绍模糊PID的运用,对PID控制器的原理不做详细介绍&#xff09;。PID控制器&#xff08;比例-积分-微分控制器&#xff09;是一个在工业控制应用中常见的反馈回路部件&#xff0c;由比例单元P、积分…

PID控制器整理分享

概述 日常开发中&#xff0c;常常需要对速度、温度等物理量进行稳态控制&#xff0c;而在目前的自动化控制原理中&#xff0c;使用最为广泛的方法就是PID控制算法。本文简要整理分享PID控制器的使用。 正文 PID控制器&#xff0c;即比例-积分-微分控制器。它是一个不依赖系统…