贪心算法总结

贪心算法

非常感谢程序员carl的讲解,下面的内容主要来自自己的理解以及参考网站 代码随想录

买卖股票的最佳时间2

之前使用动态规划解决了这个问题

d p [ i ] [ 0 ] dp[i][0] dp[i][0]表示第i天手中持有股票的状态下有多少钱

d p [ i ] [ 1 ] dp[i][1] dp[i][1]表示第i天手中不持有股票的状态下有多少钱

现在我们使用贪心算法来解决这个问题

这里有一个非常巧妙的点

请添加图片描述

就是可以把利润分成每一天的维度

请添加图片描述

55.跳跃游戏

我自己的解法把cover当做了能覆盖到的下标的位置,使用这种方法我调试了很久的代码

    bool canJump(vector<int>& nums) {
        int cover = nums[0];
        if(cover >= nums.size() - 1) return true;
        for(int i = 1; i < nums.size(); ++i){
                int tempcover = 0;
                int tempi = i;
            for(int j = i; j <= cover; ++j){
                if((nums[j] + i) > tempcover){
                    tempcover = nums[j] + i;
                    tempi = j;
                    cout << tempcover<< " "<< tempi<< endl;
                } 
                if(tempcover >= nums.size() - 1)
                    return true;
            }
            if(tempi != i){
                i = tempi - 1;
            }
            if(tempcover > cover)
                cover = tempcover;
        }
        if(cover >= nums.size() - 1) return true;
        else return false;
    }

这种解决方法明显把这道题想复杂了,下面看别人的方法,中间并不对i进行改变

遍历的时候只遍历cover内部

整个过程只维护了一个cover

class Solution {
public:
    bool canJump(vector<int>& nums) {
        int cover = 0;
        if (nums.size() == 1) return true; // 只有一个元素,就是能达到
        for (int i = 0; i <= cover; i++) { // 注意这里是小于等于cover
            cover = max(i + nums[i], cover);
            if (cover >= nums.size() - 1) return true; // 说明可以覆盖到终点了
        }
        return false;
    }
};

45.跳跃游戏2

上面一道题目要求判断能否到达结尾,这道题目要求以最少跳跃次数到达结尾

请添加图片描述

这道题目比较巧妙,我之前使用的是两个循环,外部循环遍历数组,内部循环遍历nums[i]范围内部的找到最大的cover,这样是两个循环,要维护的变量也很多

下面的解法更加的简洁,只需要维护两个距离,需要注意的是i遍历到curDistance的时候,最大的nextDistance也找到了,这时候执行跳跃一次,并且更新curDistance

    int jump(vector<int>& nums) {
        if(nums.size() == 1) return 0;
        int curDistance = 0;
        int nextDistance = 0;
        int ans = 0;
        for(int i = 0; i < nums.size(); ++i){
            nextDistance = max(nextDistance, nums[i] + i);
            if(i == curDistance){
                ans++;
                curDistance = nextDistance;
                if(nextDistance >= nums.size() - 1)
                    break;
            }
        }
        return ans;
    }

134.加油站

这道题使用两个for循环的暴力解法,处理边界情况使用我将近一个小时的时间,题目也有误导的成分在里面

    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        if(cost.size() == 1 && gas[0] >= cost[0]) return 0;//只有一个加油站我真服了,这种环形还要搞
         for(int i = 0; i < cost.size(); ++i){
            int tank = gas[i] - cost[i];
            if(tank <= 0)//要去下一个地方,必须当前邮箱里面的油比当前的花费多
                continue;
            int index = 0;
            for(int j = i + 1; j < i + cost.size(); ++j){
                index++;
                tank += gas[j % cost.size()] - cost[j % cost.size()];
                if(tank <= 0)
                    break;
            }
            if(tank >= 0 && index == cost.size() - 1)
                return i;
        }
        return -1;
    }

这时候程序员carl告诉我们,for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历,要善于使用while!

    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        for (int i = 0; i < cost.size(); i++) {
            int rest = gas[i] - cost[i]; // 记录剩余油量
            int index = (i + 1) % cost.size();
            while (rest > 0 && index != i) { // 模拟以i为起点行驶一圈(如果有rest==0,那么答案就不唯一了)
                rest += gas[index] - cost[index];
                index = (index + 1) % cost.size();
            }
            // 如果以i为起点跑一圈,剩余油量>=0,返回该起始位置
            if (rest >= 0 && index == i) return i;
        }
        return -1;
    }

23_12_14,回到我们最爱的加油站问题

很明显不管怎么样的暴力算法,最终都会超时

这时候,我们掏出我们的贪心算法!

这里需要注意的是

从任何地方出发都不能跑完一圈的只有一种情况,就是totaltank < 0

    int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
        int curtank = 0;//当前油箱里面的油量
        int totaltank = 0;//判断总的加油数是不是小于总花费,这种情况不可能跑完
        int startstation = 0;
        for(int i = 0; i < cost.size(); ++i){
            curtank += gas[i] - cost[i];
            totaltank += gas[i] - cost[i];
            if(curtank < 0){
                startstation = i + 1;
                curtank = 0;
            }
                

        }
        if(totaltank < 0) return -1;
        return startstation;
    }

这里贴上代码,判断非常巧妙,如果i位置的curtank < 0的话说明前面所有位置出发到达i位置都没法走,那么可能的出发位置只能在i+1及以后

135.分发糖果

每个孩子至少发一个糖果,难么先全部初始化发一个;

这道题目一定是要确定一边之后,再确定另一边,例如比较每一个孩子的左边,然后再比较右边,如果两边一起考虑一定会顾此失彼

先确定右边评分大于左边的情况(也就是从前向后遍历)

此时局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果

局部最优可以推出全局最优。

如果ratings[i] > ratings[i - 1] 那么[i]的糖 一定要比[i - 1]的糖多一个,所以贪心:candyVec[i] = candyVec[i - 1] + 1

// 如果只比左边的值大,那么当前孩子的糖果数量要比左边孩子的糖果多1。

// 如果只比右边的值大,那么当前孩子的糖果数量要比右边孩子的糖果多1。

// 那么i位置最终的糖果数量就是选择比左边孩子多1和比右边孩子多1中最大的那个。

// 要满足上面的第二步,可以使用两次遍历,第一次从左往右,如果右边的孩子比左边得分高,那么右边的孩子糖果数量就要比左边的多1。这样每个孩子都能满足右边的条件,就是右边比他得分高的都会比他多一个。满足了右边的条件之后我们还要满足左边的条件.

//左边遍历一次,右边遍历一次,然后在每个点取得最大值

int candy(vector<int>& ratings) {
        vector<int> leftCandy(ratings.size(), 1);
        vector<int> rightCandy(ratings.size(), 1);
        int result = 0;
        //当前位置和左边比较
        for(int i = 1; i < ratings.size(); ++i){
            if(ratings[i] > ratings[i-1])
                leftCandy[i] += 1;
        }
        //当前位置和右边比较
        for(int i = ratings.size() - 2; i >= 0; --i){
            if(ratings[i] > ratings[i + 1])
                rightCandy[i] += 1;
        }

        for(int i = 0; i < ratings.size(); ++i){
            result += max(leftCandy[i], rightCandy[i]);
        }
        return result;
}

860柠檬水找零

406.根据身高重建队列

这道题目的思路比较巧妙

请添加图片描述

首先按照身高从大到小排序,然后后面从头开始,根据k来确定在结果中放置的位置

请添加图片描述

这样做的原理就是,比如插入【6,1】不会在有比6大的数插入到他前面所以会一直满足要求

很巧妙的处理方法

vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
        sort (people.begin(), people.end(), cmp);
        list<vector<int>> que; // list底层是链表实现,插入效率比vector高的多
        for (int i = 0; i < people.size(); i++) {
            int position = people[i][1]; // 插入到下标为position的位置
            std::list<vector<int>>::iterator it = que.begin();
            while (position--) { // 寻找在插入位置
                it++;
            }
            que.insert(it, people[i]);
        }
        return vector<vector<int>>(que.begin(), que.end());
    }

注意上面代码使用是list<vector> 而不是 vector<vector>

因为list底层是链表实现的,做插入运算的速度远远比vector快

并且还要注意的是list 的迭代器不支持随机访问!!!

由于链表的结构,std::list的迭代器只支持前进和后退操作,不能像随机访问迭代器那样使用[]运算符来直接访问列表中的任意元素。这意味着不能使用迭代器的算术运算符(如+-)进行跳跃式的访问。

所以在寻找插入位置的时候vector可以使用 q u e . b e g i n ( ) + p o s i t i o n que.begin() + position que.begin()+position,而list必须使用while对迭代器进行累加来确切插入的位置。

452用最少数量的箭引爆气球

这道题目处理的也非常巧妙

如何判断当前的气球无法和前面的不使用一支箭呢?

我们维护一个同一只箭矢的最小右边界 (这也是箭矢应该射入的地方)

请添加图片描述

class Solution {
    static bool cmp(vector<int> &a, vector<int> &b){
       return a[0] < b[0];
    }
public:
    int findMinArrowShots(vector<vector<int>>& points) {
        //按照气球左边边界进行排序
        sort(points.begin(), points.end(), cmp);
        int arrayIndex = points[0][1];
        int arrayCount = 1;
        if(points.size() == 1) return 1;
        for(int i = 1; i < points.size(); ++i){
            if(points[i][0] > arrayIndex){
                arrayCount++;
                arrayIndex = points[i][1];
            }
            else{
                arrayIndex = min(arrayIndex, points[i][1]);
            }
        }
        return arrayCount;
    }
};

cmp函数传参不传引用的话会超时

如果不使用引用,而是将参数类型定义为vector<int>,则在函数调用时会发生向量的拷贝,将整个向量的内容复制到函数的形参中。对于大型向量来说,这样的拷贝操作可能会导致性能下降和额外的内存开销。

为了保证引用的数据的安全,最好加上const关键字进行修饰

435.无重叠区间

这道题目其实就是上面的题目的变体

上一道题目求的是有重叠起来的区间算一个

这道题目就是求重叠区间的数量,刚好累加加结果的位置不一样罢了,一个在if里面,一个在else里面

还要注意的是上一道题目如果重叠的位置是一个点,也是可以的

但是这道题目不可以

    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        //计算需要删除的区间数量
        //用一支箭穿过重叠的区间
        //维护一个重叠区间的最小右边界rightIndex
        sort(intervals.begin(), intervals.end(), cmp);
        int rightIndex = intervals[0][1];
        int arrowCount = 0;
        for(int i = 1; i < intervals.size(); ++i){
            if(intervals[i][0] >= rightIndex){//不重叠
                rightIndex = intervals[i][1];
            }
            else{//重叠
                rightIndex = min(rightIndex, intervals[i][1]);
                arrowCount++;
            }
        }
        return arrowCount;
    }

763.划分字母区间

非常有趣的一道题目

要注意的点全部放在了代码注释中

    vector<int> partitionLabels(string s) {
        vector<int> rightestIndex(26, 0);
        for(int i = 0; i < s.size(); ++i){ //循环过后对每个字母的最大值的位置就找到了
            rightestIndex[s[i] - 'a'] = i; 
        }
        int cutIndex = 0; //记录裁切的位置 这个裁切位置其实也是在找一段中的字母中最大的右边的位置的Index
        int lastcutIndex = 0; //记录上次裁切的位置
        vector<int> result;
        for(int i = 0; i < s.size(); ++i){
            cutIndex = max(cutIndex, rightestIndex[s[i] - 'a']); //记录当前段中的字母携带的最大的的裁切位置
            if(i == cutIndex){//如果当前位置恰好是cutIndex记录的最大的裁切位置
                result.push_back(i - lastcutIndex + 1);
                lastcutIndex = cutIndex + 1;
            }
        }
        return result;
    }

56.合并区间

    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        sort(intervals.begin(), intervals.end(), 
        [](const vector<int> &a, const vector<int> &b){ return a[0] < b[0];});
        vector<vector<int>> result;
        result.emplace_back(intervals[0]);
        for(int i = 1; i < intervals.size(); ++i){
            if(intervals[i][0] <= result.back()[1]){
                result.back()[1] = max(result.back()[1], intervals[i][1]);
            }
            else
                result.emplace_back(intervals[i]);
        }
        return result;
    }

使用了.back()函数来获取vector中的最后一个值,用来拓展重叠区间的右边,比我自己想的方法,需要记录两边的位置要方便多了。

738.单调递增的数组

说实话这道题比较抽象,要找到比当前数字小的最大的单调递增的数字

332->299

注意的点包括从后向前遍历

反正大概率如果是个大数的话,大概率后面有很多的9

    int monotoneIncreasingDigits(int n) {
        string str = to_string(n);
        int flag = -1;
        for(int i = str.size() - 1; i > 0; --i){
            if(str[i-1] > str[i]){
                flag = i;
                str[i-1]--;
            }
        }
        for(int i = flag; i < str.size(); ++i){
            str[i] = '9';
        }
        return stoi(str);
    }

注意to_string 和 stoi 的用法

968监控二叉树

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

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

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

那么有同学可能问了,为什么不从头结点开始看起呢,为啥要从叶子节点看呢?

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

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

局部最优推出全局最优,找不出反例,那么就按照贪心来!

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

来看看这个状态应该如何转移,先来看看每个节点可能有几种状态:

有如下三种:

  • 该节点无覆盖
  • 本节点有摄像头
  • 本节点有覆盖

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

  • 0:该节点无覆盖
  • 1:本节点有摄像头
  • 2:本节点有覆盖
class Solution {
private:
    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
        // 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;
    }

public:
    int minCameraCover(TreeNode* root) {
        result = 0;
        // 情况4
        if (traversal(root) == 0) { // root 无覆盖
            result++;
        }
        return result;
    }
};
 // left == 2 && right == 1 左节点有覆盖,右节点有摄像头
    // left == 1 && right == 1 左右节点都有摄像头
    // 其他情况前段代码均已覆盖
    if (left == 1 || right == 1) return 2;

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

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


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值