[洛谷] [NOIP2018 提高组] 旅行 加强版 - 基环树 | DFS / Tarjan / topoSort

简介: 题目描述小 Y 是一个爱好旅行的 OIer。她来到 X 国,打算将各个城市都玩一遍。小Y了解到, X国的 n 个城市之间有 m 条双向道路。每条双向道路连接两个城市。 不存在两条连接同一对城市的道路,也不存在一条连接一个城市和它本身的道路。并且, 从任意一个城市出发,通过这些道路都可以到达任意一个其他城市。小 Y 只能通过这些 道路从一个城市前往另一个城市。

题目链接:https://www.luogu.com.cn/problem/P5049


题目描述


小 Y 是一个爱好旅行的 OIer。她来到 X 国,打算将各个城市都玩一遍。


小Y了解到, X国的 n 个城市之间有 m 条双向道路。每条双向道路连接两个城市。 不存在两条连接同一对城市的道路,也不存在一条连接一个城市和它本身的道路。并且, 从任意一个城市出发,通过这些道路都可以到达任意一个其他城市。小 Y 只能通过这些 道路从一个城市前往另一个城市。


小 Y 的旅行方案是这样的:任意选定一个城市作为起点,然后从起点开始,每次可 以选择一条与当前城市相连的道路,走向一个没有去过的城市,或者沿着第一次访问该 城市时经过的道路后退到上一个城市。当小 Y 回到起点时,她可以选择结束这次旅行或 继续旅行。需要注意的是,小 Y 要求在旅行方案中,每个城市都被访问到。


为了让自己的旅行更有意义,小 Y 决定在每到达一个新的城市(包括起点)时,将 它的编号记录下来。她知道这样会形成一个长度为 nn 的序列。她希望这个序列的字典序 最小,你能帮帮她吗? 对于两个长度均为 nn 的序列 AA 和 BB,当且仅当存在一个正整数 xx,满足以下条件时, 我们说序列 AA 的字典序小于 BB。


对于任意正整数 1 ≤ i < x1≤i<x,序列 A 的第 i 个元素Ai 和序列 B 的第 i 个元素 Bi相同。

序列 A 的第 x 个元素的值小于序列 B 的第 x 个元素的值。


输入格式


输文件共 m+1 行。第一行包含两个整数 n,m(m≤n),中间用一个空格分隔。


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


输出格式


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


输入


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


输出


1 3 2 5 4 6


输入


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


输出:


1 3 2 4 5 6


该题m有两种情况,对于m = n - 1的情况,这是一颗树,我们只需要从1开始,往后遍历时,每次选择所连的点(未被访问过)中,编号最小的那个(贪心)

对于m = n的情况下,图中绝对会有一个环,这种图叫做基环树,

遇见基环树,首要的任务是找环

① 在学习Tarjan算法的时候,我们可以知道改环中任意两点都是可以相互到达的(SCC),所以说我们可以用Tarjan算法来找出环 最稳妥

② 可以用 dfs 来模拟Tarjan算法的过程来找环

以下为本人两种常用的dfs找环的代码,均可通过此题


bool vis[500007];
int in[500007],tot,rt;
int getLoop(int u,int fa) {
  if(vis[u]) {
    rt = u;
    return 1;
  }
  int tmp;
  vis[u] = true;
  for(int i=head[u]; ~i; i=e[i].nex) {
    int to = e[i].v;
    if(to == fa) continue;
    tmp = getLoop(to,u);
    if(tmp) {
      if(tmp == 1) {
        in[u] = 1;
        if(u != rt) return 1;
      }
      return 2;
    }
  }
  return 0;
}


int getLoop2(int u,int fa) {
  vis[u] = true;
  for(int i=head[u]; ~i; i=e[i].nex) {
    int to = e[i].v;
    if(to == fa) continue;
    if(vis[to]) {
      in[u] = 1;
      return to;
    } else {
      int ret = getLoop2(to,u);
      if(ret == 0) return 0;
      if(ret == -1) continue;
      in[u] = 1;
      if(ret == u) return 0;
      else return ret;
    }
  }
  return -1;
}


对于基环树的情况,大致有两种复杂度的情况{

1. 找到环,然后枚举环上的边,断掉一条边之后,便到了树的情况

2. 因为基环树比树多了一条边,所以在 dfs找答案的时候就可以选择一条边不走。按照贪心策略在dfs过程中,考虑在什么情况下会选择舍弃一条边?{

① 毋庸置疑,首先我们舍弃的这一条边是在环上的

② 舍弃了这条边之后,就立即进行回溯,因为舍弃的边连接的是当前点的最大儿子节点

③ 我们在回溯之后,再往后走的那个点的编号一定是比舍弃的那条边连接的点要小

}

}


以上两种找环的方式时间复杂度相差不大(几乎没有差距)

72a488d5390e407a8cac0b5819a6d225.png

int cnt,head[500007],n,m,flag;
vector<int> ans;
struct node {
  int v,nex,u;
} e[maxn];
void add(int u,int v) {
  e[cnt].u = u;
  e[cnt].v = v;
  e[cnt].nex = head[u];
  head[u] = cnt ++;
}
void init() {
  cnt = 0;
  for(int i=0; i <= 500000; i++) head[i] = -1;
}
bool vis[500007];
int in[500007],tot,rt;
int getLoop(int u,int fa) {
  if(vis[u]) {
    rt = u;
    return 1;
  }
  int tmp;
  vis[u] = true;
  for(int i=head[u]; ~i; i=e[i].nex) {
    int to = e[i].v;
    if(to == fa) continue;
    tmp = getLoop(to,u);
    if(tmp) {
      if(tmp == 1) {
        in[u] = 1;
        if(u != rt) return 1;
      }
      return 2;
    }
  }
  return 0;
}
int getLoop2(int u,int fa) {/// pre_version
  vis[u] = true;
  for(int i=head[u]; ~i; i=e[i].nex) {
    int to = e[i].v;
    if(to == fa) continue;
    if(vis[to]) {
      in[u] = 1;
      return to;
    } else {
      int ret = getLoop2(to,u);
      if(ret == 0) return 0;
      if(ret == -1) continue;
      in[u] = 1;
      if(ret == u) return 0;
      else return ret;
    }
  }
  return -1;
}
void dfs(int u,int fa) {
  ans.push_back(u);
  vis[u] = true;
  vector<int> vet;
  for(int i=head[u]; ~i; i=e[i].nex) {
    int to = e[i].v;
    if(vis[to]) continue;
    vet.push_back(to);
  }
  sort(vet.begin(),vet.end());
  int siz = vet.size();
  for(int i=0; i<siz; i++) {
    int to = vet[i];
    if(vis[to]) continue;
    if(flag && i == siz - 1 && in[u] && in[to] && to > fa) {
      flag = 0;
      continue;
    }
    if(i + 1 < siz) dfs(to,vet[i+1]);
    else dfs(to,fa);
  }
}
int main() {
  n = read,m = read;
  init();
  for(int i=1; i<=m; i++) {
    int u = read,v = read;
    add(u,v),add(v,u);
  }
  memset(vis,0,sizeof vis);
  if(n == m) getLoop(1,0),flag = 1;/// ac 2 3 4 5
  memset(vis,0,sizeof vis);
  dfs(1,n+1);
  for(int i=1; i<=n; i++) {
    printf("%d%c",ans[i-1],i<n?' ':'\n');
  }
  return 0;
}
/**
6 6
1 3
2 3
2 5
3 4
4 5
4 6
**/


Tarjan做法:

e50313b39cbf41c5a78792c84830ecf5.png


时间相差不大,可能多了个 stl 的常数吧

int cnt,head[500007],n,m,flag;
vector<int> ans;
struct node {
  int v,nex,u;
} e[maxn];
void add(int u,int v) {
  e[cnt].u = u;
  e[cnt].v = v;
  e[cnt].nex = head[u];
  head[u] = cnt ++;
}
void init() {
  cnt = 0;
  for(int i=0; i <= 500000; i++) head[i] = -1;
}
bool vis[500007];
int in[500007],tot,tim;
int dfn[500007],low[500007];///,pos[500007];
stack<int> st;///Tarjan use
void Tarjan(int x,int fa) {
  dfn[x] = low[x] = ++ tim;
  st.push(x);
  vis[x] = 1;
  for(int i=head[x]; ~i; i=e[i].nex) {
    int to = e[i].v;
    if(to == fa) continue;
    if(!dfn[to]) {
      Tarjan(to,x);
      low[x] = min(low[x],low[to]);
    } else if(vis[to] == 1) {
      low[x] = min(low[x],dfn[to]);
    }
  }
  if(low[x] == dfn[x]) {
    vector<int> path;
    path.push_back(x);
    while(st.top() != x) {
      path.push_back(st.top());
      vis[st.top()] = 0;
      st.pop();
    }
    st.pop();
    if(path.size() >= 2) {
      for(int i=0; i<path.size(); i++) in[path[i]] = 1;
      return ;
    }
  }
}
void dfs(int u,int fa) {
  ans.push_back(u);
  vis[u] = true;
  vector<int> vet;
  for(int i=head[u]; ~i; i=e[i].nex) {
    int to = e[i].v;
    if(vis[to]) continue;
    vet.push_back(to);
  }
  sort(vet.begin(),vet.end());
  int siz = vet.size();
  for(int i=0; i<siz; i++) {
    int to = vet[i];
    if(vis[to]) continue;
    if(flag && i == siz - 1 && in[u] && in[to] && to > fa) {
      flag = 0;
      continue;
    }
    if(i + 1 < siz) dfs(to,vet[i+1]);
    else dfs(to,fa);
  }
}
int main() {
  n = read,m = read;
  init();
  for(int i=1; i<=m; i++) {
    int u = read,v = read;
    add(u,v),add(v,u);
  }
  memset(vis,0,sizeof vis);
  if(n == m) Tarjan(1,0),flag = 1;/// ac 2 3 4 5
  memset(vis,0,sizeof vis);
  dfs(1,n+1);
  for(int i=1; i<=n; i++) {
    printf("%d%c",ans[i-1],i<n?' ':'\n');
  }
  return 0;
}
/**
6 6
1 3
2 3
2 5
3 4
4 5
4 6
**/


本题在找环的过程中,像极了2017年蓝桥杯决赛的一道题 发现环

其实这个题再想一下的话,就可以发现,可以用拓扑排序做出来

方法大同小异咯

干脆也写了一下,拓扑排序写法:

7414ebb5c5b343ea8a811a5bf5196fbc.png

#define Clear(x,val) memset(x,val,sizeof x)
int cnt,head[500007],n,m,flag;
vector<int> ans;
struct node {
  int v,nex,u;
} e[maxn];
void add(int u,int v) {
  e[cnt].u = u;
  e[cnt].v = v;
  e[cnt].nex = head[u];
  head[u] = cnt ++;
}
void init() {
  cnt = 0;
  for(int i=0; i <= 500000; i++) head[i] = -1;
}
bool vis[500007];
int in[500007],tot,tim;
int dfn[500007],low[500007];///,pos[500007];
stack<int> st;///Tarjan use
int deg[500007];
void topoSort() {
  queue<int> que;
  for(int i=1; i<=n; i++) {
    if(deg[i] == 1) que.push(i),vis[i] = 1;
  }
  while(que.size()) {
    int frt = que.front();
    que.pop(); 
    if(deg[frt] == 1) in[frt] = 0;
    for(int i=head[frt]; ~i; i=e[i].nex) {
      int to = e[i].v;
      deg[to] --;
      if(deg[to] == 1) que.push(to);
    }
  }
}
void dfs(int u,int fa) {
  ans.push_back(u);
  vis[u] = true;
  vector<int> vet;
  for(int i=head[u]; ~i; i=e[i].nex) {
    int to = e[i].v;
    if(vis[to]) continue;
    vet.push_back(to);
  }
  sort(vet.begin(),vet.end());
  int siz = vet.size();
  for(int i=0; i<siz; i++) {
    int to = vet[i];
    if(vis[to]) continue;
    if(flag && i == siz - 1 && in[u] && in[to] && to > fa) {
      flag = 0;
      continue;
    }
    if(i + 1 < siz) dfs(to,vet[i+1]);
    else dfs(to,fa);
  }
}
int main() {
  n = read,m = read;
  init();
  for(int i=1; i<=m; i++) {
    int u = read,v = read;
    add(u,v),add(v,u);
    deg[u] ++;
    deg[v] ++;
  }
  memset(vis,0,sizeof vis);
  if(n == m) Clear(in,1),topoSort(),flag = 1;
  memset(vis,0,sizeof vis);
  dfs(1,n+1);
  for(int i=1; i<=n; i++) {
    printf("%d%c",ans[i-1],i<n?' ':'\n');
  }
  return 0;
}


参考博客:https://blog.csdn.net/xyyxyyx/article/details/103010642

目录
相关文章
【2001NOIP普及组】T3. 求先序排列 试题解析
【2001NOIP普及组】T3. 求先序排列 试题解析
|
5月前
|
C++
【洛谷 P1307】[NOIP2011 普及组] 数字反转 题解(字符串)
**NOIP2011普及组题目:给定整数N,反转其位得到新数。新数首位非0(除非N=0)。输入0时直接输出0,其他情况输出反转后的数,考虑负数及前导0。提供的C++代码实现通过读入字符串,反转数字顺序并处理符号和前导0。**
31 0
|
5月前
|
C++
【洛谷 P1044】[NOIP2003 普及组] 栈 题解(递归+记忆化搜索)
**NOIP2003普及组栈问题**:给定操作数序列1到n,仅允许push(进栈)和pop(出栈)操作。目标是计算所有可能的输出序列总数。输入包含一个整数n(1≤n≤18)。示例输入3,输出5。当队列空时返回1,栈空则只能入栈,栈非空时可入栈或出栈。AC C++代码利用记忆化搜索求解。
74 1
|
5月前
【洛谷 P1002】[NOIP2002 普及组] 过河卒 题解(递归+记忆化搜索)
`NOIP2002`普及组的过河卒问题是一个棋盘路径计数挑战。卒从$(0,0)$出发到$(n,m)$,只能向下或向右移动,马在$(c1,c2)$固定,控制某些点。任务是计算不受马阻挡的路径数。输入是目标和马的位置,输出是路径总数。使用动态规划和记忆化搜索避免重复计算,样例输入$(6,6,3,3)$输出$6$。代码中定义了$f(x,y)$计算$(x,y)$处的路径数,利用边界条件和递推关系计算。
68 0
|
5月前
【洛谷 P2669】[NOIP2015 普及组] 金币 题解(循环)
`NOIP2015`普及组题目,骑士按周期领金币:第一天1枚,随后$n$天每天$n$枚,然后$n+1$天每天$n+1$枚。给定天数$k$,求总金币数。输入$k$,输出金币总数。样例输入6,输出14;输入1000,输出29820。代码使用循环和变量控制周期,累计金币数。
101 0
|
5月前
|
机器学习/深度学习 人工智能
【洛谷 P1028】[NOIP2001 普及组] 数的计算 题解(递推)
在NOIP2001普及组的数的计算题目中,给定自然数`n`,需构造遵循特定规则的合法数列。合法序列始于`n`,新元素不超过前一项的一半。任务是找出所有这样的数列数量。例如,当`n=6`时,合法序列包括`6`, `6,1`, `6,2`, `6,3`, `6,2,1`, `6,3,1`。程序通过动态规划求解,当`i`为奇数时,`a[i] = a[i - 1]`;为偶数时,`a[i] = a[i - 1] + a[i / 2]`。代码中预处理数组`a`并输出`a[n]`作为答案。输入`n`后,程序直接计算并打印合法数列个数。
57 0
|
5月前
|
机器学习/深度学习 存储
【洛谷 P1028】[NOIP2001 普及组] 数的计算 题解(递归)
**NOIP2001普及组数的计算**:给定自然数\( n \),构造数列,新数不超过序列最后一项一半。求合法数列个数。输入\( n \)(\(1 \leq n \leq 10^3\))。样例:输入6,输出6。递归解决,代码中定义函数`f`实现递归计算,总和存储在`cnt`中,最后输出。
51 0
|
5月前
【洛谷 P1088】[NOIP2004 普及组] 火星人 题解(全排列+向量)
**火星人问题摘要:** NOIP2004普及组竞赛中的题目,涉及火星人用手指的排列表示数字。人类需计算火星人数字与给定数值之和的新排列。给定火星人手指数N(≤10000),加上的数M(≤100),以及初始排列,要求输出新排列。30%的数据中N≤15,60%的数据中N≤50。使用`next_permutation`函数找到第M个排列。样例:N=5, M=3, 初始排列1 2 3 4 5,输出1 2 4 5 3。
43 0
|
5月前
|
算法
【洛谷 P1003】[NOIP2011 提高组] 铺地毯 题解(数组+贪心算法)
**NOIP2011 提高组问题:铺地毯** 在第一象限的颁奖典礼场地,有$n$张地毯按编号顺序铺设。需找出覆盖特定点$(x, y)$的最上方地毯编号。输入包括地毯坐标和点坐标,输出地毯编号或-1表示未覆盖。 样例:给定3张地毯,点$(2,2)$被第3张地毯覆盖,输出3;另一样例点$(4,5)$未被覆盖,输出-1。 $30\%$数据$n\leq2$,$50\%$数据坐标$\leq100$,$100\%$数据$n\leq10^4$,坐标$\leq10^5$。 解决方法:从下到上遍历地毯,更新覆盖点的地毯编号。 AC代码略。
48 0
|
5月前
|
C++
【洛谷 P1075】[NOIP2012 普及组] 质因数分解 题解(判断质数)
NOIP2012普及组题目,给定合数$n$由两个不同质数乘积组成,求较大质数。输入一个正整数$n$,输出较大质因子。例如输入21,输出7。代码使用C++,通过循环和质数判断找到能整除$n$的质数,再求另一个质数,并输出较大者。
61 0