二分图匹配算法(KM算法和暴力搜索)

什么是二分图的匹配问题?

      二分即将图中的顶点分为两类,一类顶点中可以匹配另一类顶点,组成一组带权的关系,求如何匹配总权值最大。

通过一个例题来看一下:

运动员最佳配问题。羽毛球队有男女运动员各n人。给定两个nXn矩阵P和Q。P[i][i]是男运动员i和
女运动员j配对组成混合双打的竞赛优势;Q[i][j]是女运动员i和男运动员j配合的竞赛优势。由于技术
配合和心理状态等各种因素影响,P[i][j]不一定等于Q[j][i]。设计一个算法,计算男女运动员最佳配
对法,使各组男女双方竞赛优势乘积的总和达到最大。

这个问题就是一个标准的二分图的匹配问题,解决这类问题有两种方法

暴力搜索

顺着思路去想,肯定想到的首先是暴力搜索,这个问题可以转换为全排列去做,把男生固定为层数,第一层就是第一个男生可以选取的女生,第二层就是第二个男生在第一个男生选择完的基础上继续可以选择的个数。

在这里插入图片描述
所以,每到叶子节点就可以得到一个组合,遍历整棵树就可以得到所有的组合,所以遍历整棵树使用DFS+回溯的思想去解决这个问题

/**
 *  暴力搜索树P(DFS+回溯),然后在Q中找出对应的值,计算总和
 */
public class Main_1 {
    /**
     * P二维矩阵存储p矩阵
     * Q二维矩阵存储q矩阵
     */
    static int[][] P = {{10,2,3},
            {2,3,4,},
            {3,4,5}};
    static int[][] Q = {{2,2,2},
            {3,5,3},
            {4,5,1}};
    /**
     * stack 保存回溯中男运动员选取女运动员的数组中标号
     */
    static Stack<Integer> stack;
    /**
     * sum 保存最大权值
     */
    static int sum;
    public static void main(String[] args) {
        /**
         * 初始化子数组
         */
        int[] array = new int[P.length];
        for (int i = 0; i < array.length; i++) {
            array[i] = i;
        }
        /**
         * 初始化栈
         */
        stack = new Stack<>();
        /**
         * 递归回溯
         */
        search(array);
    }

    /**
     * 递归回溯
     * @param array 子数组
     */
    public static void search(int[] array) {
        /**
         * 当数组的长度小于0时,退出递归
         */
        if(array.length<=0){
            /**
             * temp 当前组合的总权值
             * 并且输出当前的组合
             */
            int temp = 0;
            for (int i = 0; i < stack.size(); i++) {
                temp += P[i][stack.get(i)]*Q[stack.get(i)][i];
                //System.out.print(stack.get(i) + " ");
                System.out.print(i+" ");
                System.out.print(stack.get(i)+"  ");
            }
            /**
             * 但当前组合计算的总权值大于保存的最大权值就直接覆盖
             */
            if(temp > sum) {
                sum = temp;
            }
            System.out.println(temp);
            return;
        }
        int[] subArray = new int[array.length-1];
        /**
         * 每次递归将数组中未选取的内容分割出来成为一个新的数组
         * 然后就本次递归的值入栈
         * 递归结束的时候将入栈的值出栈
         * 利用栈实现回溯
         */
        for (int i = 0; i < array.length; i++) {
            System.arraycopy(array,0,subArray,0,i);
            System.arraycopy(array,i+1,subArray,i,array.length-i-1);
            stack.push(array[i]);
            search(subArray);
            stack.pop();
        }
    }
}

解释:

P代表题目描述的P数组
Q代表题目描述的Q数组
Stack表示一个栈,在回溯时会用到。
Sum 用来记录最大的权值
Main()主函数
Search()函数 递归遍历树

遍历整棵树也就是要访问到树的每一个节点,所以很明显时间复杂度为O(n!),一旦顶点数过多,这种方法是不可行的。

KM算法

KM算法是二分图的最优权值问题的解决方案,通过定点的权值不断的变化,从而推出最优权值的组合,KM算法的模板完美的解决这个问题。

图的存储使用邻接矩阵的方式

public class KM {
    int[][] graph;        //假设graph的行是顶点X集合(其中的顶点简称X顶点),列是顶点Y集合(其中的顶点简称Y顶点)
    boolean[] xUsed;      //在每次循环中每个X顶点是否访问过
    boolean[] yUsed;      //在每次循环中每个Y顶点是否访问过
    int[] match;          //每个Y顶点匹配的X顶点,即第i个Y顶点匹配的是第match[i]个X顶点
    int len;              //图的大小为len*len
    int[] less;           //保存左边节点与右边节点相加与权值的差值
    private static final int INFINITE = 0x6fffffff;

    int[] X; //每个X顶点的顶标
    int[] Y; //每个Y顶点的顶标,初始化为0

    /**
     * 初始化函数
     * @param data 传入x顶点和y顶点的领接矩阵
     */
    public KM(int[][] data) {
        this.graph = data;
        len = data.length;
        xUsed = new boolean[len];
        yUsed = new boolean[len];
        match = new int[len];
        less = new int[len];

        X = new int[len];
        Y = new int[len];

        for (int i = 0; i < len; i++) {
            match[i] = -1;
        }

        //初始化每个X顶点的顶标为与之相连的边中最大的权
        for (int k = 0; k < len; k++) {
            X[k] = data[k][0];
            for (int l = 0; l < len; l++) {
                X[k] = X[k] >= data[k][l] ? X[k] : data[k][l];
            }
        }
    }

    void km() {
        //遍历每个X顶点
        for (int i = 0; i < len; i++) {
            /**
             * 给less数组赋值为最大值,less数组为了找到顶点值的相差值
             */
            for (int j = 0; j < len; j++) {
                less[j] = INFINITE;
            }
            /**
             * 寻找能与X顶点匹配的Y顶点,如果找不到就降低X顶点的顶标继续寻找
             */
            while (true) {   
                for (int j = 0; j < len; j++) {
                    xUsed[j] = false;
                    yUsed[j] = false;
                }
                if (hungaryDFS(i)) break;  //寻找到匹配的Y顶点,退出
                //如果没有找到能够匹配的Y顶点,则降低X顶点的顶标,提升Y顶点的顶标,再次循环
                int diff = INFINITE;        //diff是顶标变化的数值
                for (int j = 0; j < len; j++) {
                    if (!yUsed[j]) diff = diff <= less[j] ? diff : less[j];
                }
                //diff等于为了使该顶点X能够匹配到一个Y顶点,其X的顶标所需要降低的最小值

                //更新顶标,左加右减
                for (int j = 0; j < len; j++) {
                    if (xUsed[j]) X[j] -= diff;
                    if (yUsed[j]) Y[j] += diff;
                    else less[j] -= diff;
                }
            }
        }

        //匹配完成,可以输出结果
        int res = 0;
        for (int i = 0; i < len; i++) {
            res += graph[match[i]][i];
        }
        System.out.println("最终最大权值:" + res);
    }

    private boolean hungaryDFS(int i) {
        //设置这个X顶点在此轮循环中被访问过
        xUsed[i] = true;

        //对于这个X顶点,遍历每个Y顶点
        for (int j = 0; j < len; j++) {
            if (yUsed[j]) continue;   //每轮循环中每个Y顶点只访问一次
            int gap = X[i] + Y[j] - graph[i][j];      //KM算法的顶标变化公式

            //只有X顶点的顶标加上Y顶点的顶标等于graph中它们之间的边的权时才能匹配成功
            if (gap == 0) {
                    yUsed[j] = true;
                    if (match[j] == -1 || hungaryDFS(match[j])) {
                        match[j] = i;
                        return true;
                    }
            } else {
                less[j] = less[j] <= gap ? less[j] : gap;
            }
        }

        return false;
    }

    public static void main(String[] args) {
        int[][] graph = {
                {20,4,6},
                {6,15,12},
                {12,20,5}
                /*{3,0,4},
                {2,1,3},
                {0,0,5}*/
        };
        new KM(graph).km();
    }
}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值