당신은 주제를 찾고 있습니까 “헝가리 안 알고리즘 – [경영과학|OR] 할당 문제: 헝가리법, 헝가리안법 (한국어) Assignment problem: How to (Kuhn’s) Hungarian Algorithm“? 다음 카테고리의 웹사이트 https://you.charoenmotorcycles.com 에서 귀하의 모든 질문에 답변해 드립니다: https://you.charoenmotorcycles.com/blog/. 바로 아래에서 답을 찾을 수 있습니다. 작성자 대학생 비둘기Undergraduate pigeon 이(가) 작성한 기사에는 조회수 4,461회 및 좋아요 39개 개의 좋아요가 있습니다.
헝가리안 알고리즘은 가중치가 있는 이분 그래프(weighted bitarted graph)에서 maximum weight matching을 찾기 위한 알고리즘입니다.
헝가리 안 알고리즘 주제에 대한 동영상 보기
여기에서 이 주제에 대한 비디오를 시청하십시오. 주의 깊게 살펴보고 읽고 있는 내용에 대한 피드백을 제공하세요!
d여기에서 [경영과학|OR] 할당 문제: 헝가리법, 헝가리안법 (한국어) Assignment problem: How to (Kuhn’s) Hungarian Algorithm – 헝가리 안 알고리즘 주제에 대한 세부정보를 참조하세요
OR (Operations Research) 할당 문제: 헝가리법, 헝가리안법 (Assignment problem: How to (Kuhn’s) Hungarian algorithm) 하는 법입니다.
Introduction to Operations Research (Hillier Lieberman)
헝가리 안 알고리즘 주제에 대한 자세한 내용은 여기를 참조하세요.
할당 문제 & 헝가리안 알고리즘 (Assignment Problem …
할당 문제 & 헝가리안 알고리즘 (Assignment Problem & Hungarian Algorithm). 가젤_gazelle 2019. 8. 7. 23:07. 이번 포스팅에서는 assignment problem과 Hungarian …
Source: gazelle-and-cs.tistory.com
Date Published: 9/12/2022
View: 9312
[알고리즘] Hungarian Maximum Matching Algorithm (Kuhn …
헝가리안 알고리즘을 통해 Assignment problem(할당문제)인 Bipartite graphs maximum-weight matchings 문제를 빠르게 해결 할 수 있습니다.
Source: supermemi.tistory.com
Date Published: 10/19/2021
View: 9580
Hungarian algorithm (헝가리안 알고리즘, 헝가리안 메소드 / C++)
배정 문제(assignment problem)를 해결할 수 있는 알고리즘. 예를 들어 보면 n명의 사람이 n개의 일을 할 때, 어떤 값의 최대(or …
Source: kibbomi.tistory.com
Date Published: 5/24/2021
View: 7351
로봇 작업 할당 – Hungarian Algorithm – 로봇이 아닙니다.
뭔가 대단한 것을 할 것처럼 서론을 띄웠지만, 오늘 얘기할 주제는 할당 문제(assignment problem)에서 가장 기본적으로 등장하는 헝가리안 알고리즘( …
Source: ropiens.tistory.com
Date Published: 5/17/2021
View: 7903
헝가리안 알고리즘 hungarian algorithm 구현
이는 n 명의 사람한테 중복없이 n개의 일을 부여하는 방식으로 경제학(?)쪽에서 많이 쓰인단다. … 배정시의 총 비용의 합이 최대 (혹은 최소)가 되어야 …
Source: skkuassa.tistory.com
Date Published: 12/11/2021
View: 3923
헝가리안(hungarian) 알고리즘, 최소 비용 작업 배분 – gaussian37
헝가리안(hungarian) 알고리즘, 최소 비용 작업 배분 · 출처 · https://www.topcoder.com/community/competitive-programming/tutorials/assignment-problem …
Source: gaussian37.github.io
Date Published: 7/23/2022
View: 6466
헝가리안 알고리즘 – Algorithm & Coding
먼저, 할당 문제(Assignment Problem)에 대해서 살펴보자. 참고> www.hungarianalgorithm.com 할당(배정) 문제는 기계를 태스크에, 일꾼을 작업에, …
Source: algostudy.wordpress.com
Date Published: 7/14/2021
View: 3036
주제와 관련된 이미지 헝가리 안 알고리즘
주제와 관련된 더 많은 사진을 참조하십시오 [경영과학|OR] 할당 문제: 헝가리법, 헝가리안법 (한국어) Assignment problem: How to (Kuhn’s) Hungarian Algorithm. 댓글에서 더 많은 관련 이미지를 보거나 필요한 경우 더 많은 관련 기사를 볼 수 있습니다.
주제에 대한 기사 평가 헝가리 안 알고리즘
- Author: 대학생 비둘기Undergraduate pigeon
- Views: 조회수 4,461회
- Likes: 좋아요 39개
- Date Published: 2019. 6. 14.
- Video Url link: https://www.youtube.com/watch?v=CldH2y9eMBw
헝가리안 알고리즘
#define N 500 #define INF 987654321 int w [ N ][ N ]; int match_x [ N ]; int match_y [ N ]; int l_x [ N ], l_y [ N ]; bool s [ N ], t [ N ]; int slack [ N ]; int slack_x [ N ]; int tree_x [ N ]; int tree_y [ N ]; void print ( int n ) { cout << "l_x: " ; for ( int i = 0 ; i < n ; ++ i ) cout << l_x [ i ] << ' ' ; cout << ' ' ; cout << "l_y: " ; for ( int i = 0 ; i < n ; ++ i ) cout << l_y [ i ] << ' ' ; cout << ' ' ; } int hungarian ( int n ) { memset ( match_x , - 1 , sizeof ( match_x )); memset ( match_y , - 1 , sizeof ( match_y )); int ret = 0 ; for ( int i = 0 ; i < n ; ++ i ) { for ( int j = 0 ; j < n ; ++ j ) { l_x [ i ] = max ( l_x [ i ], w [ i ][ j ]); } } memset ( l_y , 0 , sizeof ( l_y )); int m = 0 ; while ( m != n ) { // repeat at most V times memset ( tree_x , - 1 , sizeof ( tree_x )); memset ( tree_y , - 1 , sizeof ( tree_y )); memset ( s , 0 , sizeof ( s )); memset ( t , 0 , sizeof ( t )); int s_start ; for ( int i = 0 ; i < n ; ++ i ) { // O(V) if ( match_x [ i ] == - 1 ) { s [ i ] = 1 ; s_start = i ; break ; } } for ( int i = 0 ; i < n ; ++ i ) { // init slack slack [ i ] = l_x [ s_start ] + l_y [ i ] - w [ s_start ][ i ]; slack_x [ i ] = s_start ; } here: int y = - 1 ; for ( int i = 0 ; i < n ; ++ i ) { // compare: O(V) if ( slack [ i ] == 0 && ! t [ i ]) y = i ; } if ( y == - 1 ) { // n_l = t // update label int alpha = INF ; for ( int i = 0 ; i < n ; ++ i ) { // O(V) if ( ! t [ i ]) { alpha = min ( alpha , slack [ i ]); } } for ( int i = 0 ; i < n ; ++ i ) { // O(V) if ( s [ i ]) l_x [ i ] -= alpha ; if ( t [ i ]) l_y [ i ] += alpha ; } for ( int i = 0 ; i < n ; ++ i ) { // O(V) if ( ! t [ i ]) { slack [ i ] -= alpha ; if ( slack [ i ] == 0 ) { y = i ; } } } } // n_l != t is guaranteed if ( match_y [ y ] == - 1 ) { // free tree_y [ y ] = slack_x [ y ]; while ( y != - 1 ) { int x = tree_y [ y ]; match_y [ y ] = x ; int next_y = match_x [ x ]; match_x [ x ] = y ; y = next_y ; } m ++ ; } else { // matched int z = match_y [ y ]; tree_x [ z ] = y ; tree_y [ y ] = slack_x [ y ]; s [ z ] = 1 ; t [ y ] = 1 ; // z가 추가되었으므로 slack과 n_l이 update for ( int i = 0 ; i < n ; ++ i ) { // O(V) if ( l_x [ z ] + l_y [ i ] - w [ z ][ i ] < slack [ i ]) { slack [ i ] = l_x [ z ] + l_y [ i ] - w [ z ][ i ]; slack_x [ i ] = z ; } } goto here ; } } for ( int i = 0 ; i < n ; ++ i ) { ret += l_x [ i ]; ret += l_y [ i ]; } return ret ; }
할당 문제 & 헝가리안 알고리즘 (Assignment Problem & Hungarian Algorithm)
이번 포스팅에서는 assignment problem과 Hungarian algorithm에 대해서 알아보겠습니다. 먼저 assignment problem이 어떤 것인지에 대해서 살펴보고, 이를 해결하는 방법을 알아보도록 하겠습니다. 인터넷을 찾아보니 Hungarian algorithm 자체가 어떻게 동작하는지에 대해서는 우리말로 된 포스팅이 많이 있는 반면에 이 알고리즘이 왜 정확히 돌아가는지에 대해서 분석한 글은 보지 못하였습니다. 제 블로그는 이 간격을 열심히 줄이는 데 초점이 맞추어져 있습니다. 그러니 이번에도 왜 이 알고리즘이 올바른 결과를 출력하는지 함께 알아보도록 하죠.
문제 정의
Photo by Zulki Jrzt on Unsplash
문제를 정의하기 전에 제 하소연부터 좀 하겠습니다. 이 글을 작성하는 현재 제 자취방 에어컨이 고장 난 상태입니다. 에어컨 수리 기사님은 실외기가 고장났다고 하셨습니다. 문제는 실외기가 창문이 나지 않은 외벽에 붙어있어서 접근이 불가능하다는 점입니다. 어쩔 수 없이 중장비를 부르겠다고 하시고는 그냥 가셨습니다. 다행히 장마로 비가 추적추적 내려 그렇게 덥지는 않습니다. 그렇지만 습도는 어찌할 방도가 없네요.
이번 주제는 assignment problem인데 갑자기 제가 왜 이런 신변잡기를 주저리 늘어놓고 있는지 궁금하실 것입니다. 바로 이 문제를 어떻게 설명하면 좋을까 고민하다가 오늘 아침에 일어난 일이 좋은 예시가 될 것 같아서 가지고 왔습니다. (아, 물론 습한 상태를 약간 한탄하는 것도 있기는 합니다만, 현재로서는 에어컨이 빨리 고쳐지기를 바랄 뿐입니다.) 바로 수리 기사님을 할당하는 방법에 관한 것입니다. 분명히 저와 같이 에어컨에 문제가 생긴 사람들이 방문 수리를 신청했을 것입니다. 그리고 전국에는 서비스 센터가 있지요. 하지만 집집마다 서비스 센터가 바로 옆에 붙어있지는 않을 겁니다. 그렇다면 분명히 어느 서비스 센터의 수리 기사를 어디에 방문하도록 할당을 해 주어야 합니다.
이를 좀 더 엄밀하게 정의해 보겠습니다. 먼저, 방문 수리 신청과 같이 처리해야 할 작업이 주어지겠죠. 그리고 수리 기사님처럼 작업을 수행할 노동자가 있을 것입니다. 마지막으로 어떤 노동자가 어떤 작업을 처리할 때 들어가는 비용을 알아야 합니다. 우리의 목표는 분명 주어진 모든 작업에 노동자를 한 명씩을 가장 적은 비용으로 할당하는 것이 되겠죠.
수학적인 기호와 정의를 사용하면 문제를 좀 더 압축적으로 표현할 수 있습니다. 노동자의 집합을 \(I\), 작업의 집합을 \(J\)라고 하겠습니다. 어떤 노동자 \(i \in I\)가 작업 \(j \in J\)를 처리할 때 들어가는 비용을 \(c(i, j)\)라고 하겠습니다. 문제를 간단하게 만들기 위해서 \(|I| = |J|\)라고 가정하겠습니다. 우리의 목표는 \(I\)에서 \(J\)로 가는 일대일 대응(bijection) \(\sigma : I \rightarrow J\) 중에서 가장 비용이 적은 것을 찾는 것입니다. 다시 말해서,
\[\sum_{i \in I} c(i, \sigma (i))\]
를 최소화시키는 \(\sigma\)를 구하는 것이죠.
우리는 이 문제를 그래프 위에서 생각할 수 있습니다. 노동자 집합 \(I\)를 왼쪽에 나열하고 작업 집합 \(J\)를 오른쪽에 나열합니다. 그후, 노동자 \(i\)와 작업 \(j\)를 잇는 간선을 만들고 그것의 비용을 \(c(i, j)\)로 설정합니다. 그러면 \(I\)와 \(J\)로 양분되고 edge cost가 \(c\)인 complete bipartite graph를 얻게 됩니다. 이 그래프에서 무엇을 찾아야 할까요? 모든 노동자는 한 개의 작업을 할당받아야 하며, 모든 작업은 정확히 한 명의 노동자로부터 처리되어야 합니다. 그래프에서 이러한 성질을 만족하는 것이 무엇인지 우리는 잘 알고 있죠. 바로, matching입니다. 그 중에서도 모든 정점이 참여를 해야 하므로 perfect matching을 찾는 것이겠죠. 게다가 비용을 최소화시켜야 하므로,
\[ \sum_{(i, j) \in M} c(i, j)\]
가 가장 작은 perfect matching \(M\)을 찾는 것이 목표가 되겠습니다.
직관적으로 이해하기
문제가 무엇인지 알았으니 이제부터 알고리즘을 알아보도록 하죠. 먼저 예시를 통해서 알고리즘이 어떻게 돌아가는지 간단히 살펴보도록 하겠습니다. 문제의 입력이 다음과 같이 주어졌다고 생각해 보겠습니다. 아래의 행렬에서 노동자는 행에 대응하며 작업은 열에 대응합니다.
\(j_1\) \(j_2\) \(j_3\) \(i_1\) 3 8 9 \(i_2\) 4 12 7 \(i_3\) 4 8 5
예를 들어, 노동자 \(i_1\)이 작업 \(j_1\)을 할당받으면 3의 비용이 발생합니다. 만약 노동자 \(i_2\)가 작업 \(j_3\)를 할당받으면 7의 비용이 발생하게 되겠죠.
이제부터 solution을 찾아보도록 하죠. 먼저 노동자 \(i_1\)을 살펴보겠습니다. 첫 번째 작업을 할당받으면 3, 두 번째 작업은 8, 마지막 작업은 9의 비용이 발생합니다. 여기서 우리는 어찌되었든 비용이 3만큼은 발생한다는 것을 알 수 있습니다. 따라서 각각의 비용에 3을 모두 빼줘도 (값에는 변화가 있을지라도) solution 자체에는 변화가 없을 겁니다. 즉 이런 식으로 행렬을 만들어도 문제가 없다는 것입니다.
\(j_1\) \(j_2\) \(j_3\) \(i_1\) 0 5 6 3 \(i_2\) 4 12 7 \(i_3\) 4 8 5
이 작업을 꼭 \(i_1\)에 대해서만 해줄 필요는 없겠죠. 모든 노동자들에게 똑같이 해주면 다음과 같은 행렬을 얻게 됩니다.
\(j_1\) \(j_2\) \(j_3\) \(i_1\) 0 5 6 3 \(i_2\) 0 8 3 4 \(i_3\) 0 4 1 4
게다가 이 작업을 굳이 노동자에게만 적용할 필요도 없습니다. 모든 작업에 대해서도 비슷한 방식으로 생각할 수 있습니다.
\(j_1\) \(j_2\) \(j_3\) \(i_1\) 0 1 5 3 \(i_2\) 0 4 2 4 \(i_3\) 0 0 0 4 0 4 1
이렇게 얻은 행렬은 한 가지 중요한 성질이 있습니다. 바로, 모든 원소의 값이 0보다 크거나 같다는 것이죠. (심지어 이는 원래 행렬에 음수가 있을 때에도 성립합니다.) 따라서, 0의 값을 가지는 노동자-작업 쌍만 가지고 모든 노동자를 서로 다른 작업에 할당한다면, 이는 가장 적은 비용을 발생할 것입니다.
하지만, 위 행렬에서는 그렇게 할당할 수 없습니다. 왜 그럴까요? 그 이유는 지난 포스팅에서 얻을 수 있습니다. 바로 Kőnig’s theorem입니다.
Kőnig’s theorem
모든 bipartite graph에서 maximum-size matching의 크기는 minimum-size vertex cover의 크기와 같다.
혹시 이 정리에 대해서 잘 모르신다면 이전 포스팅을 참조해 주세요.
2019/01/28 – [조합론적 최적화/Bipartite matching] – 쾨니그의 정리 (Kőnig’s Theorem)
이 정리가 왜 0의 값을 가지는 노동자-작업 쌍만으로 할당하는 방법이 없는지를 의미하는지 알아봅시다. 이는 문제를 정의할 때 말씀드린 그래프 관점에서 생각하는 것이 좋습니다. 우리는 0의 값을 가지는 쌍으로만 할당을 하고 싶으니, 해당하는 노동자-작업 간선만 가지는 bipartite graph를 고려하면 됩니다. 즉, 다음과 같은 그래프가 되겠죠.
0의 값을 갖는 쌍으로만 이루어진 그래프
Kőnig’s theorem에 의해서 이 그래프에서 maximum-size matching은 minimum-size vertex cover와 동일합니다. 근데 위 그래프에서 우리는 크기가 2인 vertex cover를 발견할 수 있습니다.
크기가 2인 vertex cover
따라서 이 그래프에서는 perfect matching이 존재하지 않으며, 자연스럽게 이 행렬에서는 0으로만 이루어진 쌍으로 할당하는 방법은 존재하지 않는다는 것도 알 수 있게 됩니다.
여기서 알고리즘을 끝낼 수는 없습니다. 분명히 optimal solution은 존재할 테니까요. 무언가 조치를 취해야 할텐데 과연 무엇을 할 수 있을까요? 앞에서 우리가 우리 마음대로 설정한 것이 있습니다. 바로 각 노동자(행)와 각 작업(열)에 일정 값을 동일하게 빼 주었습니다. 이 값들을 우리가 잘 조정하여, 행렬의 모든 원소가 0 이상이고 0으로만 이루어진 노동자-작업 쌍으로 모든 노동자를 서로 다른 작업에 할당하는 방법이 있도록 만들어준다면 알고리즘은 성공할 것입니다.
그러기 위해서는 앞에서 구한 minimum-size vertex cover를 가지고 와야 합니다. 이는 노동자 \(i_3\)와 작업 \(j_1\)이었습니다.
\(j_1\) \(j_2\) \(j_3\) \(i_1\) 0 1 5 3 \(i_2\) 0 4 2 4 \(i_3\) 0 0 0 4 0 4 1
이 행렬을 변화시키려면 \(i_3\)와 \(j_1\)으로는 덮어지지 않는 값에 무언가 작업을 해주어야 합니다. 그중 가장 작은 값은 1이므로, 이를 덮어지지 않는 행에 다음과 같이 빼주도록 합시다.
\(j_1\) \(j_2\) \(j_3\) \(i_1\) -1 0 4 3 + 1 \(i_2\) -1 3 1 4 + 1 \(i_3\) 0 0 0 4 0 4 1
이러면 분명히 새로운 0이 하나 생길 것입니다. 하지만 여기서 또 문제는 음수가 생겼다는 점입니다. 우리는 모든 행렬의 원소들을 음이 아닌 수로 만들고자 했으므로 이를 수정해 주어야 합니다. 방법은 간단합니다. 분명 음수가 된 원소는 \(j_1\)에 의해서 cover가 되었을 것입니다. 그러니 이번에는 \(j_1\)에다가 1을 더해주면 되는 것이죠.
\(j_1\) \(j_2\) \(j_3\) \(i_1\) 0 0 4 3 + 1 \(i_2\) 0 3 1 4 + 1 \(i_3\) 1 0 0 4 -1 4 1
지금까지 제가 표의 가장자리에 있는 값들을 각 행과 열에 맞게 빼주었기 때문에 \(j_1\)에 더해주는 것은 -1로 표현하였습니다. 자, 이렇게 얻은 행렬에는 우리가 원하는 할당이 존재할까요? 그렇습니다. 바로 다음과 같이 말이죠.
\(j_1\) \(j_2\) \(j_3\) \(i_1\) 0 0 4 4 \(i_2\) 0 3 1 5 \(i_3\) 1 0 0 4 -1 4 1
이는 원래 입력에서 다음과 같습니다.
\(j_1\) \(j_2\) \(j_3\) \(i_1\) 3 8 9 \(i_2\) 4 12 7 \(i_3\) 4 8 5
알고리즘
앞에서 설명한 내용을 토대로 알고리즘을 기술하면 다음과 같습니다.
Hungarian algorithm 입력: 노동자 \(I\), 작업 \(J\) (단, \(|I|=|J|=n\)), 비용 \(c : I \times J \rightarrow \mathbb{Q} \) 출력: 가장 적은 비용으로 모든 노동자를 서로 다른 작업에 할당 입력을 행은 \(I\), 열은 \(J\), 각 \(i\) 행 \(j\) 열 원소는 \(c(i,j)\)로 이루어진 행렬을 생각한다. 모든 행에 대해서, 그 행의 각 원소에 그 행에서 가장 작은 값을 뺀다. 모든 열에 대해서, 그 열의 각 원소에 그 열에서 가장 작은 값을 뺀다. 행과 열을 합해서 \(n\) 개보다 적게 뽑아 행렬의 모든 0의 값을 갖는 원소를 덮는 방법이 없을 때까지 아래를 반복한다. 그러한 방법 중 가장 크기가 작은 것을 \(I^\prime \subset I\), \(J^\prime \subset J\)라고 하자. 즉, \(|I^\prime| + |J^\prime|\)이 가장 작은 것을 가지고 온다. \(I^\prime\)과 \(J^\prime\)에 의해서 덮어지지 않는 원소 중 가장 작은 값을 \(\epsilon\)이라고 하자. \(I^\prime\)에 속하지 않은 행에 대해서, 그 행의 각 원소를 \(\epsilon\)만큼 뺀다. \(J^\prime\)에 속하는 열에 대해서, 그 열의 각 원소를 \(\epsilon\)만큼 더한다. 행렬에서 0의 값을 갖는 원소로만 모든 노동자를 서로 다른 작업에 할당한다.
이 알고리즘이 바로 Hungarian algorithm입니다. 이 알고리즘은 1955년 H. W. Kuhn에 의해서 발표가 되었습니다. 한 가지 재미있는 점은 Kuhn이 헝가리 사람이 아닌 미국 사람이라는 점입니다. 그가 이름을 그렇게 붙인 이유는 그의 알고리즘이 두 헝가리 수학자 D. Kőnig과 J. Egerváry의 성과를 토대로 만들었기 때문입니다.
이 알고리즘이 다항 시간에 동작한다는 것은 조금 나중인 1957년 J. Munkres에 의해서 밝혀졌습니다. 하지만 이번 글에서는 이 알고리즘이 다항 시간에 동작한다는 것은 따로 보이지 않으려고 합니다. 그것까지 모두 담기에는 글이 너무 길어질 것 같아서요.
분석
그럼 이제 알고리즘이 제대로 동작한다는 것을 분석해 봅시다. 사실, 직관적으로 이해하기 절에서 중요한 내용은 모두 설명하였습니다. 이를 하나씩 짚어본다는 느낌으로 설명해 보겠습니다.
첫 번째는 바로 입력으로 주어지는 행렬의 행과 열에 같은 수를 더하거나 빼도 결과에는 영향을 미치지 않는다는 것입니다. 증명은 매우 간단합니다. 만약에 우리가 어떤 행의 각 원소에 \(k\) 씩 더했다고 가정해 봅시다. 우리가 찾고자 하는 것은 모든 노동자들이 정확히 하나의 작업을 가지는 방법을 구하는 것입니다. 즉, 어떤 할당 방법을 가지고 오더라도 현재 고려하는 노동자(행)는 정확히 하나의 작업(열)을 할당받을 것이므로 비용은 정확히 \(k\)가 증가하게 됩니다. 열에 대해서 고려하거나, 어떤 값을 뺀다고 하더라도 이는 계속 유효합니다.
따라서 알고리즘의 2번과 3번 단계를 거쳐도 optimal solution은 그대로라는 사실을 알 수 있죠. 게다가 그 단계를 거치면 행렬의 모든 원소가 0보다 크거나 같은 값을 가진다는 것도 알 수 있습니다. 이 성질은 매우 중요합니다. 왜냐면, 만약 우리가 0의 값을 갖는 원소만을 가지고 노동자에게 작업을 할당시켜줄 수 있다면, 이는 곧 optimal solution이 되기 때문이죠. 모든 원소가 0보다 크거나 같다면 0의 비용을 갖는 방법이 가장 좋은 것 아니겠어요?
안타깝지만, 3번 단계가 끝난 직후에 그런 할당이 존재하지 않을 수도 있습니다. 당장에 위의 예시에서도 그러했으니 말이죠. 그렇다면 존재하는지, 아니면 하지 않는지를 어떻게 판단할 수 있을까요? 네, 바로 Kőnig’s theorem 덕분이었습니다. 0의 비용을 갖는 노동자와 작업 쌍만 간선으로 연결한 bipartite graph를 생각해 볼 때, 거기서의 matching의 최대 크기는 vertex cover의 최소 크기와 같습니다. 그래프에서의 vertex cover는 행렬에서 행과 열을 선택하는 것에 대응합니다. 즉, 행과 열을 잘 선택해서 모든 0을 덮는 방법을 찾는 것이죠. 이는 알고리즘의 4번 단계에서 기술되어 있습니다.
4번 단계의 내부에서는 각 행과 열마다 빼준 값을 잘 조절하는 작업이 진행됩니다. 일단 이 단계에서의 작업을 모두 거친 후에도 행렬의 모든 원소가 0보다 크거나 같은지를 보여야 합니다. 조절하기 전에 주어지는 행렬의 모든 원소가 0보다 크거나 같다는 것은 가정해도 괜찮습니다. (흔한 수학적 귀납법이죠.) 알고리즘의 4-iii 단계에서 \(I^\prime\)에 속하지 않은 행에 대해서 \(\epsilon\)을 빼줍니다. 그러고 난 후, 만약 \(c(i,j) < 0\)이라면, 이는 \(j \in J^\prime\)을 의미합니다. 그렇지 않다면 4-ii 단계에 어긋나게 되니까 말이죠. 즉, \(c(i,j) = -\epsilon\)이 되며, 4-iv 단계에서는 다시 0으로 바뀌게 됩니다. 우리가 마지막으로 보일 것은 이 알고리즘이 과연 끝이 날 것인가 입니다. 무슨 말이냐면, 만약에 4번 단계에서 조절해 주는 작업이 전혀 도움이 되지 않아 계속 4번 단계에 머물게 될 수도 있지 않겠냐는 것입니다. 일단 확실한 것은 \[\epsilon > 0 \tag{1} \]
이라는 점입니다. 그렇지 않으면 \(I^\prime\)이나 \(J^\prime\)에 의해서 덮혀졌어야 합니다. 이를 어떻게 잘 써먹어 볼 수 있을까요?
만약 어떤 원소가 한번 0이 된 후에는 계속 0으로 남아있다면 아무래도 증명하기 쉬워 보입니다. 하지만 안타깝게도 그렇지 않습니다. 만약 \(i \in I^\prime\), \(j \in J^\prime\)인 경우에는 4-iii에서는 \(\epsilon\)이 빠지지 않지만 4-iv에서는 \(\epsilon\)이 더해져서 0이 되지 않습니다.
위 명제는 성립하지 않지만 다행히 다른 방식으로 증명할 수는 있습니다. 바로 4번 단계를 한 번씩 거칠 때마다 행렬의 모든 원소의 합이 유의미한 크기로 줄어든다는 성질 때문입니다. 4-iii 단계에서 우리는 \(I^\prime\)에 속하지 않은 행 위의 모든 원소마다 \(\epsilon\)을 빼주었습니다. 따라서 총
\[\epsilon \cdot (n – |I^\prime|) \cdot n \]
만큼 감소하게 됩니다. 반대로 4-ii 단계에서는 \(J^\prime\)에 속한 열의 각 원소마다 같은 값을 더해주었지요. 즉,
\[\epsilon \cdot n \cdot |J^\prime| \]
만큼 증가하게 되는 것이죠. 위의 두 식을 통해서, 4번 단계가 한 번 끝날 때마다 행렬의 모든 원소의 합이 정확히
\[\epsilon \cdot n \cdot (n – |I^\prime| – |J^\prime|) \tag{2} \]
만큼 감소한다는 것을 알 수 있습니다. 4번 단계에 들어가기 위한 조건은
\[ |I^\prime| + |J^\prime| < n\] 이었으므로, 식 1과 함께 식 2가 0보다는 항상 크다는 사실을 이끌어낼 수 있습니다. 앞에서 우리는 4번 단계가 끝나도 행렬의 모든 원소가 0보다는 크거나 같다고 하였는데 이번에는 모든 원소의 합이 감소하기만 한다는 것을 알았으니 결국 최악의 경우에는 모든 원소가 0이 되는 상황이 발생할 것이고 자명하게도 언젠간 4번 단계를 탈출할 것입니다. 물론, 모든 원소가 0이 되기 전에 탈출할 수도 있겠죠. 마치며 이번 시간에는 assignment problem과 Hungarian algorithm에 대해서 알아보았습니다. 특히, 이 알고리즘이 왜 optimal solution을 알려주는지에 대해서 좀 더 깊이 다루어 보았습니다. 아쉽게도 이 알고리즘이 다항 시간에 동작한다는 것을 보이지 못했습니다. 이에 관련하여 궁금하신 분은 다음 글을 읽어보시기 바랍니다. 2020/03/18 - [조합론적 최적화/Matching] - 헝가리안 알고리즘 시간 복잡도 분석 이 글에서는 헝가리안 알고리즘이 \( O(n^4) \)에 동작한다는 것을 보입니다. 알려진 바로는 \( O(n^3) \)에도 동작시킬 수 있다고 합니다만 개인적으로는 잘 모르겠습니다. 여기서 약간 뜯어 고쳐야 가능해 보입니다. 한 가지 더 말씀드리자면, 이 알고리즘은 LP duality를 통해서도 해석할 수 있습니다. 좀 더 자세히 말해서, 이 문제를 해결하는 linear program을 "잘" 만들면 각 행과 각 열에 빼는 값을 dual variable로 하는 dual program을 만들 수 있습니다. 이 때 행렬의 원소가 0이라는 의미는 dual의 constraint가 등식이 성립한다는 의미이고, 종국적으로는 complementary slackness를 사용하여 optimality를 증명할 수 있습니다. 처음 글을 적을 당시만 해도 이 부분까지 같이 다루어 보려고 했는데, 쓰고 보니 글이 너무 길어져서 이는 다음으로 미루어야 할 것 같네요. 지금 글을 마무리 짓는 때는 다행히도 에어컨이 고쳐졌습니다. '스카이'라고 부르는 중장비를 동원해서 얽히고 설킨 전선을 뚫고 기사님께서 실외기를 모두 수리해 주셨습니다. 평소에 그다지 에어컨을 켜고 살지는 않지만, 그래도 있으니 행복하군요. 이 행복함을 좀 누려야겠습니다. 참고로 다음 강의 자료를 주로 참조하였음을 밝힙니다. https://resources.mpi-inf.mpg.de/departments/d1/teaching/ss11/OPT/lec11.pdf 감사합니다.
[알고리즘] Hungarian Maximum Matching Algorithm (Kuhn-Munkres algorithm)
반응형
Hungarian Maximum Matching Algorithm 에 대하여 알아보자
이 글을 참고하여 작성하였습니다.
Hungarian matching algorithm 개요
Kukn-Munkres algorithm 이라고도 불리는 헝가리안 알고리즘은 \(O(|V^3|)\) 의 시간복잡도를 가지고 있습니다.
헝가리안 알고리즘을 통해 Assignment problem(할당문제)인 Bipartite graphs maximum-weight matchings 문제를 빠르게 해결 할 수 있습니다.
여기서 Bipartite graph 는 adjacency matrix 로 표현할 수 있습니다.
Bipartite_graph : Disjoint and Independent 한 두개의 set을 그래프로 연결한 것
Adjacency_matrix : 두 정점(vertex)간의 연결(edge)를 나타내는 행렬(matrix), 연결정도를 weight를 통해 나타낼 수도 있음
Bipartite graph, adjacency matrix 예시
Hungarian matching algorithm 이 풀고자 하는 문제 정의
Complete bipartite graph 에서, maximum-weight matching 을 찾는 것!
Complete_bipartite_graph : 같은 집단(subset) 안의 정점(vertex)끼리의 연결이 없으며, 다른 집단(subset)의 정점(vertext)간에는 가능한 모든 연결이 있는 그래프를 말함.
Maximum_weight_matching : 연결(edge)의 weight가 존재하는 그래프에서, 연결(matching)했을때 모든 weight의 합산이 큰 연결 방식(Matching)을 찾는 것이다. 다시말해, 어떻게 연결해야 최대한의 이익이 나오느냐에 대한 문제로 생각할 수 있다. 동일한 방식으로 Minimum-weight matching (최소 비용 연결 문제)로 치환 할 수도 있다.
(최소 비용 연결 문제)로 치환 할 수도 있다. Assignment_problem : Bipartite graph 에서의 maximum-weight matching 은 할당 문제(Assignment problem)이 된다. 이는 실생활에서 다양하게 적용된다.
예를 들어 생각해 보자.
당신이 결혼 10주년 파티를 열려고 한다.
파티에는 노래와 음식이 빠질 수 없다.
또한 파티가 끝난 후 정리하는 비용도 생각해 봐야 한다.
직접 준비하려니 귀찮다 회사를 통해 돈을 써서 파티를 열어보자!!
여러 파티 회사들이 존재한다. (Company A,B,C)
각 회사들은 음악, 요리, 정리에 대해 각각 서비스를 제공하는데 가격이 다르다.
각 항목별로 어떤 회사를 선택해야 최소한의 비용으로 파티를 할 수 있을까?
이때 각 항목별로 하나의 선택만 가능하다.
출처 : brilliant.org
(가능한 모든 조합 경우의 수를 고려하는)Brute Force 방법을 사용하면 최적의 값을 찾을 수 있다.
하지만 선택지(n)가 많아지면 그에 따른 조합도 엄청나게 늘어나서 비효율적이다 → \(O(n!)\)
이를 \(O(|V^3|)\) 의 시간복잡도로 좀 더 효율적이고 빠르게 해결하는 알고리즘이 Hungarian Algorithm 이다.
The Hungarian Algorithm Using an Adjacency Matrix
우선, Adjacency Matrix 를 이용한 헝가리안 알고리즘에 대해 알아보자.
헝가리안 알고리즘에 적용되는 핵심 아이디어는 다음과 같다.
Cost matrix에서 row, column 방향으로 값을 빼거나 더한 후 최적의 연결(optimal matching)을 찾는 것은 Original Cost matrix에서 최적의 연결을 찾은 결과와 동일하다!!
즉, 쉽게 설명하면 복잡한 숫자로 이루어진 Adjacency matrix를 쉽게 변형해서 최적의 선택이 무엇인지 알기 쉽게 만든 후 선택하자는 것이다.
진행 과정을 통해 자세히 알아보자!!
Step 1. 각 행(row)에서 최솟값을 구한 후 빼준다.
example adjacency matrix
예시 행렬을 보자.
첫번째 행에서 최솟값은?? 108
두번째 행에서 최솟값은?? 135
세번째 행에서 최솟값은?? 122
각 행의 요소들에서 최솟값을 빼준다. 그결과 다음과 같이 나온다.
step 1 결과
Step 2. 각 열(column)에서 최솟값을 구한 후 빼준다.
Step 1 의 결과로 나온 예시 행렬에서
첫번째 열에서 최솟값은?? 0
두번째 열에서 최솟값은?? 0
세번째 열에서 최솟값은?? 40
각 열의 요소들에서 최솟값을 빼준다. 그결과 다음과 같이 나온다.
step 2 결과
Step 3. 0을 가장 많이 포함하는 행 또는 열을 최소 개수로 찾아준다.
검은색 영역이 step 3에서 찾은 라인이 된다.
step 3 결과
Step 4.
만약 찾은 라인의 개수 == n, Go to step 6
만약 찾은 라인의 개수 < n, Go to step 5 우리의 예시 행렬은 n = 3 이다. 그러나 step 3의 결과로 찾은 라인이 2개이다. 그래서 step 5로 이동한다. Step 5. 5-1) step 3. 에서 찾은 라인으로 covered 되지 않은 elements 중 최솟값을 찾는다. 5-2) Crossed out 이 아닌 row에 최솟값을 빼준다. (다시말해, 이미 찾은 optimal row line 에는 변화를 주지 않는다는 의미) 5-3) Crossed out column 에 최솟값을 더해줘서, 마이너스 부분을 다시 0으로 바꿔준다.(다시말해, 이미 찾은 optimal column line 에는 변화를 주지 않는다는 의미) 5-4) Go back to step 3. 5-1) 우리의 예시 → 라인으로 커버 되지 않는 영역(흰색)에서 최솟값은 2가 된다. 5-2) Crossed out 이 아닌 첫번째, 세번째 row에서 최솟값 2를 빼준다. step 5-2 결과 5-3) 마이너스 부분을 상쇄시키기 위해 Crossed out 된 첫번째 column 에 최솟값 2를 더해준다. step 5-3 결과 5-4) step 3 로 돌아가 다시 0를 최대한으로 가지는 row 또는 column을 최소 개수로 찾는다. 총 3개의 라인이 찾아졌음으로 Go to step 6를 실행한다. Step 6. 6-1) 각 row와 column에서 하나의 0만 가지는 조합을 찾는다. 6-2) Original matrix에서 찾은 최적 조합의 위치를 적용한다. 6-1) 예시에서는 총 5개의 0이 존재한다. 선택 후보 : {(1,1),(1,3),(2,2),(2,3),(3,1)} 찾은 선택 : {} 최적의 매칭(3개)을 찾아내는 문제로 바뀐다. 먼저, 세번째 행(row 3)에서는 첫번째 열(column 1)일때만 0을 갖는다. 따라서 (3,1)은 무조건 선택해야 한다. 선택 후보 : {(1,1),(1,3),(2,2),(2,3)} 찾은 선택 : {(3,1)} (3,1)이 선택 되었기 때문에 첫번째 열(column 1)에서는 다른 선택이 있을 수 없다. 따라서 (1,1)은 후보에서 제외된다. 선택 후보 : {(1,3),(2,2),(2,3)} 찾은 선택 : {(3,1)} (1,1)이 후보에서 사라졌기 때문에 첫번째 행(row 1)에서는 (1,3)만 남게 된다. 따라서 (1,3)을 선택한다. 선택 후보 : {(2,2),(2,3)} 찾은 선택 : {(1,3),(3,1)} (1,3)이 선택 되었기 때문에 세번째 열(column 3)에서는 다른 선택이 있을 수 없다. 따라서 (2,3)은 후보에서 제외된다. 선택 후보 : {(2,2)} 찾은 선택 : {(1,3),(3,1)} 최종적으로 남은 위치를 선택해 준다. 선택 후보 : {} 찾은 선택 : {(1,3),(2,2),(3,1)} step 6-1 결과 Original matrix에 위치를 대입해보면 다음과 같다. 최종적인 비용은 : 407$ 가 된다. 찾은 조합이 최선의 조합인지 확인해 보자. Brute Force 로 찾은 모든 조합의 결과는 아래와 같다. (n! 조합의 수) 헝가리안 알고리즘의 결과가 최적의 조합임을 알 수 있다!! The Hungarian Algorithm Using a Graph 또한, 헝가리안 알고리즘은 Bipartite graph의 weight를 이용하여 적용할 수 있다! 이 과정은 perfectly match된 그래프의 Feasible labeling을 찾으면 얻을 수 있다. perfect matching 이란 그래프의 모든 정점(vertex)이 각자 하나의 연결(edgd)에만 사용되고, 모든 정점(vertex)이 어떤 임의의 연결(edge)에 사용된 상태를 의미한다. - [핵심 아이디어] Graph 의 Perfect match 에 대한 Feasible labeling 의 결과는 maximum-weighted matching 과 동일한 결과를 만들어 낸다. [핵심 아이디어 증명] 두 정점(vertex)를 연결하는 연결(edge)를 e라고 하자. 모든 정점(vertex) v 는 각각 한번씩만 사용된다. 이러한 가정 하에서 다음의 inequality 성질을 가지게 된다. \(w(M') = \sum\limits_{e \in E} w(e) \leq (l(e_x)+l(e_y))=\sum\limits_{v \in V}l(v), \) \(M'\) : 정점들(vertices)의 랜덤 할당으로 만들어진 임의의 perfect maching 을 의미한다. \(l(x)\) : Numeric label to node x 위의 식이 의미하는 것은 \(\sum\limits_{v \in V}l(v), \) 가 임의의 perfect matching 비용에 대한 upper bound 가 된다는 것이다. 즉, \(M\) 을 perfect match 라고 할때, \(w(M) = \sum\limits_{e \in E} w(e)= \sum\limits_{v \in V}l(v), \) 라고 정의 할 수 있는데, \(w(M') \leq w(M) \) 로 정리할 수 있다. 결국 이때의 \(M\)이 optimal 한 선택이 된다. 작성중... 반응형
Hungarian algorithm (헝가리안 알고리즘, 헝가리안 메소드
헝가리안 알고리즘
0. 주저리주저리
배정 문제(assignment problem)를 해결할 수 있는 알고리즘.
예를 들어 보면 n명의 사람이 n개의 일을 할 때, 어떤 값의 최대(or 최소) 값을 구하는 데 사용된다. 여기서 보통 어떤 값은 비용이나, 효율 등을 적용할 수 있겠다.
정말 간단하게 문제를 접근해보면, brute force의 형태로 모두 대입해 볼 수 있다.
3명이 있다고 했을 때, 사람(1,2,3)과 일(1,2,3)을 매칭 해보자.
1번 사람이 일을 맡을 수 있는 개수 3개, 2번 사람은 2개, 3번 사람은 남는 거 1개. 따라서 3! 만큼 시간이 들고, 따라서 O(n!)의 시간 복잡도를 갖게 된다. (사용불가.)
헝가리안 알고리즘(Hungarian algorithm & Hungarian method, Kuhn & Munkres 등등 여러 가지 이름으로 불림.)은 이 문제를 O(n^3)만큼으로 줄여준다.
[2]를 참조하면 영문이지만 O(n^4)에 대한 알고리즘도 소개하고 있다.PS공부를 하다가 헝가리안 알고리즘은 다음에 공부해야지 하고 미뤄뒀다가 논문을 읽는데 나왔다. 그냥 공부할 거 미리 한다 생각하고 Google에 찾아봤지만.. 한글로 잘 정리된 블로그가 몇 없어서 나도 다음에 공부하시는 분들에게 도움이 되고자 이렇게 공부했던 걸 정리했다.
※MCMF나 Flow관련 쪽과 비슷한 것 같다. 여기에 제약이 있다면 비용(cost) 행렬이 정방 행렬(square matrix)이어야 한다는 점.
1. 개요
이론적인 부분을 볼 때는 [1], 코드를 살펴볼 때는 [2]를 위주로 참조했다.
우선, 헝가리안 알고리즘에 필요한 정의(?)라고 해야 할까.. Notation들을 정리해보자.
여러 영어로 적혀있어서 거부감이 들지만 찬찬히 살펴보자.
그래프G=(V,E)는이분 그래프이다.(이분 그래프의 성질들은 다 안다고 가정하겠습니다.) 그리고 간선 E는 두 개의 그룹 간에 모두 연결되어있다.(한쪽 그룹에 n개의 정점이 있다면, 총간선의 개수는 n*n입니다.) w(x,y)는 x, y사이의 weight이다.
Free 정점(vertex)은 Matching 할 때 M에 포함되지 않는다면 free 한 정점이라고 할 수 있다.
Alternating Path는
Fig1. Alternating Path[1]
Fig1의 왼쪽 그림을 보면, 파란색이 간선 E이고, 빨간색이 Matching M이다. 이런 M에서, Alternating path는 경로인데, M과 E-M사이의 다른 길(?) 대안(?)인 길을 나타낸다. Y에서 X로 갈때는 Matching에 사용되지 않은 간선, X에서 Y로 갈때는 Matching에 사용한 간선을 타고 간다.
예를 들면, Y1→X2→Y2→X4→Y4→X5→Y3→X3은 alternating이다. 그리고 시작점과 끝점이 free 하기 때문에 augment (augmenting path) 하다고 할 수 있다. 왜냐하면 이 증가 경로를 통해서 Matching의 size를 증가시킬 수 있기 때문이다.
Fig1에서 저 augmenting path를 뒤집으면 3에서 4로 크기가 증가한다.
Alternating tree는 모든 경로가 alternating path인 어떤 free 한 정점 v을 root로 한 tree를 의미한다.
Fig2. Alternating Trees[1]
여기서 오른쪽 그림의 파란선이 Alternating Path이고, Y5의 모든 간선이 Alternating Path이다. 따라서 Alternating tree가 된다.
Feasible labeling & Equality Graphs
여기서 Labeling은 약간 뭐랄까.. 해당 Vertex가 cover 할 수 있는 flow의 크기라고 해야 할까?! 약간 그런 느낌으로 와 닿았다.
PDF에 나와있는 수식을 간단히 정리하면, l(x) 같으면 정점 x를 실수인 Labeling값으로 바꿔주는 함수라고 한다. (구현할 때는 그냥 배열 사용.) 그래서, Feasible labeling은 모든 X, Y에 있는 정점 x, y에 대하여, l(x) + l(y) >= w(x, y)를 만족한다고 한다.
Equality Graphs는 이제 어떤 라벨 값 l에 대해서, G=(V, E_l)로 이루어진 그래프인데, 간선 E_l이 어떤 간선이냐면은, l(x) + l(y) = w(x, y)이다.즉, X의 어떤 점 x, Y의 어떤 점 y의 두 label값을 합친 것과 비용(cost, weight)이 같은 간선만 연결한 그래프라고 할 수 있다.
여기서, 이 feasible labeling과 Equality graph를 통해서 Matching M이 최대 비용 매칭이라는 것을 증명할 수 있다.
e는 간선 집합 E의 어떤 하나의 간선이라고 하고, e = (e_x, e_y)로 나타낸다.
M`을 어떤 G에서의 Perfect Matching이라고 하자. (여기서 E_l을 만족할 필요는 없다.)
모든 정점들은 Matching M에서 한 번만 covered 되기 때문에, 다음과 같이 나타낼 수 있다.
다음과 같이 나타낼 수 있고
시그마 안의 라벨의 합들은 정의에 의해서
다시 이렇게 나타낼 수 있다. 다시 말하면
이렇게 나타낼 수 있고,
는 어떤 Perfect Matching에서의 상한 값(upper bound)이라고 할 수 있다.
이번에는 Equality graph를 적용시켜보자 그러면,
이렇게 나타낼 수 있다. 따라서 최대 비용 매칭이라고 할 수 있다.
Alternating Path/Tree를 왜? 정의하는지 왜? 필요하는지 확! 와 닿지 않을 것 같다. Notation정리는 그렇구나.. 뭐.. label을 사용하면 최댓값을 구할 수 있구나 하고 알고리즘으로 넘어가 보자.
알고리즘의 Step으로
0. Labeling l과 matching M 초기화하기.
1. M이 만약에 perfect 하다면 stop. 그렇지 않으면 X 중에서 아무 free 한 정점 u를 선택하고, S={u}, T=Ø로 설정.
2. S의 이웃이 T와 같다면, label을 업데이트하기.
3. S의 이웃이 T와 같지 않다면 이웃-T에 속하는 아무 y를 잡아서,
A. y가 free 하면 u-y는 augmenting path. M을 증가시켜주고 1단계로.
B. y가 matched 되어 있다면, u를 z로 하고 S에 z를 삽입, T에 y를 삽입하고 2단계로.
여기서 이웃은 Equality graph의 간선만 고려하고 N(S)라면 S내의 모든 x점이 뻗어 나갈 수 있는 y점이라고 할 수 있다. S는(x 정점 중에 tree에 속한 점들) T는 (y정점 중에 tree에 속한 점들이라고 할 수 있다.)
약간 알고리즘의 흐름을 살펴보면 Labeling을 엄격하게 잡아서 적은 간선들만 보고, 점차 점차 labeling을 낮춰가면서 볼 수 있는 간선을 늘리면서 매 단계 최적임을 유지한 채 최대 매칭을 찾아나가는 것 같다. 최대 매칭(매칭의 개수 = 정점 개수)이면 바로 함수를 종료한다. 매단 계 최적의 값을 갖고 가기 때문에 가능하다.
2. Code
여기서는 code를 볼 건데 코드는 [2]를 참고했다.
Step별로 살펴보자.
일단 사용할 변수들이다.
#define N 3 //단순 배열 크기 #define INF 987654321 //단순 INF값. int cost[N][N]; int n, max_match; //n : 사람or일의 개수 max_match 현재 matching의 크기. int lx[N], ly[N]; //라벨의 값. int xy[N], yx[N]; //xy[i]는 x의 i번째 정점과 연결된 정점 y의 번호. yx[i]는 반대. bool S[N], T[N]; //집합 S, T int slack[N], slackx[N]; //시간복잡도를 줄이기위한 slack int pre[N]; //마지막 tree를 쫓아갈 때 사용.
Step0의 초기화 과정이다.
void init_labels() { memset(lx, 0, sizeof(lx)); //init label memset(ly, 0, sizeof(ly)); //init label for (int x = 0; x < n; ++x) for (int y = 0; y < n; ++y) lx[x] = max(lx[x], cost[x][y]); //최댓값으로 설정 return; } 특별한 점은 x기준으로 연결된 간선 중 cost가 가장 높은 간선을 기준으로 한다는 점이다. Step 1을 보면, 0. M이 만약에 perfect 하다면 stop. 그렇지 않으면 X 중에서 아무 free 한 정점 u를 선택하고, S={u}, T=Ø로 설정. 라고 되어있는데, 이를 코드로 구현해보면 if (max_match == n) return; //finished. int x, y, root; queue
q; memset(S, false, sizeof(S)); //init memset(T, false, sizeof(T)); //init memset(pre, -1, sizeof(pre)); //init for (x = 0; x < n; ++x) if (xy[x] == -1) //집합 X에서 어떤 free한 정점x 선택 { q.push(x); root = x; //x는 alternate tree의 root가 됨. pre[x] = -2; //root의 이전 노드는 -2로 설정. S[x] = true; //alternate tree에 속하니 집합 S에 삽입. break; } for (y = 0; y < n; ++y) { //집합 S가 변경되었으니 slack값 변경 slack[y] = lx[root] + ly[y] - cost[root][y]; //slack값 변경 slackx[y] = root; //어떤 x로 계산된 slack인지 저장 } 이렇게 된다. 그리고 이제 alternate path를 찾아볼 것이다. while (true) { while (!q.empty()) { //bfs loop x = q.front(); q.pop(); for (y = 0; y < n; ++y) if (cost[x][y] == lx[x] + ly[y] && !T[y]) //find edge E_l { if (yx[y] == -1) break; //augment path 발견 T[y] = true; q.push(yx[y]); add_to_tree(yx[y], x); //tree연결 } if (y < n) break; //augment path 발견 } if (y < n) break; //augment path 발견 여기서 BFS탐색을 하는데 equality graph의 정의에 맞게 if문이 있다. S와 연결된 점 y가 free 하면, (yx[y] == -1) augment path이다. 따라서 그에 맞게 처리해주도록 하고, 그렇지 않은 부분을 이 코드에서 다루고 있는데, y가 free하지 않다면 T에 넣어주고, alternating tree에 연결해준다. void add_to_tree(int x, int prevx) { //(prevx, xy[x]) (xy[x], x)가 연결됨 S[x] = true; pre[x] = prevx; for (int y = 0; y < n; ++y) //S에 새로운 정점이 들어왔으므로 slack 업데이트 if (lx[x] + ly[y] - cost[x][y] < slack[y]) { slack[y] = lx[x] + ly[y] - cost[x][y]; slackx[y] = x; } return; } 위위 코드에서 y가 free하지 않았기 때문에 add_to_tree(yx[y],x)가 실행되었다. 그래서 그 not free 한 y와 연결된 x를 S에 넣어준다. S가 갱신이 되었기 때문에 slack도 갱신하는데, 처음 갱신한 것과 다르게 S에 들어있는 x들 중 최솟값을 취할 수 있도록 if문을 추가했다. Step 2,3의 부분이 남아있다. 이제 남은 Augment() 함수를 살펴보면 //step2 update_labels(); // N_l(S) = T , augment path가 발견되지 않음. 라벨 값 완화 q = queue (); //clear //step 3 for (y = 0; y < n; ++y) if (!T[y] && slack[y] == 0) { //slack[y] == 0 는 새로운 E_l if (yx[y] == -1) //free y발견 { x = slackx[y]; //x는 y와 연결될 것. break; } else { T[y] = true; if (!S[yx[y]]) //alternating tree를 확장 { q.push(yx[y]); add_to_tree(yx[y], slackx[y]); } } } if (y < n) break; //augment path 발견. } if (y < n) //augmeht path 발견 { max_match++; //x, y가 매칭됨. //root를 만날 때까지 정보 갱신 for (int cx = x, cy = y, ty; cx != -2; cx = pre[cx], cy = ty) { ty = xy[cx]; yx[cy] = cx; xy[cx] = cy; } augment(); //augment경로를 1개 찾음. } return; } 이렇게 쓸 수 있다. 처음 free 한 x를 root로 지정하고, 그 x에서 갈 수 있는 y점들을 찾은 뒤, 그 y가 free 하면 연결, 아니면 처음 지정한 x를 root로 하는 alternating tree에 연결하는 방식으로 점차 넓혀나간다. 그리고 bfs탐색을 다 끝냈는데 새로운 정점이 안 보인다면 라벨 재조정을 통해 간선을 늘려주는 방식. 위의 증명 덕분에 매 반복에서의 선택은 항상 최적(여기서는 최대)의 값을 선택하면서 진행한다. 따라서 n개를 찾자마자 탈출해도 그 값이 최적이라는 것이 보장된다. 간선들을 주고받는 것을 보면 약간 dfs를 이용한 이분 매칭의 진행 양상과 비슷한 것 같다. int hungarian() { int ret = 0; max_match = 0; memset(xy, -1, sizeof(xy)); memset(yx, -1, sizeof(yx)); init_labels(); //step 0; augment(); //step1-3; for (int x = 0; x < n; ++x) ret += cost[x][xy[x]]; return ret; } Hungarian 함수는 다음과 같고, main함수에서 우리가 만들어 주어야 할 점은 비용 행렬 cost[x][y], n이다. code https://github.com/Kibbomi/Algorithm/blob/master/Hungarian.cpp Reference [1] http://www.cse.ust.hk/~golin/COMP572/Notes/Matching.pdf [2]https://www.topcoder.com/community/competitive-programming/tutorials/assignment-problem-and-hungarian-algorithm/
Hungarian Algorithm
인사
안녕하세요 오랜만의 글입니다. 🙂
최근에 공부하고 있는 분야는 모바일 로봇의 임무 계획(mission planning)인데, 간단히 말하자면 로봇이 수행할 다수의 작업(task/mission)들이 존재 할 때 이를 어떤 로봇이, 어떤 순서로 수행할 지에 대한 분야입니다. 아직 로보틱스쪽에서는 control, path planning, motion planning처럼 많이 정립되고 교육되고있는 분야는 아니라서 task allocation/assignment/scheduling/planning, mission planning 등등 다양한 용어로 언급되고 있습니다.
저는 임무 계획을 다수의 로봇을 운용하는 경우 임무계획=작업할당 + 작업계획으로 나누어 생각하고 있고, 이를 계층적으로 나눠서 풀거나 한번에 푸는 방법들이 많이 연구되고 있는 것 같습니다.
오늘 포스팅하게 될 내용은 작업할당의 가장 기본적인 알고리즘으로 알려진 Hungarian Algorithm에 대한 내용인데, 로봇의 작업할당을 한다면 어떻게 쓰이는지, 그리고 매우 간단한 코드를 소개하면서 마치도록 하겠습니다. 아무래도 짬을 내서 매우 조금씩 쓰던 포스트라, 두서가 없다는 점은 미리 죄송합니다.
아, 매니퓰레이터의 경우도 많이 다뤄지는 주제이긴 한데,, 이부분은 다음에 다뤄보도록 하겠습니다. 그래서 아래부터 언급되는 로봇은 드론, 모바일 로봇 같은 무인이동체로 생각해주시면 되겠습니다.
작업 할당
로보틱스에서 언제 작업 할당이 요구될까요?
최근 단일 로봇 운용의 안정도가 상승하고, 이에 따라 많은 산업체/기관들이 로봇 개발의 상용화에 뛰어듬에 따라 여러 분야에서 로봇의 도입이 진행되고 있습니다.
로보티즈사의 일개미들
특히 배달, 물류, 서빙, 탐사, 교통 같은 분야에서는 운용하기위한 로봇의 대수가 많아질 수 밖에 없고, 이에 따라 각 로봇들에게 어떤 작업을 할당 시킬 것인지를 잘 결정하는 방법들이 많이 요구되고 있습니다.
뭔가 대단한 것을 할 것처럼 서론을 띄웠지만, 오늘 얘기할 주제는 할당 문제(assignment problem)에서 가장 기본적으로 등장하는 헝가리안 알고리즘(Hungarian Algorithm)의 활용에 대해 얘기해 보고자 합니다. 사실 이런 할당문제는 로보틱스가 아니라도 경영과학이나 산업 공학같은 분야에서도 많이 사용되고 있습니다.
기본적인 할당문제는 내가 운용할 로봇들이 주어진 작업들을 수행할 때 필요한 운용 비용(사용 연료, 전체 작업 종료 시간)을 최소화하거나, 얻을 수 있는 이득을 최대화하는 것을 목표로 합니다.
간단한 예시로 아래 표와 같이 로봇이 수행할 작업에 대한 비용(cost)를 표현 할 수 있습니다.
로봇\작업 작업 1 작업 2 작업 3 로봇 1 3 2 1 로봇 2 3 4 6 로봇 3 7 3 5
이때 각 로봇이 서로 겹치지 않게 작업 작업을 하나씩 선택하여, 전체 드는 비용을 최소화하는 조합을 찾아야 합니다.
가장 간단한 방법을 생각해보면, 모든 할당에 대한 조합을 다 구해놓고 그 중 최소 비용을 지니는 조합을 선택하면 될 것 입니다. 보통 brute force, exhaustive search라고 부릅니다. 이러면 아주 단순하고도 확실하게, 가장 최적의 조합을 선택할 수 있습니다. 그러나 이렇게 되면 주어진 로봇과 작업의 갯수가 $N$ 이라 할 때 탐색 복잡도는 $O(N!)$ 이라는 처참한 결과가 나올 것 입니다.
여기서 좀 더 빠르게 최적으로 할당 문제를 해결 할 수 있게 해주는 알고리즘이 헝가리안 알고리즘 입니다.
기본적인 헝가리안 알고리즘에 대한 설명은 아래 링크의 블로그에서 정말 자세하게 설명해주고 있으니 참고하시면 됩니다.
모바일 로봇으로 할 수 있는 다양한 작업(visiting, area coverage, loitering, delivery, tracking 등)들이 존재하지만 오늘은 가장 간단한 지역 방문(visiting)에 대한 할당을 다뤄보도록 하겠습니다. 방문 외의 작업들에 대한 cost를 어떻게 결정할 건지 등의 연구도 정말 재미있는 주제입니다.
Visiting 할당 문제의 경우 로봇과 작업 지역사이의 거리를 주로 비용으로 두고 문제를 풀게 됩니다. 즉 할당 알고리즘을 실행하는 시점에서 로봇들의 위치와 작업들의 위치가 주어졌을 때 전체 로봇의 이동거리가 최소가 되도록 할당문제를 풀고자하는 것이 오늘 예제의 목표입니다.
각 로봇이 어떤 작업에 할당돼야 좋을지 상상해 봅시다.
헝가리안 알고리즘의 경우 고맙게도 scipy 라이브러리에서 지원을 해 주고 있습니다.
핵심 코드
np.random.seed(1) # Number of robots and tasks n_robot = 4 n_task = 4 # randomly generates the pose of robots and tasks robot_pos = np.random.uniform(-1,1,size=(n_robot, 2)) task_pos = np.random.uniform(-1,1,size=(n_task, 2)) # calculates a distance matrix as the cost cost = distance_matrix(robot_pos, task_pos) # Hungarian algorithm row_ind, col_ind = linear_sum_assignment(cost) # cacluates the total cost of the result total_cost = cost[row_ind, col_ind].sum()
결과
실행 결과
위 그림을 보면 로봇들이 어떤 작업들에 할당 되었는 지 확인이 가능합니다.
여기서 드는 의문은 그냥 각 로봇에 가장 가까운 지역에 할당시켜주면 되는 거 아닌가? 라는 물음이 발생하게 되는데요, 이런 방식을 greedy algorithm이라고 합니다. 이 방식도 문제의 복잡도가 위 상황처럼 낮은경우 나름 괜찮게 작동하는 것을 볼 수 있습니다. 그러나 문제의 스케일이 커지면 커질 수록 뒤에서 나중에 할당될 로봇의 결과를 생각하지 않기 때문에 비효율적인 할당들이 일어나게 됩니다.
Greedy Algorithm과의 비교 (robot:10, task:10)
Hungarian: 6.84 / Greedy: 7.18
위 그림에 보이시는 것처럼 Hungarian Algorithm은 항상 최적을 만족하기 때문에 Greedy 알고리즘보다 더 좋은 성능을 보이는 것을 확인할 수 있습니다.
전체 코드
import numpy as np from scipy.spatial import distance_matrix from scipy.optimize import linear_sum_assignment from matplotlib import pyplot as plt np.random.seed(15) # Number of robots and tasks n_robot = 10 n_task = 10 # randomly generates the pose of robots and tasks robot_pos = np.random.uniform(-1,1,size=(n_robot, 3)) task_pos = np.random.uniform(-1,1,size=(n_task, 3)) # Create figure fig, ax = plt.subplots(1,3,figsize=(15,5)) # plot initial mission environment ax[0].plot(robot_pos[:,0], robot_pos[:,1], ‘k^’,label=”robot”) ax[0].plot(task_pos[:,0], task_pos[:,1], ‘bo’, label=”task”) ax[0].set_title(“Mission Environment”) ax[0].set(xlim=(-1.2,1.2),ylim=(-1.2,1.2)) ax[0].set_aspect(“equal”) ax[0].legend() # calculates a distance matrix as the cost cost = distance_matrix(robot_pos, task_pos) # Hungarian algorithm row_ind, col_ind = linear_sum_assignment(cost) # cacluates the total cost of the result hungarian_total_cost = cost[row_ind, col_ind].sum() print(“Hungarian Result: {}”.format(col_ind)) print(“Hungarian Cost: {:.3}”.format(hungarian_total_cost)) # plot Hungarian assignments for i in range(len(col_ind)): ax[1].plot([robot_pos[row_ind,0],task_pos[col_ind,0]],[robot_pos[row_ind,1],task_pos[col_ind,1]],’r–‘) ax[1].plot(robot_pos[:,0], robot_pos[:,1], ‘k^’,label=”robot”) ax[1].plot(task_pos[:,0], task_pos[:,1], ‘bo’, label=”task”) ax[1].set_title(“Hungarian Results: {:.3}”.format(hungarian_total_cost)) ax[1].set(xlim=(-1.2,1.2),ylim=(-1.2,1.2)) ax[1].set_aspect(“equal”) ax[1].legend() # greedy algorithm greedy_task = [] greedy_total_cost = 0 for robot in range(n_robot): cand_task = list(np.argsort(cost[robot, :])) while True: task = cand_task[0] if task not in greedy_task: greedy_task.append(task) greedy_total_cost += cost[robot, task] break else: cand_task.pop(0) greedy_task = np.array(greedy_task) print(“Greedy Result: {}”.format(greedy_task)) print(“Greedy Cost: {:.3}”.format(greedy_total_cost)) # plot Greedy assignments for i in range(len(col_ind)): ax[2].plot([robot_pos[row_ind,0],task_pos[greedy_task,0]],[robot_pos[row_ind,1],task_pos[greedy_task,1]],’g–‘) ax[2].plot(robot_pos[:,0], robot_pos[:,1], ‘k^’,label=”robot”) ax[2].plot(task_pos[:,0], task_pos[:,1], ‘bo’, label=”task”) ax[2].set_title(“Greedy Results: {:.3}”.format(greedy_total_cost)) ax[2].set(xlim=(-1.2,1.2),ylim=(-1.2,1.2)) ax[2].set_aspect(“equal”) ax[2].legend() fig.tight_layout() plt.show()
마무리
오늘은 헝가리안 알고리즘의 매우 간단한 활용에 대한 이야기를 했습니다. 사실 로봇이라는 용어를 굳이 끌고 오지 않아도 설명이 가능한 내용이긴 하지만, 생각 보다 많은 멀티로봇 연구들에서 아직 사용되고 있고 로봇을 대입하면 대강 어떤 cost를 사용하는지도 언급을 하고 싶었습니다.
또한, 오늘 언급을 안한 내용들이 있는데, 로봇이나 작업이 더 많은 상황은 어떻게 할 것인지, 단순한 지역방문이 아니라 추적이나 패트롤같은 지속적인 작업들에 대해서는 어떻게 처리할지에 대한 상황들도 생각해볼 거리입니다. 그리고 현실적인 운용을 생각했을 때 단발적인 방문 문제라 하더라도 할당된 지역의 방문을 먼저 수행한 로봇이 아직 처리되지 않는 지역을 추가적으로 방문하는 planning 관점에서의 생각도 해 볼 수 있고, 여러모로 많은 생각을 하게하는 주제들이 무궁무진하게 펼쳐져 있습니다.
앞으로도 이런 줄기의 내용들에 대해서도 시간 나면 풀어보도록 하겠습니다.
Algorithm & Coding
먼저, 할당 문제(Assignment Problem)에 대해서 살펴보자.
참고> http://www.hungarianalgorithm.com
할당(배정) 문제는 기계를 태스크에, 일꾼을 작업에, 축구 선수를 포지션에 할당하는 것이다.
목표는 효율성이 최대가 되거나 총비용이 최소가 되는 최적의 할당 방법을 결정하는 것이다.
아래 그림과 같이 4명의 일꾼과 4개의 작업이 있다.
J1 J2 J3 J4
W1 82 83 69 92
W2 77 37 49 92
W3 11 69 5 86
W4 8 9 98 23
4 명의 일꾼들이 4 개의 작업을 처리하는 시간들을 나타내는 표이다.
한명의 일꾼이 하나의 작업을 수행 할 수 있다.
모든 작업을 수행하기 위해 필요한 총 시간을 최소로 하고 싶다
최적해는 (일꾼, 작업)
(1, 3), (2, 2) , (3, 1), (4,4)
총 시간은 140이다.
헝가리안 알고리즘으로 이 최적 할당을 찾을 수 있다.
헝가리안 알고리즘의 실행 과정
n x n 배열(행렬)
각 행에 대해서, 최소값을 찾아서 행의 모두 요소의 값에서 뺀다. 각 열에 대해서, 최소값을 찾아서 열의 모는 요소의 값에서 뺀다. 최소 개수의 직선(line)으로 0을 지운다(커버한다.)
만약, n개의 직선이 필요하다면, 최적의 할당은 0값들 중에 존재한다. 알고리즘을 종료한다.
만약, n보다 적은 수라면, 4단계를 수행한다.
4. 추가적인 0을 생성한다. 직선으로 커버되지 않은 최소값(k)을 찾아서 커버되지 않은 모든 값들에서 최소값(k)을 뺀다. 두 번 커버된 모든 값들에 최소값(k)을 더한다.
키워드에 대한 정보 헝가리 안 알고리즘
다음은 Bing에서 헝가리 안 알고리즘 주제에 대한 검색 결과입니다. 필요한 경우 더 읽을 수 있습니다.
이 기사는 인터넷의 다양한 출처에서 편집되었습니다. 이 기사가 유용했기를 바랍니다. 이 기사가 유용하다고 생각되면 공유하십시오. 매우 감사합니다!
사람들이 주제에 대해 자주 검색하는 키워드 [경영과학|OR] 할당 문제: 헝가리법, 헝가리안법 (한국어) Assignment problem: How to (Kuhn’s) Hungarian Algorithm
- 동영상
- 공유
- 카메라폰
- 동영상폰
- 무료
- 올리기
YouTube에서 헝가리 안 알고리즘 주제의 다른 동영상 보기
주제에 대한 기사를 시청해 주셔서 감사합니다 [경영과학|OR] 할당 문제: 헝가리법, 헝가리안법 (한국어) Assignment problem: How to (Kuhn’s) Hungarian Algorithm | 헝가리 안 알고리즘, 이 기사가 유용하다고 생각되면 공유하십시오, 매우 감사합니다.