力扣 968 :二叉树安装最小摄像头

力扣 968 :二叉树安装最小摄像头

  • [🌿 前言](#🌿 前言)
  • [Bilibili 同步视频](#Bilibili 同步视频)
  • [📌 一、树形 DP 核心模型与状态定义](#📌 一、树形 DP 核心模型与状态定义)
    • [1.1 递归函数设计思路](#1.1 递归函数设计思路)
    • [1.2 二维状态维度规则](#1.2 二维状态维度规则)
    • [1.3 状态组合结构示意图](#1.3 状态组合结构示意图)
    • [1.4 四种状态详细释义](#1.4 四种状态详细释义)
  • [💡 二、核心状态转移逻辑深度解析](#💡 二、核心状态转移逻辑深度解析)
    • [2.1 前置约束条件](#2.1 前置约束条件)
    • [2.2 合法布设方案示意图](#2.2 合法布设方案示意图)
    • [2.3 最优解选取规则](#2.3 最优解选取规则)
  • [🔐 三、边界条件严谨定义](#🔐 三、边界条件严谨定义)
    • [3.1 边界状态数值对照表](#3.1 边界状态数值对照表)
    • [3.2 边界逻辑文字解读](#3.2 边界逻辑文字解读)
  • [📝 四、整体算法执行流程](#📝 四、整体算法执行流程)
  • [💻 五、C++ 完整源码实现](#💻 五、C++ 完整源码实现)
  • [📊 六、算法性能与复杂度分析](#📊 六、算法性能与复杂度分析)
    • [6.1 时间复杂度](#6.1 时间复杂度)
    • [6.2 空间复杂度](#6.2 空间复杂度)
  • [🌿 七、算法思维沉淀与刷题感悟](#🌿 七、算法思维沉淀与刷题感悟)

摘要

在算法刷题进阶之路上,有一类极具迷惑性的经典题型:表面以二叉树 为载体呈现,内核却是树形动态规划 的经典应用。本文将全方位拆解二叉树最小摄像头覆盖困难题,跳出传统二叉树遍历思维,从状态定义、状态释义、边界规则、状态转移、代码实现逐层深入,搭配原理示意图、状态对照表、完整 C++ 源码,带你彻底吃透树形 DP 的底层逻辑,建立标准化解题思维模板。

关键词

树形 DP;二叉树;动态规划;LeetCode 算法;递归


🌿 前言

很多算法初学者刷二叉树题目时,总会陷入固有思维:遇到二叉树就想到前中后序遍历、深度优先搜索。

但这道 LeetCode 困难题狠狠打破了这种认知🤯:它根本不是一道单纯的二叉树遍历题,而是一道纯正的动态规划考题

动态规划思维的魅力就在于此:将复杂的树形问题拆分为有限个状态子问题,以局部最优推导全局最优。只要吃透本题的状态设计与逻辑推导,后续所有树形 DP 同类题型都能做到举一反三、秒破思路。

建议大家养成提前预习刷题题目的习惯,预留充足时间揣摩题意与逻辑框架,才能跟上树形 DP 强逻辑的思维节奏。


Bilibili 同步视频

力扣 968 :二叉树安装最小摄像头


📌 一、树形 DP 核心模型与状态定义

1.1 递归函数设计思路

我们自定义一个递归处理函数,传入二叉树任意节点,最终返回一个 2×2 的 DP 二维数组

数组用来记录当前节点在父节点、自身节点 不同摄像头布设状态下,覆盖整棵子树所需的最少摄像头数量

1.2 二维状态维度规则

定义 dp[i][j] 二维状态数组,双维度含义固定不变:

  • 第一维 i:代表父节点是否放置摄像头

  • 第二维 j:代表当前节点是否放置摄像头

取值 父节点状态 当前节点状态
0 ❌ 不放置摄像头 ❌ 不放置摄像头
1 ✅ 放置摄像头 ✅ 放置摄像头

表格说明:本表为 DP 状态编码核心规则,是整道题逻辑推导的基石,所有后续计算均基于此规则展开。

1.3 状态组合结构示意图

text 复制代码
二维状态组合逻辑:
dp[父节点状态][当前节点状态]
       ↓            ↓
    0 / 1        0 / 1

衍生4种核心合法状态:
┌───────┬───────┐
│ dp[0][0] │ dp[0][1] │
├───────┼───────┤
│ dp[1][0] │ dp[1][1] │
└───────┴───────┘

图示说明:通过父节点与当前节点的布尔状态两两组合,穷尽所有摄像头布设可能性,无遗漏、无冗余。

1.4 四种状态详细释义

  1. dp00

    父节点不放置摄像头、当前节点也不放置摄像头;满足全覆盖当前节点所属子树所有节点的最小摄像头数量

  2. dp01

    父节点不放置摄像头、当前节点放置摄像头;覆盖当前整棵子树的最少摄像头数量。

  3. dp10

    父节点已放置摄像头、当前节点不放置摄像头;覆盖当前整棵子树的最少摄像头数量。

  4. dp11

    父节点已放置摄像头、当前节点也放置摄像头;覆盖当前整棵子树的最少摄像头数量。

💡 重要约束:DP 数组中统计的摄像头数量,仅计算当前子树内部,不包含父节点已布设的摄像头,保证逻辑独立、边界严谨。


💡 二、核心状态转移逻辑深度解析

我们以最难理解、最核心的 dp00 为例,拆解树形 DP 的状态转移底层逻辑。

2.1 前置约束条件

dp[0][0] 场景约束:

父节点无摄像头 + 当前节点无摄像头

此时当前节点既无法被自身覆盖,也无法被父节点覆盖,只能依靠左右子节点布设摄像头,才能实现整棵子树全节点覆盖。

2.2 合法布设方案示意图

text 复制代码
根节点(无摄像头)
       /        
左子节点        右子节点

合法三种布设方案:
1. 左✅放摄像头 | 右❌不放摄像头
2. 左❌不放摄像头 | 右✅放摄像头
3. 左✅放摄像头 | 右✅放摄像头

图示说明:以上三种是唯一能满足子树全覆盖的合法方案,不存在第四种可行逻辑。

2.3 最优解选取规则

设:

  • l:左子树的 2×2 DP 状态数组

  • r:右子树的 2×2 DP 状态数组

dp[0][0] 最终取值 = 三种布设方案的摄像头数量取最小值,用最小代价完成子树全覆盖,完美契合动态规划「局部最优推全局最优」的核心思想。


🔐 三、边界条件严谨定义

树形 DP 最容易出错的环节,就是空节点叶子节点 的边界初始化。

我们设定常量极大值 INF = 10000,用来标记逻辑非法、不可行的状态,避免参与最小值运算。

3.1 边界状态数值对照表

状态标识 空节点取值 叶子节点取值 核心逻辑说明
dp00 0 INF 空节点无需覆盖;叶子无父无自摄像头,无法覆盖
dp01 INF 1 空节点不能安放摄像头;叶子自放 1 个即可覆盖
dp10 0 0 父已放摄像头,空 / 叶子节点无需额外布设
dp11 INF 1 空节点禁止放摄像头;父子都放仅需消耗 1 个

3.2 边界逻辑文字解读

  1. 空节点:无实体结构,绝对不能放置摄像头,凡是需要自身放摄像头的状态全部标记为极大值;

  2. 叶子节点:无左右子节点,若父节点和自身都不放摄像头,永远无法被覆盖,属于非法状态;

  3. 极大值的作用:筛选最优解时,自动跳过不可能的状态,保证计算逻辑无误。


📝 四、整体算法执行流程

text 复制代码
算法完整执行步骤:
1. 传入二叉树根节点,开启递归
2. 优先判断:空节点 → 初始化空节点4种状态
3. 非空再判断:叶子节点 → 初始化叶子节点4种状态
4. 递归遍历左子树、右子树,获取左右子树DP状态
5. 根据左右子树状态,推导当前节点4种DP值
6. 根节点无父节点,取 dp[0][0]、dp[0][1] 最小值作为最终答案

💻 五、C++ 完整源码实现

适配 LeetCode 在线评测,代码注释详尽,逻辑严格遵循上述推导:

cpp 复制代码
#include <iostream>
#include <algorithm>
using namespace std;

// 定义极大值,标记非法不可行状态
const int INF = 10000;

// 二叉树节点结构
struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode() : val(0), left(nullptr), right(nullptr) {}
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};

// 树形DP递归函数,dp[2][2]存储四种状态
void getDp(TreeNode* root, int dp[2][2])
{
    // 边界1:空节点处理
    if (!root)
    {
        dp[0][0] = 0;
        dp[0][1] = INF;
        dp[1][0] = 0;
        dp[1][1] = INF;
        return;
    }

    // 边界2:叶子节点处理
    if (!root->left && !root->right)
    {
        dp[0][0] = INF;
        dp[0][1] = 1;
        dp[1][0] = 0;
        dp[1][1] = 1;
        return;
    }

    // 定义左右子树DP数组
    int l[2][2], r[2][2];
    // 递归求解左右子树状态
    getDp(root->left, l);
    getDp(root->right, r);

    // 核心:计算 dp[0][0] 三种方案取最小
    int opt1 = l[0][1] + r[1][0];
    int opt2 = l[1][0] + r[0][1];
    int opt3 = l[0][1] + r[0][1];
    dp[0][0] = min({opt1, opt2, opt3});

    // 其余三种状态转移计算
    dp[0][1] = 1 + min(l[0][0], l[0][1]) + min(r[0][0], r[0][1]);
    dp[1][0] = min(l[0][0], l[0][1]) + min(r[0][0], r[0][1]);
    dp[1][1] = 1 + min(l[1][0], l[1][1]) + min(r[1][0], r[1][1]);
}

// 主函数:求解二叉树最小摄像头数量
int minCameraCover(TreeNode* root)
{
    int dp[2][2];
    getDp(root, dp);
    // 根节点无父节点,仅需在两种状态中取最小值
    return min(dp[0][0], dp[0][1]);
}

📊 六、算法性能与复杂度分析

6.1 时间复杂度

O ( n ) O(n) O(n)

仅对二叉树每一个节点做一次遍历递归 ,无重复计算、无冗余遍历, n n n 为二叉树总节点数,时间效率达到最优。

6.2 空间复杂度

O ( h ) O(h) O(h)

额外空间消耗主要来自递归调用栈 , h h h 为二叉树高度:

  • 平衡二叉树: h = l o g n h = log n h=logn,空间消耗极小;

  • 斜二叉树最坏情况: h = n h = n h=n。

整体算法性能优秀,可适配大规模二叉树输入场景。


🌿 七、算法思维沉淀与刷题感悟

这道 LeetCode 困难题的难点,从来不是代码编写,而是状态抽象能力逻辑体系的严谨性

  1. 打破思维定势:不要看到二叉树就只想到遍历,学会用树形 DP拆解复杂问题;

  2. 树形 DP 通用套路:定义二维状态 → 划定空节点 / 叶子节点边界 → 后序递归求子树状态 → 状态转移推导 → 根节点特殊取值

  3. 解题小技巧:用极大值标记非法状态、用表格梳理状态含义、用示意图梳理逻辑结构,能快速降低树形 DP 的理解难度。

掌握本题这套思维框架,后续二叉树节点选择、子树全覆盖、监控布设等同类难题,都能快速拆解、轻松破题。