二分图[匈牙利算法 & KM算法]
二分图[匈牙利算法 & KM算法]
概念
-
二分图:把一个图的顶点划分为两个不相交集
U
和V
,使得每一条边都分别连接U
、V
中的顶点。如果存在这样的划分,则此图为一个二分图。 -
匹配:在图论中,一个「匹配」(
matching
)是一个边的集合,其中任意两条边都没有公共顶点。例如,图3
、图4
中红色的边就是图2
的匹配。 -
我们定义匹配点、匹配边、未匹配点、非匹配边,例如图
3
中1、4、5、7
为匹配点,其他顶点为未匹配点;(1,5)
、(4,7)
为匹配边,其他边为非匹配边。 -
最大匹配:一个图所有匹配中,所含匹配边数最多的匹配
-
完美匹配:如果一个图的某个匹配中,所有的顶点都是匹配点,那么它就是一个完美匹配。
(一)匈牙利算法
求解最大匹配问题的一个算法是匈牙利算法
- 交替路:从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径叫交替路。如图
5
中9→4→8→1→6→2 就是一条交替路。
-
增广路:从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发的点不算),则这条交替路称为增广路(
agumenting path
)。例如,图5
中的一条增广路如图6
所示(图中的匹配点均用红色标出):-
其实途径的那个未匹配点一定是终点
-
增广路有一个重要特点:非匹配边比匹配边多一条。
我们把匹配边和非匹配边取反, 匹配边就多了一条
-
我们可以通过不停地找增广路来增加匹配中的匹配边和匹配点。找不到增广路时,达到最大匹配(这是增广路定理)
补充定义和定理:
- 最大匹配数:最大匹配的匹配边的数目
- 最小点覆盖数:选取最少的点,使任意一条边至少有一个端点被选择
- 最大独立数:选取最多的点,使任意所选两点均不相连
- 最小路径覆盖数:对于一个 DAG(有向无环图),选取最少条路径,使得每个顶点属于且仅属于一条路径。路径长可以为 0(即单个点)。
- 定理(不会证, 能用就行)
- 定理一:最大匹配数 = 最小点覆盖数(这是 Konig 定理)
- 定理2:最大匹配数 = 最大独立数
- 定理3:最小路径覆盖数 = 顶点数 - 最大匹配数
例题: Asteroids 穿越小行星群
题目描述
贝茜想驾驶她的飞船穿过危险的小行星群.小行星群是一个的网格(1≤N≤500)
,在网格内有K
个小行星(1≤K≤10000)
.
幸运地是贝茜有一个很强大的武器,一次可以消除所有在一行或一列中的小行星,这种武器很贵,所以她希望尽量地少用.给出所有的小行星的位置,算出贝茜最少需要多少次射击就能消除所有的小行星.
输入格式
第1
行:两个整数N
和K
,用一个空格隔开.
第2
行至K+1
行:每一行有两个空格隔开的整数R, C(1≤R, C≤N)
,分别表示小行星所在的行和列.
输出格式
一个整数表示贝茜需要的最少射击次数,可以消除所有的小行星
样例
样例输入
3 4
1 1
1 3
2 2
3 2
样例输出
2
分析
考虑把所有横坐标放在一个集合, 所有纵坐标放在另一个集合, 这两个集合构成二分图, 对应的坐标之间建立一条边
我们现在要把所有横坐标, 所有纵坐标都覆盖(才能全部打下来), 也就是求 最小点覆盖数 , 根据我们上面的定理, 也就是求最大匹配数. 这里想明白了, 就是道模板题了
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 10005;
int n, k, head[maxn], tot, vis[maxn], group[maxn], ans;
struct edge{
int to, nx;
}e[maxn];
void add(int x, int y){//建边不多说
e[++tot].to = y;
e[tot].nx = head[x];
head[x] = tot;
}
int xyl(int u){//匈牙利算法,重点哦
for(int i=head[u]; i; i=e[i].nx){//遍历一个横坐标x的邻接边
int v = e[i].to;//x对应的纵坐标y
if(vis[v] == 0){//未在增广路径中
vis[v] = 1;//进入增广路径
if(group[v]==0 || xyl(group[v])){//如果这个y没有匹配的x, 或他匹配的x能找到另一个未匹配的y
group[v] = u;//更改y匹配的x
return 1;//找到了增广路
}
}
}
return 0;//未找到增广路
}
int main(){
scanf("%d%d", &n, &k);
while(k--){
int r, c; scanf("%d%d", &r, &c);
add(r, c);
}
for(int i=1; i<=n; i++){
memset(vis, 0, sizeof(vis));
if(xyl(i)) ans ++;
}
printf("%d\n", ans);
return 0;
}
(二) 二分图判定
如果一个图是连通的,可以用如下的染色法判定是否二分图:
- 从某个未染色的结点
u
开始,做BFS
或者DFS
。把u
染为1
,枚举u
的儿子v
。如果v
未染色,就染为与u
相反的颜色(-1),如果已染色,则判断u
与v
的颜色是否相同,相同则不是二分图。 - 如果一个图不连通,则在每个连通块中作判定。
#include <bits/stdc++.h>
const int maxn = 505;
using namespace std
vector<int> e[maxn];
int m,n,color[maxn];
bool flag;//全局,标记是否有环
void dfs(int u){
if(flag) return;//如果已经存在环就没必要接着递归了
for(int i = 0; i < e[u].size(); i++){//遍历所有邻接点
int v = e[u][i];
if(color[v]==0){//v还未访问,染色并递归
color[v] = -color[u];
dfs(v);
}
else if(color[v]==color[u]){
flag=1;//说明有环
return;
}
}
}
int main(){
memset(color, 0, sizeof(color));
memset(e, 0, sizeof(e));
for(int i = 0; i < m; i++){
int u,v;scanf("%d%d",&u,&v);
e[u].push_back(v);e[v].push_back(u);
}
for(int i = 0; i < n; i++){
if(color[i] == 0){//可能图是不连通的
color[i] = 1;
dfs(i);
if(flag){
printf("NOT BICOLORABLE.\n");
return;
}
}
}
printf("BICOLORABLE.\n");
return 0;
}
(三)KM算法
概念
- 顶标:设顶点 Xi 的顶标为 A[i],顶点 Yj的顶标为 B[j] ,顶点 Xi与 Yj之间的边权为
w[i][j]
,初始化时,A[i] 的值为与该点关联的最大边权值,B[j] 的值为0
- 相等子图:选择
A[i]+B[j]=w[i][j]
的边 <i,j>构成的子图,就是相等子图。 - 算法执行过程中,对任一条边<i,j> ,
A[i]+B[j]>=w[i][j]
恒成立。 slack
数组存的数是Y
部的点相等子图时,最小要增加的值
算法图示:
-
从X1X1 开始跑匈牙利,匹配的条件是:
A[i]+B[j]=w[i][j]
,显然 X1 和 Y3 匹配成功。 -
接着从 X2 开始,
A[X2]+B[Y3]=w[X2][X3]
此时 Y3 已被 X1匹配,让 X1 换一个匹配对象,但在 X1 的邻接点没有满足:A[i]+B[j]=w[i][j]
的点,这些相临边和顶标和的最小差值为:minz=1,把此时已标记的 X 部的顶标减去minz, Y 部的此时标记的顶标加上minz, 此时A[X1]+B[Y1]==w[X1][Y1]
-
最后从X3 开始找增广路,X3 匹配 Y3 ,不满足,调整顶标,即A[3]=5−1=4,匹配Y3 成功,尝试 X2 寻找新的匹配,此时 Y1 满足匹配,尝试让 X1 寻找新的匹配,此时X1已找不到新的为匹配的点,匹配失败,回溯到 X2
#include <bits/stdc++.h>
using namespace std;
const int maxn = 300 + 10,maxe=1e4+5,Inf = 0x3f3f3f3f;
struct Edee{int to,w,next;}e[maxe];
int n,m,len,head[maxn],g[maxn][maxn];
int wx[maxn], wy[maxn];//每个点的顶标值(需要根据二分图处理出来)
int match[maxn];//每个Y部点所匹配的X部的点
int visx[maxn], visy[maxn];//每个点是否加入增广路
int slack[maxn];//边权和顶标最小的差值
void Insert(int u,int v){ e[++len].to=v;e[len].next=head[u];head[u]=len; }
bool dfs(int u){//进入DFS的都是X部的点,找到增广路返回1,否则返回0
visx[u] = 1;//标记进入增广路
for(int i = head[u]; i ; i=e[i].next){
int v = e[i].to;
if(visy[v] == 0){//如果Y部的点还没进入增广路
int t = wx[u] + wy[v] - g[u][v];//边权与顶标的差值
if(t == 0){//t为0说明是相等子图
visy[v] = 1;//加入增广路
if(match[v] == -1 || dfs(match[v])){
match[v] = u;//进行匹配
return 1;
}
}
else if(t > 0) slack[v] = min(slack[v], t);//slack[v]存的是Y部的点需要变成相等子图顶标值最小增加多少
}
}
return 0;
}
int KM(){
memset(match, -1, sizeof(match));
memset(wx, 0, sizeof(wx));//wx的顶标为该点连接的边的最大权值
memset(wy, 0, sizeof(wy));//wy的顶标为0
for(int u = 1; u <= n; u++) for(int i = head[u]; i ; i=e[i].next) wx[u] = max(wx[u], g[u][e[i].to]);//预处理出顶标值
for(int i = 1; i <= n; i++){//枚举X部的点
memset(slack, 0x3f, sizeof(slack));
while(1){
memset(visx, 0, sizeof(visx));
memset(visy, 0, sizeof(visy));
if(dfs(i)) break;//已经匹配正确
int minz = Inf;
for(int j = 1; j <= n; j++) if(!visy[j] && minz > slack[j]) minz = slack[j];//找出还没经过的点中,需要变成相等子图的最小额外增加的顶标值
for(int j = 1; j <= n; j++) if(visx[j]) wx[j] -= minz; //将X部已访问的顶标减去minz,
for(int j = 1; j <= n; j++)
if(visy[j])wy[j] += minz;//Y部已访问的顶标加上minz
else slack[j] -= minz;//未在增广路,但相应的X部已访问的顶标减少了,其相邻的未访问的期望也减小
}
}
int ans = 0;//二分图最优匹配权值
for(int i = 1; i <= n; i++) if(match[i] != -1) ans += g[match[i]][i];
return ans;
}
int main(){
while(scanf("%d%d", &n,&m) != EOF){
for(int i = 1; i <= m; i++){
int u,v,w;scanf("%d%d%d", &u,&v,&w);
g[u][v]=w;Insert(u,v);
}
printf("%d\n", KM());
}
return 0;
}