leetcode——贪心算法

471 阅读7分钟

122. 买卖股票的最佳时机 II 可以交易无数次

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
     随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
     总利润为 4 + 3 = 7

因为不限制交易次数,只要今天价格比昨天高,就交易,利润为正累加,最后的和就是最大的利润,注意第一天是没有利润的,这道题之所以可以用贪心是因为局部最优:收集每天的正利润,可以推导出,全局最优:求得最大利润

var maxProfit = function (prices) {
    let ans = 0;
    let n = prices.length;
    for (let i = 1; i < n; ++i) {
        //今天价格和昨天的差值是否为正,如果为正累加进去,为负则加0
        ans += Math.max(0, prices[i] - prices[i - 1]);
    }
    return ans;
};

455. 分发饼干 (easy)

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
输入: g = [1,2,3], s = [1,1]
输出: 1
解释: 
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。

大尺寸的饼干既可以满足胃口大的孩子也可以满足胃口小的孩子,那么就应该优先满足胃口大的。

var findContentChildren = function(g, s) {
    var child = g.sort((a,b) => a-b);
    var cookie = s.sort((a,b) => a-b);
    var index = s.length - 1; 
    let res = 0;
    for(let i = g.length - 1;i >= 0; i--){
        if(index >=0 && cookie[index] >= child[i]){
            index--;
            res++;
        }
    }
    return res
};

435. 无重叠区间

给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 。
输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。

求最少要移除的区间其实就是找最多不重叠的区间
方法一:如果用动态规划来做题目,一开始陷入了一个误区就是dp[i]是和dp[i-1]有关的,这是很明显的错误,因为前一个区间是很靠前的话,第i个区间必定和他没交集(比如说有[[1,4],[0,1],[1,2]],dp[1]=1,dp[2]=2,如果i只和i-1有关我们是直接dp[3]=dp[2]+1)。所以dp[i]是和dp[0~i-1]有关

var eraseOverlapIntervals = function(intervals) {
    // 按照首位升序
    intervals = intervals.sort((a,b)=>a[0]-b[0])
    const n = intervals.length;
    const dp = new Array(n).fill(1); //初始化dp数组
    for(let i = 1; i < n; i++){
        for(let j = 0; j < i; j++){
            if(intervals[j][1] <= intervals[i][0]){
                dp[i] = Math.max(dp[i],dp[j]+1)
            }
        }
    }
    return n - Math.max(...dp)
};

方法二:贪心算法:越早结束越好
越不密集越好,下一个左边界大于这个右边界(没有重叠),就替换右边界
intervals按右边界排序,然后从左往右遍历,右边界结束的越早,留给后面的区间的空间就越大,不重合的区间个数就越多,intervals的长度减去最多的不重复的区间 就是最少删除区间的个数。 找到一个不重合,就把right换成新的的right。

var eraseOverlapIntervals = function(intervals) {
    if (!intervals.length) {
        return 0;
    }
    intervals.sort((a,b)=>a[1] - b[1]);
    let n = intervals.length
    let right = intervals[0][1];
    let res = 1
    for(let i = 1; i < n; i++){
        if(intervals[i][0] >= right){
            ++res;
            right = intervals[i][1]
        }
    }
    return n - res
};

55. 跳跃游戏

给定一个非负整数数组 `nums` ,你最初位于数组的第一个下标。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
输入: nums = [2,3,1,1,4]
输出: true
解释: 可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。

方法一:动态规划

var canJump = function(nums) {
    const n = nums.length;
    let dp = new Array(n).fill(false);
    dp[0] = true;
    for(let i = 1; i < n; i++){
        for(let j = 0; j < i; j++){
            if(dp[j] && nums[j]+j >= i){
                dp[i] = true;
                break
            }
        }
    }
    return dp[n -1]
};

方法二:贪心算法
尽可能的跳跃到最远的位置,看最多能覆盖的位置,不断更新能覆盖的距离.
一定要注意的是!!let i = 0; i <= cover; i++
因为先看第一步跳到哪才能继续往下走

var canJump = function(nums) {
    const n = nums.length;
    if(1 === n){return true}
    let cover = nums[0];
    for(let i = 0; i <= cover; i++){
        cover = Math.max(cover, i+nums[i]);
        if(cover >= n-1){
            return true
        }
    }
    return false
};

881. 救生艇

给定数组 people 。people[i]表示第 i 个人的体重 ,船的数量不限,每艘船可以承载的最大重量为 limit。
每艘船最多可同时载两人,但条件是这些人的重量之和最多为 limit。
返回 承载所有人所需的最小船数 。
输入: people = [1,2], limit = 3
输出: 1
解释: 1 艘船载 (1, 2)

这道题目最最最重要的解决点就是每艘船最多可同时载两人
注意的是:无论两个人能不能一起坐,大重量的人都要--

var numRescueBoats = function(people, limit) {
    people.sort((a,b)=>a-b);
    let pre = 0;
    let next = people.length-1;
    let count = 0;
    //两个指针表示最大重量和最小重量的人,两个人能做一个潜艇最好,不能的话大的一个人坐一辆
    while(pre <= next){
        if(people[pre]+people[next--]<=limit){
            pre++;
        }
        count++;
    }
    return count
};

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

有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。
一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足  xstart ≤ x ≤ xend,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。
给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数 。

image.png

输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:气球可以用2支箭来爆破:
-x = 6处射出箭,击破气球[2,8][1,6]-x = 11处发射箭,击破气球[10,16][7,12]

贪心算法:其实就是求最多的无重叠区间
注意:和求最多无重叠区的小区别是:x[0] > right不能等于,如果等于了就少用一根箭

var findMinArrowShots = function(points) {
    if (!points.length) {
        return 0;
    }
    points.sort((a,b)=>a[1]-b[1])
    let count = 1;
    let right = points[0][1];
    for(let x of points){
        if(x[0] > right){
            right = x[1];
            count++;
        }
    }
    return count
};

134. 加油站

在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
给定两个整数数组 gas 和 cost ,如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。
输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2]
输出: 3
解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。

image.png 总油量是否小于总油耗,如果是则肯定不能走一圈。如果否,那肯定能跑一圈。接下来就是循环数组,从第一个站开始,计算每一站剩余的油量,如果油量为负了,就以这个站为起点从新计算。如果到达某一个点为负,说明起点到这个点中间的所有站点都不能到达该点。

var canCompleteCircuit = function(gas, cost) {
    //首先计算能用的油和需要用的油的量比较
    let sumGas = gas.reduce((pre,next)=>{return pre+next},0);
    let sumCost = cost.reduce((pre,next)=>{return pre+next},0);
    if(sumGas < sumCost){
        return -1;
    }
    let current = 0;
    let gasValue = 0;
    for(let i = 0; i < gas.length; i++){
        //表示从0开始出发了,在0冲了汽油,然后看到1需要耗的油量,如果不够,重新计油量和起始位置
        gasValue = gasValue + gas[i] - cost[i]
        if(gasValue<0){
            current = i + 1;
            gasValue = 0
        }
    }
    return current
};

621. 任务调度器

给你一个用字符数组 tasks 表示的 CPU 需要执行的任务列表。其中每个字母表示一种不同种类的任务。任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间内执行完。在任何一个单位时间,CPU 可以完成一个任务,或者处于待命状态。

然而,两个 相同种类 的任务之间必须有长度为整数 n 的冷却时间,因此至少有连续 n 个单位时间内 CPU 在执行不同的任务,或者在待命状态。

你需要计算完成所有任务所需要的 最短时间 。
输入:tasks = ["A","A","A","B","B","B"], n = 2
输出:8
解释:A -> B -> (待命) -> A -> B -> (待命) -> A -> B
     在本示例中,两个相同类型任务之间必须间隔长度为 n = 2 的冷却时间,而执行一个任务只需要一个单位时间,所以中间出现了(待命)状态。 

image.png ((max-1)*(n+1)+max个数,task.length)

var leastInterval = function(tasks, n) {
    const len = tasks.length
    let arr = Array(26).fill(0);
    for(let x of tasks){
        arr[x.charCodeAt()-"A".charCodeAt()]++;
    }
    let max = 0;
    // 找出出现的最大次数
    for(let x of arr){
        max = Math.max(max,x)
    }
    let res = (max-1)*(n+1)
    for(let x of arr){
        if(x === max){
            res++;
        }
    }
    return Math.max(res,len)
};

860. 柠檬水找零

在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

注意,一开始你手头没有任何零钱。

给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
输入:bills = [5,5,5,10,20]
输出:true
解释:
前 3 位顾客那里,我们按顺序收取 35 美元的钞票。
第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
由于所有客户都得到了正确的找零,所以我们输出 true
var lemonadeChange = function(bills) {
    let five = 0;
    let ten = 0;
    for(let x of bills){
        if(x === 5){
            five++;
        }else if(x === 10){
            if (five === 0) {
               return false;
            }
            five--;
            ten++;
        }else{
            if(five>0 && ten>0){
                five--;
                ten--;
            }else if(five >= 3){
                five -=3;
            }else{
                return false
            }
        }
    }
    return true
};