匈牙利算法解指派问题(Java代码)

先介绍匈牙利算法 (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);
    }

结束了结束了,欢迎批评指正

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值