先介绍匈牙利算法 (Hungary) 的求解过程,我直接把代码贴上去就可以吧,有需要的可以联系我。
这个java代码是我根据 “数据魔术师” 公众号中的 c++ 代码改过来的,算是照猫画瓢,侵删。
关于匈牙利算法的思路,网上有很多讲解,我这里就不做过多的论述,只对代码作一定的介绍。
(1) 表示问题的Problem类
public class Problem {
private int n; // 矩阵阶数
private int[][] cost = new int[n+1][n+1]; // 成本矩阵,hungary矩阵
private int[][] zero = new int[n+1][n+1]; // 零元素矩阵
}
cost 成本矩阵为(n+1)阶矩阵,0行和0列分别表示0元素的个数。
zero 零元素矩阵为(n+1)阶矩阵,0行和0列分别用于打勾划线时的操作,2表示对此行列打勾,1和0表示的意义根据具体情况而定。矩阵的各元素位置中,0表示此位置没有零元素,1表示此位置为成本矩阵中圈中的零元素,2表示此位置为成本矩阵中划去的零元素。
(2)行规约和列规约:
本行中的每个元素减去本行中的最小元素,本列中的每个元素减去本列中的最小元素。
// 行列减去最小值
public static void zeroOut(Problem p) {
int tmp;
// 先减去每一行的最小元素
for (int i = 1; i <= p.getN(); i++) {
// 找出本行的最小元素
tmp = p.getCost()[i][1];
for (int j = 2; j <= p.getN(); j++) {
if (p.getCost()[i][j] < tmp) {
tmp = p.getCost()[i][j];
}
}
// 本行减去最小元素
for (int j = 1; j <= p.getN(); j++) {
p.getCost()[i][j] -= tmp;
}
}
// 再减去每一列的最小元素
for (int j = 1; j <= p.getN(); j++) {
// 找出本列的最小元素
tmp = p.getCost()[1][j];
for (int i = 2; i <= p.getN(); i++) {
if (p.getCost()[i][j] < tmp) {
tmp = p.getCost()[i][j];
}
}
// 本列减去最小元素
for (int i = 1; i <= p.getN(); i++) {
p.getCost()[i][j] -= tmp;
}
}
}
(2)指派任务:
【所谓独立零元素,指本行或列中只有它这一个零元素】
在行中寻找独立零元素,并圈中之,划去此独立零元素所在列的其他零元素。
在列中寻找独立零元素,并圈中之,划去此独立零元素所在行的其他零元素。
重复上述操作,直至找到不到独立零元素为止。
在理想情况下,执行指派操作,可以得到指派问题的解。但是大多数情况下会出现两种情况:
1)出现了零元素的闭合回路。2)zero 矩阵中的独立零元素个数小于任务数量,也就是矩阵中1元素的个数小于n。
在 下面[circle_zero ( )] 函数中的第五步,专门用来处理这两种情况。
public static void circle_zero(Problem problem) throws InterruptedException {
int i, j, p;
// 1.在矩阵外围增加一圈,用来标记每行每列零元素的个数
for (i = 0; i <= problem.getN(); i++) {
problem.getCost()[i][0] = 0; // 行
problem.getCost()[0][i] = 0; // 列
}
// 2.标记零元素的个数
for (i = 1; i <= problem.getN(); i++) {
for (j = 1; j <= problem.getN(); j++) {
if (problem.getCost()[i][j] == 0) {
problem.getCost()[0][0]++;
problem.getCost()[i][0]++;
problem.getCost()[0][j]++;
}
}
}
// 3.初始化零元素的矩阵
for (i = 0; i <= problem.getN(); i++) {
for (j = 0; j <= problem.getN(); j++) {
problem.getZero()[i][j] = 0;
}
}
// 4.圈出所有的独立零元素(直到所有的单零元素的行或列都已经遍历完,只能剩下无零的行或列,或者有多个零的行或列)
int flag = problem.getCost()[0][0] + 1; // +1就是为了能够在任何情况下都进入while循环
while (problem.getCost()[0][0] < flag) {
// 最后判断 cost[0][0]与flag的大小是为了得知,本次循环是否圈中或划去了零元素
flag = problem.getCost()[0][0];
// 先行后列,圈中独立零元素
for (i = 1; i <= problem.getN(); i++) {
// 找出只有一个零元素的行:i
if (problem.getCost()[i][0] == 1) {
// 1.找出这个零元素所在的列:j,此时得到了(i,j),这个独立零元素的位置
for (j = 1; j <= problem.getN(); j++) {
if (problem.getCost()[i][j] == 0 && problem.getZero()[i][j] == 0) break;
}
// 2.圈中这个零元素,更改记录中零元素的个数
problem.getZero()[i][j] = 1;
problem.getCost()[i][0]--;
problem.getCost()[0][j]--;
problem.getCost()[0][0]--;
// 3.划去这个独立零元素所在列的其他零元素
if (problem.getCost()[0][j] > 0) {
for (p = 1; p <= problem.getN(); p++) {
if (problem.getCost()[p][j] == 0 && problem.getZero()[p][j] == 0) {
problem.getZero()[p][j] = 2; // 划去
problem.getCost()[p][0]--;
problem.getCost()[0][j]--;
problem.getCost()[0][0]--;
}
}
}
}
}
// 先列后行,圈中独立零元素
for (j = 1; j <= problem.getN(); j++) {
// 只有一个零元素的列j
if (problem.getCost()[0][j] == 1) {
// 1.找出此零元素所在的行i,得到了其坐标(i,j)
for (i = 1; i <= problem.getN(); i++) {
if (problem.getCost()[i][j] == 0 && problem.getZero()[i][j] == 0) break;
}
// 2.圈中这个零元素
problem.getZero()[i][j] = 1;
problem.getCost()[0][j]--;
problem.getCost()[i][0]--;
problem.getCost()[0][0]--;
// 3.划去本行中其他零元素
if (problem.getCost()[i][0] > 0) {
for (p = 1; p <= problem.getN(); p++) {
if (problem.getCost()[i][p] == 0 && problem.getZero()[i][p] == 0) {
problem.getZero()[i][p] = 2;
problem.getCost()[i][0]--;
problem.getCost()[0][p]--;
problem.getCost()[0][0]--;
}
}
}
}
}
}
// 5.如果还有零元素(说明此时应该是出现回路了,有多个解),就圈出行列两个以上的零元素,然后进行判断
if (problem.getCost()[0][0] > 0) {
two_zero(problem);
} else {
judge(problem);
}
}
(3)处理零元素的闭合回路
当出现如下图的情况时,有闭合回路存在,可能会出现多个解,需要进行分情况讨论。
public static void two_zero(Problem p) throws InterruptedException {
int i, j;
int k, l;
int m;
int flag;
Problem backup;
// 1.找出有零元素的行 i
for (i = 1; i <= p.getN(); i++) {
if (p.getCost()[i][0] > 0) break;
}
// 2.找到此零元素所在的列 j
if (i <= p.getN()) {
for (j = 1; j <= p.getN(); j++) {
// 进行备份一次,用于求解多个解
backup = new Problem(p);
if (p.getCost()[i][j] == 0 && p.getZero()[i][j] == 0) {
// System.out.println("第"+i+"行,第"+j+"列");
// 2.1 圈中一个零 (i,j)
p.getZero()[i][j] = 1;
p.getCost()[i][0]--;
p.getCost()[0][j]--;
p.getCost()[0][0]--;
// 2.2 将本列中剩下的零划去 j
for (k = 1; k <= p.getN(); k++) {
if (p.getCost()[k][j] == 0 && p.getZero()[k][j] == 0) {
p.getZero()[k][j] = 2;
p.getCost()[k][0]--;
p.getCost()[0][j]--;
p.getCost()[0][0]--;
}
}
// 2.3 将当前元素所在行的其他元素划掉 i
for (k = 1; k <= p.getN(); k++) {
if (p.getCost()[i][k] == 0 && p.getZero()[i][k] == 0) {
p.getZero()[i][k] = 2;
p.getCost()[i][0]--;
p.getCost()[0][k]--;
p.getCost()[0][0]--;
}
}
// 2.4 对剩下的零元素,进行指派分配
flag = p.getCost()[0][0] + 1;
while (p.getCost()[0][0] < flag) {
flag = p.getCost()[0][0];
// 先行后列(这里从i+1开始,因为i行的元素已经操作过了)
for (k = i + 1; k <= p.getN(); k++) {
// 找独立零元素所在的行:k
if (p.getCost()[k][0] == 1) {
// 圈中行中的独立零元素
for (l = 1; l <= p.getN(); l++) {
// 找到独立零元素所在的列:l(得到其坐标 [k,l])
if (p.getCost()[k][l] == 0 && p.getZero()[k][l] == 0) break;
}
//System.out.println("<"+k+","+l+">");
p.getZero()[k][l] = 1; // 圈中这个零元素
p.getCost()[k][0]--;
p.getCost()[0][l]--;
p.getCost()[0][0]--;
// 划去 l 列中的其他零元素
for (m = 1; m <= p.getN(); m++) {
if (p.getCost()[m][l] == 0 && p.getZero()[m][l] == 0) {
p.getZero()[m][l] = 2;
p.getCost()[0][l]--;
p.getCost()[m][0]--;
p.getCost()[0][0]--;
}
}
}
}
// 先列后行
for (l = 1; l <= p.getN(); l++) {
// 找到独立零元素所在的列:l
if (p.getCost()[0][l] == 1) {
// 圈中列中的独立零元素
for (k = 1; k <= p.getN(); k++) {
// 找到独立零元素所在的行:k(得到坐标 [k,l])
if (p.getCost()[k][l] == 0 && p.getZero()[k][l] == 0) break;
}
p.getZero()[k][l] = 1;
p.getCost()[0][l]--;
p.getCost()[k][0]--;
p.getCost()[0][0]--;
// 划去行中的零元素
for (m = 1; m <= p.getN(); m++) {
if (p.getCost()[k][m] == 0 && p.getZero()[k][m] == 0) {
p.getZero()[k][m] = 2;
p.getCost()[0][m]--;
p.getCost()[k][0]--;
p.getCost()[0][0]--;
}
}
}
}
}
// 2.5 如果还有零元素存在,则说明还存在回路,就应该继续 two_zero操作;
// 如果没有零元素存在了,应该检验是否得到指派问题的解,如果没有得到解,需要对矩阵进行变换
if (p.getCost()[0][0] > 0) {
two_zero(p);
} else {
judge(p);
}
}
p = backup;
}
}
}
(4)当矩阵中所有标记成1的零元素小于n时,进行矩阵变换。
首先对**cost** 矩阵进行划线操作,划线覆盖所有零元素,找到未划线元素中的最小值。所有未划线的行减去此最小值,划线的列加上此最小值。
public static void refresh(Problem p) throws InterruptedException {
int i, j, min = Integer.MAX_VALUE;
boolean flag1 = true, flag2 = true;
// 1.标记出所有有零元素的行(以此来对没有零元素的行打勾)
for (i = 1; i <= p.getN(); i++) {
for (j = 1; j <= p.getN(); j++) {
if (p.getZero()[i][j] == 1) {
p.getZero()[i][0] = 1;
break;
}
}
}
// 2.打勾,覆盖所有的零元素(2表示此行或者此列打勾)
while (flag1) {
flag1 = false;
// 对没有独立0元素的行打勾,找出这些行中被划去0元素所在的列
for (i = 1; i <= p.getN(); i++) {
if (p.getZero()[i][0] == 0) {
p.getZero()[i][0] = 2; // 没有独立零元素的行打勾
for (j = 1; j <= p.getN(); j++) {
if (p.getZero()[i][j] == 2 && p.getZero()[0][j] != 2) {
p.getZero()[0][j] = 1; // 此行中,划去零元素的列标记为1
}
}
}
}
// 对此行中划去0元素的列打勾,找出这些列中无独立0元素所在的行,打勾
for (j = 1; j <= p.getN(); j++) {
if (p.getZero()[0][j] == 1) {
p.getZero()[0][j] = 2; // 对这一列打勾
for (i = 1; i <= p.getN(); i++) {
if (p.getZero()[i][j] == 1 && p.getZero()[i][0] != 2) {
// 对这一列中有独立零元素的行打勾,本应该标记为2,但是如果标记为2就不能返回去进行while循环
// 所以将当前打勾的行标记为0,便于下一次打勾
p.getZero()[i][0] = 0;
flag1 = true; // 以此来判断是否增加了划线
//System.out.println("打勾时候跳不出来了");
}
}
}
}
}
// 3.寻找未被覆盖的最小值
for (i = 1; i <= p.getN(); i++) {
if (p.getZero()[i][0] == 2) { // 打勾的行 = 未划线的列
for (j = 1; j <= p.getN(); j++) {
if (p.getZero()[0][j] != 2) { // 未打勾的列 = 未划线的列
if (flag2) {
min = p.getCost()[i][j];
flag2 = false;
} else {
if (p.getCost()[i][j] < min) {
min = p.getCost()[i][j];
}
}
}
}
}
}
// 4.未划线的行减去最小值
for (i = 1; i <= p.getN(); i++) {
if (p.getZero()[i][0] == 2) {
for (j = 1; j <= p.getN(); j++) {
p.getCost()[i][j] -= min;
}
}
}
// 5.划线的列加上最小值
for (j = 1; j <= p.getN(); j++) {
if (p.getZero()[0][j] == 2) {
for (i = 1; i <= p.getN(); i++) {
p.getCost()[i][j] += min;
}
}
}
// 6.零元素的矩阵清零
for (i = 0; i <= p.getN(); i++) {
for (j = 0; j <= p.getN(); j++) {
p.getZero()[i][j] = 0;
}
}
// 7.继续画圈求解
circle_zero(p);
}
结束了结束了,欢迎批评指正