贪心算法:leetcode 738.单调递增的数字、714.买卖股票的最佳时机含手续费、968.监控二叉树

leetcode 738.单调递增的数字

leetcode 714.买卖股票的最佳时机含手续费

leetcode 968.监控二叉树

leetcode 738.单调递增的数字

给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。

(当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。)

示例 1:

  • 输入: N = 10

  • 输出: 9

示例 2:

  • 输入: N = 1234

  • 输出: 1234

示例 3:

  • 输入: N = 332

  • 输出: 299

暴力

暴力解法就是从给定的数n开始,用for循环依次遍历从其值开始的一次减一的数,再对这些数进行“各个位数上的数字是否单调递增”的判断。

代码如下:

class Solution {
private:
    bool check(int n){  //check whether the number in each position increases monotonically
        int curValue = 10;
        while(n){
            int num = n % 10;
            if(curValue >= num) 
                curValue = num;
            else
                return false;
            n /= 10;
        }
        return true;
    }
public:
    int monotoneIncreasingDigits(int n) {
        for(int i = n; i > 0; i--){
            if(check(i))
                return i;
        }
        return 0;
    }
};
  • 时间复杂度:O(n × m) m为n的数字长度

  • 空间复杂度:O(1)

leetcode超时。

贪心算法

贪心算法就是要避免像暴力那样每种情况都详详细细的考虑到,这样就做了很多无用功,算法的时间效率也很低下。我们要做的是当给定的数字n不满足各位置单调递增的情况下,不是去遍历小于它的所有数来找到符合要求的数值最大者,而是总结出一种变换的方法能直接将原给定数n变成符合要求的最大数。

先考察各种情况:

  1. n为个位数:个位数的n其本身就是小于或等于n的最大整数。

  1. n为两位的数字:当我们有一个两个数,将其转化为字符串。若num[0] > num[1]时,此时只需将num[0]--并赋值num[1] = 9,就找到了符合题意的数。举个例子:

96 -> "9" "6" -> "8" "9" -> 89

与实际情况相符,逻辑也很容易理解。

  1. n为多位数:此时我们要进行两个数两个数的比较,方法同n为两位数字的情况。但是需要注意的是应该从左向右进行比较还是从右向左进行比较?由于题目要求是”单调递增“,对于一个三位数来说,如果前两位需要进行”操作“,那么第三位无论值为多少都可以直接赋9,同理对于一个四位数、五位数来说,只要前面的两位进行了操作,后面的值都可以直接赋9。

所以这就决定了我们不能从左到右进行比较,例如遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]减一,但此时如果strNum[i - 1]减一了,可能又小于strNum[i - 2],如332。虽然后面的数可以直接赋9,但它导致了中间的数的错乱

当从右向左进行比较时,就不需要考虑中间的数的问题,它可以始终保持前面的数单增、后面的数均为9的特性。

我们在代码中需要定义一个标记,来表示最前的不符合单增原理的需要进行”操作“的两个数的后一个数所在下标,然后对这两个数进行操作后,将其后面的所有数均赋9即可。

整体代码如下:

class Solution {
public:
    int monotoneIncreasingDigits(int n) {
        int flag = -1;
        string num = to_string(n);    // int -> string
        for(int i = num.size() - 1; i > 0; i--){
            if(num[i - 1] > num[i]){
                flag = i;
                num[i - 1]--;
            }
        }
        for(int i = flag; i < num.size(); i++){
            num[i] = '9';
        }
        return stoi(num);    // string -> int
    }
};
  • 时间复杂度:O(n),n 为数字长度

  • 空间复杂度:O(n),需要一个字符串,转化为字符串操作更方便

leetcode 714.买卖股票的最佳时机含手续费

给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。

返回获得利润的最大值。

注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

示例 1:

  • 输入: prices = [1, 3, 2, 8, 4, 9], fee = 2

  • 输出: 8

解释: 能够达到的最大利润:

  • 在此处买入 prices[0] = 1

  • 在此处卖出 prices[3] = 8

  • 在此处买入 prices[4] = 4

  • 在此处卖出 prices[5] = 9

  • 总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8.

贪心策略,就是最低值买,最高值(如果算上手续费还盈利)就卖。

无非就是要找到两个点,买入日期,和卖出日期。

  • 买入日期:其实很好想,遇到更低点就记录一下。

  • 卖出日期:这个就不好算了,但也没有必要算出准确的卖出日期,只要当前价格大于(最低价格+手续费),就可以收获利润,至于准确的卖出日期,就是连续收获利润区间里的最后一天(并不需要计算是具体哪一天)。

本题在遍历prices数组时会遇到以下几种情况:

  1. 该买入时:向后遍历遇到比前面记录的minPrice更小的值就更新minPrice,相当于买入了一个股票。

  1. 该卖出时:如果碰到prices[i] >= minPrice就考虑卖出,此时:

  1. 同时prices[i] <= minPrice + fee,此时卖出并不赚,我们就跳过这个值继续向后寻找。

  1. 同时prices[i] > minPrice + fee,这时就可以计算利润,但是需要注意的是这个利润不一定是最终的利润,因为我们可能进入到了一个获利区间内,在这个区间内的所有值都可以使我们获利,我们当前遍历的值不一定是区间内的最大值,如果此时”卖出“,那么获利就会打折扣。如下:

prices = [1, 3, 2, 8, 9] , fee = 2

我们在1买入,此后3、2均不符合卖出的条件,当遍历到8时,符合卖出的条件,但是观察后面会发现在后方的9卖出会获得更大的利润。所以我们在遍历到8时要先做一个记录:

if(prices[i] > minPrice + fee){
    result += prices[i] - minPrice - fee;
    minPrice = prices[i] - fee;
}

从代码中可以看出如果还在收获利润的区间里,并不是真正的卖出,而计算利润每次都要减去手续费,所以要让minPrice = prices[i] - fee;,这样在明天收获利润的时候,才不会多减一次手续费,而代入后:

// 8 (i)
result = prices[i] - minPrice - fee;    // result = 8 - 1 - 2 = 5
minPrice = prices[i] - fee;             // minPrice = 8 - 2 = 6
// 9 (i + 1)
result += prices[i + 1] - (prices[i] - fee) - fee;     // result = 8 - 1 - 2 + (9 - 8) = 6
minPrice = prices[i + 1] - fee;                        // minPrice = 9 - 2 = 7

可见我们使用minPrice = prices[i] - fee的操作可以实现在获利区间中逐步更新result(相当于一直变换”持有“的股票并记录其利润)。当遍历到prices[i]< minPrice时,就记录新的minPrice,相当于对前面的”持有“的股票进行了”卖出“的操作并”买入“了新的股票。

整体代码如下:

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int result = 0;
        int minPrice = prices[0];
        for(int i = 1; i < prices.size(); i++){
            if(prices[i] < minPrice){
                minPrice = prices[i];
            }
            if(prices[i] >= minPrice && prices[i] <= minPrice + fee){
                continue;
            }
            if(prices[i] > minPrice + fee){
                result += prices[i] - minPrice - fee;
                minPrice = prices[i] - fee;
            }
        }
        return result;
    }
};
  • 时间复杂度:O(n)

  • 空间复杂度:O(1)

leetcode 968.监控二叉树

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

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

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

示例 1:

  • 输入:[0,0,null,0,0]

  • 输出:1

  • 解释:如图所示,一台摄像头足以监控所有节点。

本题很容易发现摄像头可以监控其父节点、本身节点以及其左右孩子。

如果把摄像头放在叶子节点上,就浪费的一层的覆盖。所以把摄像头放在叶子节点的父节点位置,才能充分利用摄像头的覆盖面积。那么为什么是从叶子节点开始分配摄像头的覆盖呢?因为头结点放不放摄像头也就省下一个摄像头, 叶子节点放不放摄像头省下了的摄像头数量是指数阶别的。

局部最优:让叶子节点的父节点安摄像头,所用摄像头最少;整体最优:全部摄像头数量所用最少。

所以大致思路就是从叶子节点开始,给其父节点放个摄像头,然后隔两个节点放一个摄像头,直至到二叉树头结点。

既然要从叶子节点一直向上遍历,那么毫无疑问我们将选用后序遍历(左右中)。

每个节点可能有如下三种状态:

  • 该节点无覆盖

  • 本节点有摄像头

  • 本节点有覆盖

我们分别有三个数字来表示:

  • 0:该节点无覆盖

  • 1:本节点有摄像头

  • 2:本节点有覆盖

在遍历树的过程中,很有可能会遇到空节点的情况,那么应该给空节点赋什么值呢?

回归本质,为了让摄像头数量最少,我们要尽量让叶子节点的父节点安装摄像头,这样才能摄像头的数量最少。那么空节点不能是无覆盖的状态,这样叶子节点就要放摄像头了,空节点也不能是有摄像头的状态,这样叶子节点的父节点就没有必要放摄像头了,而是可以把摄像头放在叶子节点的爷爷节点上,同时也会使得摄像头数量增多。

所以要把空节点的状态设为”有覆盖“。

递归三部曲

  1. 确定递归函数的参数和返回值

题目要求计算监控树的所有节点所需的最小摄像头数量,那么将递归函数类型设置为int型。

同时参数为传入二叉树的根节点。

int traversal(TreeNode* cur)
  1. 确定终止条件

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

if(cur == NULL)
    return 2;
  1. 确定单层递归的逻辑

  • 情况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;
}

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

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

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

整体代码:

/**
 * Definition for a binary tree node.
 * 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) {}
 * };
 */
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;
        if(left == 0 || right == 0){
            result++;
            return 1;
        }
        if(left == 1 || right == 1)
            return 2;
        return -1;        // return -1 虽然逻辑不会到达这里,但是必须加上,把情况补充完整
    }
public:
    int minCameraCover(TreeNode* root) {
        result = 0;
        if(traversal(root) == 0)
            result++;
        return result;
    }
};

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值