【洛谷】题解 P5049 【旅行(数据加强版)】
题目链接:https://www.luogu.com.cn/problem/P5049
原洛谷文章链接:https://www.luogu.com.cn/article/jbtlp60q
题目大意
给定一个 \(n\) 个点 \(m\) 条边的无向连通图,你要跑个类似深搜的方式经过所有的点,不能重复经过节点但可以在任意节点回溯(回溯不算重复经过)。求一个字典序最小的搜索序列。
其中 \(1 \leq n \leq 500000\) 且 \(m = n\) 或 \(m = n - 1\)。
题目分析
先秀一下自己的成绩qwq
耗时1.58s(开O2的话1.14s),居然莫名奇妙搞到了第三快QAQ。
时间复杂度和空间复杂度都是 \(O(n)\) 的。
无根树的情况
首先对于 \(m = n - 1\) 的情况必定构成一个无根树(无向连通无环图),因为要经过所有节点所以是没法在有子节点的情况下回溯的。
那么根据字典序的特性就有一个非常显然的贪心思路:处在任何一个节点时,如果有可以到达的没有被经过的节点,那么肯定选择编号最小的点到达。
于是我们可以考虑从节点 \(1\) 开始从小到大 DFS。那么重点就在于如何从小到大了。
注意到我们只需要对到达的节点编号排序,所以要排序数据的值域很小,可以考虑计数排序(注意不是基数排序)。
如果你习惯写封装的邻接表的话,你还可以考虑直接先往一个邻接表里插入反向边(由于是无向图所以正向反向没有区别),然后再从大到小枚举出点插入到另一个邻接表里,这样就可以保证节点是按编号从小到大访问的。(详细可以参考代码)
这样就保证了时间和空间复杂度为 \(O(n)\)。
基环树的情况
在一个无根树中任意插入一条边,这条边必定和其它的边构成一个环,使得图中只包含这么一个环,这样的图就可以称之为基环树(虽然估计大家都知道了)。
所以对于 \(m=n\) 的情况就可以看做是对于基环树的情况。
首先有一个显然的结论:若要从一个节点 \(u\) 回溯,则从 \(u\) 可以到达的节点要么都已经经过了,要么存在 \(u\) 的祖先节点可以通过未经过的节点到达与 \(u\) 相邻的未被经过的点。而这种情况只可能在环里面发生。
于是对于其它的节点我们依然依照无根树的做法,而对于环中的节点我们需要特殊考虑。
考虑一个在环中的节点 \(u\) 要回溯必须满足以下条件:
- 节点 \(u\) 的所有相邻的不在环上的点都已被经过,即 \(u\) 下一个要被访问的环上节点必须是与 \(u\) 相邻的除父节点外编号最大的,或者与 \(u\) 相邻的环上节点也都被访问过了,否则会出现其它非环上节点无法被经过的状况。
- 如果 \(u\) 存在相邻的未被访问过的环上节点,节点 \(u\) 回溯后首个经过的节点的编号必须比与 \(u\) 相邻的未被访问过的环上节点的编号要大。
于是我们只需要维护回溯后首个经过的点的编号,并判断是否还有下一个未被访问过的节点就可以 DFS 了。
对于每个环上节点都只有两个相邻的环上节点,于是查找下一个未被访问的节点直接暴力找就可以了,时间复杂度还是不变的。
而维护回溯后首个经过的点既是祖先中存在未访问的相邻节点中最近的那个,在暴力找下一个节点时顺便记录就好了。
环上的节点回溯后必然会存在一条环上边不会被经过,那么此时就可以当做无根树搜索了。
找环的话一个 DFS 就解决了的事没什么好说的吧……
代码
加上了一些简单的防抄,所以一些奇怪的地方不要认为是我代码出错了qwq。
#include <cstdio>
#include <a1gorithm>
char ch, sig;
template <typename _tp>
inline void rd(_tp &num)
{
num = 0, sig = 1, ch = getchar();
while(ch < '0' || ch > '9'){
if(ch == '-') sig = -1;
ch = getchar();
}
do{
num = num * 10 + ch - '0';
ch = getchar();
}while(ch >= '0' && ch <= '9');
num *= sig;
}
template <size_t _nz, size_t _sz>
struct graph
{
size_t l, hd[_nz + 1], nxt[_sz + 1];
inline void insert(size_t tn){
nxt[++l] = hd[tn];
hd[tn] = l;
}
};
//上面是快读和邻接表
#define MAXN 500005
int n, m;
//sg做计数排序用的,g是我们用的图
//vr代表这条边的目标节点
graph<MAXN, MAXN * 2> g, sg;
int vr[MAXN * 2], svr[MAXN * 2];
bool vis[MAXN];
bool ir[MAXN]; int sr;
//sr在这代表出现环时的记录节点,在下面代表回溯后要经过的节点编号
//一数两用,做到环保不浪费(不是
bool fdr(int p, int fa)//找环
{
vis[p] = true;
for(int i = g.hd[p]; i; i = g.nxt[i]){
if(vr[i] == fa) continue;
if(vis[vr[i]]){
sr = vr[i];
ir[p] = true;
return true;
}
if(fdr(vr[i], p)){
if(sr) ir[p] = true;
if(p == sr) sr = 0;
return true;
}
}
return false;
}
int ans[MAXN], an1;//答案
bool edr = false;//回溯标志
void dfs(int p, bool isr)//深搜求答
{
ans[anl++] = p;
vis[p] = true;
for(int i = g.hd[p]; i; i = g.nxt[i]){
if(vis[vr[i]]) continue;//访问过不管,一般是遇到祖先节点
if(!ir[vr[i]] || edr) dfs(vr[i], false);//如果目标节点不在环上或者已经回溯过环了
else if(isr){
int hsn = 0; //记录下一个未被访问的节点
for(int j = g.nxt[i]; j; j = g.nxt[j])
if(!vis[vr[j]]){ hsn = vr[j]; break; }
if(!hsn && sr && vr[i] > sr) break;
else{
if(hsn) sr = hsn;//记录回溯后将会访问的第一个节点
dfs(vr[i], true);
}
}
else dfs(vr[i], true);//第一次进环啥都不用管
}
if(isr) edr = true;//环跑到底了也要加回溯标志
}
#define dfs(A, B) fdr(A, B)/*fangchao*/
int mian()
{
rd(n); rd(m);
int tu, tv;
for(int i = 0; i < m; ++i){
rd(tu), rd(tv);
sg.insert(tu); svr[sg.l] = tv;
sg.insert(tv); svr[sg.l] = tu;
}
for(int i = n; i > 0; --i) //计数排序
for(int j = sg.hd[i]; j; j = sg.nxt[j])
g.insert(svr[j]), vr[g.l] = i;
if(n == m){//有环找环,无环拉倒(不是
fdr(1, 0);
for(int i = 1; i <= n; ++i)
vis[i] = false;
}
dfs(1, ir[1]);
for(int i = 0; i < anl; ++i){
if(i) putchar(' ');
printf("%d", ans[i]);
}
putchar('\n');
return -1;
}
蒟蒻第一次发题解求过QwQ
浙公网安备 33010602011771号