力扣 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 同步视频
📌 一、树形 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 四种状态详细释义
-
dp00
父节点不放置摄像头、当前节点也不放置摄像头;满足全覆盖当前节点所属子树所有节点的最小摄像头数量。
-
dp01
父节点不放置摄像头、当前节点放置摄像头;覆盖当前整棵子树的最少摄像头数量。
-
dp10
父节点已放置摄像头、当前节点不放置摄像头;覆盖当前整棵子树的最少摄像头数量。
-
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 边界逻辑文字解读
-
空节点:无实体结构,绝对不能放置摄像头,凡是需要自身放摄像头的状态全部标记为极大值;
-
叶子节点:无左右子节点,若父节点和自身都不放摄像头,永远无法被覆盖,属于非法状态;
-
极大值的作用:筛选最优解时,自动跳过不可能的状态,保证计算逻辑无误。
📝 四、整体算法执行流程
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 困难题的难点,从来不是代码编写,而是状态抽象能力 与逻辑体系的严谨性。
-
打破思维定势:不要看到二叉树就只想到遍历,学会用树形 DP拆解复杂问题;
-
树形 DP 通用套路:定义二维状态 → 划定空节点 / 叶子节点边界 → 后序递归求子树状态 → 状态转移推导 → 根节点特殊取值;
-
解题小技巧:用极大值标记非法状态、用表格梳理状态含义、用示意图梳理逻辑结构,能快速降低树形 DP 的理解难度。

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