算法学习记录~2023.5.20~贪心Day5~738.单调递增的数字 & 968.监控二叉树


738.单调递增的数字

题目链接

力扣题目链接

思路

一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想让strNum[i - 1]–,然后strNum[i] 设为9。

至于遍历顺序:

如果从前向后遍历,遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]减一,但此时如果strNum[i - 1]减一了,可能又小于strNum[i - 2]。
比如数字:332,从前向后遍历的话,那么就把变成了329,此时2又小于了第一位的3了,真正的结果应该是299。

从后向前遍历的话,就可以重复利用上次比较得出的结果,从后向前遍历332的数值变化为:332 -> 329 -> 299

需要注意的是,由于要取最大值,所以从后向前遍历遍历时,需要找到最大的不符合大小关系的位置,也就是最前面的一处,从这开始后面全设为 9 。
如果从后往前边遍历边修改当前值,那么会出现后面也有部分是符合的,从而没有被修改,而无法获取最大值。
比如:100 -> 90

代码

class Solution {
public:
    int monotoneIncreasingDigits(int n) {
        string strN = to_string(n);     //将数值转为字符串方便取坐标处理
        int flag = strN.size();               //记录从哪里开始往后都是9 。 设置这个默认值防止第二个 for 循环在flag没有被赋值情况下进行
        for (int i = strN.size() - 1; i >= 1; i--){
            if (strN[i] < strN[i - 1]){
                flag = i;       //记录 9 开始位置
                strN[i - 1]--;  //前一位减 1
            }
        }
        for (int i = flag; i < strN.size(); i++)
            strN[i] = '9';
        int result = stoi(strN);    //转换为 int
        return result;
    }
};

总结


968.监控二叉树

题目链接

力扣题目链接

思路1:自己想的(错误思路)(又是企图枚举)

直觉上感觉一定是每 3 层为一个周期,因为一个摄像头能把本节点和上下两层都覆盖到。

因此就采用层序遍历,分别记录每 3n、3n + 1、3n + 2的节点各自总量count0、count1、count2,接着对于总层数对3进行取模,然后分情况讨论使用哪些节点总量。

(事实上没跑通,因为感觉情况想的还是不是很全也不太对,此处只是记录一下当时想法)

代码

class Solution {
public:
    int minCameraCover(TreeNode* root) {
        queue<TreeNode*> que;
        que.push(root);
        int count0 = 1;     //3n 层,初始化为第一层的 1
        int count1 = 0;     //3n + 1层
        int count2 = 0;     //3n + 2层
        int judge = 0;     //判断是哪一层,设定第一层为0。对3取模来判断是3n + x层
        while (!que.empty()){
            int size = que.size();      //先记录本层总共个数,方便本层处理完改变judge
            for (int i = 0; i < size; i++){
                TreeNode* node = que.front();
                que.pop();
                if (node -> left){
                    que.push(node -> left);
                    if (judge % 3 == 1)
                        count2++;
                    else if (judge % 3 == 2)
                        count0++;
                    else
                        count1++;
                }
                if (node -> right){
                    que.push(node -> right);
                    if (judge % 3 == 1)
                        count2++;
                    else if (judge % 3 == 2)
                        count0++;
                    else
                        count1++;
                }
            }
            judge ++;   //记录一共有多少层了,实际层数需要加1
        }
        judge++;        //记录一共有多少层了,实际层数需要加 1 所以这里加上
        if (judge % 3 == 0){
            if (count1 == 0)
                return 1;
            return count1;
        }
        else if (judge % 3 == 1){
            return count0;
        }
        else{
            if (count1 == 0)
                return 1;
            return count1;
        }
    }
};

思路2:贪心算法

从下往上看,叶子节点都没放摄像头,因为摄像头可以覆盖三层,所以放在叶子节点上会浪费,而且显然越往上节点是越少的,因为如果本层确定有节点,那父节点只会小于或等于本层,因此优先叶子节点的父节点。

大体思路就是,从下往上,先给叶子节点的父节点放个摄像头,然后每隔两个节点放一个摄像头,知道二叉树头节点。

有两个难点:

  1. 二叉树的遍历
  2. 怎样每隔两个节点放一个摄像头

问题1:二叉树的遍历

想从下向上,可以后序遍历左右中,这样就可以在回溯过程中从下到上推导了。

int traversal(TreeNode* cur) {

    // 空节点,该节点有覆盖
    if (终止条件) return ;

    int left = traversal(cur->left);    // 左
    int right = traversal(cur->right);  // 右

    逻辑处理                            // 中
    return ;
}

注意在以上代码中我们取了左孩子的返回值,右孩子的返回值,即left 和 right, 以后推导中间节点的状态

问题2:怎样每隔两个节点放一个摄像头

需要状态转移的公式。

首先一共有3 种状态:

  1. 该节点无覆盖 --> 使用 0 表示
  2. 该节点有摄像头 --> 使用 1 表示
  3. 该节点有覆盖 --> 使用 2 表示

遍历树时会遇到空节点,如何处理?
回归本质,为了让摄像头数量最少,我们要尽量让叶子节点的父节点安装摄像头,这样才能摄像头的数量最少。
那么空节点不能是无覆盖的状态,这样叶子节点就要放摄像头了。
空节点也不能是有摄像头的状态,这样叶子节点的父节点就没有必要放摄像头了,而是可以把摄像头放在叶子节点的爷爷节点上。
所以空节点的状态只能是有覆盖,这样就可以在叶子节点的父节点放摄像头了。

接下来是递推关系。

首先是递推的终止条件,是遇到了空节点,也就是对应上面说的 2 有覆盖。

// 空节点,该节点有覆盖
if (cur == NULL) return 2;

接下来是单层逻辑处理。

主要有四种情况:

  • 情况1:左右节点都有覆盖

左孩子有覆盖,右孩子有覆盖,那么此时中间节点应该就是无覆盖的状态了。
在这里插入图片描述

//左右节点都有覆盖,中间节点则为无覆盖
if (left == 2 && right == 2) return 0;
  • 情况2:左右节点至少一个无覆盖

如果是以下情况,则中间节点(父节点)应该放摄像头:

left == 0 && right == 0 左右节点无覆盖
left == 1 && right == 0 左节点有摄像头,右节点无覆盖
left == 0 && right == 1 左节点有无覆盖,右节点摄像头
left == 0 && right == 2 左节点无覆盖,右节点覆盖
left == 2 && right == 0 左节点覆盖,右节点无覆盖

这个不难理解,毕竟有一个孩子没有覆盖,父节点就应该放摄像头。
此时摄像头的数量要加一,并且return 1,代表中间节点放摄像头。

if (left == 0 || right == 0) {
    result++;
    return 1;
}
  • 情况3:左右节点至少一个有摄像头

如果是以下情况,其实就是 左右孩子节点有一个有摄像头了,那么其父节点就应该是2(覆盖的状态)

left == 1 && right == 2 左节点有摄像头,右节点有覆盖
left == 2 && right == 1 左节点有覆盖,右节点有摄像头
left == 1 && right == 1 左右节点都有摄像头

if (left == 1 || right == 1) return 2;

注意并不需要考虑一个摄像头一个无覆盖的情况,因为已经在情况 2 中讨论了,因此情况 3 只考虑没有无覆盖的 3 种情况即可。

  • 情况4:头节点没有覆盖

以上都处理完了,递归结束之后,可能头节点还有一个无覆盖的情况

在这里插入图片描述
所以递归结束之后,还要判断根节点,如果没有覆盖,result++

int minCameraCover(TreeNode* root) {
    result = 0;
    if (traversal(root) == 0) { // root 无覆盖
        result++;
    }
    return result;
}

代码

// 版本一
class Solution {
public:
    int result;

    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,该节点加摄像头,result++
        // left == 0 && right == 0 左右节点无覆盖
        // left == 1 && right == 0 左节点有摄像头,右节点无覆盖
        // left == 0 && right == 1 左节点有无覆盖,右节点摄像头
        // left == 0 && right == 2 左节点无覆盖,右节点覆盖
        // left == 2 && right == 0 左节点覆盖,右节点无覆盖
        if (left == 0 || right == 0){
            result++;
            return 1;
        }

        // 情况3,该节点有覆盖
        // left == 1 && right == 2 左节点有摄像头,右节点有覆盖
        // left == 2 && right == 1 左节点有覆盖,右节点有摄像头
        // left == 1 && right == 1 左右节点都有摄像头
        // 其他情况前段代码均已覆盖
        if (left == 1 || right == 1)
            return 2;

        // 以上代码没有使用else,主要是为了把各个分支条件展现出来,这样代码有助于理解
        // 其实这个 return -1 逻辑不会走到这里。
        return -1;
    }

    int minCameraCover(TreeNode* root) {
        result = 0;

        // 情况4,头节点加一个摄像头,result++
        if (traversal(root) == 0)
            result++;
        return result;
    }
};

//精简版,在版本一的基础上使用else直接覆盖掉一些情况

// 版本二
class Solution {
private:
    int result;
    int traversal(TreeNode* cur) {
        if (cur == NULL) return 2;
        int left = traversal(cur->left);    // 左
        int right = traversal(cur->right);  // 右
        if (left == 2 && right == 2) return 0;
        else if (left == 0 || right == 0) {
            result++;
            return 1;
        } else return 2;
    }
public:
    int minCameraCover(TreeNode* root) {
        result = 0;
        if (traversal(root) == 0) { // root 无覆盖
            result++;
        }
        return result;
    }
};

总结

不愧是hard题.jpg

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

山药泥拌饭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值