力扣贪心算法专题(一)455.分发饼干 376. 摆动序列 53. 最大子序和 122.买卖股票的最佳时机II 1005.K次取反后最大化的数组和 思路及C++实现 贪心算法 动态规划

文章介绍了贪心算法和动态规划在解决一系列问题中的应用,包括455.分发饼干、376.摆动序列、53.最大子序和等题目。贪心算法通过局部最优决策寻找全局最优,而动态规划则通过状态转移矩阵找到最优解。文章提供了各种策略的代码实现,展示了这两种方法在不同场景下的运用。
摘要由CSDN通过智能技术生成

贪心算法

  1. 贪心的本质是选择每一阶段的局部最优,从而达到全局最优。例如,从家去学校,有三条路可以走,要花时间最少,要怎么走?肯定走路程最短的。
  2. 贪心算法没有固定的模板和套路,唯一的难点就是如何通过局部最优,推出整体最优。
  3. 贪心算法最好用的策略就是举反例,如果想不到反例,那就试一试贪心。贪心算法有时候也是常识性推导,所以会认为本应该就这么做。
  4. 贪心算法的套路就是常识性推导加上举反例
  5. 贪心算法解题思路:只要想清楚局部最优是什么,如果推导出全局最优,就够了。类比于回溯/递归,想清楚递归遍历时,要做什么,什么时候停止,就可以了。
  6. 贪心算法题目
  • 简单:455.分发饼干、1005.K次取反后最大化的数组和、860.柠檬水找零
  • 中等
    • 序列问题:376.摆动序列、738.单调递增的数字
    • 股票问题:122.买卖股票的最佳时机II、714.买卖股票的最佳时机含手续费
    • 两个维度权衡问题:135.分发糖果、406.根据身高重建队列
  • 有点难
    • 区间问题:55.跳跃游戏、45.跳跃游戏Ⅱ、452.用最少数量的箭引爆气球、435.无重叠区间、763.划分字母区间、56.合并区间
    • 其他:53.最大子序和、134.加油站、968.监控二叉树

455.分发饼干

在这里插入图片描述

思路 步骤

为了满足更多的小孩,避免饼干浪费,那么需要充分利用饼干尺寸喂饱孩子,全局最优就是喂饱尽可能多的小孩
思路1: 大饼干优先满足胃口大的孩子;
思路2: 用尽量小的饼干满足胃口小的孩子

做法1: 排序、child自减(相当于双指针)、贪心。先将饼干数组和小孩数组排序,然后从后往前遍历小孩数组,用大饼干优先满足胃口大的孩子,并统计满足小孩数量。
做法2: 排序、child自加(相当于双指针)、贪心。先将饼干数组和小孩数组排序,然后从前往后遍历饼干,再遍历的胃口,用尽量小的饼干满足胃口小的孩子,并统计满足小孩数量。
做法3: 排序、双指针、贪心。先将饼干数组和小孩数组排序,然后同时遍历饼干和胃口,用尽量小的饼干满足胃口小的孩子,统计满足小孩和使用过饼干的数量。

代码

  • 做法1:排序 index自减 贪心,大饼干优先满足胃口大的孩子
class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s) {
        //排序
        sort(g.begin(), g.end());
        sort(s.begin(), s.end());
        //cookie自减+for循环(相当于双指针)
        int cookie = s.size()-1;//饼干数组下标,记录哪块饼干被使用过
        int children = 0;//被满足的孩子
        //先遍历孩子 再从后遍历饼干 相当于大饼干满足胃口大的孩子
        for(int i=g.size()-1; i>=0; i--)
        {
            if(cookie>=0 && g[i] <= s[cookie])//优先用大饼干满足胃口大的孩子
            {
                children++;//满足的孩子
                cookie--;//满足的话 这块饼干用过了
            }
        }
        return children;
    }
};

在这里,不能先遍历饼干,再遍历胃口。因为for循环的i 是固定移动的,而if里面的下标 cookie是符合条件cookie>=0 && g[i] <= s[cookie]才移动的。如果for控制饼干if控制胃口,会出现如下情况 :

if的cookie指向 胃口10, for的i指向饼干9,因为饼干9满足不了 胃口10,所以i 向前移动,而cookie走不到g[i] <= s[cookie] 的逻辑,所以cookie不会移动。i 持续向前移动,最后所有的饼干都匹配不上,一定是for控制胃口,if控制饼干。

  • 做法2:排序 index自增 贪心,用尽量小的饼干满足胃口小的孩子
class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s)
    {
        //排序
        sort(g.begin(), g.end());
        sort(s.begin(), s.end());
        int child = 0;
        for(int i=0; i<s.size(); i++)
        {
            if(child < g.size() && g[child] <= s[i]) child++;
        }
        return child;
    }
};
  • 做法3: 排序 双指针 贪心,用尽量小的饼干满足胃口小的孩子
class Solution {
public:
    int findContentChildren(vector<int>& g, vector<int>& s)
    {
        //排序
        sort(g.begin(), g.end());
        sort(s.begin(), s.end());
        int child = 0;
        int cookie = 0;
        for(;child<g.size() && cookie<s.size();)
        {
            if(g[child] <= s[cookie]) child++;//当前饼干可以满足当前孩子的需求,孩子数量+1
            cookie++;//满足,饼干被使用过了;不满足,饼干被抛弃
        }
        return child;
    }
};

376. 摆动序列

在这里插入图片描述

贪心算法 思路 分析 代码

1. 思路: 贪心算法,统计相邻数差值异号的个数,即峰值数,最大摆动序列长度=峰值数+1,注意平坡情况的考虑。

2. 要求删除元素使其达到最大摆动序列:
让峰值尽可能的保持峰值,删除单调坡度上的节点(不包括单调坡度两端的节点),该坡度就有两个局部峰值。让整个序列有最多的局部峰值,从而达到最长摆动序列,最后统计序列长度。

3. 实际操作:

  • 可以不删除节点,统计数组的峰值数量即可。
  • 计算峰值时,遍历数组,三个相邻的数,前两者差值与后两者差值异号,说明有峰值。
  • prediff=nums[i] - nums[i-1],curdiff=nums[i+1] - nums[i]
  • prediff < 0 && curdiff > 0 或者 prediff > 0 && curdiff < 0,统计峰值数,最大摆动序列长度=峰值数+1
  • 考虑三种异常情况,上下坡中有平坡、单调坡中有平坡、数组首尾两端

4. 三种异常情况:

  • 记录峰值条件:prediff < 0 && curdiff > 0 或者 prediff > 0 && curdiff < 0

  • 上下坡中有平坡:
    一共有四个2,删除左边三个2或者右边三个2都可以,统一删除左边三个2,
    i指向第一个2时,prediff > 0 && curdiff = 0 ;
    i指向第二个2时,prediff = 0 && curdiff = 0 ;
    i指向第三个2时,prediff = 0 && curdiff = 0 ;
    i指向第四个2时,prediff = 0 && curdiff < 0,要记录该峰值,删除前三个2后剩下的峰值。因此,该情况下,记录峰值条件是(preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)

  • 单调坡中有平坡:
    如果单调坡中有平坡,是不能算峰值的,只有当坡度摆动变化时,才可以计算峰值数。例如,下图要删除前两个2,有两种更新prediff情况

    • 如果坡度变化后,即出现峰值后再更新prediff,第一个2和最后一个2都会统计为峰值

      if ((preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)) result++;
      preDiff = curDiff;
      
    • 如果坡度变化时,即出现峰值才更新prediff,只有最后一个2统计为峰值

      if ((preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)) 
      {
      	result++;
      	preDiff = curDiff; 
      }
      
  • 数组首尾两端:
    题目仅有一个元素或者含两个不等元素的序列也视作摆动序列,表明数组只有两个不同的元素,摆动序列长度为2。但通过统计差值来计算峰值个数,至少需要三个数字,因此需要考虑数组最左和最右的特殊情况,两种处理方法,
    • 单独处理,如果只有两个元素,且元素不同,那么摆动序列长度为2,result=2;
    • 结合峰值判断条件统计,如果数组只有两个元素,如序列[2,5],可以假设为[2,2,5],那么坡度preDiff = 0。因此,可以假设数组最右边有一个峰值,即result初始值为1。当curDiff > 0 && preDiff <= 0满足时,result++,计算了左边的峰值,最后result=2,摆动序列长度为2。

5. 代码:

class Solution {
public:
    //贪心算法
    int wiggleMaxLength(vector<int>& nums) {
        if(nums.size() <= 1) return nums.size();//空数组或单元素数组
        int prediff = 0;//前两个元素差值
        int curdiff = 0;//后两个元素差值
        int result = 1;//峰值数 摆动序列长度 默认最右有一个峰值
        for(int i=0; i<nums.size()-1; i++)
        {
            curdiff = nums[i+1] - nums[i];
            if((prediff >= 0 && curdiff < 0) || (prediff <= 0 && curdiff > 0))//峰值出现
            {
                result++;//统计峰值
                prediff = curdiff;//更新prediff
            }
        }
        return result;
    }
};

动态规划 思路 步骤 代码

1. 思路: 动态规划,根据当前数组计算山峰山谷数

2. 确定dp数组及其下标含义:
如果nums[i]>nums[i-1],设dp状态为dp[i][0],表示前i个数中,第i个数nums[i]作为山峰的摆动子序列的最长长度;
如果nums[i]<nums[i-1],设dp状态为dp[i][1],表示前i个数中,第i个数nums[i]作为山谷的摆动子序列的最长长度

3. 转移方程:
前i个数,0 < j < i,
如果满足nums[j] < nums[i],则dp[i][0] = max(dp[i][0], dp[j][1] + 1),表示将nums[i]接到前面某个山谷后面,作为山峰
如果满足nums[j] > nums[i],则dp[i][1] = max(dp[i][1], dp[j][0] + 1),表示将nums[i]接到前面某个山峰后面,作为山谷

4. dp数组初始化: 一个数可以接到前面的某个数后面,也可以以自身为子序列的起点,所以初始状态为:dp[0][0] = dp[0][1] = 1

5. 代码

class Solution {
public:
    //动态规划
    //确定dp数组及其下标含义
    int dp[1005][2];//1.创建dp数组  第一维大小  0 <= nums[i] <= 1000
    int wiggleMaxLength(vector<int>& nums)
    {
        memset(dp, 0, sizeof(dp));//2.将数组的每个元素初始化为0
        dp[0][0] = dp[0][1] = 1;//3.dp数组初始化
        //遍历dp数组
        for(int i=1; i<nums.size(); i++)
        {
            dp[i][0] = dp[i][1] = 1;//前i个数的dp数组初始化
            for(int j=0; j<i; j++)//遍历前i个数的dp数组
            {
                if(nums[j] < nums[i]) dp[i][0] = max(dp[i][0], dp[j][1]+1);//nums[i]接到前面某个山谷后面,作为山峰
            }
            for(int j=0; j<i; j++)
            {
                if(nums[j] > nums[i]) dp[i][1] = max(dp[i][1], dp[j][0]+1);//nums[i]接到前面某个山峰后面,作为山谷
            }
        }
        return max(dp[nums.size()-1][0], dp[nums.size()-1][1]);
    }
};

53. 最大子序和

在这里插入图片描述

暴力解法 双层for循环

思路: 第一层for设置起始位置,第二层for循环遍历数组寻找最大值
代码: 力扣可以过用例,但是会超时

class Solution {
public:
    //暴力解法
    int maxSubArray(vector<int>& nums) {
        int result = INT32_MIN;
        int sum = 0;//用来记录每一次遍历连续子数组的和
        for(int i=0; i<nums.size(); i++)//决定每次连续子数组的遍历起点
        {
            sum = 0;//每次连续子数组求和前清零
            for(int j=i; j<nums.size(); j++)
            {
                sum += nums[j];//求和
                result = sum > result ? sum : result;//判断上次子数组和大还是当前子数组和大
            }
        }
        return result;
    }
};

贪心算法 思路 分析 代码

思路:

  • 如果当前当前元素的连续和是负数就放弃,从下一个元素重新计算连续和并记录,最后取最大的连续和。
  • 相当于是暴力解法中的不断调整最大子序和区间的起始位置,如果count取到最大值了,用result记录最大子序和区间和,也算是调整了区间终止位置。
  • 贪心算法就是在count为正数时,开始一个区间的统计,也就是图中红色起始位置。

代码:

class Solution {
public:
    int maxSubArray(vector<int>& nums)
    {
        int result = INT32_MIN;//最小负数
        int sum = 0;//用来记录每一次遍历连续子数组的和
        for(int i=0; i<nums.size(); i++)
        {
            sum += nums[i];
            //记录最大连续和
            if(sum > result) result = sum;
            //连续和为负数就放弃 下一个元素开始求连续和
            if(sum <= 0) sum = 0;
        }
        return result;
    }
};

注意点:

  1. 如果输入用例都是负数,结果是什么?
  2. 是遇到负数就选择起始位置,还是连续和为负选择起始位置?

动态规划 思路 步骤 代码

思路及步骤:

  1. 确定dp数组(dp table)以及下标的含义:dp[i]:以nums[i]为结尾的最大连续子序列和
  2. 确定递推公式:dp[i]只有两个方向可以推出来,一是dp[i - 1] + nums[i],即nums[i]加入当前连续子序列和;二是nums[i],即从头开始计算当前连续子序列和。最后dp[i]一定是取最大的,所以dp[i] = max(dp[i - 1] + nums[i], nums[i]);
  3. dp数组初始化:dp[i]是依赖于dp[i - 1]的状态,dp[0]是递推公式的基础。根据dp[i]的定义,dp[0]应为nums[0],即dp[0] = nums[0]
  4. 确定遍历顺序:递推公式中,dp[i]依赖于dp[i - 1]的状态,需要从前向后遍历。

代码:

class Solution {
public:
    //动态规划
    int maxSubArray(vector<int>& nums)
    {
        if(nums.size()==0) return 0;//剔除空序列
        vector<int> dp(nums.size(), 0);//dp数组定义 nums[i]结尾的子序列和
        dp[0] = nums[0];//dp数组初始化
        int result = dp[0];
        for(int i=1; i<nums.size(); i++)
        {
            dp[i] = max(dp[i-1]+nums[i], nums[i]);//推导
            if(dp[i] > result) result = dp[i];//记录最大连续和dp[i]
        }
        return result;
    }
};

122.买卖股票的最佳时机II

在这里插入图片描述

贪心算法 思路 代码

思路:

  • 把利润分解为以天为单位,而不是整体去考虑,那么根据prices,每天的利润序列:(prices[i] - prices[i - 1])…(prices[1] - prices[0])。
  • 局部最优:收集每天的正利润,收集正利润的区间,也就是股票买卖的区间;全局最优:只需要关注最终利润,不需要记录区间,求得最大利润
  • 贪心算法就是只收集正利润,最后求得最大利润

代码:

class Solution {
public:
    //贪心算法
    int maxProfit(vector<int>& prices) {
        int result = 0;
        for(int i=1; i<prices.size(); i++)//i从1开始,第二天才有利润
        {
            //累加每天的正利润,最后求的最大利润
            result += max(prices[i]-prices[i-1], 0);
        }
        return result;
    }
};

动态规划 思路 步骤 代码

思路及步骤:

  1. 确定dp数组(dp table)以及下标的含义:
  • dp[i][0]第i天持有股票后的最多现金
  • dp[i][1]第i天不持有股票所得最多现金
  1. 确定递推公式:
  • 如果第i天持有股票即dp[i][0], 则有:

    • 第i-1天就持有股票,保持现状,所得现金=昨天持有股票的所得现金,即dp[i - 1][0]
    • 第i天买入股票,所得现金=昨天不持有股票的所得现金 - 今天的股票价格 即dp[i - 1][1] - prices[i]
  • 如果第i天不持有股票即dp[i][1], 则有:

    • 第i-1天就不持有股票,保持现状,所得现金=昨天不持有股票的所得现金 即dp[i - 1][1]
    • 第i天卖出股票,所得现金=按照今天股票价格卖出后所得现金,即prices[i] + dp[i - 1][0]
  • dp[i][0],第i天持股票所剩最多现金 = max(第i-1天持股票所剩现金, 第i-1天持现金-买第i天的股票),即dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);

  • dp[i][1],第i天持有最多现金 = max(第i-1天持有的最多现金,第i-1天持有股票的最多现金+第i天卖出股票)。即dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);

  1. dp数组初始化:
  • dp[0][0]表示最初持有的股票prices[0],即dp[0][0] -= prices[0]
  • dp[0][1]表示最初持有现金,即dp[0][1]=0
  1. 确定遍历顺序:递推公式中,dp[i][0]、dp[i][1]依赖于dp[i - 1][0]、dp[i - 1][1]的状态,需要从前向后遍历。

代码:

class Solution {
public:
    //动态规划
    int maxProfit(vector<int>& prices)
    {
        // dp[i][1]第i天持有的最多现金
        // dp[i][0]第i天持有股票后的最多现金
        int n = prices.size();
        vector<vector<int>> dp(n, vector<int>(2, 0));
        dp[0][0] -= prices[0];//持股票
        dp[0][1] = 0;//持现金
        for(int i=1; i<n; i++)
        {
            // 第i天持股票所剩最多现金 = max(第i-1天持股票所剩现金, 第i-1天持现金-买第i天的股票)
            dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]);//第i天买了股票
            // 第i天持有最多现金 = max(第i-1天持有的最多现金,第i-1天持有股票的最多现金+第i天卖出股票)
            dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]);//第i天卖了股票
        }
        return max(dp[n-1][0], dp[n-1][1]);
    }
};

1005.K次取反后最大化的数组和

在这里插入图片描述

贪心算法 思路 步骤 代码

思路:

  • 序列有正有负,贪心思路,局部最优是让绝对值大的负数变为正数,当前数值达到最大;整体最优是整个数组和达到最大
  • 此时序列只有正数,且k大于0,另一个贪心思路,局部最优是只找数值最小的正整数进行反转,当前数值和可以达到最大;全局最优是整个数组和达到最大。注意整个过程

步骤:

  • 第一步:将数组按照绝对值大小从大到小排序
  • 第二步:从前向后遍历,遇到负数将其变为正数,同时K–
  • 第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完
  • 第四步:求和

代码:

class Solution {
//绝对值从大到小 排序
static bool campare(int a, int b)
{
    return abs(a) > abs(b);
}
public:
    int largestSumAfterKNegations(vector<int>& nums, int k) {
        //1. 排序
        sort(nums.begin(), nums.end(), campare);
        //2. 遇到负数就变号 k也要统计
        for(int i=0; i<nums.size(); i++)
        {
            if(nums[i]<0 && k>0)
            {
                nums[i] = -nums[i];
                k--;
            }
        }
        //3. 此时都是正序列 且k仍大于0,数值最小的数变号
        //k为偶数,偶数次反转后还是正数,不用删减,
        //k为奇数,再处理一次,取数值最小的数变号
        if(k%2 == 1) nums[nums.size()-1] = -nums[nums.size()-1];

        //4.求和
        int result = 0;
        for(int n : nums) result += n;
        return result;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值