指派問題概述:
實際中,會遇到這樣的問題,有n項不同的任務,需要n個人分別完成其中的1項,每個人完成任務的時間不一樣。於是就有一個問題,如何分配任務使得花費時間最少。
通俗來講,就是n*n矩陣中,選取n個元素,每行每列各有1個元素,使得和最小。
如下圖:
指派問題性質:
指派問題的最優解有這樣一個性質,若從矩陣的一行(列)各元素中分別減去該行(列)的最小元素,得到歸約矩陣,其最優解和原矩陣的最優解相同.
匈牙利法:
12 |
7 |
9 |
7 |
9 |
8 |
9 |
6 |
6 |
6 |
7 |
17 |
12 |
14 |
9 |
15 |
14 |
6 |
6 |
10 |
4 |
10 |
7 |
10 |
9 |
1.行歸約:
每行元素減去該行的最小元素
5 |
0 |
2 |
0 |
2 |
2 |
3 |
0 |
0 |
0 |
0 |
10 |
5 |
7 |
2 |
9 |
8 |
0 |
0 |
4 |
0 |
6 |
3 |
6 |
5 |
2.列歸約:
每列元素減去該列的最小元素
5 |
0 |
2 |
0 |
2 |
2 |
3 |
0 |
0 |
0 |
0 |
10 |
5 |
7 |
2 |
9 |
8 |
0 |
0 |
4 |
0 |
6 |
3 |
6 |
5 |
3.試指派:
(1)找到未被畫線的含0元素最少的行列,即,遍歷所有未被畫線的0元素,看下該0元素所在的行列一共有多少個0,最終選取最少個數的那個0元素。
(2)找到該行列中未被畫線的0元素,這就是一個獨立0元素。對該0元素所在行和列畫線。
5 |
0 |
2 |
0 |
2 |
2 |
3 |
0 |
0 |
0 |
0 |
10 |
5 |
7 |
2 |
9 |
8 |
0 |
0 |
4 |
0 |
6 |
3 |
6 |
5 |
5 |
0 |
2 |
0 |
2 |
2 |
3 |
0 |
0 |
0 |
0 |
10 |
5 |
7 |
2 |
9 |
8 |
0 |
0 |
4 |
0 |
6 |
3 |
6 |
5 |
5 |
0 |
2 |
0 |
2 |
2 |
3 |
0 |
0 |
0 |
0 |
10 |
5 |
7 |
2 |
9 |
8 |
0 |
0 |
4 |
0 |
6 |
3 |
6 |
5 |
5 |
0 |
2 |
0 |
2 |
2 |
3 |
0 |
0 |
0 |
0 |
10 |
5 |
7 |
2 |
9 |
8 |
0 |
0 |
4 |
0 |
6 |
3 |
6 |
5 |
(3)暫時不看被線覆蓋的元素,重復(1)(2)直到沒有線可以畫。
(4)根據(2)找到的0元素個數判斷,找到n個獨立0元素則Success,小於n個則Fail.(本例子中,n=5,可以看到,第一次試指派之后,獨立0元素有4個,不符合)
4.畫蓋0線:
目標:做最少的直線數覆蓋所有0元素,直線數就是獨立0元素的個數。
注意:這跟3的線不同;不能用貪心法去畫線,比如
1 0 0
1 1 0
1 0 1
若先畫橫的,則得畫3條線,實際只需2條;若先畫豎的,將矩陣轉置后同理。
步驟3得出的獨立0元素的位置
5 |
0 |
2 |
0 |
2 |
2 |
3 |
0 |
0 |
0 |
0 |
10 |
5 |
7 |
2 |
9 |
8 |
0 |
0 |
4 |
0 |
6 |
3 |
6 |
5 |
(1)對沒有獨立0元素的行打勾、
(2)對打勾的行所含0元素的列打勾
(3)對所有打勾的列中所含獨立0元素的行打勾
(4)重復(2)(3)直到沒有不能再打勾
(5)對打勾的列和沒有打勾的行畫畫線,這就是最小蓋0線。
5 |
0 |
2 |
0 |
2 |
|
2 |
3 |
0 |
0 |
0 |
|
0 |
10 |
5 |
7 |
2 |
√ |
9 |
8 |
0 |
0 |
4 |
|
0 |
6 |
3 |
6 |
5 |
√ |
√ |
5 |
0 |
2 |
0 |
2 |
|
2 |
3 |
0 |
0 |
0 |
|
0 |
10 |
5 |
7 |
2 |
√ |
9 |
8 |
0 |
0 |
4 |
|
0 |
6 |
3 |
6 |
5 |
√ |
√ |
5.更新矩陣:
(1)對沒有被線划到的數中,找到最小的數。
(2)對沒有被線划到的數中,減去最小的數。
(3)對被2條線划到的數中,加上最小的數。
7 |
0 |
2 |
0 |
2 |
4 |
3 |
0 |
0 |
0 |
0 |
8 |
3 |
5 |
0 |
11 |
8 |
0 |
0 |
4 |
0 |
4 |
1 |
4 |
3 |
6.重復3-5直到成功。
0 |
1 |
0 |
0 |
0 |
0 |
0 |
1 |
0 |
0 |
0 |
0 |
0 |
0 |
1 |
0 |
0 |
0 |
1 |
0 |
1 |
0 |
0 |
0 |
0 |
sum = 7+6+9+6+4 = 32
練習:http://soj.me/show_problem.php?pid=1002&cid=1085
注意題目是求最大值,所以需要對矩陣做一點處理。
代碼:

#include <iostream> #include <algorithm> #include <cstring> #include <climits> using namespace std; #define Max 17 int n; //維數 int s[Max][Max]; //原始矩陣 int p[Max][Max]; //歸約矩陣 int q[Max][Max]; //0:未被畫線 1:畫了1次 2: 畫了2次(交點) int row[Max], col[Max]; //行列0元素個數 int r[Max][Max]; //0:非0元素 1:非獨立0元素 2:獨立0元素 int x[Max],y[Max]; //畫線時是否被打勾,1是0不是 //數每行每列的0元素個數 void countZero() { memset(row, 0, sizeof(row)); memset(col, 0, sizeof(col)); memset(r, 0, sizeof(r)); for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) { if (p[i][j] == 0) row[i]++, col[j]++; } } } //畫最少的線覆蓋所有0元素 int drawLine() { memset(q, 0, sizeof(q)); for (int i = 0; i < n; ++i) x[i] = 1, y[i] = 0; //row 對所有不含獨立0元素的行打勾! for (int i = 0; i < n; ++i) { for (int j = 0; j < n; ++j) { if (r[i][j] == 2) { x[i] = 0; break; } } } bool is = 1; while (is) //循環直到沒有勾可以打 { is = 0; //col 對打勾的行中含0元素的未打勾的列打勾 for (int i = 0; i < n; ++i) { if (x[i] == 1) { for (int j = 0; j < n; ++j) { if (p[i][j] == 0 && y[j] == 0) { y[j] = 1; is = 1; } } } } //row 對打勾的列中含獨立0元素的未打勾的行打勾 for (int j = 0; j < n; ++j) { if (y[j] == 1) { for (int i = 0; i < n; ++i) { if (p[i][j] == 0 && x[i] == 0 && r[i][j] == 2) { x[i] = 1; is = 1; } } } } } //沒有打勾的行和有打勾的列畫線,這就是覆蓋所有0元素的最少直線數 int line = 0; for (int i = 0; i < n; ++i) { if (x[i] == 0) { for (int j = 0; j < n; ++j) q[i][j]++; line++; } if (y[i] == 1) { for (int j = 0; j < n; ++j) q[j][i]++; line++; } } return line; } //找獨立0元素個數 /*1.找含0最少的那一行/列 2.划掉,更新該行/列0元素所在位置的row[],col[] 3.直到所有0被划線,這里定義為row[]col[]全為INT_MAX,表示該行/列無0元素*/ int find() { countZero(); int zero = 0; //獨立0元素的個數 while (1) { //row[i] = INT_MAX表示該行無0元素,防止與*min_element()沖突 for (int i = 0; i < n; ++i) { if (row[i] == 0) row[i] = INT_MAX; if (col[i] == 0) col[i] = INT_MAX; } bool stop = 1; if (*min_element(row, row+n) <= *min_element(col, col+n)) //行最少0元素 { //找含0最少的那一行 int tmp = INT_MAX, index = -1; for (int i = 0; i < n; ++i) { if (tmp > row[i]) tmp = row[i], index = i; } /*找該行任意一個沒被划掉的0元素(獨立0元素),找到一個就行*/ int index2 = -1; //該行獨立0元素的列值 for (int i = 0; i < n; ++i) if (p[index][i] == 0 && col[i] != INT_MAX) { index2 = i; stop = 0; //找到獨立0元素則繼續循環 zero++; //獨立0元素的個數 break; } //找不到獨立0元素了 if (stop) break; //標記 row[index] = col[index2] = INT_MAX; r[index][index2] = 1; //獨立0元素,等等會++. //更新其所在行列的col,row for (int i = 0; i < n; ++i) if (p[index][i] == 0 && col[i] != INT_MAX) //若該行還有0且沒被划掉才更新 col[i]--; for (int i = 0; i < n; ++i) if (p[i][index2] == 0 && row[i] != INT_MAX) row[i]--; } else //列最少0元素 { int tmp = INT_MAX, index = -1; for (int i = 0; i < n; ++i) { if (tmp > col[i]) tmp = col[i], index = i; } int index2 = -1; for (int i = 0; i < n; ++i) if (p[i][index] == 0 && row[i] != INT_MAX) { index2 = i; stop = 0; zero++; break; } if (stop) break; row[index2] = col[index] = INT_MAX; r[index2][index] = 1; for (int i = 0; i < n; ++i) if (p[index2][i] == 0 && col[i] != INT_MAX) col[i]--; for (int i = 0; i < n; ++i) if (p[i][index] == 0 && row[i] != INT_MAX) row[i]--; } } //r[i][j] 0:非0元素 1:非獨立0元素 2:獨立0元素 for (int i = 0; i < n; ++i) for (int j = 0; j < n; ++j) if (p[i][j] == 0) r[i][j]++; return zero; } int main() { int m; cin >> m; while (m--) { cin >> n; for (int i = 0; i < n; ++i) for (int j = 0; j < n; ++j) cin >> s[i][j]; //行歸約 for (int i = 0; i < n; ++i) for (int j = 0; j < n; ++j) p[i][j] = *max_element(s[i], s[i]+n)-s[i][j]; //求和最大 //p[i][j] = s[i][j]-*min_element(s[i],s[i]+j); //求和最小 //列歸約 for (int j = 0; j < n; ++j) { int tmp = INT_MAX; for (int i = 0; i < n; ++i) { if (tmp > p[i][j]) tmp = p[i][j]; } for (int i = 0; i < n; ++i) p[i][j] -= tmp; } while (find() < n) { drawLine(); //最小的未被划線的數 int min = INT_MAX; for (int i = 0; i < n; ++i) for (int j = 0; j < n; ++j) if (q[i][j] == 0 && min > p[i][j]) min = p[i][j]; //更新未被划到的和交點 for (int i = 0; i < n; ++i) for (int j = 0; j < n; ++j) if (q[i][j] == 0) p[i][j] -= min; else if (q[i][j] == 2) p[i][j] += min; } //求和 int ans = 0; for (int i = 0; i < n; ++i) for (int j = 0; j < n; ++j) if (r[i][j] == 2) ans += s[i][j]; cout << ans << endl; } }