【题目来源】
https://www.luogu.com.cn/problem/P1377
【题目描述】
众所周知,二叉查找树的形态和键值的插入顺序密切相关。准确的讲:
1.空树中加入一个键值 k,则变为只有一个结点的二叉查找树,此结点的键值即为 k。
2.在非空树中插入一个键值 k,若 k 小于其根的键值,则在其左子树中插入 k,否则在其右子树中插入 k。
我们将一棵二叉查找树的键值插入序列称为树的生成序列,现给出一个生成序列,求与其生成同样二叉查找树的所有生成序列中字典序最小的那个,其中,字典序关系是指对两个长度同为 n 的生成序列,先比较第一个插入键值,再比较第二个,依此类推。
【输入格式】
第一行,一个整数 n,表示二叉查找树的结点个数。
第二行,有 n 个正整数 k1,k2,⋯,kn,表示生成序列,简单起见,k1∼kn 为一个 1 到 n 的排列。
【输出格式】
一行,n 个正整数,为能够生成同样二叉查找树的所有生成序列中最小的。
【输入样例】
4
1 3 4 2
【输出样例】
1 3 2 4
【数据范围及约定】
对于 20% 的数据,1≤n≤10。
对于 50% 的数据,1≤n≤100。
对于 100% 的数据,1≤n≤10^5。
【算法分析】
● 题目中所提及到的“现给出一个生成序列,求与其生成同样二叉查找树的所有生成序列中字典序最小的那个”是一个必须理清的关键。意思是说,存在多个序列,能生成同样的二叉查找树,需输出字典序最小的那个。如本题样例中,1 3 4 2 与 1 3 2 4 都能够生成同样的二叉查找树,但 1 3 2 4 的字典序更小,所以输出了 1 3 2 4。
那么本题如何保证输出字典序最小的序列呢?由题设的构建条件知,所构建的二叉查找树的所有子树结点值相对于根结点而言都满足“左小右大”的规则,因此对其进行先序遍历便能得到字典序最小的序列。
例如,按照题设规则,即“左小右大”规则,对本题样例 1,3,4,2 构建生成的二叉查找树如下所示。
然后,对此图进行先序遍历,可得到字典序最小的输出 1,3,2,4。
● 本题可采用笛卡尔树进行求解。笛卡尔树结构由 Vuillmin(1980) 在解决范围搜索的几何数据结构问题时提出。笛卡尔树是一种非常特殊的二叉查找树(BST)。每个结点有两个信息 (pri, val),如果只考虑 pri,它是一棵二叉查找树,如果只考虑 val,它是一个小根堆。其中,pri 是结点插入顺序,或称优先级。val 是结点的值。也就是说,笛卡尔树具有二叉查找树和堆的双重特性。
● 如何构建笛卡尔树?(参考于:https://wansuanfa.com/index.php/776)
笛卡尔树的构建步骤如下:(构建前需对 pri 递增排序,或选择数组元素的默认递增下标)
(1)用数组中的第一个元素创建根结点。
(2)遍历数组中剩下的元素,如果新元素比根结点小,让根结点成为新结点的左子结点,然后新结点成为根结点。
(3)如果新结点比根结点大,就从根结点沿着最右边这条路径一种往右走,直到找到比新结点大的结点 node ,也可能没找到。如果找到,就让新结点替换 node 结点的位置,让 node 结点变成新结点的左子结点。如果没找到,就让新结点成为最后查找的那个结点的右子结点。
(4)重复上面步骤(2),(3),直到元素全部遍历完。
● 构建笛卡尔树的关键点解析(参考于:https://wansuanfa.com/index.php/776)
(1)如果新结点比根结点小,根据小根堆(元素的值满足小根堆)的性质,那么新结点一定是根结点的父节点。又因为根结点的下标比新结点小,根据二叉查找树(元素的下标满足二叉查找树)的性质,根结点只能是新结点的左子树。
(2)如果新结点比根结点大,那么新结点只能往右边查找,不能往左边,如果往左边就不满足二叉查找树的性质了。如果比右子结点还大,就继续往右,所以只要比右子结点大就会一直往右。如果比右子结点小,根据小根堆的性质,新结点就会成为这个右子结点的父结点,根据二叉查找树的性质,这个右子结点要成为新结点的左子结点(因为右子结点的下标比新结点小)。
下图为构建笛卡尔树的过程示意图。通过构建过程,易知如何维护笛卡尔树每个结点的两个信息 (pri, val),以及如何保障笛卡尔树具有二叉查找树和堆的双重特性。由图可知,构建方法为以 val 值作为结点,并用其构建小根堆。当需要调整时,以结点插入顺序(或称优先级 pri)为依据,将对应结点置于左子树或右子树,从而保证“如果只考虑 pri,它是一棵二叉查找树”的特性。
简言之,一棵笛卡尔树,结点的值为 val,结点的位置为 pri 所致。
● 单调栈:https://blog.csdn.net/hnjzsyjyj/article/details/117370314
单调栈是一种非常适合处理“下一个更大元素(Next Greater Number)”问题的数据结构。
笛卡尔树的构建过程中,常还需借助“单调栈”这种数据结构,以保证在线性时间内完成笛卡尔树的构建。单调栈中保存的始终是笛卡尔树的右链,即由笛卡尔树的“根结点、根结点的右儿子、根结点的右儿子的右儿子、……”组成的链。并且,为了维护笛卡尔树的小根堆特性,单调栈中的元素值从栈顶到栈底需依次递减,且栈底存储的是根结点。由于每个元素最多进出站各一次,所以时间复杂度为 O(n)。
● 快读:https://blog.csdn.net/hnjzsyjyj/article/details/120131534
在数据量比较大的时候,即使使用 scanf 函数读入数据也会超时,这时就需要使用“快读”函数了。“快读”函数之所以快,是因为其中的 getchar 函数要比 scanf 函数快。 网传,“快读”函数能以比 scanf 函数快5倍的速度读入任意整数。
貌似,笛卡尔树问题常用到快读。
● 本题代码与 https://blog.csdn.net/hnjzsyjyj/article/details/138353654 及其相似,可对比学习。
【算法代码】
#include <bits/stdc++.h>
using namespace std;typedef long long LL;
const int maxn=1e5+5;int a[maxn];
int n;
LL lson[maxn],rson[maxn];stack<int> s;int read() { //fast readint x=0,f=1;char c=getchar();while(c<'0' || c>'9') { //!isdigit(c)if(c=='-') f=-1; //- is a minus signc=getchar();}while(c>='0' && c<='9') { //isdigit(c)x=x*10+c-'0';c=getchar();}return x*f;
}void print(int x) {if(x==0) return;cout<<x<<" ";print(lson[x]);print(rson[x]);
}int main() {scanf("%d",&n);for(int i=1; i<=n; i++) {int id;id=read();a[id]=i;}for(int i=1; i<=n; i++) {while(!s.empty() && a[s.top()]>a[i]) {lson[i]=s.top();s.pop();}if(!s.empty()) rson[s.top()]=i;s.push(i);}int root;while(!s.empty()) {root=s.top();s.pop();}print(root);return 0;
}/*
in:
4
1 3 4 2out:
1 3 2 4
*/
【参考文献】
https://blog.csdn.net/hnjzsyjyj/article/details/138353654
https://blog.csdn.net/hnjzsyjyj/article/details/138353654
https://blog.csdn.net/liufengwei1/article/details/107866930
https://www.cnblogs.com/LSlzf/p/11150484.html
https://www.cnblogs.com/yinyuqin/p/15000037.html
https://zhuanlan.zhihu.com/p/674774129
https://wansuanfa.com/index.php/776
https://blog.csdn.net/pi9nc/article/details/9388761