아이디어는 다음과 같다. 정점의 집합 , 가 있고, 이분그래프 의 어떤 매칭 이 있다고 하자. (매칭은 간선의 집합이라고 보고, 자명히 다.) 이때 에 속하면서 아직 매칭되지 않은 정점에서 출발해, 에 속한 간선과 에 속한 간선을 번갈아 가면서 에 속하면서 아직 매칭되지 않은 정점으로 도착하는 단순 경로를 확장 경로(augmenting path)라고 한다. 이때 어떤 매칭인지 언급해야 할 필요가 있을 경우 M-확장 경로(augmenting path with respect to M)라고 한다. 확장 경로를 찾으면 매칭의 크기를 1 늘릴 수 있다.
위의 그림을 보자. 첫 번째 그림에서 점선은 매칭에 속하지 않은 간선, 실선은 매칭에 속한 간선이다. 이때 두 번째 그림에서 주황색으로 표시한 확장 경로에서 매칭에 속한 간선은 매칭에서 제외하고, 매칭에 속하지 않은 간선은 매칭에 포함시킨다. 확장 경로에 속한 간선의 상태를 뒤집는다고 생각하면 된다. 이러면 세 번째 그림처럼 매칭의 크기가 1 늘어나게 된다. 이를 좀 더 엄밀하게 증명해보자. 확장 경로를 라고 하면 확장 경로에 속한 간선의 상태를 뒤집는 과정은 대신 를 매칭으로 택한다고 볼 수 있다. (은 대칭차집합 기호로, 이다.)
가 보다 크기가 1 큰 매칭임을 증명하자.
먼저 가 매칭임을 증명하자. 가 매칭이 아니라면 에 속한 두 간선이 공유하는 점인 가 있을 것이다. 이 간선들을 , 라고 하면 와 은 서로소이므로 각 간선은 두 집합 중 한 곳에만 속해야 한다. 이라면 이 매칭이 아니므로 모순이고, 이라면 매칭에 속하지 않은 간선을 두 번 지나서 가 확장 경로가 아니므로 모순이다. 따라서 일반성을 잃지 않고 , 이다. 그런데 에 의해 는 매칭된 정점이므로 확장 경로의 양 끝점이 될 수 없다. 따라서 로 들어온/나가는 확장경로는 에서 멈추지 않고 에 속한 유일한 간선인(은 매칭이므로 해당되는 간선은 유일하다) 으로 나가야/들어와야 한다. 이때 이므로 모순이 된다. 따라서 귀류법에 의해 은 매칭이다.
이제 가 보다 크기가 1 크다는 사실을 증명하자. 의 양 끝점은 매칭되지 않은 정점이므로, 의 양 끝 간선은 에 속하지 않는다. 는 에 속한 간선과 에 속한 간선을 번갈아 가므로, 는 에 속한 개의 간선과 에 속한 개의 간선을 지나게 된다. 따라서 이다. ■
따라서 확장 경로를 찾으면 매칭의 크기를 1 늘릴 수 있다. 이제 확장 경로가 없을 때 최대 매칭이 되는 것을 증명하자.
어떤 그래프 의 매칭 , 가 을 만족한다면, 은 적어도 개의 정점이 겹치지 않는(vertex disjoint) M-확장 경로를 포함한다.
그래프 를 생각하자. 이 그래프의 모든 정점은 차수가 2 이하다. (그래프의 모든 간선은 이나 에 속한다. 만약 차수가 3 이상인 정점이 존재한다면 비둘기집의 원리에 의해 이나 중 어느 한 쪽에 간선이 2개 이상 있어야 하므로 모순이다.) 따라서 는 단순 경로와 사이클로 이루어져 있다. 이때 매칭은 한 정점을 공유하는 2개 이상의 간선을 가질 수 없으므로 의 모든 경로는 에 속한 간선과 에 속한 간선을 번갈아 가야 한다.
이분그래프의 사이클의 길이는 짝수이므로 과 에서 지나간 간선 수가 같다. 따라서 과 에서 지나간 간선 수가 다르려면 단순 경로여야 하며, 에 속한 간선이 1개 더 많거나 에 속한 간선이 1개 더 많아야 한다. 이때 에 속한 간선이 1개 더 많으려면 양 끝 간선이 에 속해야 하므로 이 단순 경로는 M-확장 경로가 된다. 따라서 그래프 는 적어도 개의 정점이 겹치지 않는(vertex disjoint) M-확장 경로를 포함한다.■
최대 매칭이 아닌 경우 확장 경로가 존재하므로, 확장 경로가 없을 경우 최대 매칭이 된다. 따라서 더 이상 확장 경로를 찾을 수 없을 때까지 확장 경로를 찾아서 확장 경로에 속한 간선의 상태를 뒤집으면 최대 매칭을 구할 수 있다. Hopcroft–Karp 알고리즘은 여기서 길이가 제일 짧은 확장 경로들부터 찾는 알고리즘이다.
는 최단 M-확장 경로들을 정점이 안 겹치게 최대한 고른 집합
이면 이 최대 매칭이므로 알고리즘을 종료한다.
이면 후 과정 2로 돌아간다.
추후에 보겠지만, 최단 M-확장 경로들을 찾고 간선의 상태를 뒤집는 과정을 BFS 한 번, DFS 한 번으로 구할 수 있어서 의 시간이 걸린다. 또한 알고리즘의 반복문은(과정 2~4) 최대 번 돌아간다. 따라서 Hopcroft-Karp 알고리즘의 시간복잡도는 다.
반복문이 최대 번 돌아가는 것을 증명하기 위해 몇 가지를 정의하고 들어가자. 매칭 이 있을 때 를 최단 M-확장 경로들을 정점이 안 겹치게 최대한 고른 집합으로 정의하고 최단 M-확장 경로의 길이를 이라고 한다. 또한, 매칭 에서 를 임의의 최단 M'-확장 경로라고 하자.
가 와 정점이 안 겹친다면 의 길이는 보다 길다.
간선이 겹치면 정점이 겹치므로 정점이 겹치지 않으면 간선이 겹치지 않는다. 따라서 에 속한 임의의 간선 에 대해 이라면 이고, 이라면 이므로, 는 M-확장 경로다. 이라면 최단 M-확장 경로의 길이가 이라는 조건과 모순이고, 이라면 들이 최대한 고른 집합이라는 조건과 모순이므로 이다.■
가 와 정점이 겹친다면 간선도 겹친다.
겹친 정점을 라고 하고 중 를 포함하는 M-확장 경로를 라고 하자. 이때 가 에서 매칭된 정점이라면 의 양 끝점이 될 수 없으므로 에 속하지 않은 간선으로 들어오면/나가면 는 에서 멈추지 않고 에 속한 간선으로 나가야/들어와야 한다. 또한 가 에서 매칭되지 않은 정점이라면 에 속한 간선이 없으므로 에 속하지 않은 간선으로 나가야/들어와야 한다. 따라서 는 에 속하지 않고 끝점이 인 간선이 존재하며, 이 간선은 의 정의에 의해 에서 가 매칭된 간선이 된다.
이제 는 에서 매칭된 정점이므로 의 양 끝점이 될 수 없고, 에 속하지 않은 간선으로 들어오면/나가면 는 에서 멈추지 않고 에 속한 유일한 간선으로 나가야/들어와야 한다. 이 간선은 와 에 동시에 속해있으므로, 와 는 겹치는 간선이 존재한다.■
가 와 정점이 겹친다면 의 길이는 보다 길다.
라고 하자. 는 매칭이고, 에서 확장 경로 개를 찾은 후의 매칭이므로, 이다. 따라서 위에서 증명한대로는 적어도 개의 정점이 겹치지 않는 M-확장 경로를 포함한다. 최단 M-확장 경로의 길이는 이고, 정점이 겹치지 않으면 간선이 겹치지 않으므로, 이다.
대칭차는 교환 법칙과 결합 법칙이 성립하므로 의 정의에 의해 다. 정점이 겹치지 않으면 간선이 겹치지 않으므로 이다. 따라서 이다. 이때 위에서 증명한대로와 에 모두 포함된 간선이 있으므로 이다.■
Hopcroft-Karp 알고리즘의 반복문은 최대 번 돌아간다.
의 길이는 보다 길으므로, 반복문을 돌 때마다 최단 확장 경로의 길이는 길어진다. 따라서 만약 반복문을 번 돌았다면, 최단 확장 경로의 길이가 이상이므로 정점이 겹치지 않는 확장 경로는 최대 개 이하다. 를 임의의 최대 매칭이라고 하면 위에서 증명한 정리의 대우로 인해 다. 확장 경로를 개 찾아야 하고, 반복문을 돌 때마다 확장 경로를 1개 이상 찾으므로 반복문은 앞으로 최대 번 돌 수 있다. 따라서 반복문은 최대 번 돌아간다.■
이제 남은 것은 Hopcroft-Karp 알고리즘을 실제로 코드로 구현하는 것이다. C++로 구현한 코드가 아래에 있다.
classHopcroft_Karp{private:int n;
vector<int> X, Y, dist;
vector<vector<int>> graph;// n = X그룹의 원소 수, 원소들은 1부터 시작해야 함 (1base)// X, Y = X, Y그룹의 i번 원소와 매칭된 상대편 원소 번호// dist = 매칭되지 않은 X그룹의 원소로부터 X그룹의 i번 원소까지의 거리// graph = X그룹의 i번 원소와 연결된 Y그룹의 원소 번호들// 확장 경로가 존재하는지를 리턴함boolbfs(){
queue<int> q;
dist[0]= INT_MAX;for(int i =1; i <= n; i++){if(!X[i]){
dist[i]=0;
q.push(i);}else dist[i]= INT_MAX;}while(!q.empty()){int x = q.front();
q.pop();for(int y : graph[x]){if(dist[Y[y]]== INT_MAX){
dist[Y[y]]= dist[x]+1;
q.push(Y[y]);}}}return dist[0]!= INT_MAX;}// x를 지나가는 확장 경로가 있는지를 리턴함booldfs(int x){for(auto y : graph[x]){if(dist[Y[y]]== dist[x]+1&&(!Y[y]||dfs(Y[y]))){
X[x]= y; Y[y]= x;returntrue;}}
dist[x]= INT_MAX;returnfalse;}public:Hopcroft_Karp(int x_count,int y_count):n(x_count),X(x_count +1),Y(y_count +1),dist(x_count +1),graph(x_count +1){}voidaddEdge(int x_node,int y_node){// 1 <= x_node <= x_count, 1 <= y_node <= y_count를 만족해야 함
graph[x_node].push_back(y_node);}// 최대 매칭의 크기를 리턴함intsolve(){int match =0;while(bfs()){for(int i =1; i <= n; i++){if(!X[i]&&dfs(i)){
match++;}}}return match;}const vector<int>&getX()const{return X;}const vector<int>&getY()const{return Y;}};
생성자로 , 를 넘겨주고, addEdge로 간선들을 추가한 다음 solve를 돌리면 최대 매칭의 크기를 리턴한다. 어떤 정점이 매칭됐는지 알아야 한다면 getX, getY로 나 의 정점과 매칭된 정점을 확인할 수 있다.
제일 중요한 solve를 보자. 먼저 BFS로 최단 확장 경로를 구한다. BFS로 최단거리를 구하는 원리와 똑같다. 이때 시간과 메모리를 줄이기 위해 의 정점만 계산하며, 의 정점은 즉시 와 매칭된 정점으로(Y[y]) 이동한다고 생각한다. BFS는 확장 경로를 따라서 이동하면서 매칭되지 않은 의 정점으로부터 최단거리를 dist에 저장한다. 확장 경로 마지막에는 매칭되지 않은 정점에 도착하므로 Y[y]는 0이 된다. 따라서 확장 경로가 있다면 제일 짧은 확장 경로의 길이가 (정확히는 의 정점만 셌을 때 길이가) dist[0]에 저장되고, 확장 경로가 없다면 dist[0]은 바뀌지 않아서 INT_MAX 상태로 남아있게 된다. 이를 이용해 확장 경로가 존재하는지 확인할 수 있고, 이후에 DFS를 통해 이동할 때 모든 확장 경로가 아니라 최단 확장 경로를 통해서만 이동할 수 있게 만든다.
이제 DFS를 통해 최단 확장 경로들을 찾고 매칭에 포함시켜야 한다. 확장 경로는 매칭되지 않은 정점부터 시작해야 하므로 X[i]==0인 정점에서 DFS를 시작한다. 이때 BFS에서 구한 최단거리를 따라서 이동하면 DFS에서도 확장 경로를 따라서 이동할 수 있다. 만약 Y[y]==0이거나, 이후 DFS로 확장 경로를 찾았다면 X[x]= y; Y[y]= x;로 간선의 상태를 뒤집고 true를 리턴한다. 이 정점에서 갈 수 있는 모든 정점을 봤지만 가능한 확장 경로가 없다면 false를 리턴한다.
보통 여기서 DFS에서 같은 정점을 두 번 이상 지나가지 않기 위해 visit 배열 등을 사용하지만, 이 코드에서는 그럴 필요가 없다. 먼저 매칭되지 않은 정점, 즉 X[i]==0인 정점은 DFS 안에서 방문할 방법이 없으므로 solve의 for문에서만 방문할 수 있는데, for문은 자명하게 정점을 최대 1번씩만 방문한다. 다음으로 이미 매칭된 정점인 경우에는 Y[y]= x나 dist[x]= INT_MAX 때문에 dist[Y[y]]== dist[x]+1를 더 이상 만족하지 않아서 두 번 이상 지나가지 않는다. 따라서 visit 배열 등의 추가적인 조치가 필요없다.