P5022 旅行 题解

CSDN同步

原题链接

简要题意:

给定一棵树(或基环树),每个节点只能至多回溯一次,求遍历整棵树的最小字典序。

基环树概念:树多一条边,即树上出现且仅出现一个环。

作为 \(\texttt{NOIP2018 Day2 T1}\),确实有些难度。不过我们从部分分开始想。

对于 \(60 \%\) 的数据,给定的是树。

那么就有这样的性质:

  • \(1\) 为根,并从 \(1\) 开始遍历肯定是最优的。(因为 \(1\) 的字典序最小)

  • 假设 \(u\) 的父亲节点是 \(v\),那么 \(u\) 想要回溯到 \(v\) 的条件应该是 \(u\) 的所有子树已经遍历完毕。因为如果没有遍历完就回溯到 \(v\),后面不可能有其它边伸向这个子树。(当然除非是基环树)

根据上面两条性质,我们草稿一些程序步骤:

  1. 对于当前节点,按照其儿子的大小进行遍历。即 先遍历第 \(1\) 小,然后第 \(2\)\(\cdots \cdots\)

  2. 当前是叶子节点,那么就结束,回溯到第 \(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}\) 算法上找灵感。强连通分量的出现 必然会有一个环

\(\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;
}
posted @ 2020-04-04 18:53  bifanwen  阅读(143)  评论(0编辑  收藏  举报