目錄
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
