匈牙利算法【匹配问题】
匈牙利算法:
匈牙利算法几乎是二分图匹配的核心算法,除了二分图多重匹配外均可使用
匈牙利算法实际上就是一种网络流的思想,其核心就是寻找增广路。具体操作就是嗯。。拉郎配
匈牙利算法适用的场合:
- 二分图匹配
- 链上的匹配问题
- 一般图匹配等其他点的个数较小的匹配问题
举个以前写的博客的例子
通过数代人的努力,你终于赶上了剩男剩女的大潮,假设你是一位光荣的新世纪媒人,在你的手上有N个剩男,M个剩女,每个人都可能对多名异性有好感(-_-||暂时不考虑特殊的性取向),如果一对男女互有好感,那么你就可以把这一对撮合在一起,现在让我们无视掉所有的单相思(好忧伤的感觉),你拥有的大概就是下面这样一张关系图,每一条连线都表示互有好感。
本着救人一命,胜造七级浮屠的原则,你想要尽可能地撮合更多的情侣,匈牙利算法的工作模式会教你这样做:
===============================================================================
一: 先试着给1号男生找妹子,发现第一个和他相连的1号女生还名花无主,got it,连上一条蓝线
===============================================================================
二:接着给2号男生找妹子,发现第一个和他相连的2号女生名花无主,got it
===============================================================================
三:接下来是3号男生,很遗憾1号女生已经有主了,怎么办呢?
我们试着给之前1号女生匹配的男生(也就是1号男生)另外分配一个妹子。
(黄色表示这条边被临时拆掉)
与1号男生相连的第二个女生是2号女生,但是2号女生也有主了,怎么办呢?我们再试着给2号女生的原配重新找个妹子(注意这个步骤和上面是一样的,这是一个递归的过程)
此时发现2号男生还能找到3号女生,那么之前的问题迎刃而解了,回溯回去
2号男生可以找3号妹子~~~ 1号男生可以找2号妹子了~~~ 3号男生可以找1号妹子
所以第三步最后的结果就是:
===============================================================================
四: 接下来是4号男生,很遗憾,按照第三步的节奏我们没法给4号男生腾出来一个妹子,我们实在是无能为力了……香吉士同学走好。
===============================================================================
这就是匈牙利算法的流程,其中找妹子是个递归的过程,最最关键的字就是“腾”字
其原则大概是:有机会上,没机会创造机会也要上
算法步骤(基于二分图)
- 如果题目初始的时候给出的是一副一般图,我们可以利用拓扑染色(2-SAT、种类并查集等等)的方式来让他变成一个二分图。
- 将二分图分成X部和Y部,我们现在的做法是通过X部去匹配Y部;
- 我们从1~cnt_X的去枚举,如果没有被对面的Y给匹配过的,我们试着让他去匹配对应的有链接边的Y部,如果对面的Y部对应的点y是没有被匹配的,或者链接对应y的x是可以换一个没有被匹配过的点的,那么我们就让x去换,并且让我们现在枚举的1~cnt_X去链接对应的点
- 在算法上的优化,我们不难发现,如果目前的1~cnt_Y中的某个y在这一回合的匹配中是被利用过了的,那么也就是说,后面的点都是已经被操作过(且为false的),说明我们不用再继续往下跑了,所以可以利用一个vis标记来进行优化。在枚举x的1~cnt_X的同时,我们进入“腾”这个操作的时候,就是需要去对1~cnt_Y进行vis清空操作。
算法步骤(基于链)
- 链上的匈牙利算法的算法结构并不特殊,只是对于建边的时候需要慎重考虑。
- 它是在一条链上的匹配,我们当去寻求匹配对象的时候,我们要尽可能的先把前面的link[x]覆盖,不然当我们匹配到后面的时候却已经被覆盖了,不是最优解。
- 所以,我们的建边尽可能是要从后面的结点指向前面的结点,例如i -> j(i > j)。
- 然后,我们1~N的枚举,当前的点一定是没有被匹配过的,因为操作3保证了。所以,我们直接去寻求匹配即可。
例题讲解(不按难度顺序排列)
Oil Skimming
这道题的精髓在于,i点和j点是成对匹配的。也就是说,当我们匹配上j点的时候,事实上,i点同时被j点匹配,所以我们一次操作的时候是需要赋值两次。
link[v] = u;
link[u] = v;
也就是这样。
Taxi Cab Scheme
这道题就是一道很模版的匈牙利算法在链上的操作了。
有N个人,他们在特定的时间(hour : minute)要从地点(a, b)前往地点(c, d),然后呢,我们如果说前一个人i的出租车可以开回来再接j这个人,那么他们就可以少叫一辆出租车了。现在,题目想知道最少的需求的出租车的个数。
过山车
匈牙利算法的模板题,这里给出主要的结构。理清楚,我们在哪个位置要清空X的内容,在哪个位置是要去清空Y部的内容,我这里并没有去用拆点或者使用memset()来逃避这两个问题。
bool match(int u)
{
for(int i=head[u], v; ~i; i=edge[i].nex)
{
v = edge[i].to;
if(vis[v]) continue;
vis[v] = true;
if(!link[v] || match(link[v]))
{
link[v] = u;
return true;
}
}
return false;
}
inline int Hungery()
{
int ans = 0;
for(int i=1; i<=M; i++) link[i] = 0;
for(int i=1; i<=N; i++)
{
for(int j=1; j<=M; j++) vis[j] = false;
if(match(i)) ans++;
}
return ans;
}
这两行是主要的代码,要仔细看清哪里用的是X,哪里用的是Y。(N对应X,M对应Y)
棋盘游戏
看着题意很复杂,但是我们可以简化一下。
不能横竖给交叉了,实际上指的是每个对应的x坐标对应唯一的y坐标,我们基于这点去对于二维平面进行匈牙利算法即可。复杂度是,其中的源于匈牙利算法的复杂度。
判断是否是重要结点,可以自己想想看。
#include <iostream>
#include <cstdio>
#include <cmath>
#include <string>
#include <cstring>
#include <algorithm>
#include <limits>
#include <vector>
#include <stack>
#include <queue>
#include <set>
#include <map>
#include <bitset>
#include <unordered_map>
#include <unordered_set>
#define lowbit(x) ( x&(-x) )
#define pi 3.141592653589793
#define e 2.718281828459045
#define INF 0x3f3f3f3f
#define HalF (l + r)>>1
#define lsn rt<<1
#define rsn rt<<1|1
#define Lson lsn, l, mid
#define Rson rsn, mid+1, r
#define QL Lson, ql, qr
#define QR Rson, ql, qr
#define myself rt, l, r
#define MP(a, b) make_pair(a, b)
using namespace std;
typedef unsigned long long ull;
typedef unsigned int uit;
typedef long long ll;
const int maxN = 105;
int N, M, K, head[maxN], cnt, cn_X, cn_Y;
vector<pair<int, int>> vt;
struct Eddge
{
int nex, to;
Eddge(int a=-1, int b=0):nex(a), to(b) {}
}edge[maxN * maxN];
inline void addEddge(int u, int v)
{
edge[cnt] = Eddge(head[u], v);
head[u] = cnt++;
}
int link[maxN];
bool vis[maxN];
inline bool match(int u)
{
for(int i=head[u], v; ~i; i=edge[i].nex)
{
v = edge[i].to;
if(u == cn_X && v == cn_Y) continue;
if(vis[v]) continue;
vis[v] = true;
if(!link[v] || match(link[v]))
{
link[v] = u;
return true;
}
}
return false;
}
inline int Hungery()
{
int ans = 0;
for(int i=1; i<=M; i++) link[i] = 0;
for(int i=1; i<=N; i++)
{
for(int j=1; j<=M; j++) vis[j] = false;
if(match(i)) ans++;
}
return ans;
}
inline void init()
{
cnt = 0; cn_X = cn_Y = 0; vt.clear();
for(int i=1; i<=N; i++) head[i] = -1;
}
int main()
{
int Cas = 0;
while(scanf("%d%d%d", &N, &M, &K) != EOF)
{
init();
for(int i=1, u, v; i<=K; i++)
{
scanf("%d%d", &u, &v);
addEddge(u, v);
vt.push_back(MP(u, v));
}
int const_ans = Hungery(), Important = 0;
for(int i=0; i<K; i++)
{
cn_X = vt[i].first; cn_Y = vt[i].second;
if(Hungery() < const_ans) Important++;
}
printf("Board %d have %d important blanks for %d chessmen.\n", ++Cas, Important, const_ans);
}
return 0;
}