尤拉回路Eulerian Cycle:透過圖中每條邊恰好一次的迴路
尤拉通路Eulerian Path:透過圖中每條邊恰好一次的通路
尤拉圖:具有尤拉回路的圖
半尤拉圖:具有尤拉通路但不具有尤拉回路的圖
尤拉圖中所有頂點的度數都是偶數。
若 G 是尤拉圖,則它為若干個環的並,且每條邊被包含在奇數個環內。
判別法
無向圖是尤拉圖當且僅當:
非零度頂點是連通的
頂點的度數都是偶數
這個條件很容易滿足。
無向圖是半尤拉圖當且僅當:
非零度頂點是連通的
恰有2個奇度頂點
這個條件很容易滿足。
有向圖是尤拉圖當且僅當:
非零度頂點是強連通的
每個頂點的入度和出度相等
(強連通 = 從同一個強連通的每個點出發都能到達分量上的所有點)
有向圖是半尤拉圖當且僅當:
非零度頂點是弱連通的
至多一個頂點的出度與入度之差為1
至多一個頂點的入度與出度之差為1
其他頂點的入度和出度相等
關於零度頂點連通與否,是否屬於尤拉圖的問題,看每個問題具體的定義。
Hierholzer 演算法
過程
演算法流程為從一條迴路開始,每次任取一條目前回路中的點,將其替換為一條簡單迴路,以此尋找到一條尤拉回路。如果從路開始的話,就可以尋找到一條尤拉路。(或者先把缺失的那條邊補上,找到一個尤拉回路,再從缺失的邊那裡剪開,即可。)首先要確定要求的尤拉回路或者尤拉通路存在,如果是尤拉回路存在可以從任何點開始dfs,如果只有尤拉通路存在,則要在無向圖的奇數度數點開始dfs,有向圖在唯一的出度比入度大1的點開始dfs。dfs完成後棧中的元素逆序就是一條尤拉回路/尤拉通路。為了保證複雜度,要在演算法的過程中跳過已經遍歷過的邊,這個問題是這個演算法最讓人頭疼的地方,最簡單的方式是使用set,但是會涉及到在for迭代中刪除set中元素的問題。
這裡一般用set來存圖,然後實現刪邊,從而使得演算法的複雜度為 \(O(m\log{m})\) ,注意,stl中提供erase的容器,返回值為刪除之後的下一個迭代器。也就是說,如果刪除成功,則直接用erase的返回值就可以繼續迴圈,否則直接迭代器++即可。
這個是C++ 11的標準寫法:
for (auto it = my_set.begin(); it != my_set.end(); ) {
if (some_condition) {
it = numbers.erase(it);
} else {
++it;
}
}
對於有向圖,下面的演算法是有效的(僅限無平行邊的簡單情況,不推薦):
set<int> G[MAXN];
stack<int> stk;
void dfs (int u) {
for (auto it = G[u].begin(); it != G[u].end(); ) {
int v = *it;
it = G[u].erase(it);
dfs(v);
}
stk.push (u);
}
先判斷尤拉通路存在(最多隻有一對節點入度和出度不相等,並且一個是大一一個是小一)。只需要在出度唯一比入度大1的節點開始dfs,或者如果沒有這樣的節點,從任意節點開始dfs,然後把stk中的節點逆序輸出,如果stk中的節點數量恰好等於邊數+1,則尤拉通路/尤拉回路已經被找到,否則此圖不連通,無解。
但是如果是無向圖,這個演算法要刪除別的地方的set迴圈中的迭代器,又引入了新的不確定。
從luogu https://www.luogu.com.cn/article/kv9n9167 這篇文章學到了一個很好的方式。
使用鏈式前向星的方法存圖,相鄰的邊要同時加入。然後每個節點維護一個連結串列的頭節點,當在dfs到u,然後刪除u中指向v的邊時,容易知道v一定是此時連結串列的頭節點,此時把頭節點向前移動1,如果下一次再遇到u節點則不會再遍歷同一個頭。同時將(u,v)這一條邊打上已被使用的標記。當遍歷節點v時,如果遇到了指向u的那條邊(反向邊),那麼檢查這個反向邊是否已經被遍歷,如果是的話則繼續移動頭節點。這個也是鏈式前向星寫法無法被vector寫法替代的一個地方。
有向圖版本
namespace EulerianPath {
int n;
struct Edge {
int u;
int v;
int next;
};
vector<Edge> edge;
vector<int> head;
void Init (int _n) {
n = _n;
edge.clear();
edge.push_back ({0, 0, 0});
head.clear();
head.resize (n + 1);
}
void AddDirectedEdge (int u, int v) {
Edge e = {u, v, head[u]};
head[u] = (int) edge.size();
edge.push_back (e);
}
int HaveEulerianCycle() {
vector<int> outdeg (n + 1);
vector<int> indeg (n + 1);
for (auto [u, v, next] : edge) {
++outdeg[u];
++indeg[v];
}
for (int i = 1; i <= n; ++i) {
if (outdeg[i] != indeg[i]) {
return -1;
}
}
// 如果連通
return 1;
}
int HaveEulerianPath() {
vector<int> outdeg (n + 1);
vector<int> indeg (n + 1);
for (auto [u, v, next] : edge) {
++outdeg[u];
++indeg[v];
}
int inV = 0, outV = 0;
for (int i = 1; i <= n; ++i) {
if (outdeg[i] == indeg[i]) {
continue;
}
if (outdeg[i] > indeg[i]) {
if (outV == 0) {
outV = i;
} else {
return -1;
}
} else {
if (inV == 0) {
inV = i;
} else {
return -1;
}
}
}
// 如果連通
return outV ? outV : 1;
}
vector<int> FindEulerCyclerOrPath() {
int S = HaveEulerianPath();
if (S == -1) {
return {};
}
vector<int> stk;
auto dfs = [&] (auto self, int u) {
for (int &i = head[u]; i;) {
auto [_u, v, next] = edge[i];
i = next;
dfs (v);
}
stk.push (u);
}
if (stk.size() == edge.size()) {
// 因為edge有一條編號為0的虛擬邊,所以大小剛好相等
reverse (stk.begin(), stk.end());
return stk;
}
return {};
}
}
上述演算法未經驗證
無向圖版本
namespace EulerianPath {
int n;
struct Edge {
int u;
int v;
int next;
bool vis;
};
vector<Edge> edge;
vector<int> head;
void Init (int _n) {
n = _n;
edge.clear();
edge.push_back ({0, 0, 0, false});
head.clear();
head.resize (n + 1);
}
void AddUndirectedEdge (int u, int v) {
Edge e = {u, v, head[u], false};
head[u] = (int) edge.size();
edge.push_back (e);
e = {v, u, head[v], false};
head[v] = (int) edge.size();
edge.push_back (e);
}
int HaveEulerianCycle() {
vector<int> deg (n + 1);
for (auto [u, v, next] : edge) {
++deg[u];
++deg[v];
}
for (int i = 1; i <= n; ++i) {
if (deg[i] % 2 != 0) {
return -1;
}
}
// 如果連通
return 1;
}
int HaveEulerianPath() {
vector<int> deg (n + 1);
for (auto [u, v, next] : edge) {
++deg[u];
++deg[v];
}
int oddV1 = 0, oddV2 = 0;
for (int i = 1; i <= n; ++i) {
if (deg[i] % 2 == 0) {
continue;
}
if (oddV1 == 0) {
oddV1 = i;
continue;
}
if (oddV2 == 0) {
oddV2 = i;
continue;
}
return -1;
}
// 如果連通
if (oddV1 == 0 && oddV2 == 0) {
return 1;
}
if (oddV1 != 0 && oddV2 != 0) {
return oddV1;
}
return -1;
}
vector<int> FindEulerCyclerOrPath() {
int S = HaveEulerianPath();
if (S == -1) {
return {};
}
vector<int> stk;
auto dfs = [&] (auto self, int u) {
for (int &i = head[u]; i;) {
auto [_u, v, next] = edge[i];
Edge &cur_edge = edge[i];
Edge &back_edge = (i % 2 == 1) ? edge[i + 1] : edge[i - 1];
i = next;
if (!cur_edge.vis) {
cur_edge.vis = true;
back_edge.vis = true;
dfs (v);
}
}
stk.push (u);
}
if (stk.size() == (edge.size() + 1) / 2) {
// 因為edge有一條編號為0的虛擬邊,所以大小剛好相等
reverse (stk.begin(), stk.end());
return stk;
}
return {};
}
}
上述演算法未經驗證