day 10 图论选讲之欧拉回路

图论选讲之欧拉回路

欧拉回路

定义:

  • 欧拉路径:经过图中每条边恰好一次的路径(点可以重复经过)。
  • 欧拉回路:起点和终点相同的欧拉路径(也是每条边恰好一次)。

存在条件:

  • 图必须连通。
  • 对于有向图,若所有点的入度出度都相同,则存在欧拉回路;如果有两个点,其中一个入度比出度少 1,另一个入度比出度多 1,那么存在一个欧拉路径,这两个点分别为路径的起点和终点。
  • 对于无向图,若所有点的度都为偶数,则存在欧拉回路;若奇点(度数为奇数的点)个数为 0 或 2,那么存在一个欧拉路径,奇点为 2 的点分别为这个路径的起点和终点。

求法:Hierholzer算法

有向图(P7771 【模板】欧拉路径)

从一个度数为奇数的点开始 dfs,每次转移,要把走的边删掉,最后倒序输出。

注意删边的处理,可以开数组记录每个点已经处理了前几条边,跳过它们。(开一个数组 del[u] 记录节点 u 的邻接表当前遍历到的位置,每次访问一条边后,del[u]++,确保每条边只被扫描一次,保证复杂度正确)

如果想得到字典序最小的欧拉序,在 DFS 前将每个点的邻接表按终点编号从小到大排序即可。

void dfs(int u) {
    // del[u] 是节点 u 的邻接表“当前待处理位置”指针(从 0 开始)
    // 循环条件:i 从 del[u] 开始,直到邻接表末尾
    // 每次循环后 i 更新为 del[u] 的新值(因为 del[u] 会在循环体内修改)
    for (int v : g[u]) {
        // 关键:立即将 del[u] 后移一位,表示“这条边已经被尝试过了”
        // 这样无论后续是否递归或跳过,下次循环都不会再访问这条边
        del[u] = i + 1;
        // 递归进入目标节点,继续深度优先搜索
        dfs(v);
    }
    // 当 u 的所有邻接边都处理完后,将 u 压入栈
    // 这是后序入栈,最终栈从栈底到栈顶是逆序的欧拉路径
    st.push(u);
}

无向图(P2731 骑马修栅栏)

与刚刚不同的是,无向图的边有两个方向,我们不知道这条边的另一个方向之前走没走过,所以需要对边进行编号,然后开一个 used 数组,表示每条边是否已经被走过。每次走边的时候要标记正反两个方向的边都走过,有向图有重边的情况也可以这样处理。

void dfs(int u) {
    for (int i = del[u]; i < g[u].size(); i = del[u]) {
        // 取出当前边:g[u][i] 是一个 pair<int,int>,first 是邻接点,second 是边的编号
        int v = g[u][i].first;
        int id = g[u][i].second;
        del[u] = i + 1;
        // 如果这条边已经被使用过(正反任一次),则直接跳过
        if (used[id]) continue;
        // 标记这条无向边的正反两个方向为已使用
        // 无向边在邻接表中存了两份,且编号成对(id 和 id^1 互为反向)
        used[id] = used[id ^ 1] = 1;
        dfs(v);
    }
    st.push(u);
}

下面看一些例题。

T4 P1341 无序字母对

给定 n 个各不相同的无序字母对(区分大小写,无序即字母对中的两个字母可以位置颠倒)。请构造一个有 (n+1) 个字母的字符串使得每个字母对都在这个字符串中出现。(每对的两个字母在字符串中相邻)要求线性。

不同的字母相当于点,字母对相当于一条连接两个字母的无向边,因为走一条边就相当于走了无序字母对的两个字母。

要让所有无序字母对出现,就相当于要走所有边,跑一遍欧拉路径即可,正好得到走过的所以字母,注意要先判是否有解,上面讲过了。

#include<bits/stdc++.h>
using namespace std;
const int N=505,M=2048;
int n,m;
vector<pair<int,int>>g[N];
bool used[M];
stack<int>st;
int du[N];
void dfs(int u){
    for(auto &e:g[u]){
        int v=e.first,id=e.second;
        if(used[id])continue;
        used[id]=used[id^1]=1;
        dfs(v);
    }
    st.push(u);
}
int main(){
    cin>>m;n=200;
    int cnt=0;int s=10000;
    for(int i=1;i<=m;i++){
        char u,v;cin>>u>>v;
        g[u].push_back({v,cnt++});
        g[v].push_back({u,cnt++});
        du[u]++;du[v]++;
        s=min(s,min((int)u,(int)v));
    }
    for(int i=1;i<=n;i++)sort(g[i].begin(),g[i].end());
    int flag=0;
    for(int i=1;i<=n;i++){
        if(du[i]&1){
            if(!flag)s=i;
            flag++;
        }
    }
    if(flag!=0&&flag!=2){
        cout<<"No Solution";
        return 0;
    }
    dfs(s);
    while(!st.empty()){
        cout<<(char)st.top()<<"";
        st.pop();
    }
    return 0;
}

T6 [POI2011] SMI-Garbage

给定一张 n 个点 m 条边的无向图,每条边居黑白二色之一,且有一个黑或白的目标颜色。

每次操作,能把一个环反色。

请给出一个合法的方案,使得每条边最终都变为目标颜色,或判定不可行。

\(n\le 10^5,m\le10^6\)

显然,有些边需要边颜色,有些不需要,可以证明一条边变换 1 次以上颜色不是最优的,所以每条边只需要变换 0/1 次颜色,我们把初始颜色和目标颜色不同的边拿出来,这些边就是我们要走的。

问题转化为对这张图进行若干次对环反色的操作,使每条边经过一次。

注意到一个环的首尾点相同,剩下所以点出现一次,所以我们在求欧拉回路的 dfs 中,不断走到的点栈中是否有,如果有,把这中间的元素全弹出来,这些元素构成一个环。

#include<bits/stdc++.h>
using namespace std;
const int N=200005,M=4000005;
int n,m;
vector<pair<int,int>>g[N];
bool used[M];
stack<int>st;
int du[N];
int vis[N];
vector<int>ans[N];
int cnt;
void dfs(int u){
    while(!g[u].empty()){
        // 这里运用了直接删边的方式
        auto [v,id]=g[u].back();
        g[u].pop_back();
        if(used[id])continue;
        used[id]=used[id^1]=1;
        dfs(v);
    }
    st.push(u);
    if(vis[u]){
        cnt++;
        st.pop();
        ans[cnt].push_back(u);
        while(st.top()!=u){
            ans[cnt].push_back(st.top());
            vis[st.top()]=0;
            st.pop();
        }
        ans[cnt].push_back(u);
    }
    vis[u]=1;
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin>>n>>m;
    int id=0;
    for(int i=1;i<=m;i++){
        int u,v,s,t;
        cin>>u>>v>>s>>t;
        if(s==t)continue;
        g[u].emplace_back(v,id++);
        g[v].emplace_back(u,id++);
        du[u]++;du[v]++;
    }
    int ji=0;
    for(int i=1;i<=n;i++){
        if(du[i]&1)ji++;
    }
    if(ji!=0){
        cout<<"NIE\n";
        return 0;
    }
    for(int i=1;i<=n;i++){
        if(!vis[i])dfs(i);
    }
    cout<<cnt<<'\n';
    for(int i=1;i<=cnt;i++){
        cout<<ans[i].size()-1<<' ';
        for(int j:ans[i])cout<<j<<' ';
        cout<<'\n';
    }
    return 0;
}

T9 CF209C Trails and Glades

给定一张 n 个点 m 条边的无向图,求最少添加多少条边后,使得图存在从1号点出发又回到1号点的欧拉回路。

回忆欧拉回路的判断条件:若所有点的度都为偶数,则存在欧拉回路。显然我们需要把所以奇点都变成度数为偶数的点。

假设图是连通的,那么设奇点数量为 num,那么我们可以连 num/2 条边把他们相互连接,使得他们的度都变为偶数。

那么如果图不连通,对于每个连通块:

  • 如果这个连通块中没有边,且没有结点 1(因为是从结点 1 开始走的,需要把结点 1 与其他连通块联络),那么这个连通块可以不管,贡献为 0。
  • 否则如果这个连通块有奇点,那么可以把奇点与其他连通块的奇点相连,需要建 num/2 条边。
  • 特殊地,如果没有奇点,但是我们还是要和其他连通块联络的,我们只能选任意一个点,与其他连通块相连,贡献为 1。

都加起来就做完了。

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int n,m;
int du[N];
bool vis[N];
int cnt,a[N];
vector<int>g[N];
vector<int>k[N];
bool e[N];
int ji[N];
void dfs(int u){
    a[u]=cnt;
    k[cnt].push_back(u);
    vis[u]=1;
    for(int v:g[u]){
        e[cnt]=1;
        if(vis[v])continue;
        dfs(v);
    }
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int u,v;cin>>u>>v;
        g[u].push_back(v);
        g[v].push_back(u);
        du[u]++;du[v]++;
    }
    for(int i=1;i<=n;i++){
        if(!vis[i]){
            cnt++;
            dfs(i);
        }
    }
    int x=0;
    for(int i=1;i<=cnt;i++){
        if(a[1]==i||e[i])x++;
    }
    for(int i=1;i<=cnt;i++){
        for(int j:k[i]){
            if(du[j]%2)ji[i]++;
        }
    }
    if(x==1)cout<<ji[1]/2;
    else{
        int ans=0;
        for(int i=1;i<=cnt;i++){
            if(!(a[1]==i||e[i]))continue;
            ans+=ji[i]/2;
            if(ji[i]==0)ans++;
        }
        cout<<ans;
    }
    return 0;
}

T12 CF2110E Melody

机器人拥有一种特殊的乐器,能够产生 n 种不同的声音。每种声音由其音量和音高来表征。如果任意两个连续的声音仅在音量或仅在音高上有所不同,则该音乐被认为是优美的。如果存在三个连续的声音在音量或音高上相同,则该音乐被认为是单调的。你需要创作一段优美且不单调的音乐,其中包含乐器产生的每个声音恰好一次。\(n\le 2\times 10^5,v_i,p_i\le10^9\)

两个连续声音在一方面不同、三个连续声音在一方面不同,这两个性质就相当于两个方面交替变化,即如果第一、二个声音音量不同,那么第二、三个声音必定音调不同,那么第三、四个声音必定音量不同……以此类推。

那么,一个声音可以看作一个 \((v,p)\) 的连边,注意边无向,那么一段优美且不单调的音乐,就是图上的一个路径,因为这条路径上的 v,p 交替改变。

所以,在这个无向图上找一个欧拉路径,就是每个声音出现一次且满足要求的答案。

细节:要先对 \(v_i,p_i\) 离散化,注意要一起离散化,否则可能出现一个 v 和一个 p 在图上的信息相同。

#include<bits/stdc++.h>
using namespace std;
const int N=400005;
int n,m,t;
vector<pair<int,int>>g[N];
int deg[N];
bool vis[N];
struct node{
    int x,y,id;
}v[N],p[N];
bool cmp(node a,node b){
    return a.x<b.x;
}
bool cmp2(node a,node b){
    return a.id<b.id;
}
bool used[N*2];
int a[N*2];
stack<int>st;
void dfs(int u){
    while(!g[u].empty()){
        auto pr=g[u].back();
        g[u].pop_back();
        int v=pr.first;
        int id=pr.second;
        if(used[id])continue;
        used[id]=used[id^1]=1;
        dfs(v);
        st.push(a[id]);
    }
}
void dfs2(int u){
    vis[u]=1;
    for(auto pr:g[u]){
        int v=pr.first;
        if(!vis[v])dfs2(v);
    }
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>t;
    while(t--){
        cin>>m;
        for(int i=1;i<=m;i++){
            cin>>v[i].x>>p[i].x;
            v[i].id=p[i].id=i;
        }
        sort(v+1,v+m+1,cmp);
        int cnt1=0;
        for(int i=1;i<=m;i++){
            if(i==1||v[i].x!=v[i-1].x)cnt1++;
            v[i].y=cnt1;
        }
        sort(p+1,p+m+1,cmp);
        int cnt2=0;
        for(int i=1;i<=m;i++){
            if(i==1||p[i].x!=p[i-1].x)cnt2++;
            p[i].y=cnt2;
        }
        sort(v+1,v+m+1,cmp2);
        sort(p+1,p+m+1,cmp2);
        int tot=cnt1+cnt2;
        for(int i=1;i<=tot;i++){
            g[i].clear();
            deg[i]=vis[i]=0;
        }
        while(!st.empty())st.pop();
        int cnt=1;
        for(int i=1;i<=m;i++){
            int u=v[i].y;
            int w=cnt1+p[i].y;
            int id1=++cnt;
            int id2=++cnt;
            g[u].push_back({w,id1});
            g[w].push_back({u,id2});
            a[id1]=a[id2]=i;
            deg[u]++;
            deg[w]++;
        }
        int s=-1;
        int odd=0;
        for(int i=1;i<=tot;i++){
            if(deg[i]==0)continue;
            if(deg[i]&1){
                odd++;
                s=i;
            }
        }
        if(odd>2){
            cout<<"NO\n";
            continue;
        }
        if(s==-1){
            for(int i=1;i<=tot;i++){
                if(deg[i]>0){
                    s=i;
                    break;
                }
            }
        }
        fill(used,used+cnt+2,0);
        dfs2(s);
        bool ok=1;
        for(int i=1;i<=tot;i++){
            if(deg[i]&&!vis[i]){
                ok=0;
                break;
            }
        }
        if(!ok){
            cout<<"NO\n";
            continue;
        }
        dfs(s);
        if(st.size()!=m){
            cout<<"NO\n";
            continue;
        }
        cout<<"YES\n";
        while(!st.empty()){
            cout<<st.top()<<" ";
            st.pop();
        }
        cout<<"\n";
    }
    return 0;
}
posted on 2026-07-16 20:56  郑浩崎  阅读(7)  评论(0)    收藏  举报