贪心算法的第一个周末

190 阅读2分钟

正文

贪心算法

什么是贪心算法?

贪心算法的本质是选择每一阶段的局部最优,从而达到全局最优。

贪心算法的一般解题步骤:

贪心算法一般分为如下的四步:

  1. 将问题分解为若干个子问题
  2. 找出合适的贪心策略
  3. 求解每一个子问题的最优解
  4. 将局部最优解堆叠为全局的最优解

那么我们开始我们的刷题之路吧。

455. 分发饼干

image.png

思路分析:

其实是可以这么考虑的,目前是有两种思考的方式, 优先满足于小胃口的孩子还是优先于满足大胃口的孩子。

如果说我尝试的是从小胃口的开始满足的话, g[i]胃口值都要小于等于每块饼干的尺寸s[i]

g[i] <= s[i]

所以我们需要对饼干和胃口的数组进行一次升序的排列

g.sort((a, b) => a - b)
s.sort((a, b) => a - b)

那么实际中我们去思考的点是:

  • 谁去满足于谁
  • 需要 s[i]去满足于 g[i] 从小到大的胃口

代码实现:

/**
 * @param {number[]} g
 * @param {number[]} s
 * @return {number}
 */
var findContentChildren = function(g, s) {
    g.sort((a, b) => a-b)
    s.sort((a, b) => a-b)
    
    let index = 0; // 返回满足目标的最大值
    for (let i = 0; i < s.length; i++) {
        if (g[index] < s[i]) {
           index++;
        }
    }
    return index;
}
376. 摆动序列

image.png

image.png

思路分析:

image.png

局部最优: 删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部的峰值

整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列

我们从局部最优推出到全局的最优,并举不出反例的话, 我们就尝试一下贪心的操作:

在我们实际的操作过程中, 其实连删除的操作都不用做,因为题目的要求是最长摆动子序列的长度,我们只需要统计数组峰值的数量就可以了。

大致会分为两种情况:

  1. preDiff <= 0 和 curDiff > 0
  2. preDiff >= 0 和 curDiff < 0

image.png

代码实现:

/**
 * @param {number[]} nums
 * @return {number}
 */

var wiggleMaxLength = function(nums) {
    // base case
    if (nums.length <= 1) return nums.length;
    let result = 1;
    
    let curDiff = 0; // 当前的一堆差值
    let preDiff = 0; // 前一对差值
    
    for (let i = 0; i < nums.length; i++) {
        curDiff = nums[i+1] - nums[i];
        // 如果当前差值和上一个差值为一正一负
        // 等于0的情况表示 初始化的 preDiff
        if ((preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)) {
          result++;
          // 更新前一个 
          preDiff = curDiff;
        }
    }
    return result;
}

53. 最大子数组和

image.png

思路分析:

那么这道题如何贪心呢? 我们举个例子,如果 -2, 1 在一起计算起点的时候, 一定是从1开始的,为啥呢?因为负数肯定会拉低总和的, 这就是贪心贪的地方。

那么,如果做到局部最优呢?

当前的"连续和"为负数的时候就应该立刻放弃, 从下一个元素重新计算"连续和",因为负数加上下一个元素“连续和”只会越来越小的。

全局最优: 选取最大"连续和"

我们在局部最优的情况下, 并记录最大的"连续和",可以推出全局最优。

如果是从我们写代码的角度来说,遍历nums, 从头开始用count 累加和, 如果count 一旦加上nums[i] 变为负数了, 那么就应该从 nums[i]+ nums[i+1] 开始。因为变为负数的count, 只会拖累总和。

那么如何调整区间的终止位置呢?如何保证能获得最大的"连续和"?

if (count > result) result = count

就相当于是用result来记录最大子序列区间和(变相调整了终止的位置)

代码实现:

/**
 * @param {number[]} nums
 * @return {number}
 */
 
 var maxSubArray = function(nums) {
     // base case
     let count = 0;
     let reslut = -Infinity;
     
     for (let i = 0; i < nums.length; i++) {
         // 累加和
         count += nums[i]
         // 去区间的最大值
         if (count > result) {
            result = count;
         }
         // 其实这里的设计就是说先比较 count 和 result 的大小
         // 可能在下一次的累加中 count < 0 我们直接返回上次的结果就可以了
         // 相当于重置最大子序起始位置,因为遇到了负数一定是拉低了总和
         if (count <= 0) count = 0;
     }
     return result;
 }