• P5022 旅行 题解


    CSDN同步

    原题链接

    简要题意:

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

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

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

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

    那么就有这样的性质:

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

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

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

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

    2. 当前是叶子节点,那么就结束,回溯到第 (1) 步。

    最后统计答案即可,其实就是一个 ( ext{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
    ",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 ightarrow 2 ightarrow 5 ightarrow 4).

    你会发现,你走的路径始终是一棵树,就是说会有一条边不走。

    但是,由于你走的 是树,所以必然不存在环,也就是说 不走的那条边一定在环上

    那么很简单了,找出环,然后枚举删去环上的每一条边,接着跑上面的暴力即可。

    那你会问了:删边我会,有些细节我也会,那怎么 基环树找环 呢?

    我们可以从 ( exttt{Tarjan}) 算法上找灵感。强连通分量的出现 必然会有一个环

    ( exttt{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;
    }
    
  • 相关阅读:
    多线程学习
    Redis学习2
    Redis学习1
    封装
    创建对象内存分析
    稀疏数组
    反转数组、冒泡排序
    可变参数
    .net core 3.x Web Api + Docker个人练手项目
    .net 性能优化手段
  • 原文地址:https://www.cnblogs.com/bifanwen/p/12633247.html
Copyright © 2020-2023  润新知