题解 P7771【【模板】欧拉路径】

$\text{Link}$

$\text{upd2021.8.1}:$修改时间复杂度错误的代码,通过评论区的 $\text{hack}$。

$\text{upd2021.8.5}:$增加时间复杂度更优的做法。

题意

给出一个有 $n$ 个点,$m$ 条边构成的有向图,求其字典序最小的欧拉路径。

$1\le n\le10^5,1\le m\le2\times10^5$

思路

是新模板,写个题解。

首先,我们需要了解什么是欧拉路径

通过图中所有边恰好一次且行遍所有顶点的通路称为欧拉路径,又称欧拉通路、欧拉道路。

如何判断一个图是否存在欧拉路径?

当然,如果图是欧拉图的话,即有欧拉回路时肯定存在欧拉路径。对于其余情况,考虑欧拉路径中相邻两个结点之间有边相连,除了路径中第 $1$ 个和第 $m+1$ 个点,每个点都会被进入一次、离开一次,且每条边都被遍历过。即除了起点、终点,每个点的入度都等于出度对于起点,入度比出度少一,对于终点,入度比出度大一

至此,我们就可以 $O(n+m)$ 判断是否有解了。若有解,还可求出起点与终点。

考虑求解,从起点开始 $\text{dfs}$,对于当前搜到的点,随意选择一条以前未选过的与其相连的边继续搜索,退出搜索时压入栈中,最后将栈中所有元素输出即可。

显然,连入每个点和其连出的边都会被遍历一次,对于求任意一条欧拉路径,我们无需考虑每个点被遍历的先后顺序,每次搜至某一点都会使其入度减少一,继续搜时有两种情况:

  • 若有回到该点的环,即该点还有入度:
    • 回到该点的环,则此次遍历环使得其入度、出度均减一;
    • 进入其他环内,则由于连向此点的点还有出度,必有入度,能够对进入的环进行此分析,最终将进入的环内的点的入度均变为 $0$(若里面有终点,其入度为 $1$)后必可回到该点(因为已经证明有欧拉路径);
  • 没有回到该点的环,则此时入度已为 $0$,出度只可能为 $1$,向下搜之后此点出、入度均变为 $0$。

最终每个点的入度、出度均变为 $0$,故此算法是正确的,时间复杂度为 $O(n+m)$。

考虑原问题,即求字典序最小的欧拉路径。

考虑贪心,靠前的位置应尽可能小,使每次转移的点尽量小即可,可以预先将每个点连出的所有点按照编号大小排序,时间复杂度 $O(n+m\log m)$。

其中有一个细节:若每扫边次都从 $0$ 开始,即以下搜索代码:

inline void dfs(int x){
    for(int i=0;i<a[x].size();i++){
        int t=a[x][i].first;
        if(!vis[a[x][i].second]){
            vis[a[x][i].second]=1;
            dfs(t);
        }
    }
    ans.push(x);
}

的复杂度可以通过构造 $10^5$ 条 $1\to2$,$10^5$ 条 $2\to1$ 的边卡到 $O(m^2)$。

解决方案就是记录目前删到第几条边,从删到的下一条边开始枚举,时间复杂度才能正确。

$O(n+m\log m)$ 代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
namespace IO{//by cyffff

}
#define mpr make_pair
const int N=1e5+10;
int cnt,n,m,ind[N],oud[N],st[N];
vector<pair<int,int>>a[N];
bool vis[N<<1];
stack<int>ans;
inline void dfs(int x){
    for(int i=st[x];i<a[x].size();i=max(i+1,st[x])){
        int t=a[x][i].first;
        if(!vis[a[x][i].second]){
            vis[a[x][i].second]=1;
            st[x]=i+1;
            dfs(t);
        }
    }
    ans.push(x);
}
int main(){
    int s=0,t=0;
    n=read(),m=read();
    for(int i=1;i<=m;i++){
        int u=read(),v=read();
        oud[u]++,ind[v]++;
        a[u].push_back(mpr(v,i));
    }
    for(int i=1;i<=n;i++)
        if(ind[i]!=oud[i]){
            cnt++;
            if(ind[i]==oud[i]-1)
                s=i;
            if(ind[i]==oud[i]+1)
                t=i;
        }
    if(cnt!=0&&cnt!=2)
        return puts("No"),0;
    if(cnt==0)
        s=t=1;
    if(!s||!t)
        return puts("No"),0;
    for(int i=1;i<=n;i++)
        sort(a[i].begin(),a[i].end());
    dfs(s);
    while(!ans.empty()){
        write(ans.top()),putc(' ');
        ans.pop();
    }
    flush();
}

当然,如果使用计数排序/基数排序等方法可以将时间复杂度降至 $O(n+m)$,但由于常数原因,在实际运行中没有 sort 快,具体代码见这里

再见 qwq~

posted @ 2021-07-31 22:26  ffffyc  阅读(51)  评论(0)    收藏  举报  来源