LeetCode树结构难题解析:子树缺失最小基因值
2026/7/18 6:21:24 网站建设 项目流程

1. 问题背景与核心概念解析

今天我们来拆解一道LeetCode上的树结构难题——"2003. 每棵子树内缺失的最小基因值"。这道题在周赛中出现时让不少选手感到棘手,但其实只要掌握几个关键结论,就能化繁为简。我们先明确题目要求:

给定一棵以节点0为根的树结构,每个节点都有一个唯一的基因值(即所有节点的基因值互不相同)。需要为每个子树找出其内部缺失的最小正整数基因值。举个例子,如果某子树包含的基因值为[2,3,5],那么缺失的最小正整数就是1;如果包含[1,2,3],则缺失的最小值是4。

关键提示:题目中的"缺失最小基因值"实际上就是数学中的"最小缺失正整数"概念(Mex, Minimum Excluded Value),这与经典的数组找缺失最小正整数问题一脉相承。

2. 三大核心结论及其证明

2.1 结论一:无1子树的处理策略

现象观察:当某子树中完全不包含基因值1时,该子树的答案必定是1。这是所有正整数中最小的一个,且题目保证基因值互不相同。

逻辑证明

  1. 根据Mex定义,我们需要找的是不在集合中的最小正整数
  2. 若集合中不存在1,则1就是最小的缺失值
  3. 这一结论与子树的具体结构无关,是全局性结论

实现技巧

  • 可以通过一次DFS预处理,标记出哪些子树包含1
  • 对于不包含1的子树,可以直接返回1,无需进一步计算

2.2 结论二:含1子树的链式特性

关键发现:所有包含基因值1的子树,它们的根节点实际上形成了一条从原始根节点(节点0)到基因值为1的节点的路径。这是因为:

  1. 子树包含1的条件是该节点在1的祖先路径上
  2. 树结构中,一个节点的子树包含另一个节点的条件是前者是后者的祖先
  3. 因此,只有1节点及其祖先节点的子树才会包含1

实例说明: 考虑如下树结构:

0 (val=2) └── 1 (val=1) └── 2 (val=3)

这里只有节点0和节点1的子树包含1,它们正好形成0→1的路径。

2.3 结论三:链上节点的计算顺序

最优策略:对于包含1的那些子树(即链上的节点),我们应该从基因值为1的节点开始,沿着祖先路径向上计算。这是因为:

  1. 子树的Mex值具有单调不减的性质
  2. 越靠近叶子节点的子树,包含的基因值越少,Mex计算越简单
  3. 可以利用已计算的子节点结果来优化父节点的计算

算法选择

  • 使用哈希集合来维护当前路径上的基因值
  • 从1节点开始,初始Mex设为2
  • 沿着父节点回溯,动态更新Mex值

3. 算法实现与优化细节

3.1 预处理阶段:建立树结构和定位1节点

def build_tree(parents): tree = defaultdict(list) one_pos = -1 for i, p in enumerate(parents): tree[p].append(i) if vals[i] == 1: one_pos = i return tree, one_pos

这个预处理步骤完成了两件事:

  1. 将父节点表示法转换为邻接表形式
  2. 记录基因值1所在的节点位置

3.2 标记包含1的子树

def mark_contains_one(tree, one_pos): contains_one = [False] * len(vals) stack = [one_pos] while stack: node = stack.pop() contains_one[node] = True for child in tree[node]: stack.append(child) return contains_one

使用DFS遍历从1节点开始的所有子树,标记这些节点为"包含1"。

3.3 主算法实现

def smallestMissingValueSubtree(parents, vals): n = len(parents) res = [1] * n tree, one_pos = build_tree(parents) if one_pos == -1: # 没有基因值1的特殊情况 return res contains_one = mark_contains_one(tree, one_pos) # 处理链上的节点 current_mex = 2 visited = set() node = one_pos while node != -1: # 收集当前节点的所有子节点基因值(不包括已访问的) stack = [node] while stack: curr = stack.pop() if vals[curr] in visited: continue visited.add(vals[curr]) for child in tree[curr]: stack.append(child) # 更新Mex值 while current_mex in visited: current_mex += 1 res[node] = current_mex node = parents[node] # 移动到父节点 return res

4. 复杂度分析与边界情况

4.1 时间复杂度

  1. 建树和定位1节点:O(N)
  2. 标记包含1的子树:O(N)
  3. 主处理流程:
    • 每个节点最多被访问一次
    • Mex更新过程整体是O(N)(因为Mex最多增加到N+1)
    • 总复杂度:O(N)

4.2 空间复杂度

  1. 树结构的存储:O(N)
  2. 包含1的标记数组:O(N)
  3. 已访问基因值的集合:最坏O(N)
    • 总空间:O(N)

4.3 边界情况处理

  1. 没有基因值1的情况

    • 所有子树的Mex都是1
    • 直接返回全1数组
  2. 基因值1在根节点

    • 只有根节点需要计算Mex
    • 其他节点可以直接返回1
  3. 单节点树

    • 如果节点值为1,Mex为2
    • 否则Mex为1

5. 实战技巧与常见错误

5.1 调试技巧

  1. 可视化小规模测试用例: 手工绘制树结构,标注每个节点的基因值,手动计算预期结果。

  2. 关键检查点

    • 确认1节点定位正确
    • 验证包含1的子树标记正确
    • 检查Mex更新逻辑是否准确

5.2 常见错误

  1. 错误假设基因值范围

    • 误认为基因值在1-N范围内
    • 实际上基因值可以是任意正整数
  2. Mex更新顺序错误

    • 应该先收集所有基因值,再统一计算Mex
    • 而不是边收集边计算
  3. 忽略父节点指针终止条件

    • 根节点的父节点通常是-1
    • 需要正确处理这个边界

5.3 性能优化

  1. 基因值离散化

    • 如果基因值范围很大但数量少,可以先映射到连续区间
    • 但题目中N≤10^5,通常不需要
  2. 双指针Mex优化

    • 维护当前Mex候选值
    • 使用位图代替哈希集合(当N很大时)
  3. 并行处理非链节点

    • 不包含1的子树可以并行处理
    • 但在编程竞赛中通常不需要

6. 同类问题扩展

掌握了这道题的解法后,可以解决一系列类似问题:

  1. 子树Mex查询

    • 动态版本:支持修改节点值
    • 需要更复杂的数据结构
  2. 路径Mex问题

    • 计算树上路径的Mex
    • 可能需要树链剖分
  3. 带权Mex问题

    • 每个值有权重
    • 求最小权重和Mex
  4. 区间Mex问题

    • 数组区间查询
    • 离线处理+线段树

这道题很好地展示了如何将树结构的特性与算法设计相结合。通过分析问题本质得出的三个核心结论,不仅适用于本题,也为处理其他树结构问题提供了思路框架。在实际编码时,注意处理好预处理和Mex更新的细节,就能高效解决这个问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询