贪心算法

贪心的本质是 局部最优,到 全局最优

贪心 没有固定的套路

如何验证可不可以用贪心算法: 举反例,如果想不到反例,那么就试一试贪心吧

一些总结

假设某道题需要 找到一堆 单增 或 单减,数据中 相反的那个,则用栈。

贪心要 找对贪心的方向,有时候从前向后,有时候从后向前

每一个数 需要和前一个数进行比较 并且含某种规律 用栈

打算循环里套循环跳过数的时候,想想能不能去掉内层循环

455. 分发饼干

leetcode

var findContentChildren = function(g, s) {
    let gp = 0,sp = 0
    g.sort((a,b) => a - b)
    s.sort((a,b) => a - b)
    while(gp < g.length && sp < s.length) {
        if(g[gp] <= s[sp]) gp ++;
        sp ++
    }
    return gp
};

376. 摆动序列

leetcode

var wiggleMaxLength = function(nums) {
    let up = 1,down = 1
    for(let i = 1;i < nums.length; i++) {
        if(nums[i] > nums[i-1]) { 
            // 注意这里是 up = down+1, 假设一直是上升,则实际up不会增加
            up = down + 1
        } else if(nums[i] < nums[i-1]) {
            down = up + 1
        } // 等于,就两边都不加
    }
    return nums.length ? Math.max(up,down) : 0
};

题目是可以删除任意位置的数,换句话说,只要计算满足最小摆动序列的数个数就行

只记录峰顶,上升和下降过程中间的值,就是可以不计入的值

53. 最大子序和

leetcode
子数组是连续的,子序列 同序不连续的
当连续区间和为负,则立刻放弃该区间,也即为该区间的右边界

// 1维动态规划
var maxSubArray = function (nums) {
    // dp[i] 以nums[i]结尾的最大连续子数组和
    const dp = Array(nums.length + 1).fill(0);
    let result = -Infinity;
    for (let i = 1; i <= nums.length; i++) {
        dp[i] = Math.max(dp[i - 1] + nums[i - 1], nums[i - 1]);
        result = Math.max(result, dp[i]);
    }
    return result;
};
// 0维动态规划
const maxSubArray = (nums) => {
    let sum = 0, result = -Infinity;
    for (let i = 0; i < nums.length; i++) {
        sum = Math.max(sum + nums[i], nums[i]);
        result = Math.max(result, sum);
    }
    return result;
};
// 贪心 其实就是 0维
var maxSubArray = function(nums) {
    let [sum, result] = [0, -Infinity]
    for(let i = 0; i < nums.length; i++) {
        sum < 0 && (sum = 0)
        sum += nums[i]
        sum > result && (result = sum)
    }
    return result
};

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

leetcode

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

除了贪心 还可以动态规划

55. 跳跃游戏

leetcode

var canJump = function(nums) {
    let end = nums.length - 1
    for(let i = end; i >= 0; i--) {
        if(nums[i] >= end - i) end = i
    }
    return end === 0
};

贪心要 找对贪心的方向,这里正向反向的思路都行

从后往前: 能够到end,就一定能够到 末尾位

从前往后: 遍历每一位的最大cover位,cover = Math.max(cover, i + nums[i])

如果当前位已经被cover了则被忽略,如果未被前面任意数cover,则可能无法到末位,很合理

45. 跳跃游戏II

leetcode

var jump = function(nums) {
    let result = 0;
    let curDistance = 0;
    let nextDistance = 0;

    for(let i = 0; i < nums.length - 1; i++) {
        nextDistance = Math.max(nextDistance, i + nums[i])
        if(i === curDistance) {
            curDistance = nextDistance
            result++
        }
    }
    return result
};
// 2
console.log(jump([2,3,1,1,4]));

i从前往后,记录 当前最远下标 curDistance

[i, curDistance] 内寻找 下一步远下标 nextDistance

遍历完时模拟跳跃,nextDistance 赋值给 curDistance, result步数+1

for循环中i < nums.length - 1无等号, 因为:
假设 末位i(nums.length - 1) 恰好等于 curDistance, 会多执行一次 result++
为什么说这次是多执行的,因为当前 nextDistance能cover的范围是已经计算过 result++的
在i为0的第一轮 curDistance, nextDistance 均为 0, 已经 result++, nextDistance 赋值

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

[leetcode](https://leetcode.cn/problems/maximize-sum-of-array-after-k-negations/

var largestSumAfterKNegations = function (nums, k) {
    nums.sort((a, b) => a - b);
    let sum = 0;
    let min = Number.POSITIVE_INFINITY;
    for (let num of nums) {
        if (k > 0 && num < 0) {
            num = -num;
            k--;
        }
        sum += num;
        min = Math.min(min, num);
    }
    return sum - (k % 2 === 1 ? min * 2 : 0);
};
// 11
console.log(largestSumAfterKNegations([-2, 5, 0, 2, -2], 3));

134. 加油站

leetcode

var canCompleteCircuit = function (gas, cost) {
    let start = 0;
    let curGain = 0;
    let totalGain = 0;

    for (let i = 0; i < gas.length; i++) {
        curGain += gas[i] - cost[i];
        totalGain += gas[i] - cost[i];

        // 如果当前油量小于 0,说明无法从 start 到 i+1
        if (curGain < 0) {
            start = i + 1; // 改变起点
            curGain = 0; // 重置油量
        }
    }

    return totalGain < 0 ? -1 : start;
};

// 代码含义: 最后一个 totalGain < 0 的路段的下一个坐标即为 start

// 首先 totalGain >= 0,说明总油量能走完,必然有答案

// 假设路段(0-i)totalGain变负,代表其无法走到下一站,
// 也代表 其缺少了前面的gain,即 段内无start
// 代码执行过程: 前期连续发现 totalGain为负 的路段,
// 但由于总 totalGain >= 0,最后一段必为start

// 另外,假设路段(0-i)totalGain一直为正,从未变负,则0就是 start
// 当然,这情况也可能是多解,但题目条件保证了单解,因此只能是0

// 最后,从某点出发后,任何中途的油量都不为负
// 这题其实就是找 最小前缀和的 下一个坐标

135. 分发糖果

leetcode

var candy = function (ratings) {
    const l = ratings.length;
    const rank = new Array(l).fill(1);

    for (let i = 1; i < l; i++) {
        if (ratings[i] > ratings[i - 1])
            rank[i] = rank[i - 1] + 1;
    }
    for (let i = l - 2; i >= 0; i--) {
        if (ratings[i] > ratings[i + 1])
            rank[i] = Math.max(rank[i], rank[i + 1] + 1);
    }

    return rank.reduce((pre, cur) => pre + cur, 0);
};
// 两次遍历,每个item满足积累rank的 左规则或右规则时,取二者大值
// 比较左边时,必须 从左往右 遍历
// 因为最左边一个0号本身已是最终结果,rank是需要从最边开始累加的
// 比较右边时,必须 从右往左 遍历
// 此时则要比较两次rank 取大值

860. 柠檬水找零

leetcode

var lemonadeChange = function (bills) {
    let rank1 = 0;
    let rank2 = 0;
    for (let i = 0; i < bills.length; i++) {
        if (bills[i] === 5) {
            rank1 += 1;
        }
        if (bills[i] === 10) {
            if (!rank1) return false;
            rank2 += 1;
            rank1 -= 1;
        }
        if (bills[i] === 20) {
            if (rank1 && rank2) {
                rank1 -= 1;
                rank2 -= 1;
            } else if (rank1 >= 3){
                rank1 -= 3;
            } else return false
        }
    }
    return true;
};

406. 根据身高重建队列

leetcode

var reconstructQueue = function (people) {
    people.sort((a, b) => b[0] - a[0] || a[1] - b[1]);
    const queue = [];
    for (let i = 0; i < people.length; i++) {
        queue.splice(people[i][1], 0, people[i]);
    }
    return queue;
};

两个维度先确定一个维度
一个是 身高无明显提示 一个是 要求前面比自己高的人数
一定是尽量大h的i在前,先根据h从高到低排
贪心: 大h小k的i先入栈
优先满足限定要求最小的
排序完成后,对i来说,左侧调换序 都不影响 后续i的座序
相较左侧,i是小h,往前插也不会影响左侧已经排好的限制k
同h小k 必须在 同h大k 前入queue,即排序时靠左,
如果同h小k在同h大k 的后续压入queue,则必被在 同h大k 左侧,同h大k超额

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

其实就是区间覆盖,给几个区间,每个区间存在重复与不充分,找出最多的重复区间

leetcode

var findMinArrowShots = function (points) {
    points.sort((a, b) => a[1] - b[1]);
    let result = 0;
    let preEnd = -Infinity;

    for (const [start, end] of points) {
        if (preEnd < start) {
            result++;
            preEnd = end;
        }
    }
    return result;
};

贪心: 尽可能留下i[end]小的区间,给后面留位置
思路: i[end] 由小到大排序,找不重叠区间数
preEnd < i[start]:当前箭无法命中i,result++新加箭射当前i[end]
每一箭 必然在这n个气球中最靠左的右边界位置

435. 无重叠区间

leetcode

var eraseOverlapIntervals = function(intervals) {
    intervals.sort((a,b) => a[1] - b[1]);
    let count = 0;
    let preEnd = -Infinity;

    for (const [start, end] of intervals) {
        if (preEnd <= start) {
            count++;
            preEnd = end;
        }
    }
    return intervals.length - count;
};

贪心: 尽可能留下i[end]小的区间,给后面留位置
思路: i[end] 由小到大排序,找不重叠区间数
与 452. 引爆气球(找不重叠区间数) 完全一致
这里是开区间,if (preEnd <= start) 代替 if (preEnd < start) 就行

763. 划分字母区间

leetcode

var partitionLabels = function (s) {
    const map = {};
    for (let i = 0; i < s.length; i++) map[s[i]] = i;
    const result = [];
    let start = 0;
    let end = 0;
    for (let i = 0; i < s.length; i++) {
        end = Math.max(end, map[s[i]]);
        if (i === end) {
            result.push(end - start + 1);
            start = end + 1;
        }
    }
    return result;
};
// [ 9, 7, 8 ]
console.log(partitionLabels('ababcbacadefegdehijhklij'));

56. 合并区间

leetcode

var merge = function(intervals) {
    intervals.sort((p, q) => p[0] - q[0]);
    const result = [];
    for (const range of intervals) {
        const l = result.length;
        if (l && result[l - 1][1] >= range[0]) {
            result[l - 1][1] = Math.max(result[l - 1][1], range[1]);
        } else {
            result.push(range);
        }
    }
    return result;
};

452「射最少箭」、435「保留最多不重叠区间」
贪婪: 希望尽早结束,给后续留更多空间 => i[end]升序
56「合并区间」
贪婪: 希望尽早吞并,保证连续 => i[start]升序

如果这里还 i[end]升序 正序遍历, [[1,2],[4,5],[1,6]] 就会出问题
实际上 i[end]升序 倒序遍历 确实也能做这题

另外,发现新range立即push,再在result中修改,这种写法很优秀
而不是使用额外变量,计算到每个区间最大最终结果时(发现下一个区间时)才push
因为计算最后一个区间时,是没有(发现下一个区间)这种时机的。

738. 单调递增的数字

leetcode

var monotoneIncreasingDigits = function(n) {
    let result = `${n}`.split('')
    const l = result.length;
    let flag = l
    for(let i = l - 1; i > 0; i--) {
        if(result[i-1] > result[i]) {
            flag = i
            result[i-1]-- 
        }
    }
    for(let i = flag; i < l; i++) result[i] = 9
    return +result.join('')
};
console.log(monotoneIncreasingDigits(53321));
// 只要发现 n[i-1] > n[i], n[i-1]退一位, i到末尾全变9
// 如果左往右遍历, 当 n[i-1] === n[i] > n[i+1] 会出错
// 332, 从前往后轮 得到错误答案 329(正确答案299)
// 53321
// 53319
// 53299
// 52999
// 49999