[NOIP - 2018]旅行 O(n log n) —— 基环树

P5049 旅行(数据加强版)

题目描述

  • 小 Y 是一个爱好旅行的 OIer。她来到 X 国,打算将各个城市都玩一遍。
  • 小Y了解到, X国的 n 个城市之间有 m 条双向道路。每条双向道路连接两个城市。 不存在两条连接同一对城市的道路,也不存在一条连接一个城市和它本身的道路。并且, 从任意一个城市出发,通过这些道路都可以到达任意一个其他城市。小 Y 只能通过这些 道路从一个城市前往另一个城市。
  • 小 Y 的旅行方案是这样的:任意选定一个城市作为起点,然后从起点开始,每次可 以选择一条与当前城市相连的道路,走向一个没有去过的城市,或者沿着第一次访问该 城市时经过的道路后退到上一个城市。当小 Y 回到起点时,她可以选择结束这次旅行或 继续旅行。需要注意的是,小 Y 要求在旅行方案中,每个城市都被访问到。
  • 为了让自己的旅行更有意义,小 Y 决定在每到达一个新的城市(包括起点)时,将 它的编号记录下来。她知道这样会形成一个长度为 n 的序列。她希望这个序列的字典序 最小,你能帮帮她吗? 对于两个长度均为 n 的序列 A 和 B,当且仅当存在一个正整数 x,满足以下条件时, 我们说序列 A 的字典序小于 B。
    • 对于任意正整数 \(1 ≤ i < x\),序列 A 的第 i 个元素 \(A_i\)​ 和序列 B 的第 i 个元素 \(B_i\)​ 相同。
      序列 A 的第 x 个元素的值小于序列 B 的第 x 个元素的值。

输入格式

  • 输入文件共 m + 1 行。第一行包含两个整数 \(n,m(m ≤ n)\),中间用一个空格分隔。
  • 接下来 m 行,每行包含两个整数 \(u,v (1 ≤ u,v ≤ n)\) ,表示编号为 u 和 v 的城市之 间有一条道路,两个整数之间用一个空格分隔。

输出格式

  • 输出文件包含一行,n 个整数,表示字典序最小的序列。相邻两个整数之间用一个空格分隔。

样例输入1

6 5 
1 3 
2 3 
2 5 
3 4 
4 6

样例输出1

1 3 2 5 4 6

样例输入2

6 6 
1 3 
2 3 
2 5 
3 4 
4 5 
4 6

样例输出2

1 3 2 4 5 6

说明/提示

【数据规模与约定】

  • 对于 \(100\%\) 的数据和所有样例, \(1 \le n \le 500000\)\(m = n - 1\)\(m = n\)

  • 具体规定详见正常版本(除testcase11-13)

Solve

  • 题目大意 : 在一颗树或一颗基环树中求字典序最小的 DFS 序

  • 对于在一颗树上的情况(m==n-1):

    • 用vector存边,Dfs前对每个点可以到达的点的编号用 \(O(n\log n)\) 的时间从小到大排个序,再正常的Dfs一遍就是字典序最小的Dfs序
  • 对于在一颗基环树的情况(m==n):

    • Dfs 后肯定是一颗树,树和基环树其实只是差了环上的一条边,先找到环(基本上涉及到基环树的题都需要先找环),\(O(n^2)\) 的做法就是先 \(O(n)\) 的暴力枚举环上的边,进行标记并 \(O(n)\) 的求出Dfs序,最后找到最优的方案即可。
    • 这样在正常版的题目中还需要一些些优化,更别说对于 n 的范围已经达到 5e5 的数据加强版了。
    • 考虑优化,\(O(n\log n)\) 的排序还是可以接受的,\(O(n)\) 的求 Dfs 序也是无法避免的,只能在考虑断那条边这里优化到 \(O(\log n)\) 甚至 \(O(1)\) 的复杂度。
    • 首先想一下最优的遍历方式应该是啥样呢?考虑到可以不走一条边,那就是当前要走的那个点比回溯后第一个点编号大的时候进行回溯就是最优的了,那直接记录一下如果要回溯,递归到的第一个没有递归过的点的编号,每次递归前判断一下,若回溯更优那就强制回溯。
    • 强制回溯需要满足的条件:
      1. 这个点 x 要在环上,否则直接强制回溯后 x 以及他的子树都不会在遍历到了。
      2. 这个点 x 的父亲节点也必须在环上,否则直接强制回溯后整个环就不会被遍历到了。
      3. 之前没有回溯过,因为每强制回溯一次就会有一条边被忽略,多次强制回溯会造成多条边被忽略,而只能有一条边被忽略才能保证能遍历到所有的点,所以只能强制回溯一次。
      4. 最后当然是强制回溯比不回溯更优才回溯。

Code

#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5;
vector<int> t[N];//存的图
int n, m, pre[N], a[N], cnt;
bool v[N], g, cir[N];
void Fcircle(int x) {//找环
    if (g) return;//基环树只有一个环,找到环就不用再遍历了,直接退出
    v[x] = 1;
    for (int i = 0; i < t[x].size(); ++i) {
        int y = t[x][i];
        if (y == pre[x]) continue;
        //pre[x]存储着由哪个节点到达的x,其实就是父节点,找环的时候会用到
        if (v[y] && !g) {//遇到已经访问过的点就表示找到环了
            int z = x;
            do {
                cir[z] = 1;//cir表示是否在环上
                z = pre[z];
            } while (z != y);//do while标记环上每个点
            cir[y] = g = 1;//对y进行标记,并记录环已经找到
            return;
        }
        pre[y] = x;
        Fcircle(y);
    }
}
void Dfs(int x, int fa, int next) {//next是如果回溯,第一个访问的点的编号
    if (!g && cir[x] && cir[fa] && next < x)
        return g = 1, void();
    //这个判断就是核心了,依次对应上述的 3,1,2,4 ,忘记的可以再翻上去看看
    if (v[x]) return;
    v[x] = 1;
    a[++cnt] = x;//a数组记录dfs序列
    for (int i = 0; i < t[x].size(); ++i) {
        int y = t[x][i];
        if (y == fa) continue;
        int nex = next;
        for (int j = i + 1, b = 0; j < t[x].size() && !b; ++j)
            if (t[x][j] != fa) nex = t[x][j], b = 1;
        //这里找next也是一个重点,上面的for循环找的是y后面第一个非父节点的兄弟节点
        Dfs(y, x, nex);
    }
}
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= m; ++i) {
        int x, y;
        scanf("%d%d", &x, &y);
        t[x].push_back(y);
        t[y].push_back(x);//建图
    }
    for (int i = 1; i <= n; ++i)
        sort(t[i].begin(), t[i].end());
    //对点的编号进行排序,保证每次都会先遍历到编号小的点
    if (n == m) {//如果是基环树就找环
        Fcircle(1); 
        g = 0;//清空找环标记,在之后Dfs中表示是否回溯过
        memset(v, 0, sizeof(v));//清空访问标记
    }
    Dfs(1, 0, n);
    for (int i = 1; i <= n; ++i)
        printf("%d ", a[i]);
    return 0;
}
posted @ 2020-08-21 19:57  Shawk  阅读(121)  评论(1编辑  收藏  举报