歐拉回路
技術標籤:ACM--圖論
文章目錄
歐拉回路
1.演算法分析
幾個概念:
1.尤拉路:
給定一張無向圖,若存在一條從節點S到節點T的路徑,恰好不重不漏地經過每條邊一次(可以重複經過圖中的節點),則稱該路徑為S到T的尤拉路
2.歐拉回路:
若存在一條從S出發的路徑,恰好不重不漏地經過每條邊一次(可以重複經過圖中的節點),最終回到起點S,則稱該路徑為歐拉回路
3.尤拉圖:
存在歐拉回路的無向圖稱為尤拉圖
結論:
1.對於無向圖,所有邊都是連通的
(1) 存在尤拉路徑的充分必要條件: 度數為奇數的點只能是0或者2個(0個為歐拉回路的情況)
(2) 存在歐拉回路的充分必要條件: 度數為奇數的點只能有0個
2.對於有向圖,所有邊都是連通的
(1) 存在尤拉路徑的充分必要條件: 要麼所有點出度均等於入度(歐拉回路情況);要麼除了兩個點之外,其餘點的出度等於入度,剩餘的兩個點:一個滿足出度比入度多1(起點),另一個滿足入度比出度多1(終點)
(2) 存在歐拉回路的充分必要條件: 所有點的出度均等於入度
性質:
1.按照如下演算法求出來的歐拉回路是逆序的,即:從終點->起點的 歐拉回路
2.對於尤拉路而言,最先搜尋的點會出現在搜尋序列seq(棧的)的結尾(靠近棧頂的位置),所以如果想要得到一個字典序最小的尤拉路,只需要將seq倒置一下即可。
2.模板
2.1 記錄點的情形
void dfs(int x)
對於從x出發的每條邊(x, y)
如果該邊沒有被訪問過
把這條邊刪去
dfs(y)
把x入棧
main()
dfs(1)
倒序輸出棧中所有的節點
2.2 記錄邊的情形
void dfs(int x) 對於從x出發的每條邊i(x, y) 如果該邊沒有被訪問過 把這條邊刪去 dfs(y) 把邊i入棧 main() dfs(1) 倒序輸出棧中所有的邊
3.典型例題
3.1 歐拉回路判定
acwing1185單詞遊戲
**題意: ** 有 N 個盤子,每個盤子上寫著一個僅由小寫字母組成的英文單詞。你需要給這些盤子安排一個合適的順序,使得相鄰兩個盤子中,前一個盤子上單詞的末字母等於後一個盤子上單詞的首字母。判斷是否能達到這一要求。 1 ≤ N ≤ 1 0 5 1≤N≤10^5 1≤N≤105,單詞長度均不超過1000
題解: 可以把每個單詞的首位字母當成一個點,每個單詞當成一條邊,這樣建圖。然後判斷每個建立的圖是否存在尤拉路。同時還要判斷整張圖是否連通(可以使用並查集來判斷:判斷每個點是否最上面的父節點是同一個)
程式碼:
#include <bits/stdc++.h>
using namespace std;
int n, m, t;
int const N = 30, M = N * N;
char str[1100]; // 記錄每個單詞
int fa[N]; //
int dout[N], din[N], st[N];
int get(int x) {
if (x == fa[x]) return x;
return fa[x] = get(fa[x]);
}
int main() {
cin >> t;
while (t--) {
memset(dout, 0, sizeof dout);
memset(din, 0, sizeof din);
memset(st, 0, sizeof st);
cin >> m;
// 並查集初始化
for (int i = 0; i < 26; ++i) fa[i] = i;
// 建圖
for (int i = 0; i < m; ++i) {
scanf("%s", str);
int len = strlen(str);
int a = str[0] - 'a', b = str[len - 1] - 'a';
st[a] = 1, st[b] = 1; // 記錄是否存在
dout[a]++, din[b]++; // 記錄出度和入度
fa[get(a)] = get(b); // 並查集
}
bool success = true; // 判斷是否存在歐拉回路
int start = 0, end = 0; // 記錄起點、終點的個數
for (int i = 0; i < 26; ++i) {
if (dout[i] != din[i]) {
if (din[i] == dout[i] + 1)
end++; // 終點個數加一
else if (dout[i] == din[i] + 1)
start++; // 起點個數加一
else // 無解的情況
{
success = false;
break;
}
}
}
if (!((end == 0 && start == 0) || (start == 1 && end == 1)))
success = false; // 起點個數和終點個數只能為0和1
// 判斷整張圖是否連通
int repre = -1;
for (int i = 0; i < 26; ++i) {
if (!st[i]) continue;
if (repre == -1)
repre = get(i);
else if (repre != get(i)) {
success = false;
break;
}
}
if (success)
printf("Ordering is possible.\n");
else
printf("The door cannot be opened.\n");
}
return 0;
}
3.2 尤拉路記錄邊的情景
acwing1184 歐拉回路
題意: 給定一張圖,請你找出歐拉回路,即在圖中找一個環使得每條邊都在環上出現恰好一次。列印環上的邊的編號,如果是反邊,那麼編號取負數。 1 ≤ n ≤ 1 0 5 , 0 ≤ m ≤ 2 × 1 0 5 1≤n≤10^5, 0≤m≤2×10^5 1≤n≤105,0≤m≤2×105
題解: 本題是尤拉路記錄邊的情景, 用type來記錄是無向圖還是有向圖
程式碼:
#include <bits/stdc++.h>
using namespace std;
int const N = 1e5 + 10, M = 4e5 + 10;
int h[N], e[M], ne[M], idx;
int n, m, type; // type=1:無向邊,type=2,有向邊
int din[N], dout[N]; // 記錄每個點的出度和入度
bool used[M]; // 記錄每條邊是否被使用
int ans[M / 2], cnt; // 記錄答案
void add(int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx++; }
void dfs(int u) {
for (int &i = h[u]; ~i; ) // 引用表示這條邊,每次使用完移動i指標等價於移動頭指標,等價於刪去這條邊
{
if (used[i]) // 這條邊用過
{
i = ne[i]; // 刪去
continue;
}
used[i] = true; // 記錄用過
int t; // 計算這條邊的編號
if (type == 1) // 無向邊
{
used[i ^ 1] = true;
t = i / 2 + 1;
if (i & 1) t = -t; // 反邊取負號
} else
t = i + 1; // 有向邊
int j = e[i]; // 下一個點
i = ne[i]; // 刪去這條邊
dfs(j); // 對下個點dfs
ans[++cnt] = t; // 記錄答案
}
}
int main() {
memset(h, -1, sizeof h);
cin >> type >> n >> m;
for (int i = 0; i < m; ++i) {
int a, b;
scanf("%d %d", &a, &b);
add(a, b);
if (type == 1) add(b, a);
din[b]++, dout[a]++;
}
// 判斷是否有解
for (int i = 1; i <= n; ++i)
if ((type == 1 && (din[i] + dout[i] & 1)) ||
(type == 2 && (din[i] != dout[i]))) {
printf("NO\n");
return 0;
}
// 判掉孤立點,同時要保證整張圖連通,所有一次dfs後break
for (int i = 1; i <= n; ++i)
if (h[i] != -1) {
dfs(i);
break;
}
// 不是連通圖
if (cnt < m) {
printf("NO\n");
return 0;
}
// 倒序輸出答案
printf("YES\n");
for (int i = cnt; i >= 1; --i) printf("%d ", ans[i]);
return 0;
}
3.3 尤拉路記錄點的情景
acwing1124 騎馬修柵欄
題意: 給定一張無向圖,要求找到尤拉路,按照逆序列印尤拉路上的所有點。點數 1 < = n < = 1024 , 網 格 大 小 1 < = R , W < = 500 1<=n<=1024,網格大小1<=R, W<=500 1<=n<=1024,網格大小1<=R,W<=500
題解: 本題要處理無向圖的歐拉回路,記錄的是點的情形;因為要處理的點數比較小,直接使用鄰接表儲存即可。答案要求字典序從小到大輸出,由於歐拉回路的特殊性質,只需要列舉的時候按照從小到大列舉,即可得到最小的字典序
程式碼:
#include <bits/stdc++.h>
using namespace std;
int const N = 510, M = N * N;
int g[N][N]; // 鄰接表處理
int d[N]; // 記錄度
int ans[M], cnt; // 記錄答案
int n = 500, m;
// 求歐拉回路
void dfs(int u) {
for (int i = 1; i <= n; ++i) // 遍歷所有的點,如果u->i有路徑
{
if (g[u][i]) // 如果有路徑
{
g[u][i]--, g[i][u]--; // 刪去這條路
dfs(i); // 遍歷對應的點
}
}
ans[++cnt] = u; // 這個點的所有邊對應的所有點都完成dfs後才能把這個點入棧
}
int main() {
// 建圖
cin >> m;
for (int i = 0; i < m; ++i) {
int a, b;
scanf("%d %d", &a, &b);
g[a][b]++, g[b][a]++;
d[a]++, d[b]++;
}
// 找到起點
int start = 1;
while (!d[start]) start++;
// 找到一個入度為奇數的點作為起點
for (int i = 1; i <= n; ++i) {
if (d[i] % 2 & 1) {
start = i;
break;
}
}
// dfs找歐拉回路
dfs(start);
// 逆序輸出答案
for (int i = cnt; i >= 1; --i) printf("%d\n", ans[i]);
return 0;
}
acwing366 看牛
題意: 給定N個點M條邊的無向圖,求一條路徑,從節點1出發,最後回到節點1,並且滿足每條邊恰好被沿著正、反兩個方向分別經過一次。若有多種方案,輸出任意一種即可。 1 ≤ N ≤ 1 0 4 , 1 ≤ M ≤ 5 ∗ 1 0 4 1≤N≤10^4, 1≤M≤5∗10^4 1≤N≤104,1≤M≤5∗104
題解: 本題要求每條邊被走過兩次,因此我們修改模板:只需要記錄一條邊vis[i] = 1,而不需要進行vis[i] = vis[i ^ 1] = 1
程式碼:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 10, M = 5e4 + 10;
int n, m, cnt, ans[M << 1];
int idx, h[N], e[M << 1], ne[M << 1];
void add(int a, int b) { e[idx] = b, ne[idx] = h[a], h[a] = idx++; }
void dfs(int x) {
for (int i = h[x]; ~i; i = h[x]) {
int y = e[i];
h[x] = ne[i];
dfs(y);
}
ans[++cnt] = x;
}
int main() {
memset(h, -1, sizeof h);
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int x, y;
scanf("%d%d", &x, &y);
add(x, y), add(y, x);
}
dfs(1);
for (int i = cnt; i >= 1; i--) printf("%d\n", ans[i]);
return 0;
}