【圖論】尤拉圖

purinliang發表於2024-06-08

尤拉回路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 {};
}

}

上述演算法未經驗證