오일러 회로

오일러 회로

저자 Сергей Слотин

정의. 오일러 경로는 그래프의 모든 간선을 지나는 경로입니다.

정의. 오일러 회로는 회로를 이루는 오일러 경로입니다.

간단히 하기 위해 두 경우 모두 그래프가 무방향 그래프라고 가정하겠습니다.

정점 다섯 개로 이루어진 그래프와 그 그래프의 오일러 회로 중 하나: CDCBBADEBC

또한 해밀턴 경로와 사이클이라는 개념도 있습니다. 이들은 간선이 아니라 모든 정점을 한 번씩 방문합니다. 해밀턴 사이클을 찾는 문제(외판원 문제, 영어로 travelling salesman problem)는 가장 잘 알려진 NP-완전 문제 중 하나인 반면, 오일러 사이클을 찾는 문제는 선형 시간에 해결되며, 그 방법을 지금부터 보이겠습니다.

#오일러 회로 찾기

정리. 오일러 회로는 그래프가 연결되어 있고 모든 정점의 차수가 짝수일 때, 그리고 그때에만 존재합니다.

증명. 필요성은 다음과 같이 보일 수 있습니다. 오일러 회로를 하나 잡고, 그 모든 간선을 순회 순서대로 방향을 정하면 됩니다. 그러면 각 정점에서 나가는 간선의 수와 들어오는 간선의 수가 같아지므로, 원래의 무방향 그래프에서 모든 정점의 차수는 짝수였다는 것을 알 수 있습니다.

충분성은 구성적으로 증명하겠습니다. 즉, 사이클을 찾는 알고리즘을 제시하겠습니다:

void euler(int v) {
    while (!g[v].empty()) {
        u = *g[v].begin(); // берем любое ребро
        remove_edge(v, u); // удаляем его
        euler(u);            // запускаемся от противоположного конца
    }
    cout << v <<  " ";     // выписываем текущую вершину
}

다음을 관찰할 수 있습니다:

  • 정확히 $(m + 1)$개의 정점으로 이루어진 수열이 출력되며,
  • 출력된 정점들 중 인접한 모든 쌍 사이에는 간선이 있으며,
  • 각 간선은 정확히 한 번씩 기록됩니다.

따라서 연결성과 차수의 짝수 조건이 충족된다면, 출력된 정점 순서는 실제로 오일러 회로가 되며, 이는 방향 그래프의 경우에도 마찬가지입니다.

#간선을 삭제하는 방법

가장 간단한 방법은 모든 인접 리스트를 set과 같은 구조에 저장하고 그곳에서 직접 간선을 삭제하는 것입니다:

const int maxn = 1e5;
set<int> g[maxn];

void euler(int v) {
    while (!g[v].empty()) {
        u = *g[v].begin();
        g[v].erase(u);
        g[u].erase(v); // если граф ориентированный, обратное ребро удалять не надо
        euler(u);
    }
    cout << v <<  " ";
}

이 방법은 $O(m \log n)$ 시간에 동작하지만, 트리를 해시 테이블(unordered_set)로 바꾸기만 해도 시간을 선형으로 줄일 수 있습니다.

간선이 어떤 식으로든 변경되는 문제에서 자주 쓰이는 더 일반적인 방법을 사용할 수도 있습니다. 간선에 대한 메타 정보를 담은 별도의 배열을 만들고, 인접 리스트에는 정점 번호 대신 간선 번호를 저장합시다:

struct edge {
    int to;
    bool deleted;
};

vector<edge> edges;
stack<int> g[maxn];

void add_edge(int u, int v) {
    g[u].push(edges.size());
    edges.add({v, false});
    g[u].push(edges.size());
    edges.add({u, false});
}

무방향 그래프의 각 간선을 add_edge로 추가하면, 그 결과로 얻어지는 간선 번호 매기기에는 흥미로운 성질이 있습니다. 간선 e의 역방향 간선을 얻으려면 e^1을 계산하면 됩니다.

그러면 구조를 복잡하게 수정하는 대신, 탐색하는 동안 이 정보를 유지하면 됩니다.

void euler(int v) {
    while (!g[v].empty()) {
        e = g[v].top();
        g[v].pop();
        if (!edges[e].deleted) {
            edges[e].deleted = edges[e^1].deleted = true;
            euler(e.to);
        }
    }
    cout << v <<  " ";
}

모든 구현 방식에서 그래프에 루프와 다중 간선이 있는 경우에는 조금 더 주의해야 합니다.

#오일러 경로

이제 오일러 경로에 대해 이야기해 봅시다. 모든 정점의 차수가 짝수인 것은 아니더라도, 그래도 무언가 할 수 있지 않겠습니까?

각 간선은 정확히 두 정점의 차수의 홀짝성을 바꾸고, 빈 그래프에서는 처음에 모든 차수가 0이므로, 차수가 홀수인 정점의 개수는 짝수라는 점에 주목합시다.

홀수 차수 정점이 정확히 두 개라면 다음과 같이 할 수 있습니다. 두 정점을 간선으로 연결하고, 오일러 회로를 구성한 다음(이제 모든 정점의 차수가 짝수이기 때문입니다), 이 간선을 회로에서 제거합니다. 이 회로를 올바르게 회전시키면 오일러 경로를 얻게 됩니다.

또는, 단순히 홀수 차수 정점 중 하나에서 탐색을 시작해도 같은 결과를 얻을 수 있습니다.

홀수 정점이 두 개보다 많으면 더 이상 오일러 경로를 만들 수 없습니다. 오일러 경로는 양 끝점이 될 수 있는 두 정점을 제외한 모든 정점에 짝수 번 들어가거나 나오기 때문입니다.

따름정리. 오일러 경로는 그래프가 연결되어 있고 차수가 홀수인 정점의 개수가 2 이하일 때, 그리고 그때에만 존재합니다.

연습 문제. 연결된 무향 그래프가 주어집니다. 이 그래프의 간선을 서로 겹치지 않는 최소 개수의 경로로 덮어야 합니다. 시간 복잡도는 $O(n + m)$입니다.

연습 문제. 방향 그래프의 경우에 대해 유사한 명제를 세우고 증명하십시오.