欧拉图 学习笔记

欧拉图 学习笔记

离散课没有认真听,但是做到了需要使用欧拉路径相关算法的题目,索性学了一下,弥补一下图论板块基础内容的空缺。

欧拉图

分为有向图和无向图两种情况。

必要条件

无论如何,都需要满足图联通的条件,有些时候题目并不保证这个条件成立,所以需要自己并查集判断一下。

欧拉路径

有向图中,满足:有且仅有两个点的入度不相等,且其差值只为 1。

无向图中:满足有且仅有两个奇数度数的点。

有向图

Fleury

“避桥法”,顾名思义,在 dfs 的过程中,我们优先走非桥边,这样可以保证把回路遍历完之后再前进,时间复杂度从线性到平方,取决于是否提前用 tarjan 预处理出割边。

Code(AI写的)
#include <iostream>
#include <vector>
#include <stack>
#include <list>
using namespace std;

bool isBridge(int u, int v, vector<list<int>>& adj) {
    // 暂时删除边 (u, v)
    adj[u].remove(v);
    adj[v].remove(u);

    // 使用DFS检查图是否仍然连通
    vector<bool> visited(adj.size(), false);
    stack<int> dfsStack;
    dfsStack.push(u);
    visited[u] = true;
    while (!dfsStack.empty()) {
        int node = dfsStack.top();
        dfsStack.pop();
        for (int neighbor : adj[node]) {
            if (!visited[neighbor]) {
                visited[neighbor] = true;
                dfsStack.push(neighbor);
            }
        }
    }

    // 恢复边 (u, v)
    adj[u].push_back(v);
    adj[v].push_back(u);

    // 检查是否所有顶点都已访问
    for (int i = 0; i < adj.size(); ++i) {
        if (!visited[i] && !adj[i].empty()) {
            return true; // 如果有未访问的顶点,则 (u, v) 是桥
        }
    }
    return false; // 否则 (u, v) 不是桥
}

void fleury(int start, vector<list<int>>& adj, vector<int>& path) {
    path.push_back(start);
    while (!adj[start].empty()) {
        int next = adj[start].front();
        adj[start].remove(next);
        adj[next].remove(start);

        if (isBridge(start, next, adj)) {
            // 如果 (start, next) 是桥,则恢复边并选择另一条边
            adj[start].push_back(next);
            adj[next].push_back(start);
            continue;
        }

        path.push_back(next);
        start = next;
    }
}

int main() {
    vector<list<int>> adj = {
        {1, 2}, // 0
        {0, 2, 3}, // 1
        {0, 1, 3}, // 2
        {1, 2} // 3
    };
    int start = 0; // 选择起点
    vector<int> path;
    fleury(start, adj, path);
    for (int v : path) {
        cout << v << " ";
    }
    return 0;
}

Hierholzer

CP 领域比较主流的求欧拉路径和回路的算法,核心思想在于利用 dfs 的回溯和递归。在对于一个节点访问完毕其所有出边之后,把节点加入栈中。这样整个算法结束之后,栈里面的序号序列就是欧拉路径序列的反序。

Code(自己写的)
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
vector<int> e[N];
int st[N],in[N];
int n,m;
int s[N],top;
inline void dfs(int x)
{
    while(st[x]<e[x].size())dfs(e[x][st[x]++]);
    s[++top]=x;
}
inline void solve()
{
    cin>>n>>m;
    for(int i=1,u,v;i<=m;++i)
    {
        cin>>u>>v;
        e[u].push_back(v);
        in[v]++;
    }
    int S=0;
    for(int i=1;i<=n;++i)
    {
        if(abs((int)e[i].size()-in[i])>1)return cout<<"No",void();
        if(e[i].size()>in[i])
        {
            if(S)return cout<<"No",void();
            else S=i;
        }
    }
    for(int i=1;i<=n;++i)sort(e[i].begin(),e[i].end());
    dfs(S?S:1);
    if(top!=m+1)return cout<<"No",void();
    for(int i=m+1;i>=1;--i)cout<<s[i]<<' ';
}
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    return solve(),0;
}

无向图

对于 Hierholzer 算法来讲,有向图和无向图的代码实现差别其实不大,主要的不同就是在于“拆边”,由于这里建的是双向边,但是只能走一次,所以在从一个方向访问完了一条边之后,同样得将其反边删去。

  1. 如果用邻接矩阵实现,那么就是简单地打上一个标记即可;
  2. 如果是前向星实现,那么就要涉及到网络流里面通过异或获得反边的 trick;
  3. 还有一种方法,就是用 \(n\) 个 set存边,这样可以 \(O(\log n)\) 地进行删除。

邻接矩阵的办法过于 trival,所以下面仅仅展示用前向星和 set 的写法。

前向星

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int head[N<<2],nex[N<<2],to[N<<2];
int tmp[N<<2];
int tote=1;
//无向图中充要条件是:要么度数全都是偶数,这时形成欧拉回路 ;要么就有且只有两个奇度数点,这时是普通的欧拉路径
//这是用前向星实现的版本
//并且根据CF原题的要求输出了边的编号,这里用map存
inline void add(int u,int v)
{
    tote++;
    nex[tote]=head[u];
    head[u]=tote;
    to[tote]=v;
}
int vis[N<<1];
vector<int> stk;
map<pair<int,int> , int> ans;
inline void dfs(int x)
{
    for(;head[x];head[x]=nex[head[x]])
    {
        if(vis[head[x]])continue;
        vis[head[x]]=vis[head[x]^1]=1;
        dfs(to[head[x]]);
    }   
    stk.push_back(x);
}   
int a[N],b[N];
int n;
inline void solve()
{   
    cin>>n;
    int cnt=0;
    for(int i=1;i<=n;++i)
    {
        cin>>a[i]>>b[i];
        tmp[++cnt]=a[i],tmp[++cnt]=b[i];
    }
    sort(tmp+1,tmp+cnt+1);
    cnt=unique(tmp+1,tmp+cnt+1)-tmp-1;
    vector<int> deg(2*cnt+2,0);  
    for(int i=1;i<=n;++i)
    {
        a[i]=lower_bound(tmp+1,tmp+cnt+1,a[i])-tmp;
        b[i]=lower_bound(tmp+1,tmp+cnt+1,b[i])-tmp;
        add(a[i],b[i]+cnt),add(b[i]+cnt,a[i]);
        deg[a[i]]++,deg[b[i]+cnt]++;
        ans[{a[i],b[i]+cnt}]=ans[{b[i]+cnt,a[i]}]=i;
    }    
    int count=0,S=0;
    for(int i=1;i<=2*cnt;++i)
    {
        if(deg[i]&1)
        {
            count++;
            if(!S)S=i;
        }
    }
    if(count&&count!=2)return cout<<"NO\n",void();
    if(!S)for(int i=1;i<=2*cnt;++i)if(deg[i]){S=i;break;}
    dfs(S);
    if(stk.size()!=n+1)return cout<<"NO\n",void();
    reverse(stk.begin(),stk.end());
    cout<<"YES\n";
    for(int i=0;i<n;++i)
        cout<<ans[{stk[i],stk[i+1]}]<<" \n"[i==n-1];
}
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    solve();
    return 0;
}

set

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int tmp[N<<1];
vector<int> stk;
set<int> e[N<<1];
map<pair<int,int> , int> ans;
//无向图中充要条件是:要么度数全都是偶数,这时形成欧拉回路 ;要么就有且只有两个奇度数点,这时是普通的欧拉路径
//这是用set实现的版本 相比前向星,写起来十分方便,唯一不足的地方可能在于常数较大
//并且根据CF原题的要求输出了边的编号,这里用map存
inline void dfs(int x)
{
    while(e[x].size())
    {
        int v=*e[x].begin();
        e[x].erase(v),e[v].erase(x);
        dfs(v);
    }
    stk.push_back(x);
}
int a[N],b[N];
int n;
inline void solve()
{   
    cin>>n;
    int cnt=0;
    for(int i=1;i<=n;++i)
    {
        cin>>a[i]>>b[i];
        tmp[++cnt]=a[i],tmp[++cnt]=b[i];
    }
    sort(tmp+1,tmp+cnt+1);
    cnt=unique(tmp+1,tmp+cnt+1)-tmp-1;

    vector<int> deg(2*cnt+2,0);  
    for(int i=1;i<=n;++i)
    {
        a[i]=lower_bound(tmp+1,tmp+cnt+1,a[i])-tmp;
        b[i]=lower_bound(tmp+1,tmp+cnt+1,b[i])-tmp;
        e[a[i]].insert(b[i]+n),e[b[i]+n].insert(a[i]);
        deg[a[i]]++,deg[b[i]+cnt]++;
        ans[{a[i],b[i]+cnt}]=ans[{b[i]+cnt,a[i]}]=i;
    }    
    int count=0,S=0;
    for(int i=1;i<=2*cnt;++i)
    {
        if(deg[i]&1)
        {
            count++;
            if(!S)S=i;
        }
    }
    if(count&&count!=2)return cout<<"NO\n",void();
    if(!S)for(int i=1;i<=2*cnt;++i)if(deg[i]){S=i;break;}
    dfs(S);
    if(stk.size()!=n+1)return cout<<"NO\n",void();
    reverse(stk.begin(),stk.end());
    cout<<"YES\n";
    for(int i=0;i<n;++i)
        cout<<ans[{stk[i],stk[i+1]}]<<" \n"[i==n-1];
}
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    solve();
    return 0;
}

Bonus

实际上如果要求输出边的编号而非节点编号,可以把前驱节点的编号作为 dfs 的参数,然后在相应应该入栈的时候把边的编号入栈即可。

(感谢zeq大绳的教诲)

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int head[N<<2],nex[N<<2],to[N<<2];
int tmp[N<<2];
int tote;
inline void add(int u,int v)
{
    tote++;
    nex[tote]=head[u];
    head[u]=tote;
    to[tote]=v;
}
int vis[N<<1];
vector<int> stk;
inline void dfs(int pre_edge,int x)
{
    for(;head[x];head[x]=nex[head[x]])
    {
        if(vis[head[x]])continue;
        vis[head[x]]=vis[head[x]^1]=1;
        dfs(head[x]>>1,to[head[x]]);
    }   
    if(pre_edge)stk.push_back(pre_edge);
}   
int a[N],b[N];
int n;
inline void reset(int cnt)
{
    tote=1;
    stk.clear();
    for(int i=0;i<=2*cnt;++i)head[i]=0;
    for(int i=0;i<=2*n+10;++i)vis[i]=0;
}
inline void solve()
{   
    cin>>n;
    int cnt=0;
    for(int i=1;i<=n;++i)
    {
        cin>>a[i]>>b[i];
        tmp[++cnt]=a[i],tmp[++cnt]=b[i];
    }
    sort(tmp+1,tmp+cnt+1);
    cnt=unique(tmp+1,tmp+cnt+1)-tmp-1;
    reset(cnt);
    vector<int> deg(2*cnt+2,0);  
    for(int i=1;i<=n;++i)
    {
        a[i]=lower_bound(tmp+1,tmp+cnt+1,a[i])-tmp;
        b[i]=lower_bound(tmp+1,tmp+cnt+1,b[i])-tmp;
        add(a[i],b[i]+cnt),add(b[i]+cnt,a[i]);
        deg[a[i]]++,deg[b[i]+cnt]++;
    }    
    int count=0,S=0;
    for(int i=1;i<=2*cnt;++i)
    {
        if(deg[i]&1)
        {
            count++;
            if(!S)S=i;
        }
    }
    if(count&&count!=2)return cout<<"NO\n",void();
    if(!S)for(int i=1;i<=2*cnt;++i)if(deg[i]){S=i;break;}
    dfs(0,S);
    if(stk.size()!=n)return cout<<"NO\n",void();
    reverse(stk.begin(),stk.end());
    cout<<"YES\n";
    for(int i=0;i<n;++i)
        cout<<stk[i]<<" \n"[i==n-1];
}
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    int T;cin>>T;
    while(T--)solve();
    return 0;
}

汉密尔顿路径和欧拉路径

实际上很多时候如果建模稍稍偏差,那么本可以用欧拉路径解决的问题,就会变成 NP-Hard 的 Hamilton 路径问题。

图的构成要素无非两点:点和边,如果有一个问题,要求在给定限制条件下用上所有给定的 \(n\) 个元素组成一个序列。

  1. 如果把这 \(n\) 个元素作为点,那这就是个 Hamilton 路径问题了。
  2. 如果把其视为边,那么就变成了 Euler 路径问题,可以正常求解较大范围的数据。

以上例子见 洛谷P1127 和 CF2210E。

posted @ 2025-06-02 22:24  Hanggoash  阅读(47)  评论(0)    收藏  举报
动态线条
动态线条end