<think>我们正在讨论笛卡尔树上的欧拉序。首先需要明确笛卡尔树和欧拉序的概念,然后讲解在笛卡尔树上构建欧拉序的方法,最后介绍其应用。
### 1. 笛卡尔树 (Cartesian Tree)
笛卡尔树是一种二叉树结构,由满足以下性质的节点组成:
- 每个节点包含两个值:键值(key)和优先级(priority)。
- 键值满足二叉搜索树(BST)性质:左子树所有节点的键值小于根节点,右子树所有节点的键值大于根节点。
- 优先级满足堆性质(通常是最小堆或最大堆):每个节点的优先级小于(或大于)其子节点的优先级。
通常,笛卡尔树根据一个序列构建,其中键值为序列的索引,优先级为序列的值(或反过来)。构建笛卡尔树可以在$O(n)$时间内完成。
### 2. 欧拉序 (Euler Tour)
欧拉序是对树进行深度优先遍历(DFS)时,按照访问顺序记录节点的序列。它记录每个节点入栈和出栈的时刻,因此每个节点在欧拉序中出现两次(除了根节点可能只出现一次,但通常我们记录两次以统一处理)。具体来说:
- 当第一次访问节点时,将其加入欧拉序(入栈)。
- 当回溯离开节点时,再次将其加入欧拉序(出栈)。
因此,一棵有$n$个节点的树的欧拉序长度为$2n-1$(如果根节点在开始和结束各记录一次,则长度为$2n$)。
### 3. 笛卡尔树上的欧拉序
在笛卡尔树上进行深度优先遍历,记录欧拉序。这样,笛卡尔树的结构信息就被编码在欧拉序中。
#### 构建示例
考虑一个序列:`[3,1,2]`,以索引为键值(0-indexed),值为优先级构建最小堆笛卡尔树。
- 键值:索引0,1,2;优先级:3,1,2。
- 根据优先级,最小值是1(索引1),所以根节点是索引1。
- 左子树:索引0(小于1)且优先级3>1,所以作为左子节点(因为满足堆性质,这里最小堆,所以左子节点优先级大于根)。
- 右子树:索引2(大于1)且优先级2>1,所以作为右子节点。
笛卡尔树结构:
```
1 (优先级1)
/ \
0 2 (优先级2)
(优先级3)
```
对该笛卡尔树进行DFS(先序遍历):
- 入1 -> 入0 -> 出0 -> 出1 -> 入2 -> 出2
- 欧拉序:1, 0, 0, 1, 2, 2(注意:出栈时记录同一节点,但通常我们记录节点编号,所以是[1,0,0,1,2,2]?)
但通常欧拉序记录节点编号,且入栈和出栈都记录同一个节点编号。所以正确的欧拉序应该是:
- 开始:1(入)
- 访问左子:0(入)
- 回溯:0(出)-> 回到1,然后访问右子:2(入)
- 回溯:2(出)-> 1(出)
所以欧拉序为:1, 0, 0, 1, 2, 2? 这样不对,因为1在中间被回溯了一次(在0出栈后,1并没有立即出栈,而是继续访问右子树,所以1的出栈在最后)。所以正确的顺序应该是:
- 入1:记录1
- 入0:记录0
- 出0:记录0(第二次)
- 然后从1(此时1还在栈中)转向右子树,入2:记录2
- 出2:记录2(第二次)
- 出1:记录1(第二次)
所以欧拉序:1,0,0,2,2,1。或者更常见的写法是记录每个节点两次(入栈和出栈),所以序列为:[1,0,0,2,2,1]。注意,这个序列的长度为6(节点数3,2*3=6)。
### 4. 应用
笛卡尔树上的欧拉序主要用于解决一些序列问题,特别是区间最值查询(RMQ)问题。具体应用包括:
#### (1) 将RMQ转化为LCA(最近公共祖先)
对于一个序列,我们可以构建笛卡尔树,其中优先级为序列的值(假设求最小值),键值为序列索引。这样,序列任意区间$[l,r]$的最小值就是笛卡尔树中节点$l$和节点$r$的最近公共祖先(LCA)的值。
然后,通过欧拉序,我们可以将LCA问题转化为RMQ问题(在欧拉序上求区间最小值)。具体步骤:
- 构建笛卡尔树,并得到欧拉序。
- 在欧拉序中,每个节点出现多次,但LCA对应的是第一次出现的深度最小的节点。
- 同时记录每个节点在欧拉序中第一次出现的位置(记为`first_occurrence`)。
- 对于查询节点$u$和$v$,其LCA在欧拉序中位于`first_occurrence[u]`和`first_occurrence[v]`之间的深度最小的节点。
因此,我们通过笛卡尔树的欧拉序,将原序列的RMQ转化为欧拉序上的RMQ(即区间深度最小值查询)。而欧拉序上的RMQ可以使用稀疏表(Sparse Table)在$O(n \log n)$预处理,$O(1)$查询。
#### (2) 线性时间构建后缀树
在字符串处理中,笛卡尔树结合欧拉序可用于构建后缀数组的线性时间算法(如DC3算法)中,用于解决后缀数组的RMQ问题。
#### (3) 树形结构的序列表示
欧拉序将笛卡尔树的结构转化为一个序列,可以用于树形结构的持久化或序列化操作。
### 5. 算法实现示例(笛卡尔树构建与欧拉序生成)
#### 笛卡尔树构建(数组实现)
以下代码用数组构建笛卡尔树(最小堆性质),其中`arr`为输入序列,`n`为序列长度。返回每个节点的左右子节点。
```python
def build_cartesian_tree(arr):
n = len(arr)
parent = [-1] * n # 父节点
left = [-1] * n # 左子节点
right = [-1] * n # 右子节点
stack = []
for i in range(n):
last = -1
# 栈中优先级大于当前值的节点弹出
while stack and arr[stack[-1]] > arr[i]:
last = stack.pop()
if stack:
parent[i] = stack[-1]
if last != -1:
parent[last] = i
left[i] = last
if stack:
right[stack[-1]] = i
stack.append(i)
# 找到根节点(没有父节点的节点)
root = -1
for i in range(n):
if parent[i] == -1:
root = i
break
return root, left, right
```
#### 生成欧拉序
通过DFS遍历笛卡尔树,记录欧拉序。
```python
def euler_tour(root, left, right):
tour = []
stack = [root]
while stack:
node = stack.pop()
tour.append(node) # 入栈记录
if node == -1:
continue
# 注意:我们使用先序遍历,先右后左(因为栈是先进后出,所以先压右再压左)
# 这样出栈顺序就是先左后右,符合DFS先序遍历
# 但是欧拉序需要记录回溯,所以我们在每个节点出栈时还要记录一次
# 因此,我们使用两次访问:第一次访问(入栈)和第二次访问(回溯)
# 这里我们模拟递归DFS,使用栈来存储节点和状态(是否已经访问过子节点)
# 更简单的方法是递归DFS,但这里用迭代DFS并记录回溯
# 使用递归DFS生成欧拉序(包括回溯)
tour = []
def dfs(u):
tour.append(u)
if left[u] != -1:
dfs(left[u])
tour.append(u) # 回溯到u
if right[u] != -1:
dfs(right[u])
tour.append(u) # 回溯到u
dfs(root)
return tour
```
注意:上述递归DFS生成的欧拉序在访问每个子节点后会回溯到父节点,因此每个节点出现次数为(1+子节点个数)。但通常我们要求每个节点出现两次(入栈和出栈),所以上述递归在每次递归返回时都添加了父节点,导致根节点在最后没有回溯记录(因为根节点没有父节点),所以我们需要在最后再添加一次根节点?或者另一种方法:在DFS开始和结束时都记录节点。但通常我们使用迭代DFS来精确控制。
修正:使用迭代DFS,显式记录入栈和出栈。
```python
def euler_tour_iterative(root, left, right):
stack = [(root, 0)] # (node, state:0表示未访问子节点,1表示已访问)
tour = []
while stack:
node, state = stack.pop()
if state == 0:
# 第一次访问(入栈)
tour.append(node)
# 标记为已访问,并重新压栈(等子节点访问完后再处理)
stack.append((node, 1))
# 先压右子节点(因为后访问)
if right[node] != -1:
stack.append((right[node], 0))
if left[node] != -1:
stack.append((left[node], 0))
else:
# 回溯(出栈)
tour.append(node)
return tour
```
这样得到的欧拉序中每个节点出现两次(入栈和出栈各一次),除了根节点在开始和结束都出现,所以整个序列长度为2n(n为节点数)。
### 6. 总结
笛卡尔树上的欧拉序将树形结构转化为线性序列,为处理树上的查询(如LCA)提供了序列上的方法。在算法竞赛中,笛卡尔树结合欧拉序常用于高效解决区间最值查询问题。
§§相关问题§§
1. 笛卡尔树还有哪些应用场景?
2. 如何利用笛卡尔树的欧拉序解决区间最值查询(RMQ)问题?
3. 笛卡尔树构建算法的时间复杂度如何分析?</think>### 笛卡尔树上的欧拉序详解
#### 1. **笛卡尔树基础**
笛卡尔树(Cartesian Tree)是一种**二叉树结构**,具有两种约束:
- **二叉搜索树性质**:中序遍历序列等于原序列顺序
- **堆性质**:父节点的优先级值小于子节点(最小堆)或大于子节点(最大堆)
给定序列 $A = [a_1, a_2, \dots, a_n]$,笛卡尔树构建规则:
1. 序列中**最小值**(或最大值)作为根节点
2. 最小值左侧子序列构成左子树
3. 最小值右侧子序列构成右子树
**示例**:序列 $[3,1,2,5,4]$ 的笛卡尔树(最小堆):
```
1
/ \
3 2
/ \
5 4
```
#### 2. **欧拉序定义**
欧拉序(Euler Tour)是对树进行**深度优先遍历**时记录所有访问点的序列,特点:
- 每个节点出现**两次**(入栈和出栈各一次)
- 边被访问时记录相邻节点
- $n$ 个节点的树欧拉序长度为 $2n-1$
**标准DFS序**:$[A,B,D,D,E,E,B,C,C,A]$
**欧拉序**:$[A,B,D,B,E,B,A,C,A]$(记录所有访问点)
#### 3. **笛卡尔树上的欧拉序**
在笛卡尔树上进行欧拉遍历时,序列隐含**原序列的顺序关系**和**堆性质**。构建步骤:
1. 从根节点开始DFS
2. 每次访问节点时记录
3. 回溯到父节点时再次记录
**示例**:上述笛卡尔树的欧拉序:
```
访问路径:1→3→回溯→1→2→5→回溯→2→4→回溯→2→回溯→1
欧拉序: [1, 3, 1, 2, 5, 2, 4, 2, 1]
```
#### 4. **关键性质与应用**
##### (1) 区间最值查询(RMQ)
笛卡尔树的**LCA(最近公共祖先)** 对应原序列的**区间最值**:
- 节点 $i$ 和 $j$ 的LCA值 = $\min/\max(A[i..j])$
- **欧拉序实现**:
1. 构建笛卡尔树并生成欧拉序 $E$
2. 记录节点首次出现位置 $F$
3. 查询 $[l,r]$ 的最值 → 求 $E[F[l]]$ 和 $E[F[r]]$ 的LCA
```python
# 伪代码:笛卡尔树RMQ
def range_min_query(l, r):
lca = find_lca(F[l], F[r]) # 在欧拉序中查询
return values[lca]
```
##### (2) 序列重建
通过欧拉序可**重建笛卡尔树**结构:
- 入栈顺序确定父子关系
- 出栈顺序确定子树边界
**重建规则**:
- 遇到新节点:创建为当前节点的子节点
- 遇到回溯节点:返回父节点
##### (3) 子树操作
欧拉序将子树映射到**连续区间**:
- 节点 $u$ 的子树对应欧拉序 $[L_u, R_u]$
- 支持**子树求和/修改**等操作
```python
# 子树求和(树状数组维护)
def subtree_sum(u):
l = first_occurrence[u]
r = last_occurrence[u]
return fenwick_query(l, r)
```
##### (4) 路径分析
欧拉序中**两点间路径**对应:
- 从 $u$ 到 $v$ 的路径 = 欧拉序中 $F[u]$ 到 $F[v]$ 的子序列
- 移除重复访问点后得到唯一路径
#### 5. **实际应用场景**
1. **高效RMQ**
- 将序列RMQ转化为欧拉序上的LCA问题
- 结合稀疏表实现 $O(1)$ 查询(预处理 $O(n \log n)$)
2. **树形结构持久化**
- 欧拉序将树结构线性化,便于存储和传输
- 应用于分布式系统中的树结构同步
3. **语法树分析**
- 在编译器设计中,笛卡尔树表示运算符优先级
- 欧拉序快速定位子树(如表达式子树)
4. **生物信息学**
- DNA序列的区间最小/最大值查询
- 基因比对中的相似区域发现
#### 6. **算法实现示例**
```python
# 笛卡尔树构建 + 欧拉序生成
def build_cartesian_tree(arr):
n = len(arr)
parent = [-1] * n
stack = []
for i in range(n):
last = -1
while stack and arr[i] < arr[stack[-1]]:
last = stack.pop()
if stack:
parent[i] = stack[-1]
if last != -1:
parent[last] = i
stack.append(i)
# 找到根节点(无父节点)
root = next(i for i in range(n) if parent[i] == -1)
return parent, root
def euler_tour(tree, root):
tour = []
stack = [(root, 0)] # (node, state: 0=未访问子节点, 1=已访问)
while stack:
node, state = stack.pop()
tour.append(node)
if state == 0: # 首次访问
stack.append((node, 1))
for child in [tree[node][0], tree[node][1]]: # 左/右子节点
if child != -1:
stack.append((child, 0))
return tour
```
#### 7. **性能分析**
- **时间复杂度**:
- 笛卡尔树构建:$O(n)$(单调栈)
- 欧拉序生成:$O(n)$(DFS遍历)
- **空间复杂度**:$O(n)$(存储树结构和序列)
#### 8. **与其他结构对比**
| **特性** | **普通BST** | **笛卡尔树** | **线段树** |
|------------------|-------------|--------------|------------|
| 区间最值查询 | $O(n)$ | $O(1)$ | $O(\log n)$|
| 构建复杂度 | $O(n \log n)$ | $O(n)$ | $O(n)$ |
| 支持动态更新 | 是 | 否 | 是 |
| 隐含序列顺序 | 否 | 是 | 否 |
### 总结
笛卡尔树上的欧拉序通过**线性序列**同时编码了:
1. 原序列的**顺序关系**(中序遍历)
2. 优先级**堆结构**(父节点约束)
3. **树形拓扑**(DFS遍历路径)
这种双重编码特性使其在**区间查询**、**树结构分析**和**序列重建**中具有独特优势,尤其适合静态序列的高效处理。在算法竞赛和数据处理系统中,笛卡尔树结合欧拉序是解决RMQ问题的经典方案[^1]。