记录142
#include<bits/stdc++.h>
using namespace std; // 使用标准命名空间std
string in_str,pre_str; // 全局变量存储中序和前序遍历字符串
// 递归函数:根据前序和中序的区间,输出后序遍历
// l1,r1: 中序遍历字符串的左右边界 [l1,r1]
// l2,r2: 前序遍历字符串的左右边界 [l2,r2]
void dfs(int l1,int r1,int l2,int r2) {
// 递归终止条件:如果区间无效(左边界大于右边界),说明子树为空,直接返回
if(l1>r1) return;
// 1. 找根节点:前序遍历的第一个字符就是根
char root=pre_str[l2];
// 2. 在中序遍历中找到根节点的位置 p
int p=l1;
while(in_str[p]!=root) {
p++;
}
// 计算左子树的节点个数
int left_size=p-l1;
// 3. 递归处理左子树
// 中序左子树范围:[l1,p-1]
// 前序左子树范围:[l2+1,l2+left_size]
dfs(l1,p-1,l2+1,l2+left_size);
// 4. 递归处理右子树
// 中序右子树范围:[p+1,r1]
// 前序右子树范围:[l2+left_size+1,r2]
dfs(p+1,r1,l2+left_size+1,r2);
// 5. 输出根节点(后序遍历:左右根,根放在最后输出)
cout<<root;
}
int main() { // 主函数入口
ios::sync_with_stdio(false); // 关闭同步,加速IO
cin.tie(0);
// 读入中序遍历和前序遍历
cin>>in_str>>pre_str;
// 启动递归,初始区间都是整个字符串的长度
dfs(0,in_str.size()-1,0,pre_str.size()-1);
cout<<"\n"; // 输出换行
return 0;
}
题目传送门
https://www.luogu.com.cn/problem/P1827
前言
我是一名专注信奥赛(CSP-J/S、NOIP)的教练。
- 如果你觉得这篇题解对你有帮助,欢迎点击关注我的优快云账号,我会持续更新高质量算法解析。
- 我深知算法思维的构建远比单纯通过题目更重要,本系列题解不局限于AC代码的堆砌,而是致力于拆解题目背后的逻辑链条与核心知识点
- 备赛路上若遇瓶颈,欢迎随时评论或私信,我将甄选典型疑难问题,通过视频讲解或撰写专项文章的形式,为你提供深度答疑。
核心解题思路
这道题是一道非常经典的二叉树遍历与递归重建问题。
-
核心原理:
在二叉树的三种遍历方式中,前序遍历(根-左-右)的第一个元素永远是当前子树的根节点;而中序遍历(左-根-右)中,根节点会将序列完美地分割为左子树和右子树两部分。利用这两个特性,我们可以通过递归的方式,不断地从字符串中“切”出左子树和右子树,从而在逻辑上重建这棵二叉树。 -
算法设计(分治递归):
题目要求输出后序遍历(左-右-根)。我们不需要真正地在内存中构建出树的结构,只需要在递归的过程中,按照“先递归处理左子树 -> 再递归处理右子树 -> 最后输出根节点”的顺序进行操作,即可直接得到后序遍历的结果。
代码分块详细解释
1. 头文件与全局变量定义
#include<bits/stdc++.h>
using namespace std; // 使用标准命名空间std
string in_str, pre_str; // 全局变量存储中序和前序遍历字符串
- 详细分析:由于树的节点用唯一的字母表示,且数量不超过 26 个,使用
string类型来存储遍历序列非常方便。将这两个字符串定义为全局变量,可以避免在递归函数中反复传递庞大的字符串副本,提高程序运行效率。
2. 核心逻辑:分治递归函数(dfs)
// 递归函数:根据前序和中序的区间,输出后序遍历
// l1,r1: 中序遍历字符串的左右边界 [l1,r1]
// l2,r2: 前序遍历字符串的左右边界 [l2,r2]
void dfs(int l1, int r1, int l2, int r2) {
// 递归终止条件:如果区间无效(左边界大于右边界),说明子树为空,直接返回
if(l1 > r1) return;
// 1. 找根节点:前序遍历的第一个字符就是根
char root = pre_str[l2];
// 2. 在中序遍历中找到根节点的位置 p
int p = l1;
while(in_str[p] != root) {
p++;
}
// 计算左子树的节点个数
int left_size = p - l1;
// 3. 递归处理左子树
// 中序左子树范围:[l1, p-1]
// 前序左子树范围:[l2+1, l2+left_size]
dfs(l1, p-1, l2+1, l2+left_size);
// 4. 递归处理右子树
// 中序右子树范围:[p+1, r1]
// 前序右子树范围:[l2+left_size+1, r2]
dfs(p+1, r1, l2+left_size+1, r2);
// 5. 输出根节点(后序遍历:左右根,根放在最后输出)
cout << root;
}
- 详细分析:这是本题的灵魂所在,完美体现了“分治”思想。
- 确定根节点:前序遍历的特点是“根节点在最前面”,所以当前前序区间的第一个字符
pre_str[l2]必定是当前子树的根。 - 划分左右子树:在中序遍历区间
[l1, r1]中找到这个根节点的位置p。根据中序遍历“左-根-右”的规则,[l1, p-1]这一段全是左子树的节点,[p+1, r1]这一段全是右子树的节点。 - 计算子树大小:左子树的节点个数
left_size = p - l1。这个数值至关重要,因为它帮助我们在前序遍历中准确切分出左右子树的区间。前序遍历中,根节点之后紧跟着左子树的所有节点,所以左子树在前序中的区间是[l2+1, l2+left_size],剩下的就是右子树。 - 后序输出:函数先递归处理左子树,再递归处理右子树,最后才
cout << root。这种“左右根”的输出顺序,恰好就是后序遍历的定义。
- 确定根节点:前序遍历的特点是“根节点在最前面”,所以当前前序区间的第一个字符
3. 主函数:输入处理与启动递归
int main() {
ios::sync_with_stdio(false); // 关闭同步,加速IO
cin.tie(0);
// 读入中序遍历和前序遍历
cin >> in_str >> pre_str;
// 启动递归,初始区间都是整个字符串的长度
dfs(0, in_str.size()-1, 0, pre_str.size()-1);
cout << "\n"; // 输出换行
return 0;
}
- 详细分析:主函数非常简洁,主要负责读取输入并启动递归。
dfs(0, in_str.size()-1, 0, pre_str.size()-1)表示最初我们要处理的是完整的中序和前序字符串,左右边界分别是0和字符串长度-1。
核心逻辑总结表
| 代码模块 | 核心变量/操作 | 精炼作用 | 解决的痛点 |
|---|---|---|---|
| 根节点定位 | char root = pre_str[l2] | 利用前序遍历特性提取当前子树的根 | 确立了递归分治的基准点,是划分左右子树的前提 |
| 区间划分 | while(in_str[p]!=root) | 在中序遍历中定位根节点的位置 p | 利用中序遍历特性,将序列完美切割为左子树和右子树 |
| 子树大小计算 | int left_size = p - l1 | 计算左子树包含的节点数量 | 桥接中序与前序,帮助在前序字符串中准确切分出左右子树的区间 |
| 递归左子树 | dfs(l1, p-1, l2+1, ...) | 递归处理左半部分区间 | 深入树的左侧分支,遵循后序遍历“先左”的规则 |
| 递归右子树 | dfs(p+1, r1, ...+1, r2) | 递归处理右半部分区间 | 深入树的右侧分支,遵循后序遍历“后右”的规则 |
| 根节点输出 | cout << root | 在左右子树递归结束后输出根 | 遵循后序遍历“最后根”的规则,直接拼接出最终答案 |
1993



被折叠的 条评论
为什么被折叠?



