力扣日记13:贪心

文章目录

        • 455. 分发饼干
        • 376. 摆动序列
        • 53. 最大子数组和
        • 122. 买卖股票的最佳时机 II
        • 55. 跳跃游戏
        • 45. 跳跃游戏 II
        • 1005. K 次取反后最大化的数组和
        • 134. 加油站
        • 135. 分发糖果
        • 860. 柠檬水找零
        • 406. 根据身高重建队列
        • 452. 用最少数量的箭引爆气球
        • 435. 无重叠区间
        • 763. 划分字母区间
        • 56. 合并区间
        • 738. 单调递增的数字
        • 968. 监控二叉树

455. 分发饼干

  • 最初代码
var findContentChildren = function(g, s) {
    g.sort((a, b) => a - b);
    s.sort((a, b) => a - b);
    let sum = 0, i = 0, j = 0;
    while (i < g.length && j < s.length) {
        if (s[j] >= g[i]) {
            sum++;
            i++;
            j++;
        }
        else j++;
    }
    return sum;
};
  • 接下来是正解,小饼干先喂小胃口,不符合就增
var findContentChildren = function(g, s) {  
    g.sort((a, b) => a - b);
    s.sort((a, b) => a - b);
    let res = 0, index = s.length - 1;
    for (let i = g.length - 1; i >= 0; i--) {
        if (index >= 0 && s[index] >= g[i]) {
            res++;
            index--;
        }
    }
    return res;
};

376. 摆动序列

  • 实际上不需要删除元素,只需要将坡上的节点忽略即可
    力扣日记13:贪心_第1张图片
var wiggleMaxLength = function(nums) {
    if (nums.length <= 1) return nums.length;
    let curDiff = 0, preDiff = 0;
    let res = 1;
    for (let i = 0; i < nums.length - 1; i++) {
        curDiff = nums[i + 1] - nums[i];
        if ((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)) {
            res++;
            preDiff = curDiff;
        }
    }
    return res;
};

53. 最大子数组和

  • 用dp比较容易,但是对于贪心来说有点难
  • 当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。
  • 每次循环都要比较记录最大的总和
var maxSubArray = function(nums) {
    let result = -Infinity
    let count = 0
    for(let i = 0; i < nums.length; i++) {
        count += nums[i]
        if(count > result) {
            result = count
        }
        if(count < 0) {
            count = 0
        }
    }
    return result
};

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

假如第0天买入,第3天卖出,那么利润为:prices[3] - prices[0]。

相当于(prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0])。

此时就是把利润分解为每天为单位的维度,而不是从0天到第3天整体去考虑!

我们就只要收集每天的正收入,累加即可得最大收益

var maxProfit = function(prices) {
    let res = 0;
    for (let i = 1; i < prices.length; i++) {
        res += Math.max(0, prices[i] - prices[i - 1]);
    }  
    return res;
};

55. 跳跃游戏

  • 每次的跳跃步数即为跳跃的覆盖范围,那么问题就转换为跳跃的覆盖范围能不能覆盖到终点
  • 每次移动取最大的跳跃步数,同时更新覆盖范围
    力扣日记13:贪心_第2张图片
var canJump = function(nums) {
    let cover = 0;
    for (let i = 0; i <= cover; i++) {
        cover = Math.max(cover, nums[i] + i);
        if (cover >= nums.length - 1) return true;
    }
    return false;
};

45. 跳跃游戏 II

  • 就是移动下标达到了当前覆盖的最远距离下标时,步数就要加一,来增加覆盖距离。最后的步数就是最少步数
var jump = function(nums) {
    let res = 0, curDistance = 0, nextDistance = 0;
    if (nums.length === 1) return 0;
    for (let i = 0; i < nums.length; i++) { 
        nextDistance = Math.max(nextDistance, nums[i] + i);
        if (i === curDistance) {
            if (curDistance !== nums.length - 1) {
                res++;
                curDistance = nextDistance;
                if (curDistance >= nums.length - 1) break;
            } else break;
        } 
    }
    return res;
};
  • 第二种方法简化了,移动下标只要遇到当前覆盖最远距离的下标,直接步数加一,不考虑是不是终点的情况。移动下标最大只能移动到nums.size - 2
var jump = function(nums) {
    let curIndex = 0
    let nextIndex = 0
    let steps = 0
    for(let i = 0; i < nums.length - 1; i++) {
        nextIndex = Math.max(nums[i] + i, nextIndex)
        if(i === curIndex) {
            curIndex = nextIndex
            steps++
        }
    }

    return steps
};

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

  • 先把数组排序,从头开始让负数取正,同时求和,接下来会有几种情况
    1. k用完,返回求和结果
    2. k为偶数,对结果没有影响
    3. k为奇数,我们需要找到最小的数(当前数组已全部为正),将求和结果 - 2 x 最小值
var largestSumAfterKNegations = function (nums, k) {
    let res = 0;
    nums.sort((a, b) => a - b);
    for (let i = 0; i < nums.length; i++) {
        if (nums[i] < 0 && k > 0) {
            nums[i] *= -1;
            k--;
        }
        res += nums[i];
    }
    if (k === 0) {
        return res;
    } else {
        if (k % 2 === 0) {
            return res;
        } else {
            return res - 2 * Math.min(...nums);
        }

    }
};
  • 题解还有优化的空间
  • 改sort,让数组以绝对值从大到小排序,这样后面减的时候只需要拿数组的最后一个元素(最小元素)即可
// 版本二 (优化: 一次遍历)
var largestSumAfterKNegations = function(nums, k) {
    nums.sort((a, b) => Math.abs(b) - Math.abs(a)); // 排序
    let sum = 0;
    for(let i = 0; i < nums.length; i++) {
        if(nums[i] < 0 && k-- > 0) { // 负数取反(k 数量足够时)
            nums[i] = -nums[i];
        }
        sum += nums[i]; // 求和
    }
    if(k % 2 > 0) { // k 有多余的(k若消耗完则应为 -1)
        sum -= 2 * nums[nums.length - 1]; // 减去两倍的最小值(因为之前加过一次)
    }
    return sum;
};

134. 加油站

  • 不太简单的暴力
var canCompleteCircuit = function(gas, cost) {
    for(let i = 0; i < cost.length; i++) {
        let rest = gas[i] - cost[i]  //记录剩余油量
        // 以i为起点行驶一圈,index为下一个目的地
        let index = (i + 1) % cost.length
        while(rest > 0 && index !== i) {
            rest += gas[index] - cost[index]
            index = (index + 1) % cost.length
        }
        if(rest >= 0 && index === i) return i
    }
    return -1
};
  • 还可以从全局着眼

情况一:如果gas的总和小于cost总和,那么无论从哪里出发,一定是跑不了一圈的

情况二:rest[i] = gas[i]-cost[i]为一天剩下的油,i从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点。

情况三:如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能这个负数填平,能把这个负数填平的节点就是出发节点。
例如:例1的rest(gas - cost)数组为[-2, -2, -2, 3, 3],min = -6,我们需要从后往前找出一段能够填补min的一段路,即一段路亏油,我们要找一段路存储到足够他亏的油

var canCompleteCircuit = function(gas, cost) {
    let curSum = 0, min = Infinity;
    for (let i = 0; i < cost.length; i++) {
        curSum += gas[i] - cost[i];
        if (curSum < min) min = curSum;
    }
    if (curSum < 0) return -1;
    if (min > 0) return 0;
    for (let i = cost.length - 1; i >= 0; i--) {
        min += gas[i] - cost[i];
        if (min >= 0) {
            return i;
        }
    } 
    return -1;
};

135. 分发糖果

  • 两次贪心的策略:

一次是从左到右遍历,只比较右边孩子评分比左边大的情况。
一次是从右到左遍历,只比较左边孩子评分比右边大的情况。

var candy = function(ratings) {
    let candys = new Array(ratings.length).fill(1);
    for (let i = 1; i < candys.length; i++) {
        if (ratings[i] > ratings[i - 1]) {
            candys[i] = candys[i - 1] + 1;
        }
    }
    for (let i = candys.length - 2; i >= 0; i--) {
        if (ratings[i] > ratings[i + 1]) {
            candys[i] = Math.max(candys[i], candys[i + 1] + 1);
        }
    }
    return candys.reduce((a, b) => {
        return a + b;
    })
};

860. 柠檬水找零

  • 简单模拟即可,对于20的情况首选10+5为贪心思想
var lemonadeChange = function(bills) {
    let five = 0, ten = 0;
    for (let i = 0; i < bills.length; i++) {
        if (bills[i] === 5) five++;
        if (bills[i] === 10) {
            if (five) {
                five--;
                ten++;
            } else {
                return false;
            }
        }
        if (bills[i] === 20) {
            if (ten && five) {
                ten--;
                five--;
            } else if (five >= 3) {
                five -= 3;
            } else {
                return false;
            }
        }
    }
    return true;
};

406. 根据身高重建队列

  • 与发糖果一样,有两个维度先单一考虑一个维度,这里我们若按k排序没有意义,按身高排序,前面的节点一定比本节点高,这样后续就可以不顾忌的按k插入
    力扣日记13:贪心_第3张图片
var reconstructQueue = function(people) {
    let que = [];
    people.sort((a, b) => {
        if (a[0] !== b[0]) {
            return b[0] - a[0];
        } else {
            return a[1] - b[1];
        }
    })
    for (let i = 0; i < people.length; i++) {
        que.splice(people[i][1], 0, people[i]);
    }
    return que;
};

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

  • 将数组照头位置排序后,若后一个气球左边界大于了前一个气球的最小右边界,则需要添加箭
var findMinArrowShots = function(points) {
    let res = 1;
    points.sort((a, b) => {
        return a[0] - b[0];
    })
    for (let i = 1; i < points.length; i++) {
        if (points[i][0] > points[i - 1][1]) {
            res++;
        } else {
            points[i][1] = Math.min(points[i - 1][1], points[i][1]);
        }
    }
    return res;
};

435. 无重叠区间

  • 按照右边界排序,就要从左向右遍历,因为右边界越小越好,只要右边界越小,留给下一个区间的空间就越大,所以从左向右遍历,优先选右边界小的。
  • 从左向右记录非交叉区间的个数。最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了。
  • 右边界排序之后,局部最优:优先选右边界小的区间,所以从左向右遍历,留给下一个区间的空间大一些,从而尽量避免交叉。全局最优:选取最多的非交叉区间。
    力扣日记13:贪心_第4张图片
var eraseOverlapIntervals = function(intervals) {
    intervals.sort((a, b) => a[1] - b[1]);
    let end = intervals[0][1], count =  1;
    for (let i = 1; i < intervals.length; i++) {
        if (end <= intervals[i][0]) {
            end = intervals[i][1];
            count++;
        }
    }
    return intervals.length - count;
};

763. 划分字母区间

var partitionLabels = function(s) {
    let hash = {};
    let res = [];
    for (let i = 0; i < s.length; i++) {
        hash[s[i]] = i;  // // 统计每一个字符最后出现的位置
    }
    let right = 0, left = 0;
    for (let i = 0; i < s.length; i++) {
        right = Math.max(right, hash[s[i]]);   // 找到字符出现的最远边界
        if (i === right) {
            res.push(right - left + 1);
            left = i + 1;
        }
    }
    return res;
};

56. 合并区间

  • 按左边界从小到大排序,然后针对重叠的区间取最大的右边界
var merge = function(intervals) {
    let res = [];
    intervals.sort((a, b) => a[0] - b[0]);
    let pre = intervals[0];
    for (let i = 1; i < intervals.length; i++) {
        let cur = intervals[i];
        if (cur[0] > pre[1]) {
            res.push(pre);
            pre = cur;
        } else {
            pre[1] = Math.max(cur[1], pre[1]);
        }
    }
    res.push(pre);
    return res;
};

738. 单调递增的数字

  • 最重要的是遍历的顺序,若从左到右遍历,那么332的情况我们就只能得到329,因为无法判断改变后是否还符合递增顺序;而从右到左遍历,我们就可以使用到上一次改变的数字
var monotoneIncreasingDigits = function(n) {
    let flag;
    n = n.toString();
    n = n.split('').map((item) => +item);
    for (let i = n.length - 1; i > 0; i--) {
        if (n[i] < n[i - 1]) {
            flag = i;
            n[i - 1]--;
            n[i] = 9;
        }
    }
    for (let i = flag; i < n.length; i++) {
        n[i] = 9;
    }
    return +n.join('');
};

968. 监控二叉树

你可能感兴趣的:(力扣日寄,leetcode,算法,数据结构)