大家好,由于蒟蒻 tzc 最近被动态点分治这个学也学不会的毒瘤玩意儿虐得不轻,所以就准备换换脑筋来 Van 同样学也学不会的后缀数组了。

考虑一个非常经典的问题:【模板】后缀排序

一些定义(very important,不要搞混):

  • \(sa_i\) 表示字典序排在第 \(i\) 的字符串是谁,也就是一个排名 \(\to\) 下标的映射
  • \(rk_i\) 表示后缀 \(i\)((注:在本文中,后缀 \(i\) 就是开头位置为 \(i\) 的后缀)的排名是啥,也就是一个下标 \(\to\) 排名的映射

字符串比较是 \(\mathcal O(n)\) 的,所以暴力排序复杂度是 \(n^2\log n\) 的,显然不是我们所满足的。

考虑模拟暴力排序的过程,我们以这 \(n\) 个后缀的第一位为第一关键字,再以它们的第二位为第二关键字,再以它们的第三位为第三关键字……如果后缀长度不足 \(n\) 就在它后面补上 \(c_0\),其中 \(c_0\) 为某个小于 \(s\) 中所有字符的字符,比如说 \(!\)。

不难发现整个过程可以分“阶段”进行,我们记第 \(k\) 阶段为比较完前 \(k\) 个字符后的结果,设 \(rk_i(k)\) 表示将所有后缀的长度为 \(k\) 的前缀排序后,后缀 \(i\) 的排名。如果有相同那么它们的 \(rk\) 也应该相同

很明显暴力排序的过程就是每次从第 \(k\) 阶段转移到第 \(k+1\) 阶段,具体方法是对每个 \(i\) 将 \((rk_i(k),s_{i+k})\) 当作一个 pair 进行排序,如果 \(i+k>n\) 那么 \(s_{i+k}=0\),即上文中所说的 \(c_0\)。

那么我们考虑,每次能不能不从第 \(k\) 阶段转移到第 \(k+1\) 阶段,而是从第 \(k\) 阶段转移到第 \(2k\) 阶段呢

答案是肯定的。

我们从第 \(k\) 阶段转移到第 \(k+1\) 阶段的思路是:一个长度为 \(k+1\) 个字符串可以断成一个长度为 \(k\) 的子串和一个字符,而这个长度为 \(k\) 的字符串的大小关系我们是知道的,单个字符的大小关系我们也是知道的,所以可以转移。

同样道理一个长度为 \(2k\) 个字符串可以断成两个长度为 \(k\) 的子串,而这两个长度为 \(k\) 的字符串的大小关系我们也是知道的,所以也可以转移。

具体来说,你将 \((rk_i(k),rk_{i+k}(k))\) 当作一个 pair 进行排序,若 \(i+k>n\) 则 \(rk_{i+k}(k)=0\),就可以得到 \(rk_{i}(2k)\)。

算下时间复杂度:倍增 1 log,sort n log,总复杂度 \(n\log^2n\)。

for(int i=1;i<=n;i++) x[i]=mp(mp(s[i],0),i);
sort(x+1,x+n+1);int gr=0;//stage 1
for(int i=1;i<=n;i++){
if(x[i].fi!=x[i-1].fi) gr++;//note that if s[i]=s[j],rk(1,i)=rk(1,j), so we have to do a process similar to discretion
rk[x[i].se]=gr;
}
for(int k=1;k<n;k<<=1){//from stage k to stage 2k
for(int i=1;i<=n;i++){
if(i+k<=n) x[i]=mp(mp(rk[i],rk[i+k]),i);//sort the pairs of (rk(i,k),rk(i+k,k))
else x[i]=mp(mp(rk[i],0),i);//i+k>n
}
sort(x+1,x+n+1);gr=0;
for(int i=1;i<=n;i++){
if(x[i].fi!=x[i-1].fi) gr++;
rk[x[i].se]=gr;
}
}

然而这个程序是 TLE 90 的,这说明有比 \(n\log^2n\) 更优的算法。那么怎么进一步优化呢?

不难发现在我们所有 pair 中的两个数最大不过 \(n\),于是我们回忆起一个东西叫做 gay♂ 基数排序。

基数排序是一种能够在 \(\mathcal O(n+m)\) 的时间内对某个数组进行排序的算法,其中 \(m\) 为数组的值域

感觉 SA 鸽了这么久就是因为不会这玩意儿啊 qwq。

先考虑一维的基数排序,我们及一个桶 \(c_i\) 表示值为 \(i\) 的 \(a_i\) 有多少个。然后对 \(c_i\) 做一遍前缀和。

显然,如果 \(a_i\) 互不相同排名为 \(c_{a_i}\) 的数就是 \(a_i\),因为 \(c_{a_i}\) 就是比 \(a_i\) 小的个数。

那如果有 \(a_i\) 相同怎么办呢?那显然排名在 \((c_{a_i-1},c_{a_i}]\) 之间的数都等于 \(a_i\)。

考虑从后到前扫一遍,每一次将 \(a_i\) 的排名设为 \(c_{a_i}\) 并令 \(c_{a_i}\) 减 \(1\),这样你发现所有相同的 \(a_i\) 都被赋上了一个不同的排名,并且对于相同的 \(a_i\),越靠后的排名越靠后,也就是我们对 \((a_i,i)\) 这个 pair 排了一遍序。

回过头来,如果是二维的,例如在后缀排序中遇到的这些 pair 怎么办呢?

考虑我们在刚才的基数排序中为什么要从后到前扫,因为这样一来如果有 \(a_i\) 相同的那么靠后的排名永远比考前的排名高,也就是按照第二维降序的顺序更新桶。那这样一来就好办了,先将所有 \(y_i\) 排个序,然后按照 \(y_i\) 从大到小的顺序更新桶。就可以实现 \(\mathcal O(n)\) 排序了。

注意到如果有相同的 pair 那么它们的 \(rk\) 也应该相同,所以排完序后还要做一遍类似于离散化的操作。

const int M=123;//the ASCII of 'z' is 122
pii x[MAXN+5];
int n,sa[MAXN+5],rk[MAXN+5],buc[MAXN+5],seq[MAXN+5];
void get_sa(){
for(int i=1;i<=n;i++) buc[s[i]]++;
for(int i=1;i<=M;i++) buc[i]+=buc[i-1];
for(int i=n;i;i--) sa[buc[s[i]]--]=i;
int gr=0;
for(int i=1;i<=n;i++){
if(s[sa[i]]!=s[sa[i-1]]) gr++;
rk[sa[i]]=gr;
}//calculate the ranks of stage 1
for(int k=1;k<n;k<<=1){//from stage k to stage 2k
for(int i=1;i<=n;i++){
if(i+k<=n) x[i]=mp(rk[i],rk[i+k]);//sort the pairs of (rk(i,k),rk(i+k,k))
else x[i]=mp(rk[i],0);
}
memset(buc,0,sizeof(buc));//clear the bucket
for(int i=1;i<=n;i++) buc[x[i].se]++;//sort the values of the second dimension
for(int i=1;i<=n;i++) buc[i]+=buc[i-1];
for(int i=n;i;i--) seq[buc[x[i].se]--]=i;//seq[i] means the i-smallest index sorted by the second dimension
memset(buc,0,sizeof(buc));
for(int i=1;i<=n;i++) buc[x[i].fi]++;//sort the values of the first dimension
for(int i=1;i<=n;i++) buc[i]+=buc[i-1];
for(int i=n;i;i--) sa[buc[x[seq[i]].fi]--]=seq[i];//update the bucket in descending order of the second dimension
gr=0;
for(int i=1;i<=n;i++){
if(x[sa[i]]!=x[sa[i-1]]) gr++;//note that if x[i]=x[j],rk(i)=rk(j), so we have to do a process similar to discretion
rk[sa[i]]=gr;
}
}
}

这样过是过了,但是我们发现最慢的一个点跑了整整 1s!显然常数上天了,我们考虑进行常数优化。

首先我们发现其实不用用桶第二维进行排序。对于 \(i=n-k+1\sim n\) 显然它们的第二维都是 \(0\),应该排在最前面;对于 \(i\leq n-k\) 我们可以用之前求得的 \(k\) 阶段的排名算出,所以 2 遍循环就行了。

现在最慢的一个点跑了 750ms,大约省去了 \(1/4\) 的常数

void get_sa(){
for(int i=1;i<=n;i++) buc[s[i]]++;
for(int i=1;i<=M;i++) buc[i]+=buc[i-1];
for(int i=n;i;i--) sa[buc[s[i]]--]=i;
int gr=0;
for(int i=1;i<=n;i++){
if(s[sa[i]]!=s[sa[i-1]]) gr++;
rk[sa[i]]=gr;
}
// for(int i=1;i<=n;i++) printf("%d%c",rk[i],(i==n)?'\n':' ');
for(int k=1;k<=n;k<<=1){
for(int i=1;i<=n;i++){
if(i+k<=n) x[i]=mp(rk[i],rk[i+k]);
else x[i]=mp(rk[i],0);
}
int num=0;
for(int i=n-k+1;i<=n;i++) seq[++num]=i;
for(int i=1;i<=n;i++) if(sa[i]>k) seq[++num]=sa[i]-k;
memset(buc,0,sizeof(buc));
for(int i=1;i<=n;i++) buc[x[i].fi]++;
for(int i=1;i<=n;i++) buc[i]+=buc[i-1];
for(int i=n;i;i--) sa[buc[x[seq[i]].fi]--]=seq[i];
gr=0;
for(int i=1;i<=n;i++){
if(x[sa[i]]!=x[sa[i-1]]) gr++;
rk[sa[i]]=gr;
}
}
}

继续卡!我们发现,并不是每次的值域都能达到 \(n\) 那么大,所以可以记录一个 \(vmax\) 表示当前阶段的值域,更新桶的时候只用对 \([1,vmax]\) 做前缀和。

现在最慢的一个点跑了 600ms:

void get_sa(){
int vmax=123;
for(int i=1;i<=n;i++) buc[s[i]]++;
for(int i=1;i<=vmax;i++) buc[i]+=buc[i-1];
for(int i=n;i;i--) sa[buc[s[i]]--]=i;
int gr=0;
for(int i=1;i<=n;i++){
if(s[sa[i]]!=s[sa[i-1]]) gr++;
rk[sa[i]]=gr;
} vmax=gr;
// for(int i=1;i<=n;i++) printf("%d%c",rk[i],(i==n)?'\n':' ');
for(int k=1;k<=n;k<<=1){
for(int i=1;i<=n;i++){
if(i+k<=n) x[i]=mp(rk[i],rk[i+k]);
else x[i]=mp(rk[i],0);
}
int num=0;
for(int i=n-k+1;i<=n;i++) seq[++num]=i;
for(int i=1;i<=n;i++) if(sa[i]>k) seq[++num]=sa[i]-k;
memset(buc,0,sizeof(buc));
for(int i=1;i<=n;i++) buc[x[i].fi]++;
for(int i=1;i<=vmax;i++) buc[i]+=buc[i-1];
for(int i=n;i;i--) sa[buc[x[seq[i]].fi]--]=seq[i];
gr=0;
for(int i=1;i<=n;i++){
if(x[sa[i]]!=x[sa[i-1]]) gr++;
rk[sa[i]]=gr;
}
vmax=gr;
}
}

我们还注意到,如果 \(n\) 个字符串都不相同了就可以 break 了,所以可以再加一句 if(vmax==n) break;

发现一句的效果比之前好得多,现在最慢的一个点只跑了 200ms 了:

void get_sa(){
int vmax=123;
for(int i=1;i<=n;i++) buc[s[i]]++;
for(int i=1;i<=vmax;i++) buc[i]+=buc[i-1];
for(int i=n;i;i--) sa[buc[s[i]]--]=i;
int gr=0;
for(int i=1;i<=n;i++){
if(s[sa[i]]!=s[sa[i-1]]) gr++;
rk[sa[i]]=gr;
} vmax=gr;
// for(int i=1;i<=n;i++) printf("%d%c",rk[i],(i==n)?'\n':' ');
for(int k=1;k<=n;k<<=1){
for(int i=1;i<=n;i++){
if(i+k<=n) x[i]=mp(rk[i],rk[i+k]);
else x[i]=mp(rk[i],0);
}
int num=0;
for(int i=n-k+1;i<=n;i++) seq[++num]=i;
for(int i=1;i<=n;i++) if(sa[i]>k) seq[++num]=sa[i]-k;
memset(buc,0,sizeof(buc));
for(int i=1;i<=n;i++) buc[x[i].fi]++;
for(int i=1;i<=vmax;i++) buc[i]+=buc[i-1];
for(int i=n;i;i--) sa[buc[x[seq[i]].fi]--]=seq[i];
gr=0;
for(int i=1;i<=n;i++){
if(x[sa[i]]!=x[sa[i-1]]) gr++;
rk[sa[i]]=gr;
}
vmax=gr;if(vmax==n) break;
}
}

至此我们有了常数较小的 SA 的实现。

那么 SA 有什么用呢?SA 一个用途就是与 LCP 结合。

LCP,即 Longest Common Prefix,指两串的最长公共前缀。

我们记 \(LCP(i,j)\) 为后缀 \(sa_i\) 与 \(sa_j\) 的最长公共前缀,注意这边是 \(sa_i\) 与 \(sa_j\) 的最长公共前缀,即排名为 \(i\) 的串与排名为 \(j\) 的串的最长公共前缀

那么 LCP 有哪些性质呢?

  1. \(LCP(i,j)=LCP(j,i)\)

  2. \(LCP(i,i)=n-sa_i+1\)

  3. \(LCP(i,j)=\min(LCP(i,k),LCP(k,j)) (i<k<j)\),该定理我们称之为 LCP Lemma

  4. \(LCP(i,j)=\min\limits_{k=i+1}^{j}LCP(k-1,k)(i<j)\),该定理我们称之为 LCP Theorem

首先结论 \(1,2\) 肯定是显然的,用脚趾头都能想出来。

结论 \(3\) 和 \(4\) 显然是同一类的,证出结论 \(3\) 就能顺带着证出结论 \(4\)。

考虑如何证明结论 \(3\),设 \(u\) 为后缀 \(sa_i\),\(v\) 为后缀 \(sa_j\),\(w\) 为后缀 \(sa_k\),\(x=\min(LCP(i,k),LCP(k,j))\)

由于 \(x=\min(LCP(i,k),LCP(k,j))\),\(u,v,w\) 的前 \(x\) 位应当都是相同的,故 \(LCP(i,j)\geq x\)。

我们要证明 \(LCP(i,j)=x\),即 \(LCP(i,j)>x\) 不可能实现。

考虑反证法,假设 \(LCP(i,j)>x\),则 \(u\) 的第 \(x+1\) 位等于 \(v\) 的第 \(x+1\) 位。

由于 \(i<k<j\),故 \(u,w,v\) 在字典序上依次递增,而 \(u,v,w\) 的前 \(x\) 位都相等,故 \(u_{x+1}\leq w_{x+1}\leq v_{x+1}\)。

而 \(x=\min(LCP(i,k),LCP(k,j))\) 说明 \(u_{x+1},v_{x+1},w_{x+1}\) 不可能全相等,就说明 \(u\) 的 \(x+1\) 位不可能等于 \(v\) 的 \(x+1\) 位。

故 \(LCP(i,j)=x\)。

而结论 4 其实是结论 3 的变形。

如果我们记 \(ht_i=LCP(i-1,i)\),那么 \(LCP(i,j)=\min\limits_{k=i+1}^jht_i\),这东西显然可以 RMinQ 维护。

现在的问题就转化为怎么求 \(ht_i\)。暴力吗?一氧化氮。

如果我们设 \(h_i\) 为 \(ht_{rk_i}\),即后缀 \(i\) 与后缀 \(i\) 字典序前一位的 LCP,那么有一个很重要的公式叫做 \(h_i\geq h_{i-1}-1\)

怎么证明这个公式呢?

考虑设 \(j=sa_{rk_i-1}\),\(k=sa_{rk_{i-1}-1}\),分两种情况讨论:

  1. 若后缀 \(k\) 与后缀 \(i-1\) 第一位不同,则 \(h_{i-1}=0\),\(h_i\geq -1\) 显然成立。
  2. 若后缀 \(k\) 与后缀 \(i-1\) 第一位相同,则刨掉第一位还剩下后缀 \(k+1\) 和后缀 \(i\),它们的 \(LCP\) 为 \(h_{i-1}-1\),而由于后缀 \(k\) 字典序排在后缀 \(i-1\) 之前,后缀 \(k\) 与后缀 \(i-1\) 第一位相同,故后面的后缀 \(k+1\) 的字典序小于后缀 \(i\) 的字典序。根据 LCP Theorem,后缀 \(k+1\) 与后缀 \(i\) 的 LCP 为 \(\min\limits_{t=rk_{k+1}+1}^{rk_i}ht_t=h_{i-1}-1\),故 \(ht_{rk_i}\geq h_{i-1}-1\),故 \(h_i\geq h_{i-1}-1\)

附图:

有了这个式子,就可以用类似 two pointers 的东西求出 \(h_i\),也就顺带着求出 \(ht\) 了。

void getht(){
int k=0;
for(int i=1;i<=n;i++){//calculate h[i]
if(rk[i]==1) continue;
if(k) k--;//h[i]>=h[i-1]-1
int j=sa[rk[i]-1];
while(s[i+k]==s[j+k]) k++;//violently match suffix i and suffix sa[rk[i]-1]
ht[rk[i]]=k;//use formula ht[rk[i]]=h[i] to calculate ht[i]
}
}

后缀数组求一个子串在原串中的出现次数:

由于这个东西应用比较广泛就在这里一并讲掉了罢。

倘若我们要求 \(s\) 的子串 \(s[l...r]\) 在 \(s\) 中的出现次数,那么等价于求有多少个后缀 \(j\) 满足 \(\text{LCP}(s[j...n],s[l...n])\geq r-l+1\),因为如果 \(\text{LCP}(s[j...n],s[l...n])\geq r-l+1\),那么一定有 \(s[j...j+r-l]=s[l...r]\),也就对 \(s[l...r]\) 在 \(s\) 中的出现次数产生了 \(1\) 的贡献,反之一定有 \(s[j...j+r-l]\neq s[l...r]\)。

于是考虑怎样求有多少个后缀 \(j\) 满足 \(\text{LCP}(s[j...n],s[l...n])\geq r-l+1\)。考虑将这 \(n\) 个后缀放在字典序数组 \(rk_i\) 上。显然对于所有 \(\text{LCP}(s[j...n],s[l...n])\geq r-l+1\),它们的 \(rk\) 值应当是一段连续的区间 \([L,R]\)。而这个区间又可以通过二分+ST 表求出。具体来说,二分找出最小的满足 \(\min\limits_{i=t+1}^{rk_l}ht_i\ge r-l+1\) 的 \(t\) 即为该区间的左端点 \(L\);二分找出最大的满足 \(\min\limits_{i=rk_l+1}^{t}ht_i\ge r-l+1\) 的 \(t\) 即为该区间的左端点 \(R\)。然后返回 \(R-L+1\) 即可,正确性显然。

时间复杂度 \(\mathcal O(\log n)\)。

总结

后缀数组的知识点就这么多,学好后缀数组首先需掌握后缀数组的求法以及它与 LCP 相结合的一些定理的应用。

后缀数组最常用的场景是处理与子串有关的问题,因为后缀的前缀即是子串,因此看到“子串”等字眼就要尽量往后缀数组上想。

后缀数组的难题一般难点都不在怎样求后缀数组 \(sa_i\) 及 \(ht_i\),而是在于求完 \(sa_i\) 和 \(ht_i\) 之后怎样求出答案,由于 LCP Theorem 涉及到区间取 \(\min\) 的问题,所以这部分题也常与数据结构结合,需要较强的数据结构基础。

SA 复习笔记的更多相关文章

  1. 树的直径,LCA复习笔记

    前言 复习笔记第6篇. 求直径的两种方法 树形DP: dfs(y); ans=max( ans,d[x]+d[y]+w[i] ); d[x]=max( d[x],d[y]+w[i] ); int di ...

  2. Java基础复习笔记系列 九 网络编程

    Java基础复习笔记系列之 网络编程 学习资料参考: 1.http://www.icoolxue.com/ 2. 1.网络编程的基础概念. TCP/IP协议:Socket编程:IP地址. 中国和美国之 ...

  3. Java基础复习笔记系列 八 多线程编程

    Java基础复习笔记系列之 多线程编程 参考地址: http://blog.csdn.net/xuweilinjijis/article/details/8878649 今天的故事,让我们从上面这个图 ...

  4. Java基础复习笔记系列 七 IO操作

    Java基础复习笔记系列之 IO操作 我们说的出入,都是站在程序的角度来说的.FileInputStream是读入数据.?????? 1.流是什么东西? 这章的理解的关键是:形象思维.一个管道插入了一 ...

  5. Java基础复习笔记系列 五 常用类

    Java基础复习笔记系列之 常用类 1.String类介绍. 首先看类所属的包:java.lang.String类. 再看它的构造方法: 2. String s1 = “hello”: String ...

  6. Java基础复习笔记系列 四 数组

    Java基础复习笔记系列之 数组 1.数组初步介绍? Java中的数组是引用类型,不可以直接分配在栈上.不同于C(在Java中,除了基础数据类型外,所有的类型都是引用类型.) Java中的数组在申明时 ...

  7. Java基础复习笔记基本排序算法

    Java基础复习笔记基本排序算法 1. 排序 排序是一个历来都是很多算法家热衷的领域,到现在还有很多数学家兼计算机专家还在研究.而排序是计算机程序开发中常用的一种操作.为何需要排序呢.我们在所有的系统 ...

  8. Angular复习笔记7-路由(下)

    Angular复习笔记7-路由(下) 这是angular路由的第二篇,也是最后一篇.继续上一章的内容 路由跳转 Web应用中的页面跳转,指的是应用响应某个事件,从一个页面跳转到另一个页面的行为.对于使 ...

  9. Angular复习笔记7-路由(上)

    Angular复习笔记7-路由(上) 关于Angular路由的部分将分为上下两篇来介绍.这是第一篇. 概述 路由所要解决的核心问题是通过建立URL和页面的对应关系,使得不同的页面可以用不同的URL来表 ...

随机推荐

  1. 什么是关系图 (ERD)?

    首先,什么是实体关系图? 实体关系图,也称为ERD,ER图或ER模型,是一种用于数据库设计的结构图.一个ERD包含不同的符号和连接器,它们可视化两个重要信息:系统范围内的主要实体,以及这些实体之间的相 ...

  2. 谈谈BEM规范(含代码)

    css规范之BEM规范 前言 引用一句经典名言在编程的世界里其中一件最难的事情就是命名,不管是设计到编程语言还是标记语言都会有命名的需求.今天聊的就是关于css的命名规范的发展过程以及演变. 命名的发 ...

  3. CCD摄像头视场角计算公式

    视场角大小和CCD传感器尺寸和镜头焦距有关: 水平视场角 = 2 × arctan(w / 2f); 垂直视场角 = 2 × arctan(h / 2f); 视场角 = 2 × arctan(d / ...

  4. 镜头Lens Image circle像圈的解释是什么意思

    Image circle镜头中指的是:像圈 像圈(image circle)是指入射光线通过镜头后,在焦平面上呈现出的圆形的明亮清晰的影像幅面,也称像面大小.镜头像圈由镜头光学结构决定,一旦设计完成, ...

  5. 嵌入式单片机之STM32F103C8T6最小系统板电路设计参考

    STM32F103C8T6最小系统板电路设计 一.电源部分 设计了一个XH插座,以便使用3.7V锂电池供电,接入电压不允许超过6V. 二.指示灯部分 电源指示灯可以通过一个短路帽控制亮灭,以达到节电的 ...

  6. Vulnhub实战-dr4g0n b4ll靶机👻

    Vulnhub实战-dr4g0n b4ll靶机 地址:http://www.vulnhub.com/entry/dr4g0n-b4ll-1,646/ 描述:这篇其实没有什么新奇的技巧,用到的提权方式就 ...

  7. linux shell脚本中的开头#!/bin/bash的含义

    对于linux上需要执行 的shell脚本,通常第一行的内容是 #!/bin/bash 当然有很多时候不规范的写法可以忽略掉这一句,执行起来好像也是ok,结果没什么不一样 .. 这只是因为在我们常用 ...

  8. Swift-技巧(一)缩放并填充图片

    摘要 直接操作图片来实现它的缩放或者填充多余空间,首选 UIGraphicsBeginImageContext 函数来实现,它就相当于一个画布,你甚至可以用它来涂鸦. 最近有一个需求,就是将图片先等比 ...

  9. ELK 脚本自动化删除索引

    kibana有自带接口,可通过自带的API接口 通过传参来达到删除索引的目的. # 删除15天前的索引 curl -XDELETE "http://10.228.81.161:9201/pa ...

  10. PTA 根据后序和中序遍历输出先序遍历 (25分)

    PTA 根据后序和中序遍历输出先序遍历 (25分) 本题要求根据给定的一棵二叉树的后序遍历和中序遍历结果,输出该树的先序遍历结果. 输入格式: 第一行给出正整数N(≤30),是树中结点的个数.随后两行 ...