前部分算法解释引自https://blog.csdn.net/dark_scope/article/details/8880547感谢大神的讲解
【书本上的算法往往讲得非常复杂,我和我的朋友计划用一些简单通俗的例子来描述算法的流程】
匈牙利算法是由匈牙利数学家Edmonds于1965年提出,因而得名。匈牙利算法是基于Hall定理中充分性证明的思想,它是部图匹配最常见的算法,该算法的核心就是寻找增广路径,它是一种用增广路径求二分图最大匹配的算法。
-------等等,看得头大?那么请看下面的版本:
通过数代人的努力,你终于赶上了剩男剩女的大潮,假设你是一位光荣的新世纪媒人,在你的手上有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号男生腾出来一个妹子,我们实在是无能为力了……香吉士同学走好。
===============================================================================
这就是匈牙利算法的流程,其中找妹子是个递归的过程,最最关键的字就是“腾”字
其原则大概是:有机会上,没机会创造机会也要上
【code】
[cpp] view plain copy
在主程序我们这样做:每一步相当于我们上面描述的一二三四中的一步
[cpp] view plain copy
接下来让我们来看看在考试分配问题中的运用
某校经预赛选出A、B、C、D四名学生,将派他们去参加该地区各学校之间的竞赛。此次竞赛的四门功课考试在同一时间进行,因而每人只能参加一门,比赛结果将以团体总分计名次(不计个人名次)。设下表是四名学生选拔时的成绩,问应如何组队较好?。
课程 学生 |
数学 |
物理 |
化学 |
外语 |
A B C D |
90 85 93 79 |
95 89 91 85 |
78 73 88 84 |
83 80 79 87 |
第一步:每一行减去每一行的最小值,每一列减去每一列最小值
第1行减去-95,第2行减去-89,第3行减去-93,第4行减去-87。
第1列减去0,第2列减去0,第3列减去3,第4列减去0
第二步:利用最少的水平线或垂直线覆盖所有的0
第三步:由于水平线和垂直线的总数是3,少于阶数4,进入第四步。
第四步.没有被覆盖的最小值是2,没有被覆盖的每行减去最小值2,被覆盖的每列加上最小值2,然后跳转到第二步。
第五步:当覆盖的线覆盖所有的0时,线总数为4时,算法结束,可得矩阵
0 1 0 0
1 0 0 0
0 0 1 0
0 0 0 1
即A参加物理、B参加数学、C参加化学、D参加英语。总分为355分
java代码实现
package qty;
public class ayu{
public static void appoint(double[][] m){
int N = m.length;
//行规约
for(int i = 0;i < N;i ++){
double min = Double.MAX_VALUE;
for(int j = 0;j < N;j ++){
if(m[i][j] < min)
min = m[i][j];
}
for(int j = 0;j < N;j ++)
m[i][j] -= min;
}
//列规约
for(int j = 0;j < N;j ++){
double min = Double.MAX_VALUE;
for(int i = 0;i < N;i ++){
if(m[i][j] < min)
min = m[i][j];
}
if(min == 0)
continue;
for(int i = 0;i < N;i ++)
m[i][j] -=min;
}
printM(m);
//进行试分配
while(true){
boolean zeroExist = true;
while(zeroExist){
zeroExist = false;
if(rowAppoint(m))
zeroExist = true;
if(colAppoint(m))
zeroExist = true;
printM(m);
}
//判断是否达到最优分配
if(isOptimal(m))
break;
//变换矩阵
updataM(m);
//将0元素恢复
for(int i = 0;i < N;i ++){
for(int j = 0;j < N;j ++)
if(m[i][j]<0)
m[i][j] = 0;
}
printM(m);
}
}
public static void updataM(double[][] m){
int N = m.length;
//记录行、列是否打钩
boolean[] rowIsChecked = new boolean[N];
boolean[] colIsChecked = new boolean[N];
//给没有被圈的行打钩
for(int i = 0;i < N;i ++){
for(int j = 0;j < N;j ++){
if(m[i][j] == -1){
rowIsChecked[i] = false;
break;
}else{
rowIsChecked[i] = true;
}
}
}
boolean isChecked = true;
while(isChecked){
isChecked = false;
//对所有打钩行的0元素所在列打钩
for(int i = 0;i < N;i ++){
if(rowIsChecked[i]){
for(int j = 0;j < N;j ++){
if(m[i][j]==-2 && !colIsChecked[j]){
colIsChecked[j] = true;
isChecked = true;
}
}
}
}
//对打钩列上的独立零元素行打钩
for(int j = 0;j < N;j ++){
if(colIsChecked[j]){
for(int i = 0;i < N;i ++){
if(m[i][j] == -1 && !rowIsChecked[i]){
rowIsChecked[i] = true;
isChecked = true;
}
}
}
}
}
//寻找盖零线以外最小的数
double min = Double.MAX_VALUE;
for(int i = 0;i < N;i ++){
if(rowIsChecked[i]){
for(int j = 0;j < N;j ++){
if(!colIsChecked[j]){
if(m[i][j] < min)
min = m[i][j];
}
}
}
}
//打钩各行减去min
for(int i=0;i < N;i ++){
if(rowIsChecked[i]){
for(int j = 0;j < N;j ++){
if(m[i][j] > 0)
m[i][j] -= min;
}
}
}
//打钩各列加上min
for(int j=0;j < N;j ++){
if(colIsChecked[j]){
for(int i = 0;i < N;i ++){
if(m[i][j] > 0)
m[i][j] += min;
}
}
}
}
//统计被圈起来的0数量,判断是否找到最优解
public static boolean isOptimal(double[][] m){
int count = 0;
for(int i = 0;i < m.length;i ++){
for(int j = 0;j < m.length;j ++)
if(m[i][j] == -1)
count ++;
}
return count==m.length;
}
//寻找只有一个0元素的行,将其标记为独立0元素(-1),对其所在列的0元素画叉(-2)
//若存在独立0元素返回true
public static boolean rowAppoint(double[][] m){
boolean zeroExist = false;
int N = m.length;
//寻找只有一个0元素的行(列)
for(int i = 0;i < N;i ++){
int zeroCount = 0;
int colIndex = -1;
for(int j = 0;j < N;j ++){
if(m[i][j]==0){
zeroCount ++;
colIndex = j;
zeroExist = true;
}
}
//将独立0元素标记为-1(被圈起来),对应的列上的零标记为-2(被划去)
if(zeroCount == 1){
m[i][colIndex] = -1;
for(int k = 0;k < N;k ++){
if(k == i)
continue;
else if(m[k][colIndex] == 0)
m[k][colIndex] = -2;
}
}else if(zeroCount == 2){//如果存在2组解,随机选择其一标记,解决多解问题
if(Math.random()>0.95){
m[i][colIndex] = -1;
for(int k = 0;k < N;k ++){
if(k == i)
continue;
else if(m[k][colIndex] == 0)
m[k][colIndex] = -2;
}
for(int j = 0;j < N;j ++){
if(j == colIndex)
continue;
else if(m[i][j] == 0)
m[i][j] = -2;
}
}
}
}
return zeroExist;
}
//寻找只有一个0元素的列,将其标记为独立0元素(-1),对其所在行的0元素画叉(-2)
//若存在独立0元素返回true
public static boolean colAppoint(double[][] m){
boolean zeroExist = false;
int N = m.length;
for(int j = 0;j < N;j ++){
int zeroCount = 0;
int rowIndex = -1;
for(int i = 0;i < N;i ++){
if(m[i][j]==0){
zeroCount ++;
rowIndex = i;
zeroExist = true;
}
}
if(zeroCount == 1){
m[rowIndex][j] = -1;
for(int k = 0;k < N;k ++){
if(k == j)
continue;
else if(m[rowIndex][k] == 0)
m[rowIndex][k] = -2;
}
}
}
return zeroExist;
}
public static void main(String[] args) {
double[][] m = new double[][]{{-90,-95,-78,-83},
{-85,-89,-73,-80},
{-93,-91,-88,-79},
{-79,-85,-84,-87}, };
appoint(m);
printResult(m);
}
public static void printM(double[][] m){
System.out.println("---------------");
for(int i = 0;i < m.length;i ++){
for(int j = 0;j < m.length;j ++)
System.out.print(m[i][j] + " ");
System.out.println();
}
}
public static void printResult(double[][] m){
System.out.println("-----Result------");
for(int i = 0;i < m.length;i ++){
for(int j = 0;j < m.length;j ++)
if(m[i][j] == -1)
System.out.print(i+"--"+j+", ");
}
}
}