贪心算法整理|周末学习

306 阅读4分钟

本文已参与周末学习计划,点击链接查看详情:juejin.cn/post/696572…

LeetCode从低效到高效,点击

贪心算法

核心思路

贪心算法一般用于每次取局部最优就能得到全局最优的问题,此类题目的难点有点像动态规划,需要找到问题求解的正确顺序。下面将找一些经典例题用来体会算法思想。

经典例题

  1. 分发糖果

    老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。

    你需要按照以下要求,帮助老师给这些孩子分发糖果:

    每个孩子至少分配到 1 个糖果。 评分更高的孩子必须比他两侧的邻位孩子获得更多的糖果。 那么这样下来,老师至少需要准备多少颗糖果呢?

本题的求解难点在如何确定已经满足了表现好的比左右给的都多的条件,如果每次都同时考虑左右那就会调入到这道题的陷阱,这道题的解法是先左到右扫面一边,只考虑是不是比左边大;然后再从右边向左边扫描,看是不是比右边大。这种两次扫描的方式跟搜索那边的一些题有点像。

int candy(vector<int>& ratings) {
    vector<int> value(ratings.size(),1);
    for(int i=1;i<ratings.size();i++){
        if(ratings[i]>ratings[i-1]){
            value[i] = value[i-1]+1;
        }
    }
    for(int j=ratings.size()-2;j>=0;j--){
        if(ratings[j]>ratings[j+1])
            value[j] = max(value[j],value[j+1]+1);
    }
    return accumulate(value.begin(),value.end(),0);
}
  1. 无重叠区间

    给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。

    注意:

    可以认为区间的终点总是大于它的起点。 区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。 示例 1:

    输入: [ [1,2], [2,3], [3,4], [1,3] ]

    输出: 1

    解释: 移除 [1,3] 后,剩下的区间没有重叠。 示例 2:

    输入: [ [1,2], [1,2], [1,2] ]

    输出: 2

    解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。 示例 3:

    输入: [ [1,2], [2,3] ]

    输出: 0

    解释: 你不需要移除任何区间,因为它们已经是无重叠的了。

本题是经典的贪心算法问题,难点就是如何设置求解顺序,可以考到判断交叉有两个范围要去卡,如果左右都去考虑那就不知道从哪个开始要,所以先要想到按照左边界排个序,接下来考虑如何取舍。由于已经按照左边界排序,判断是否交叉只要看我的左边界会不会跟上一个右边界有冲突。当出现冲突时如何处理?首先如果上一个放进去没有发生冲突,那么由于本次的左边界一定大于等于上一个,那么就肯定不会跟上上个出现冲突。为了保证最多的范围放进去,保留右边界小的。从第一个开始这么做,直到遍历结束。

int eraseOverlapIntervals(vector<vector<int>> &intervals){
    sort(intervals.begin(), intervals.end(), [](vector<int> &x, vector<int> &y) {
        return x[0] < y[0];
    });
    int flag = intervals[0][1];
    int count = 0;
    for (int i = 1; i < intervals.size(); i++)
    {
        // 发生碰撞,保留结尾小的那个,count不变,修正flag
        if (intervals[i][0] < flag)
        {
            flag = min(flag, intervals[i][1]);
            count++;
        }
        else
        {
            
            flag = intervals[i][1];
        }
    }
    return count;
}
  1. 判断子序列

    给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

    字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

    示例 1:

    输入:s = "abc", t = "ahbgdc" 输出:true 示例 2:

    输入:s = "axc", t = "ahbgdc" 输出:false

本题是关于子序列的简单题,具体使用双指针就可以实现

class Solution {
public:
    bool isSubsequence(string s, string t) {
        int i=0,j=0;
        while(i<s.size()&&j<t.size()){
            if(s[i]==t[j]) i++;
            j++;
        }
        return i==s.size();
    }
};
  1. 跳跃游戏

    给定一个非负整数数组 nums ,你最初位于数组的 第一个下标 。

    数组中的每个元素代表你在该位置可以跳跃的最大长度。

    判断你是否能够到达最后一个下标。

     

    示例 1:

    输入:nums = [2,3,1,1,4] 输出:true 解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。 示例 2:

    输入:nums = [3,2,1,0,4] 输出:false 解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。

本题乍一看是一道搜索的题目,因为每个值大于1都代表有多种不同的走法,首先让人想到的思路就是dfs搜索到合适的就停止搜素,但是这样效率太差。那么换一种思路考虑,只有数组中的值为0时才可能出现跳不过取得情况,那么问题就变成了搜索到0判断能不能跳过这个位置,使用一个变量保存之前的位置能够跳跃到的最远的位置,这样就可以判断是否能够通过这个0,将整体复杂度降低到O(n)

// 方案一:dfs
// 方法比较暴力,基本是穷举所有走的方式看能不能跳过去
class Solution {
public:
    bool canJump(vector<int>& nums) {
        vector<bool> isVisited(nums.size(),false);
        stack<int> sk;
        sk.emplace(0); 
        isVisited[0] = true;
        while(!sk.empty()&&(!isVisited.back())){
            int idx = sk.top();
            sk.pop();
            for(int i=1;i<=nums[idx];i++){
                if(idx+i<nums.size()&&!isVisited[idx+i]){
                    isVisited[idx+i] = true;
                    sk.push(idx+i);
                }
            }
        }
        return isVisited.back();
    }
};
// 针对性编码,可以想象到唯一能够阻止跳跃的只有0,那么从后往前搜索,拿到0就看一下能不能够消除这个0的影响,但是这个
// 就需要处理正搜索一个零的时候又出现另一个零的问题

class Solution {
public:
    bool canJump(vector<int>& nums) {
        int gap = nums[0];
        int len = nums.size();
        for(int i=1;i<len;i++){
            if(gap<i){
                return false;
            }
            else{
                
                gap = max(gap,i+nums[i]);
            }
        }
        return true;
    }
};
  1. 数组中超过一半的数

    数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。

首先数组中超过一半的数存在什么特性,那就跟找中位数一样如果拿到一个数更令一个数不一样,那两者就要互相抵消,本题如果能够保证一定存在唱过一半的数那就没有多难了,但是现在可能没有超过一半的数,就需要再次检查一次。从这道题可以看出来不是所有题目都是要能够一趟就完全解决。

class Solution {
public:
    int MoreThanHalfNum_Solution(vector<int> numbers) {
        int len = numbers.size();
        if(len==0) return 0;
        int count=1,numb = numbers[0];
        for(int i=1;i<len;i++){
            if(numbers[i]==numb){
                count++;
            }
            else{
                count--;
            }
            if(count==0){
                numb = numbers[i];
                count = 1;
            }
        }
//         check
        if(count>0){
            count = 0;
            for(int i=0;i<len;i++){
                if(numbers[i]==numb){
                    count++;
                }
            }
            if(count*2>len) return numb;
        }
        return 0;
        
    }
};