指派問題與匈牙利解法


指派問題概述:

實際中,會遇到這樣的問題,有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;
    }
}
View Code

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM