跳转至

歐拉圖

本頁面將簡要介紹歐拉圖的概念、實現和應用。

定義

  • 歐拉回路:通過圖中每條邊恰好一次的迴路
  • 歐拉通路:通過圖中每條邊恰好一次的通路
  • 歐拉圖:具有歐拉回路的圖
  • 半歐拉圖:具有歐拉通路但不具有歐拉回路的圖

性質

歐拉圖中所有頂點的度數都是偶數。

\(G\) 是歐拉圖,則它為若干個環的並,且每條邊被包含在奇數個環內。

判別法

  1. 無向圖是歐拉圖當且僅當:
    • 非零度頂點是連通的
    • 頂點的度數都是偶數
  2. 無向圖是半歐拉圖當且僅當:
    • 非零度頂點是連通的
    • 恰有 2 個奇度頂點
  3. 有向圖是歐拉圖當且僅當:
    • 非零度頂點是強連通的
    • 每個頂點的入度和出度相等
  4. 有向圖是半歐拉圖當且僅當:
    • 非零度頂點是弱連通的
    • 至多一個頂點的出度與入度之差為 1
    • 至多一個頂點的入度與出度之差為 1
    • 其他頂點的入度和出度相等

求歐拉回路或歐拉路

Fleury 算法

也稱避橋法,是一個偏暴力的算法。

算法流程為每次選擇下一條邊的時候優先選擇不是橋的邊。

一個廣泛使用但是錯誤的實現方式是先 Tarjan 預處理橋邊,然後再 DFS 避免走橋。但是由於走圖過程中邊會被刪去,一些非橋邊會變為橋邊導致錯誤。最簡單的實現方法是每次刪除一條邊之後暴力跑一遍 Tarjan 找橋,時間複雜度是 \(\Theta(m(n+m))=\Theta(m^2)\)。複雜的實現方法要用到動態圖等,實用價值不高。

Hierholzer 算法

也稱逐步插入迴路法。

過程

算法流程為從一條迴路開始,每次任取一條目前回路中的點,將其替換為一條簡單迴路,以此尋找到一條歐拉回路。如果從路開始的話,就可以尋找到一條歐拉路。

實現

Hierholzer 算法的暴力實現如下:

\[ \begin{array}{ll} 1 & \textbf{Input. } \text{The edges of the graph } e , \text{ where each element in } e \text{ is } (u, v) \\ 2 & \textbf{Output. } \text{The vertex of the Euler Road of the input graph}.\\ 3 & \textbf{Method. } \\ 4 & \textbf{Function } \text{Hierholzer } (v) \\ 5 & \qquad circle \gets \text{Find a Circle in } e \text{ Begin with } v \\ 6 & \qquad \textbf{if } circle=\varnothing \\ 7 & \qquad\qquad \textbf{return } v \\ 8 & \qquad e \gets e-circle \\ 9 & \qquad \textbf{for} \text{ each } v \in circle \\ 10& \qquad\qquad v \gets \text{Hierholzer}(v) \\ 11& \qquad \textbf{return } circle \\ 12& \textbf{Endfunction}\\ 13& \textbf{return } \text{Hierholzer}(\text{any vertex}) \end{array} \]

性質

這個算法的時間複雜度約為 \(O(nm+m^2)\)。實際上還有複雜度更低的實現方法,就是將找回路的 DFS 和 Hierholzer 算法的遞歸合併,邊找回路邊使用 Hierholzer 算法。

如果需要輸出字典序最小的歐拉路或歐拉回路的話,因為需要將邊排序,時間複雜度是 \(\Theta(n+m\log m)\)(計數排序或者基數排序可以優化至 \(\Theta(n+m)\))。如果不需要排序,時間複雜度是 \(\Theta(n+m)\)

應用

有向歐拉圖可用於計算機譯碼。

設有 \(m\) 個字母,希望構造一個有 \(m^n\) 個扇形的圓盤,每個圓盤上放一個字母,使得圓盤上每連續 \(n\) 位對應長為 \(n\) 的符號串。轉動一週(\(m^n\) 次)後得到由 \(m\) 個字母產生的長度為 \(n\)\(m^n\) 個各不相同的符號串。

構造如下有向歐拉圖:

\(S = \{a_1, a_2, \cdots, a_m\}\),構造 \(D=\langle V, E\rangle\),如下:

\(V = \{a_{i_1}a_{i_2}\cdots a_{i_{n-1}} |a_i \in S, 1 \leq i \leq n - 1 \}\)

\(E = \{a_{j_1}a_{j_2}\cdots a_{j_{n-1}}|a_j \in S, 1 \leq j \leq n\}\)

規定 \(D\) 中頂點與邊的關聯關係如下:

頂點 \(a_{i_1}a_{i_2}\cdots a_{i_{n-1}}\) 引出 \(m\) 條邊:\(a_{i_1}a_{i_2}\cdots a_{i_{n-1}}a_r, r=1, 2, \cdots, m\)

\(a_{j_1}a_{j_2}\cdots a_{j_{n-1}}\) 引入頂點 \(a_{j_2}a_{j_3}\cdots a_{j_{n}}\)

這樣的 \(D\) 是連通的,且每個頂點入度等於出度(均等於 \(m\)),所以 \(D\) 是有向歐拉圖。

任求 \(D\) 中一條歐拉回路 \(C\),取 \(C\) 中各邊的最後一個字母,按各邊在 \(C\) 中的順序排成圓形放在圓盤上即可。

例題

洛谷 P2731 騎馬修柵欄

給定一張有 500 個頂點的無向圖,求這張圖的一條歐拉路或歐拉回路。如果有多組解,輸出最小的那一組。

在本題中,歐拉路或歐拉回路不需要經過所有頂點。

邊的數量 m 滿足 \(1\leq m \leq 1024\)

解題思路

用 Fleury 算法解決本題的時候只需要再貪心就好,但是由於複雜度不對,所以要換用 Hierholzer 算法。

保存答案可以使用 std::stack<int>,因為如果找的不是迴路的話必須將那一部分放在最後。

注意,不能使用鄰接矩陣存圖,否則時間複雜度會退化為 \(\Theta(nm)\)。由於需要將邊排序,建議使用前向星或者 std::vector 存圖。示例代碼使用 std::vector

示例代碼
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <algorithm>
#include <cstdio>
#include <stack>
#include <vector>
using namespace std;

struct edge {
  int to;
  bool exists;
  int revref;

  bool operator<(const edge& b) const { return to < b.to; }
};

vector<edge> beg[505];
int cnt[505];

const int dn = 500;
stack<int> ans;

void Hierholzer(int x) {  // 关键函数
  for (int& i = cnt[x]; i < (int)beg[x].size();) {
    if (beg[x][i].exists) {
      edge e = beg[x][i];
      beg[x][i].exists = 0;
      beg[e.to][e.revref].exists = 0;
      ++i;
      Hierholzer(e.to);
    } else {
      ++i;
    }
  }
  ans.push(x);
}

int deg[505];
int reftop[505];

int main() {
  for (int i = 1; i <= dn; ++i) {
    beg[i].reserve(1050);  // vector 用 reserve 避免动态分配空间,加快速度
  }

  int m;
  scanf("%d", &m);
  for (int i = 1; i <= m; ++i) {
    int a, b;
    scanf("%d%d", &a, &b);
    beg[a].push_back((edge){b, 1, 0});
    beg[b].push_back((edge){a, 1, 0});
    ++deg[a];
    ++deg[b];
  }

  for (int i = 1; i <= dn; ++i) {
    if (!beg[i].empty()) {
      sort(beg[i].begin(), beg[i].end());  // 为了要按字典序贪心,必须排序
    }
  }

  for (int i = 1; i <= dn; ++i) {
    for (int j = 0; j < (int)beg[i].size(); ++j) {
      beg[i][j].revref = reftop[beg[i][j].to]++;
    }
  }

  int bv = 0;
  for (int i = 1; i <= dn; ++i) {
    if (!deg[bv] && deg[i]) {
      bv = i;
    } else if (!(deg[bv] & 1) && (deg[i] & 1)) {
      bv = i;
    }
  }

  Hierholzer(bv);

  while (!ans.empty()) {
    printf("%d\n", ans.top());
    ans.pop();
  }
}

習題