练习题(2024/4/25)

1监控二叉树

困难

相关标签

相关企业

给定一个二叉树,我们在树的节点上安装摄像头。

节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。

计算监控树的所有节点所需的最小摄像头数量。

示例 1:

输入:[0,0,null,0,0]
输出:1
解释:如图所示,一台摄像头足以监控所有节点。

示例 2:

输入:[0,0,null,0,null,0,null,null,0]
输出:2
解释:需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。


提示:

  1. 给定树的节点数的范围是 [1, 1000]
  2. 每个节点的值都是 0。

贪心算法思路:

1确定摄像头的放置

从题目中示例,其实可以得到启发,我们发现题目示例中的摄像头都没有放在叶子节点上!

这是很重要的一个线索,摄像头可以覆盖上中下三层,如果把摄像头放在叶子节点上,就浪费的一层的覆盖。

所以把摄像头放在叶子节点的父节点位置,才能充分利用摄像头的覆盖面积。

为什么不从头结点开始看起呢,为啥要从叶子节点看呢?

因为头结点放不放摄像头也就省下一个摄像头, 叶子节点放不放摄像头省下了的摄像头数量是指数阶别的。

所以我们要从下往上看,局部最优:让叶子节点的父节点安摄像头,所用摄像头最少,整体最优:全部摄像头数量所用最少!

此时,大体思路就是从低到上,先给叶子节点父节点放个摄像头,然后隔两个节点放一个摄像头,直至到二叉树头结点。

2确定遍历顺序

在二叉树中如何从低向上推导呢?

可以使用后序遍历也就是左右中的顺序,这样就可以在回溯的过程中从下到上进行推导了

回归本质,为了让摄像头数量最少,我们要尽量让叶子节点的父节点安装摄像头,这样才能摄像头的数量最少。

3确定二叉树底部叶子节点下空节点的初始状态

那么空节点不能是无覆盖的状态,这样叶子节点就要放摄像头了,空节点也不能是有摄像头的状态,这样叶子节点的父节点就没有必要放摄像头了,而是可以把摄像头放在叶子节点的爷爷节点上。

所以空节点的状态只能是有覆盖,这样就可以在叶子节点的父节点放摄像头了

递归的终止条件应该是遇到了空节点,此时应该返回2(有覆盖)

4确定每个节点的处理过程

递归的函数,以及终止条件已经确定了,再来看单层逻辑处理

每个节点可能有3种状态

  1. 0:无需覆盖:表示当前节点及其子树都已被相机覆盖,无需额外的相机。
  2. 1:已覆盖(有摄像头):表示当前节点已被相机覆盖,但子节点需要额外的相机。
  3. 2:需要覆盖:表示当前节点及其子树中至少有一个节点未被相机覆盖。

在遍历每个节点时,会根据其左右子节点的覆盖状态进行判断,具体逻辑如下:

  1. 如果当前节点为空,则返回状态2,表示该节点已被覆盖。
  2. 分别递归遍历左右子树,获取其覆盖状态。
  3. 如果左右子树的覆盖状态都为2,说明当前节点无需覆盖,返回0。
  4. 如果左右子树中有一个为0,或者左右子树中有一个为1,表示当前节点需要覆盖,将结果加1,返回1。
  5. 如果左右子树中有一个为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 控制,它表示当前处理的数字。

  1. 选择数字

    • 我们从 startIndex 开始遍历可选的数字,这些数字是 1 到 9
    • 假设当前处理的数字是 i
  2. 加入组合

    • 我们将数字 i 加入当前组合中,因为我们正在尝试这个数字是否能够组成符合条件的组合。
    • 将 i 添加到 path 中,表示我们选择了这个数字。
  3. 递归调用

    • 在加入了当前数字后,我们需要继续处理下一个数字,以验证它是否能够组成符合条件的组合。
    • 我们递归调用 backtracking 函数,但是这次的起始位置是 i + 1,因为我们不允许重复使用数字。
    • 我们传入的参数是 targetSumksum + i(当前组合的总和),以及 i + 1(下一个可选数字的起始位置)。
  4. 回溯

    • 当递归调用结束后,表示我们已经尝试了以当前数字开头的所有可能组合。
    • 我们需要进行回溯,撤销对当前数字 i 的选择,以便尝试其他可能的选择。
    • 我们从当前组合 path 中移除数字 i,并将当前总和 sum 减去 i,以回到上一层的状态。
  5. 终止条件

    • 在每次递归调用之前,我们会检查当前组合的长度是否达到了要求 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"]

思路:

  1. 准备数据结构

    • 我们需要一个数组 letterMap 来存储数字到字符集的映射关系,例如,数字 2 对应着字符集 “abc”。
    • 为了存储结果,我们使用一个向量 result,用于存储所有可能的字母组合。
    • 另外,我们需要一个临时字符串 s,用于暂存每一种组合的字母序列。
  2. 回溯函数

    当使用回溯算法时,通常需要遵循三个关键步骤,也被称为回溯的三部曲:

  3. 确定回溯函数参数

    • 在这个问题中,回溯函数 backtracking 的参数是 (const string& digits, int index),其中 digits 是输入的数字字符串,index 是当前处理的数字索引。
    • 这两个参数用于确定当前处理的数字以及对应的字符集。
  4. 确定终止条件

    • 终止条件是指在何种情况下停止递归并返回结果。在这个问题中,终止条件是当 index 等于 digits 的长度时,即已经处理完所有数字。
    • 当达到终止条件时,将当前组合加入结果集合中,并结束当前递归。
  5. 确定单层遍历逻辑

    • 单层遍历逻辑指的是每一层递归中的处理逻辑,包括做出选择、递归调用、撤销选择这三个步骤。
    • 在这个问题中,单层遍历逻辑如下:
      • 做出选择:选择当前数字对应的一个字母加入到当前组合中,即 s.push_back(letters[i])
      • 递归调用:递归调用 backtracking 函数处理下一个数字,即 backtracking(digits, index + 1)
      • 撤销选择:在递归调用结束后,撤销上一步的选择,即 s.pop_back(),以便尝试其他可能的字符。
  6. 主函数

    • 我们的入口函数是 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                  |
+----------+---------+-------------+----------------+--------------------+-----------------------+

思路:

  1. 问题概述

    • 需要从 Transactions 表中查询数据,并按照月份和国家分组统计交易相关信息,包括总交易数量、已批准交易数量、总交易金额以及已批准交易的总金额。
  2. 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 字段分组。
  3. 查询结果解释

    • 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 -- 按月份和国家分组

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值