算法笔记_139:二分图的最大权分配(Java)

目录

1 问题描述

2 解决方案

 


1 问题描述

何为二分图的最大权匹配问题?

最大权二分匹配问题就是给二分图的每条边一个权值,选择若干不相交的边,得到的总权值最大。


2 解决方案

对于此问题的讲解,引用文末参考资料1:

解决这个问题可以用KM算法。理解KM算法需要首先理解“可行顶标”的概念。可行顶标是指关于二分图两边的每个点的一个值lx[i]或ly[j],保证对于每条边w[i][j]都有lx[i]+ly[j]-w[i][j]>=0。如果所有满足lx[i]+ly[j]==w[i][j]的边组成的导出子图中存在一个完美匹配,那么这个完美匹配肯定就是原图中的最大权匹配。理由很简单:这个匹配的权值之和恰等于所有顶标的和,由于上面的那个不等式,另外的任何匹配方案的权值和都不会大于所有顶标的和。

但问题是,对于当前的顶标的导出子图并不一定存在完美匹配。这时,可以用某种方法对顶标进行调整。调整的方法是:根据最后一次不成功的寻找交错路的DFS,取所有i被访问到而j没被访问到的边(i,j)的lx[i]+ly[j]-w[i][j]的最小值d。将交错树中的所有左端点的顶标减小d,右端点的顶标增加d。经过这样的调整以后:原本在导出子图里面的边,两边的顶标都变了,不等式的等号仍然成立,仍然在导出子图里面;原本不在导出子图里面的边,它的左端点的顶标减小了,右端点的顶标没有变,而且由于d的定义,不等式仍然成立,所以他就可能进入了导出子图里。

初始时随便指定一个可行顶标,比如说lx[i]=max{w[i][j]|j是右边的点},ly[i]=0。然后对每个顶点进行类似Hungary算法的find过程,如果某次find没有成功,则按照这次find访问到的点对可行顶标进行上述调整。这样就可以逐步找到完美匹配了。

值得注意的一点是,按照上述d的定义去求d的话需要O(N^2)的时间,因为d需要被求O(N^2)次,这就成了算法的瓶颈。可以这样优化:设slack[j]表示右边的点j的所有不在导出子图的边对应的lx[i]+ly[j]-w[i][j]的最小值,在find过程中,若某条边不在导出子图中就用它对相应的slack值进行更新。然后求d只要用O(N)的时间找到slack中的最小值就可以了。

下面代码所使用的测试数据如下图:

具体代码如下:

package com.liuzhen.practice;

import java.util.Scanner;

public class Main {
    public static int MAX = 100;
    public static int n;
    public static int[][] value = new int[MAX][MAX];   //给定二分图的权重值
    public static int[] lx = new int[MAX];   //记录二分图左半部分顶点的可行顶标
    public static int[] ly = new int[MAX];   //记录二分图右半部分顶点的可行顶标
    public static boolean[] sx = new boolean[MAX];//用于记录二分图左半部分顶点是否在最终结果中
    public static boolean[] sy = new boolean[MAX];//用于记录二分图右半部分顶点是否在最终结果中
    public static int[] pre = new int[MAX];  //用于记录最终结果中顶点y匹配的顶点x

    public boolean dfs(int x) {   //采用匈牙利算法找增广路径
        sx[x] = true;       //代表左半部分顶点x包含在最终结果中
        for(int y = 0;y < n;y++) {
            if(!sy[y] && lx[x] + ly[y] == value[x][y]) {
                sy[y] = true;   //代表右半部分顶点y包含在最终结果中
                if(pre[y] == -1 || dfs(pre[y])) {
                    pre[y] = x;
                    return true;
                }
            }
        }
        return false;
    }

    public int getKM(int judge) {
        if(judge == -1) {  //代表寻找二分图的最小权匹配
            for(int i = 0;i < n;i++)
                for(int j = 0;j < n;j++)
                    value[i][j] = -1 * value[i][j];  //把权值变为相反数,相当于找最大权匹配
        }
        //初始化lx[i]和ly[i]
        for(int i = 0;i < n;i++) {
            ly[i] = 0;
            lx[i] = Integer.MIN_VALUE;
            for(int j = 0;j < n;j++) {
                if(value[i][j] > lx[i])
                    lx[i] = value[i][j];
            }
        }

        for(int i = 0;i < n;i++)
            pre[i] = -1;      //初始化右半部分顶点y的匹配顶点为-1

        for(int x = 0;x < n;x++) { //从左半部分顶点开始,寻找二分图完美匹配的相等子图完美匹配
            while(true) {
                for(int i = 0;i < n;i++) {//每次寻找x的增广路径,初始化sx[i]和sy[i]均为被遍历
                    sx[i] = false;
                    sy[i] = false;
                }
                if(dfs(x))  //找到从x出发的增广路径,结束循环,寻找下一个x的增广路径
                    break;
                //下面对于没有找到顶点x的增广路径进行lx[i]和ly[i]值的调整
                int min = Integer.MAX_VALUE;
                for(int i = 0;i < n;i++) {
                    if(sx[i]) {  //当sx[i]已被遍历时
                        for(int j = 0;j < n;j++) {
                            if(!sy[j]) {  //当sy[j]未被遍历时
                                if(lx[i] + ly[j] - value[i][j] < min)
                                    min = lx[i] + ly[j] - value[i][j];
                            }
                        }
                    }
                }
                if(min == 0)
                    return -1;
                for(int i = 0;i < n;i++) {
                    if(sx[i])
                        lx[i] = lx[i] - min;
                    if(sy[i])
                        ly[i] = ly[i] + min;
                }
            }
        }

        int sum = 0;
        for(int y = 0;y < n;y++) {
            System.out.println("y顶点"+y+"和x顶点"+pre[y]+"匹配");
            if(pre[y] != -1)
                sum = sum + value[pre[y]][y];
        }
        if(judge == -1)
            sum = -1 * sum;
        return sum;
    }

    public static void main(String[] args) {
        Main test = new Main();
        Scanner in = new Scanner(System.in);
        n = in.nextInt();
        int k = in.nextInt();   //给定二分图的有向边数目
        for(int i = 0;i < k;i++) {
            int x = in.nextInt();
            int y = in.nextInt();
            int v = in.nextInt();
            value[x][y] = v;
        }
        System.out.println(test.getKM(1));
    }
}

运行结果:

5
10
0 0 2
0 1 3
1 0 2
2 0 4
2 2 2
3 2 1
3 3 3
3 4 2
4 3 8
4 4 3
y顶点0和x顶点2匹配
y顶点1和x顶点0匹配
y顶点2和x顶点1匹配
y顶点3和x顶点4匹配
y顶点4和x顶点3匹配
17

参考资料:

1.求最大权二分匹配的KM算法

2.二分图最大权匹配-km算法

3.二分图带权匹配 KM算法与费用流模型建立

时间: 2024-10-14 19:43:01

算法笔记_139:二分图的最大权分配(Java)的相关文章

算法笔记_131:出现次数超过一半的数(Java)

目录 1 问题描述 2 解决方案 2.1 每次删除两个不同的数 2.2 记录两个值   1 问题描述 数组中有一个数出现的次数超过了数组长度的一半,请找出这个数. 2 解决方案 2.1 每次删除两个不同的数 具体代码如下: package com.liuzhen.practice; public class Main { public int getResult(int[] A) { //找到两个不相等的元素,将这两个元素变为0 for(int start = 0, i = 1;i < A.le

算法笔记_198:历届试题 打印十字图(Java)

目录 1 问题描述 2 解决方案   1 问题描述 问题描述 小明为某机构设计了一个十字型的徽标(并非红十字会啊),如下所示: ..$$$$$$$$$$$$$....$...........$..$$$.$$$$$$$$$.$$$$...$.......$...$$.$$$.$$$$$.$$$.$$.$...$...$...$.$$.$.$$$.$.$$$.$.$$.$.$...$...$.$.$$.$.$.$$$$$.$.$.$$.$.$...$...$.$.$$.$.$$$.$.$$$.$.

算法笔记_182:历届试题 核桃的数量(Java)

目录 1 问题描述 2 解决方案   1 问题描述 问题描述 小张是软件项目经理,他带领3个开发组.工期紧,今天都在加班呢.为鼓舞士气,小张打算给每个组发一袋核桃(据传言能补脑).他的要求是: 1. 各组的核桃数量必须相同 2. 各组内必须能平分核桃(当然是不能打碎的) 3. 尽量提供满足1,2条件的最小数量(节约闹革命嘛) 输入格式 输入包含三个正整数a, b, c,表示每个组正在加班的人数,用空格分开(a,b,c<30) 输出格式 输出一个正整数,表示每袋核桃的数量. 样例输入1 2 4 5

算法笔记_189:历届试题 横向打印二叉树(Java)

目录 1 问题描述 2 解决方案   1 问题描述 问题描述 二叉树可以用于排序.其原理很简单:对于一个排序二叉树添加新节点时,先与根节点比较,若小则交给左子树继续处理,否则交给右子树. 当遇到空子树时,则把该节点放入那个位置. 比如,10 8 5 7 12 4 的输入顺序,应该建成二叉树如下图所示,其中.表示空白. ...|-1210-|...|-8-|.......|...|-7.......|-5-|...........|-4 本题目要求:根据已知的数字,建立排序二叉树,并在标准输出中横

算法笔记_181:历届试题 回文数字(Java)

目录 1 问题描述 2 解决方案   1 问题描述 问题描述 观察数字:12321,123321 都有一个共同的特征,无论从左到右读还是从右向左读,都是相同的.这样的数字叫做:回文数字. 本题要求你找到一些5位或6位的十进制数字.满足如下要求: 该数字的各个数位之和等于输入的整数. 输入格式 一个正整数 n (10<n<100), 表示要求满足的数位和. 输出格式 若干行,每行包含一个满足要求的5位或6位整数. 数字按从小到大的顺序排列. 如果没有满足条件的,输出:-1 样例输入 44 样例输

算法笔记_013:汉诺塔问题(Java递归法和非递归法)

目录 1 问题描述 2 解决方案  2.1 递归法 2.2 非递归法 1 问题描述 Simulate the movement of the Towers of Hanoi Puzzle; Bonus is possible for using animation. e.g. if n = 2 ; A→B ; A→C ; B→C; if n = 3; A→C ; A→B ; C→B ; A→C ; B→A ; B→C ; A→C; 翻译:模拟汉诺塔问题的移动规则:获得奖励的移动方法还是有可能的.

算法笔记168:历届试题 矩阵翻硬币(Java)

目录 1 问题描述 2 解决方案   1 问题描述 问题描述 小明先把硬币摆成了一个 n 行 m 列的矩阵. 随后,小明对每一个硬币分别进行一次 Q 操作. 对第x行第y列的硬币进行 Q 操作的定义:将所有第 i*x 行,第 j*y 列的硬币进行翻转. 其中i和j为任意使操作可行的正整数,行号和列号都是从1开始. 当小明对所有硬币都进行了一次 Q 操作后,他发现了一个奇迹--所有硬币均为正面朝上. 小明想知道最开始有多少枚硬币是反面朝上的.于是,他向他的好朋友小M寻求帮助. 聪明的小M告诉小明,

算法笔记_185:历届试题 格子刷油漆(Java)

目录 1 问题描述 2 解决方案   1 问题描述 问题描述 X国的一段古城墙的顶端可以看成 2*N个格子组成的矩形(如下图所示),现需要把这些格子刷上保护漆. 你可以从任意一个格子刷起,刷完一格,可以移动到和它相邻的格子(对角相邻也算数),但不能移动到较远的格子(因为油漆未干不能踩!) 比如:a d b c e f 就是合格的刷漆顺序. c e f d a b 是另一种合适的方案. 当已知 N 时,求总的方案数.当N较大时,结果会迅速增大,请把结果对 1000000007 (十亿零七) 取模.

算法笔记_191:历届试题 大臣的旅费(Java)

目录 1 问题描述 2 解决方案   1 问题描述 问题描述 很久以前,T王国空前繁荣.为了更好地管理国家,王国修建了大量的快速路,用于连接首都和王国内的各大城市. 为节省经费,T国的大臣们经过思考,制定了一套优秀的修建方案,使得任何一个大城市都能从首都直接或者通过其他大城市间接到达.同时,如果不重复经过大城市,从首都到达每个大城市的方案都是唯一的. J是T国重要大臣,他巡查于各大城市之间,体察民情.所以,从一个城市马不停蹄地到另一个城市成了J最常做的事情.他有一个钱袋,用于存放往来城市间的路费