1监控二叉树
困难
相关标签
相关企业
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量。
示例 1:
输入:[0,0,null,0,0] 输出:1 解释:如图所示,一台摄像头足以监控所有节点。
示例 2:
输入:[0,0,null,0,null,0,null,null,0] 输出:2 解释:需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。
提示:
- 给定树的节点数的范围是
[1, 1000]
。 - 每个节点的值都是 0。
贪心算法思路:
1确定摄像头的放置
从题目中示例,其实可以得到启发,我们发现题目示例中的摄像头都没有放在叶子节点上!
这是很重要的一个线索,摄像头可以覆盖上中下三层,如果把摄像头放在叶子节点上,就浪费的一层的覆盖。
所以把摄像头放在叶子节点的父节点位置,才能充分利用摄像头的覆盖面积。
为什么不从头结点开始看起呢,为啥要从叶子节点看呢?
因为头结点放不放摄像头也就省下一个摄像头, 叶子节点放不放摄像头省下了的摄像头数量是指数阶别的。
所以我们要从下往上看,局部最优:让叶子节点的父节点安摄像头,所用摄像头最少,整体最优:全部摄像头数量所用最少!
此时,大体思路就是从低到上,先给叶子节点父节点放个摄像头,然后隔两个节点放一个摄像头,直至到二叉树头结点。
2确定遍历顺序
在二叉树中如何从低向上推导呢?
可以使用后序遍历也就是左右中的顺序,这样就可以在回溯的过程中从下到上进行推导了
回归本质,为了让摄像头数量最少,我们要尽量让叶子节点的父节点安装摄像头,这样才能摄像头的数量最少。
3确定二叉树底部叶子节点下空节点的初始状态
那么空节点不能是无覆盖的状态,这样叶子节点就要放摄像头了,空节点也不能是有摄像头的状态,这样叶子节点的父节点就没有必要放摄像头了,而是可以把摄像头放在叶子节点的爷爷节点上。
所以空节点的状态只能是有覆盖,这样就可以在叶子节点的父节点放摄像头了
递归的终止条件应该是遇到了空节点,此时应该返回2(有覆盖)
4确定每个节点的处理过程
递归的函数,以及终止条件已经确定了,再来看单层逻辑处理
每个节点可能有3种状态
- 0:无需覆盖:表示当前节点及其子树都已被相机覆盖,无需额外的相机。
- 1:已覆盖(有摄像头):表示当前节点已被相机覆盖,但子节点需要额外的相机。
- 2:需要覆盖:表示当前节点及其子树中至少有一个节点未被相机覆盖。
在遍历每个节点时,会根据其左右子节点的覆盖状态进行判断,具体逻辑如下:
- 如果当前节点为空,则返回状态2,表示该节点已被覆盖。
- 分别递归遍历左右子树,获取其覆盖状态。
- 如果左右子树的覆盖状态都为2,说明当前节点无需覆盖,返回0。
- 如果左右子树中有一个为0,或者左右子树中有一个为1,表示当前节点需要覆盖,将结果加1,返回1。
- 如果左右子树中有一个为1,表示当前节点已被覆盖,但有子节点需要额外的相机,返回2
代码:
class Solution {
private:
int result; // 用于记录所需的相机数量
// 遍历节点,返回节点状态:0-无需覆盖,1-已覆盖(有摄像头),2-需要覆盖
int traversal(TreeNode* cur) {
// 空节点,该节点已被覆盖
if (cur == NULL) return 2;
// 递归遍历左子树和右子树
int left = traversal(cur->left); // 左子树递归
int right = traversal(cur->right); // 右子树递归
// 情况1:左右子树均已被覆盖,当前节点无需相机
if (left == 2 && right == 2) return 0;
// 情况2:当前节点需要覆盖
// 左右子树至少有一个未被覆盖
// 或者左右子树有一个已被覆盖但另一个需要覆盖
if (left == 0 || right == 0) {
result++; // 相机数量加1
return 1; // 返回1,表示当前节点已被相机覆盖
}
// 情况3:当前节点已被覆盖,但有子节点需要摄像头
if (left == 1 || right == 1) return 2;
// 以上代码覆盖了所有情况,这里不会执行到,但为了代码完整性,保留返回-1
return -1;
}
public:
int minCameraCover(TreeNode* root) {
result = 0; // 初始化相机数量为0
// 情况4:如果根节点无需覆盖,则根节点需要相机
if (traversal(root) == 0) {
result++;
}
return result; // 返回所需的相机数量
}
};
2组合
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
示例 2:
输入:n = 1, k = 1 输出:[[1]]
提示:
1 <= n <= 20
1 <= k <=
1. 回溯算法的基本思路:
那组合问题抽象为如下树形结构:
可以看出这棵树,一开始集合是 1,2,3,4, 从左向右取数,取过的数,不再重复取。
第一次取1,集合变为2,3,4 ,因为k为2,我们只需要再取一个数就可以了,分别取2,3,4,得到集合[1,2] [1,3] [1,4],以此类推。
每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围。
- 路径:在搜索过程中,记录已经做出的选择。
- 选择列表:当前可以做出的选择。
- 结束条件:到达决策树的底部,无法再做出选择的条件。
2. 解题思路:
- 递归函数的返回值和参数:递归函数
backtracking
的返回值是void
,参数包括n
(1 到 n 的数字范围)、k
(组合的长度要求)、startIndex
(当前处理的数字)。 - 回溯函数终止条件:如果当前组合长度达到要求
k
,则将当前组合加入结果集合result
中,并返回。 - 单层搜索的过程:从
startIndex
开始遍历到n
,对于每个数字i
,将其加入当前组合path
中,然后递归处理下一个位置的数字,即调用backtracking(n, k, i + 1)
。递归的过程中,不断向path
中添加数字,直到达到长度要求,然后回溯,撤销对当前数字的处理,尝试其他可能的选择。
代码:
class Solution {
private:
vector<vector<int>> result; // 存放符合条件结果的集合
vector<int> path; // 用来存放符合条件结果
// 回溯函数,生成长度为 k 的 1 到 n 的数字组合
void backtracking(int n, int k, int startIndex) {
// 如果当前组合长度达到要求,将其加入结果集合
if (path.size() == k) {
result.push_back(path);
return;
}
// 遍历可能的选择
for (int i = startIndex; i <= n; i++) {
path.push_back(i); // 处理节点
backtracking(n, k, i + 1); // 递归处理下一个节点
path.pop_back(); // 回溯,撤销处理的节点
}
}
public:
// 组合函数,生成长度为 k 的 1 到 n 的数字组合
vector<vector<int>> combine(int n, int k) {
result.clear(); // 清空结果集合
path.clear(); // 清空当前路径
backtracking(n, k, 1); // 回溯搜索组合
return result; // 返回结果集合
}
};
3组合总和 III
找出所有相加之和为 n
的 k
个数的组合,且满足下列条件:
- 只使用数字1到9
- 每个数字 最多使用一次
返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
示例 1:
输入: k = 3, n = 7 输出: [[1,2,4]] 解释: 1 + 2 + 4 = 7 没有其他符合的组合了。
示例 2:
输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]] 解释: 1 + 2 + 6 = 9 1 + 3 + 5 = 9 2 + 3 + 4 = 9 没有其他符合的组合了。
示例 3:
输入: k = 4, n = 1 输出: [] 解释: 不存在有效的组合。 在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。
回溯算法思路:
把组合总和抽象为如下树形结构:
- 递归函数的返回值和参数:递归函数的返回值是
void
,参数包括targetSum
(目标和)、k
(组合的长度要求)、sum
(当前组合的和)以及startIndex
(当前处理的数字)。 - 回溯函数终止条件:如果当前组合长度达到要求
k
,并且总和等于目标和targetSum
,则将当前组合加入结果集合中,并返回。 - 单层搜索的过程:从
startIndex
开始遍历可选数字,对于每个数字i
,将其加入当前组合中,并递归处理下一个位置的数字,即调用backtracking(targetSum, k, sum, i + 1)
。递归的过程中,不断向当前组合中添加数字,直到达到长度要求和目标和,然后回溯,撤销对当前数字的处理,尝试其他可能的选择。
具体过程实现
当我们进行单层搜索时,我们需要从一个起始位置开始遍历可选的数字。这个起始位置由参数 startIndex
控制,它表示当前处理的数字。
-
选择数字:
- 我们从
startIndex
开始遍历可选的数字,这些数字是1
到9
。 - 假设当前处理的数字是
i
。
- 我们从
-
加入组合:
- 我们将数字
i
加入当前组合中,因为我们正在尝试这个数字是否能够组成符合条件的组合。 - 将
i
添加到path
中,表示我们选择了这个数字。
- 我们将数字
-
递归调用:
- 在加入了当前数字后,我们需要继续处理下一个数字,以验证它是否能够组成符合条件的组合。
- 我们递归调用
backtracking
函数,但是这次的起始位置是i + 1
,因为我们不允许重复使用数字。 - 我们传入的参数是
targetSum
、k
、sum + i
(当前组合的总和),以及i + 1
(下一个可选数字的起始位置)。
-
回溯:
- 当递归调用结束后,表示我们已经尝试了以当前数字开头的所有可能组合。
- 我们需要进行回溯,撤销对当前数字
i
的选择,以便尝试其他可能的选择。 - 我们从当前组合
path
中移除数字i
,并将当前总和sum
减去i
,以回到上一层的状态。
-
终止条件:
- 在每次递归调用之前,我们会检查当前组合的长度是否达到了要求
k
,以及当前总和是否已经超过了目标和targetSum
。 - 如果当前组合的长度已经满足要求,并且总和等于目标和,我们将当前组合加入结果集合中。
- 如果超过了目标和,则直接返回,不再继续搜索。
- 在每次递归调用之前,我们会检查当前组合的长度是否达到了要求
代码:
class Solution {
private:
vector<vector<int>> result; // 存放结果集合
vector<int> path; // 用来存放当前组合
// 回溯函数,生成和为 targetSum,长度为 k 的数字组合
void backtracking(int targetSum, int k, int sum, int startIndex) {
// 如果当前的sum已经超过了目标值,则不再搜索直接返回
if (sum > targetSum) return;
// 达到组合长度要求且总和等于目标值时,将当前组合加入结果集合
if (path.size() == k) {
if (sum == targetSum) result.push_back(path);
return;
}
// 从 startIndex 开始遍历可选数字
for (int i = startIndex; i <= 9; i++) {
sum += i; // 统计当前总和
path.push_back(i); // 将当前数字加入组合中
backtracking(targetSum, k, sum, i + 1); // 递归处理下一个数字
sum -= i; // 回溯,撤销当前数字的处理
path.pop_back(); // 回溯,撤销当前数字的处理
}
}
public:
// 组合函数,生成和为 n,长度为 k 的数字组合
vector<vector<int>> combinationSum3(int k, int n) {
backtracking(n, k, 0, 1); // 回溯搜索组合
return result; // 返回结果集合
}
};
4电话号码的字母组合
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例 1:
输入:digits = "23" 输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
示例 2:
输入:digits = "" 输出:[]
示例 3:
输入:digits = "2" 输出:["a","b","c"]
思路:
-
准备数据结构:
- 我们需要一个数组
letterMap
来存储数字到字符集的映射关系,例如,数字 2 对应着字符集 “abc”。 - 为了存储结果,我们使用一个向量
result
,用于存储所有可能的字母组合。 - 另外,我们需要一个临时字符串
s
,用于暂存每一种组合的字母序列。
- 我们需要一个数组
-
回溯函数:
当使用回溯算法时,通常需要遵循三个关键步骤,也被称为回溯的三部曲:
-
确定回溯函数参数:
- 在这个问题中,回溯函数
backtracking
的参数是(const string& digits, int index)
,其中digits
是输入的数字字符串,index
是当前处理的数字索引。 - 这两个参数用于确定当前处理的数字以及对应的字符集。
- 在这个问题中,回溯函数
-
确定终止条件:
- 终止条件是指在何种情况下停止递归并返回结果。在这个问题中,终止条件是当
index
等于digits
的长度时,即已经处理完所有数字。 - 当达到终止条件时,将当前组合加入结果集合中,并结束当前递归。
- 终止条件是指在何种情况下停止递归并返回结果。在这个问题中,终止条件是当
-
确定单层遍历逻辑:
- 单层遍历逻辑指的是每一层递归中的处理逻辑,包括做出选择、递归调用、撤销选择这三个步骤。
- 在这个问题中,单层遍历逻辑如下:
- 做出选择:选择当前数字对应的一个字母加入到当前组合中,即
s.push_back(letters[i])
。 - 递归调用:递归调用
backtracking
函数处理下一个数字,即backtracking(digits, index + 1)
。 - 撤销选择:在递归调用结束后,撤销上一步的选择,即
s.pop_back()
,以便尝试其他可能的字符。
- 做出选择:选择当前数字对应的一个字母加入到当前组合中,即
-
主函数:
- 我们的入口函数是
letterCombinations
,它用于生成数字字符串对应的所有字母组合。 - 首先,我们清空临时字符串
s
和结果集合result
。 - 如果输入的数字字符串为空,则直接返回空结果。
- 否则,我们调用
backtracking
函数开始生成结果。 - 最后返回结果集合。
- 我们的入口函数是
代码:
class Solution {
private:
const string letterMap[10] = {
"", // 0
"", // 1
"abc", // 2
"def", // 3
"ghi", // 4
"jkl", // 5
"mno", // 6
"pqrs", // 7
"tuv", // 8
"wxyz", // 9
};
public:
vector<string> result; // 存储最终结果的向量
string s; // 临时存储每个组合的字符串
// 回溯函数,用于生成数字对应的字母组合
void backtracking(const string& digits, int index) {
// 如果处理到了数字字符串的末尾,将当前字符串加入结果集合中并返回
if (index == digits.size()) {
result.push_back(s);
return;
}
int digit = digits[index] - '0'; // 将index指向的数字转为int
string letters = letterMap[digit]; // 取数字对应的字符集
for (int i = 0; i < letters.size(); i++) {
s.push_back(letters[i]); // 处理
backtracking(digits, index + 1); // 递归,注意index+1,一下层要处理下一个数字了
s.pop_back(); // 回溯
}
}
// 主函数,生成数字字符串对应的所有字母组合
vector<string> letterCombinations(string digits) {
s.clear(); // 清空临时字符串
result.clear(); // 清空结果集合
if (digits.size() == 0) {
return result; // 如果输入为空字符串,则直接返回空结果
}
backtracking(digits, 0); // 调用回溯函数生成结果
return result; // 返回结果集合
}
};
5每月交易 I
表:Transactions
+---------------+---------+ | Column Name | Type | +---------------+---------+ | id | int | | country | varchar | | state | enum | | amount | int | | trans_date | date | +---------------+---------+ id 是这个表的主键。 该表包含有关传入事务的信息。 state 列类型为 ["approved", "declined"] 之一。
编写一个 sql 查询来查找每个月和每个国家/地区的事务数及其总金额、已批准的事务数及其总金额。
以 任意顺序 返回结果表。
查询结果格式如下所示。
示例 1:
输入:
Transactions
table:
+------+---------+----------+--------+------------+
| id | country | state | amount | trans_date |
+------+---------+----------+--------+------------+
| 121 | US | approved | 1000 | 2018-12-18 |
| 122 | US | declined | 2000 | 2018-12-19 |
| 123 | US | approved | 2000 | 2019-01-01 |
| 124 | DE | approved | 2000 | 2019-01-07 |
+------+---------+----------+--------+------------+
输出:
+----------+---------+-------------+----------------+--------------------+-----------------------+
| month | country | trans_count | approved_count | trans_total_amount | approved_total_amount |
+----------+---------+-------------+----------------+--------------------+-----------------------+
| 2018-12 | US | 2 | 1 | 3000 | 1000 |
| 2019-01 | US | 1 | 1 | 2000 | 2000 |
| 2019-01 | DE | 1 | 1 | 2000 | 2000 |
+----------+---------+-------------+----------------+--------------------+-----------------------+
思路:
-
问题概述:
- 需要从 Transactions 表中查询数据,并按照月份和国家分组统计交易相关信息,包括总交易数量、已批准交易数量、总交易金额以及已批准交易的总金额。
-
SQL 查询语句:
- 使用 select语句从
Transactions
表中选择需要的字段和聚合函数结果。 - 使用
date_format(trans_date, '%Y-%m')
将trans_date
字段格式化为年-月形式作为month
字段。 - 使用
count(*)
统计总交易数量,count(if(state = 'approved', 1, null))
统计已批准的交易数量。 - 使用
sum(amount)
计算交易总金额,sum(if(state = 'approved', amount, 0))
计算已批准交易的总金额。 - 最后使用 group by子句按照
month
和country
字段分组。
- 使用 select语句从
-
查询结果解释:
month
:按照年-月格式显示交易日期。country
:显示交易发生的国家信息。trans_count
:每个月每个国家的交易总数量。approved_count
:每个月每个国家已批准的交易数量。trans_total_amount
:每个月每个国家的交易总金额。approved_total_amount
:每个月每个国家已批准的交易总金额。
代码:
select date_format(trans_date, '%Y-%m') as month, -- 选择月份并格式化为年-月形式
country, -- 选择国家信息
count(*) as trans_count, -- 统计交易数量
count(if(state = 'approved', 1, null)) as approved_count, -- 统计已批准的交易数量
sum(amount) as trans_total_amount, -- 计算交易总金额
sum(if(state = 'approved', amount, 0)) as approved_total_amount -- 计算已批准交易的总金额
from transactions -- 从交易表中查询数据
group by month, country -- 按月份和国家分组