简要题意:
给定一棵树(或基环树),每个节点只能至多回溯一次,求遍历整棵树的最小字典序。
基环树概念:树多一条边,即树上出现且仅出现一个环。
作为 \(\texttt{NOIP2018 Day2 T1}\),确实有些难度。不过我们从部分分开始想。
对于 \(60 \%\) 的数据,给定的是树。
那么就有这样的性质:
-
以 \(1\) 为根,并从 \(1\) 开始遍历肯定是最优的。(因为 \(1\) 的字典序最小)
-
假设 \(u\) 的父亲节点是 \(v\),那么 \(u\) 想要回溯到 \(v\) 的条件应该是 \(u\) 的所有子树已经遍历完毕。因为如果没有遍历完就回溯到 \(v\),后面不可能有其它边伸向这个子树。(当然除非是基环树)
根据上面两条性质,我们草稿一些程序步骤:
-
对于当前节点,按照其儿子的大小进行遍历。即 先遍历第 \(1\) 小,然后第 \(2\) 小 \(\cdots \cdots\)
-
当前是叶子节点,那么就结束,回溯到第 \(1\) 步。
最后统计答案即可,其实就是一个 \(\text{dfs}\) 遍历树的变版。
时间复杂度:\(O(n+m)\).
实际得分:\(60pts\).
#pragma GCC optimize(2)
#include<bits/stdc++.h>
using namespace std;
const int N=5e3+1;
inline int read(){char ch=getchar();int f=1;while(ch<'0' || ch>'9') {if(ch=='-') f=-f; ch=getchar();}
int x=0;while(ch>='0' && ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();return x*f;}
int n,m,fa[N]; //fa[i] 表示 i 的父亲节点
vector<int>G[N]; //图
vector<int>son[N]; //儿子节点
inline void dfs_fa(int dep,int bs) { //表示 bs 是 dep 的父亲
// printf("%d %d\n",dep,bs);
fa[dep]=bs;
for(int i=0;i<G[dep].size();i++)
if(G[dep][i]!=bs) son[dep].push_back(G[dep][i]); //处理儿子
for(int i=0;i<son[dep].size();i++) dfs_fa(son[dep][i],dep); //遍历儿子,预处理 fa 和 son 数组
}
vector<int> v;
inline void dfs(int dep) {
if(!dep) return;
v.push_back(dep);
for(int i=0;i<son[dep].size();i++) dfs(son[dep][i],num);
}
int main(){
n=read(),m=read();
for(int i=1;i<=m;i++) {
int x=read(),y=read();
G[x].push_back(y);
G[y].push_back(x);
} if(m==n-1) {
dfs_fa(1,0);
for(int i=1;i<=n;i++) sort(son[i].begin(),son[i].end());
dfs(1,1); for(int i=0;i<v.size();i++) printf("%d ",v[i]);
return 0;
}
return 0;
}
那么基环树怎么做呢?
首先,我们找到这个环。以样例 \(2\) 为例,其中的环为 \(3 \rightarrow 2 \rightarrow 5 \rightarrow 4\).
你会发现,你走的路径始终是一棵树,就是说会有一条边不走。
但是,由于你走的 是树,所以必然不存在环,也就是说 不走的那条边一定在环上 。
那么很简单了,找出环,然后枚举删去环上的每一条边,接着跑上面的暴力即可。
那你会问了:删边我会,有些细节我也会,那怎么 基环树找环 呢?
我们可以从 \(\texttt{Tarjan}\) 算法上找灵感。强连通分量的出现 必然会有一个环。
找环和找强连通分量有异曲同工之妙。因此注意细节即可找到。
本人因为常数问题卡在本题多次,请务必注意常数优化。
时间复杂度:\(O(n^2 + m)\).
实际得分:\(100pts\).
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#pragma GCC optimize(5000000)
#pragma GCC optimize("Ofast")
#pragma GCC optimize("inline")
#pragma GCC optimize("-fgcse")
#pragma GCC optimize("-fgcse-lm")
#pragma GCC optimize("-fipa-sra")
#pragma GCC optimize("-ftree-pre")
#pragma GCC optimize("-ftree-vrp")
#pragma GCC optimize("-fpeephole2")
#pragma GCC optimize("-ffast-math")
#pragma GCC optimize("-fsched-spec")
#pragma GCC optimize("unroll-loops")
#pragma GCC optimize("-falign-jumps")
#pragma GCC optimize("-falign-loops")
#pragma GCC optimize("-falign-labels")
#pragma GCC optimize("-fdevirtualize")
#pragma GCC optimize("-fcaller-saves")
#pragma GCC optimize("-fcrossjumping")
#pragma GCC optimize("-fthread-jumps")
#pragma GCC optimize("-funroll-loops")
#pragma GCC optimize("-fwhole-program")
#pragma GCC optimize("-freorder-blocks")
#pragma GCC optimize("-fschedule-insns")
#pragma GCC optimize("inline-functions")
#pragma GCC optimize("-ftree-tail-merge")
#pragma GCC optimize("-fschedule-insns2")
#pragma GCC optimize("-fstrict-aliasing")
#pragma GCC optimize("-fstrict-overflow")
#pragma GCC optimize("-falign-functions")
#pragma GCC optimize("-fcse-skip-blocks")
#pragma GCC optimize("-fcse-follow-jumps")
#pragma GCC optimize("-fsched-interblock")
#pragma GCC optimize("-fpartial-inlining")
#pragma GCC optimize("no-stack-protector")
#pragma GCC optimize("-freorder-functions")
#pragma GCC optimize("-findirect-inlining")
#pragma GCC optimize("-fhoist-adjacent-loads")
#pragma GCC optimize("-frerun-cse-after-loop")
#pragma GCC optimize("inline-small-functions")
#pragma GCC optimize("-finline-small-functions")
#pragma GCC optimize("-ftree-switch-conversion")
#pragma GCC optimize("-foptimize-sibling-calls")
#pragma GCC optimize("-fexpensive-optimizations")
#pragma GCC optimize("-funsafe-loop-optimizations")
#pragma GCC optimize("inline-functions-called-once")
#pragma GCC optimize("-fdelete-null-pointer-checks")
#pragma GCC optimize(2)
//上面是 O3 优化
//注:所有的 i=-~i 等同于 i++ ,用来卡常
//注:所有循环变量的 register int 可以认为等同于 int,用来卡常
#include<bits/stdc++.h>
using namespace std;
const int N=5e3+1;
inline int read(){char ch=getchar();int f=1;while(ch<'0' || ch>'9') {if(ch=='-') f=-f; ch=getchar();}
int x=0;while(ch>='0' && ch<='9') x=(x<<3)+(x<<1)+ch-'0',ch=getchar();return x*f;}
int n,m,fa[N],cnt=0;
vector<int>G[N],v,v1; // v 和 v1 是答案打擂的两个 vector
vector<int>son[N];
int dfn[N],ans[N],f=0; //dfn[i] 是 Tarjan 的时间戳,ans[i] 记录环,f 是环长度
inline void dfs_fa(int dep,int bs) {
fa[dep]=bs;
for(register int i=0;i<G[dep].size();i=-~i)
if(G[dep][i]!=bs) son[dep].push_back(G[dep][i]);
for(register int i=0;i<son[dep].size();i=-~i) dfs_fa(son[dep][i],dep);
}
inline void dfs(int dep) {
if(!dep) return;
v.push_back(dep);
for(register int i=0;i<son[dep].size();i=-~i) dfs(son[dep][i]);
}
inline void find(int u) { //找环
dfn[u]=++cnt;
for(register int i=0;i<G[u].size();i=-~i) {
int v=G[u][i];
if(v==fa[u]) continue;
if(dfn[v]) { //出现返祖边即有环
if(dfn[v]<dfn[u]) continue;
ans[++f]=v;
for(;v!=u;v=fa[v]) ans[++f]=fa[v]; //倒着搜索到环
} else fa[v]=u,find(v); //否则继续
}
}
inline void ep(int u,int v) {
for(register int i=0;i<G[u].size();i=-~i)
if(G[u][i]==v) {
G[u].erase(G[u].begin()+i);
return;
}
} //ep(u,v) 表示在 u 的连边中删掉 v,这里用 vector 较为方便
inline void write(int x) {
if(x<10) {putchar(char(x%10+'0'));return;}
write(x/10); putchar(char(x%10+'0'));
} //卡常多次,加上快写
int main(){
n=read(),m=read();
for(register int i=1;i<=m;i=-~i) {
int x=read(),y=read();
G[x].push_back(y);
G[y].push_back(x);
} if(m==n-1) { //树
dfs_fa(1,0);
for(register int i=1;i<=n;i=-~i) sort(son[i].begin(),son[i].end()); //排序,助于循环遍历
dfs(1); for(register int i=0;i<v.size();i=-~i) printf("%d ",v[i]);
return 0;
} else { //基环树
find(1); //找环
ans[++f]=ans[1]; //细节,因为最后一个点和第一个点也有边,因此扩展
for(register int i=1;i<=m;i++) v1.push_back(n);
for(register int i=1;i<f;i=-~i) {
int u=ans[i],vv=ans[i+1];
memset(fa,0,sizeof(fa));
ep(u,vv); ep(vv,u); //先删边
dfs_fa(1,0); //预处理 fa 和 son 数组
for(register int j=1;j<=n;j++) sort(son[j].begin(),son[j].end()); //排序
dfs(1); if(v<v1) for(int i=0;i<v1.size();i++) v1[i]=v[i]; //比较得到答案,打擂
// vector 的比较就是按照字典序,很方便
v.clear();
for(register int j=1;j<=n;j++) son[j].erase(son[j].begin(),son[j].end()); //把 son 删掉
G[u].push_back(vv);
G[vv].push_back(u);
sort(G[u].begin(),G[u].end());
sort(G[vv].begin(),G[vv].end()); //加边重新排序,其实可以用二分(插入排序)实现
} for(register int i=0;i<v1.size();i=-~i) write(v1[i]),putchar(' '); //输出
}
return 0;
}