Day29-贪心算法,leetcode134,135,860,406

  1. 加油站
  • 在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
  • 你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
  • 给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。

  • 思路

  • for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历

  • 全局贪心

    • 情况一:如果gas的总和小于cost总和,那么无论从哪里出发,一定是跑不了一圈的
    • 情况二:rest[i] = gas[i]-cost[i]为一天剩下的油,i从0开始计算累加到最后一站,如果累加没有出现负数,说明从0 - 出发,油就没有断过,那么0就是起点。
    • 情况三:如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能把这个负数填平,能把这个负数填平的节点就是出发节点。
  • 局部最优推全局最优

    • 首先如果总油量减去总消耗大于等于零那么一定可以跑完一圈,说明 各个站点的加油站 剩油量rest[i]相加一定是大于等于零的。
    • 每个加油站的剩余量rest[i]为gas[i] - cost[i]。
    • i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,因为这个区间选择任何一个位置作为起点,到i这里都会断油,那么起始位置从i+1算起,再从0计算curSum。
/** 暴力
 * 外层 for 循环:尝试每个加油站作为起点。
 * 内层 while 循环:模拟从当前起点出发,环形遍历一圈。
 * rest:当前剩余油量,初始为出发站加油减去到下一站消耗。
 * index:当前所在加油站,环形用 % 保证不会越界。
 * 如果能回到起点且剩余油量不为负,返回当前起点编号。
 * 如果所有起点都不行,返回 -1。

 * 暴力模拟每个加油站作为起点,判断能否绕环路一周,能则返回起点编号,否则返回 -1。
 */
var canCompleteCircuit = function(gas, cost) {
    for (let i = 0; i < cost.length; i++) {
        let rest = gas[i] - cost[i]; //  记录从第i站出发的剩余油量
        let index = (i + 1) % cost.length; // 下一个加油站(环形)
        // 只要油量大于0且没回到起点,就一直往下走
        while (rest > 0 && index !== i) { // 模拟以i为起点行驶一圈
            rest += gas[index] - cost[index];  // 加油再减去消耗
            index = (index + 1) % cost.length;// 下一个加油站
        }
        // 如果以i为起点跑一圈,剩余油量>=0,返回该起始位置
        if (rest >= 0 && index === i) return i;
    }
    return -1;// 所有起点都不行,返回-1
};

/** 整体最优
1. 第一轮遍历:
    计算每一站加油减去消耗的净值 rest,累计到 curSum。
    同时记录从起点出发时油箱里的最小油量 min。
2. 判断三种情况:
    curSum < 0:总油量小于总消耗,无论从哪出发都不可能绕一圈,返回 -1。
    min >= 0:从 0 出发,油量从未为负,说明 0 就是起点。
    其他情况:需要从后往前找,找到第一个能让油量不为负的位置作为起点。
3. 第二轮遍历(逆序):
    从最后一站往前累加 rest 到 min,一旦 min >= 0,说明从该位置出发可以绕一圈,返回该下标。

先判断总油量是否足够,再找最早能让油量不为负的起点,保证只需一次遍历即可得到唯一解。
 */
var canCompleteCircuit = function(gas, cost) {
    let curSum = 0;
    let min = Infinity; // 油箱里的油量最小值
    for (let i = 0; i < gas.length; i++) {
        let rest = gas[i] - cost[i];
        curSum += rest;
        if (curSum < min) {
            min = curSum;
        }
    }
    if (curSum < 0) return -1; // 情况1:总油量小于总消耗,肯定无法绕一圈
    if (min >= 0) return 0;    // 情况2:从0出发油量一直不为负,起点就是0
    // 情况3:从后往前找,找到最早能让油量不为负的位置
    for (let i = gas.length - 1; i >= 0; i--) {
        let rest = gas[i] - cost[i];
        min += rest;
        if (min >= 0) {
            return i;
        }
    }
    return -1;
};

/** 局部最优推全局最优
1. curSum:记录从当前起点到当前位置的累计剩余油量。
2. totalSum:记录全程的总剩余油量(所有站点的gas[i]-cost[i]之和)。
3. start:当前假设的起点。
4. 遍历每个加油站:
    累加当前站点的剩余油量到 curSum 和 totalSum。
    如果 curSum < 0,说明从当前起点到i无法到达i+1,必须把起点更新为i+1,并重置curSum为0。
5. 遍历结束后,如果 totalSum < 0,说明总油量不够,怎么走都无法绕一圈,返回-1。
6. 否则返回start,即为唯一的可行起点。

只要总油量足够,遇到累计油量为负就换起点,最后返回能跑一圈的唯一起点。
 */
var canCompleteCircuit = function(gas, cost) {
    let curSum = 0;
    let totalSum = 0;
    let start = 0;
    for (let i = 0; i < gas.length; i++) {
        curSum += gas[i] - cost[i];
        totalSum += gas[i] - cost[i];
        if (curSum < 0) {   // 当前累计油量小于0,说明从start到i无法到达i+1
            start = i + 1;  // 起点更新为i+1
            curSum = 0;     // 累计油量重置为0
        }
    }
    if (totalSum < 0) return -1; // 总油量不够,无法绕一圈
    return start;
};


  1. 分发糖果
  • n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。

  • 你需要按照以下要求,给这些孩子分发糖果:

    • 每个孩子至少分配到 1 个糖果。
    • 相邻两个孩子评分更高的孩子会获得更多的糖果。
  • 请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。


  • 思路
  • 每个小孩首先都有一个糖果
  • 比较的时候,先比较一边,确定之后再比较另一边,先确定右边评分大于左边的情况(也就是从前向后遍历),再确定左孩子大于右孩子的情况(从后向前遍历)
  • 从前往后比较,右边孩子比左边孩子的得分高,右边孩子糖果比左边孩子多一个
  • 从后往前比较,左边孩子比右边孩子的得分高,左边孩子比右边孩子多一个(如果一个得分比左边得分大,也比右边得分大,那么对应的糖果也要既比左边大也比右边大,这时取上述两种情况中最大的糖果数量)
  • 遍历累加所有孩子的糖果数
/**
1. 初始化:每个孩子先分 1 个糖果,candyVec 数组全为 1。
2. 从前向后遍历:如果右边孩子评分高于左边,右边孩子糖果数 = 左边孩子糖果数 + 1。
3. 从后向前遍历:如果左边孩子评分高于右边,左边孩子糖果数 = max(当前糖果数, 右边孩子糖果数 + 1)。这样保证如果一个孩子两边都比邻居高,能取到最大糖果数。
4. 累加结果:最后把所有孩子的糖果数加起来返回。

两次遍历,分别保证每个孩子比左/右邻居评分高时糖果更多,最终累加得到最少糖果数。

从后向前遍历的原因:只有从后向前遍历,才能保证左边孩子的糖果数在比较时已经是最优的。
1. 如果你要保证“左边孩子比右边孩子评分高,左边孩子糖果比右边多一个”,你需要知道右边孩子最终会分到多少糖果。
2. 只有先把右边孩子的糖果数确定下来(也就是先处理右边的孩子),才能正确地给左边孩子分配糖果。
3. 所以要从后往前遍历,每次用右边孩子的糖果数来更新左边孩子的糖果数,确保左边孩子比右边多一个。
4. 如果你从前往后遍历,右边孩子的糖果数还没确定,左边孩子就无法正确比较和更新。

总结:
从后向前遍历,是为了保证每次比较时,右边孩子的糖果数已经是最优的,这样左边孩子才能正确地分配到比右边多的糖果。
 */
var candy = function(ratings) {
    const n = ratings.length;
    const candyVec = new Array(n).fill(1); // 每个孩子先分1个糖果

    // 从前向后遍历,右边孩子评分高,糖果比左边多一个
    for (let i = 1; i < n; i++) {
        if (ratings[i] > ratings[i - 1]) {
            candyVec[i] = candyVec[i - 1] + 1;
        }
    }

    // 从后向前遍历,左边孩子评分高,糖果比右边多一个
    for (let i = n - 2; i >= 0; i--) {
        if (ratings[i] > ratings[i + 1]) {
            candyVec[i] = Math.max(candyVec[i], candyVec[i + 1] + 1);
        }
    }

    // 统计总糖果数
    return candyVec.reduce((sum, num) => sum + num, 0);
};


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

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

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

  • 给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。


  • 思路
  • 定义变量,统计5、10、20的数量,初始都为0
  • 遍历数组,遇到5,统计5的数量,不找零,遇到10,判断是否有5,找一个5,统计10的数量,遇到20,判断是否有10+5组合或者5+5+5组合进行找零,能找零true,不能找零false
var lemonadeChange = function(bills) {
    let fiveNum = 0
    let tenNum = 0
    let twNum = 0
    for (let i = 0; i < bills.length; i++) {
        if (bills[i] === 5) {
            fiveNum++
        } 
        if (bills[i] === 10) {
            if (fiveNum > 0) {
                fiveNum--
                tenNum++
            } else {
                return false
            }
        }
        if (bills[i] === 20) {
            // 优先消耗10美元,因为5美元的找零用处更大,能多留着就多留着
            if (fiveNum > 0 && tenNum > 0) {
                fiveNum--
                tenNum--
                twNum++
            } else if (fiveNum >= 3) {
                fiveNum -= 3
                twNum++
            } else {
                twNum++
                return false
            }
        }
    }
    return true
};


  1. 根据身高重建队列
  • 假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。

  • 请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。


  • 思路
  • 遇到两个维度权衡的时候,一定要先确定一个维度,再确定另一个维度。先确定一个纬度进行排序,排序之后再检测另一个纬度是否满足条件
  • 先根据身高从高到低进行排序,身高相同的话,k小的排在前面,然后根据k属性进行插入调整顺序
/**
1. 排序
  先按身高降序排(高的在前),身高相同按k升序排(k小的在前)。
  这样保证每次插入时,前面的人都比当前人高或一样高。
2. 插入
  遍历排序后的数组,把每个人插入到队列的第k个位置。
  因为前面已经排好了高个子,所以插入第k个位置时,前面正好有k个比他高或一样高的人。

总结
  先排序,再按k插入,保证每个人前面有k个比他高或一样高的人。
  这样就能重建出符合条件的队列。

先按身高降序、k升序排序,再按k插入队列即可。
 */
var reconstructQueue = function(people) {
    let queue = [];
    // 先按身高从高到低排序,身高相同按k从小到大排序
    people.sort((a, b) => {
        if (b[0] !== a[0]) {
            return b[0] - a[0]; // 身高高的在前
        } else {
            return a[1] - b[1]; // k小的在前
        }
    });

    // 按排序后的顺序插入队列
    for (let i = 0; i < people.length; i++) {
        // 在数组 queue 的下标为 people[i][1] 的位置插入当前的 people[i],并且不删除任何元素(第二个参数是 0)
        queue.splice(people[i][1], 0, people[i]);
        // 在下标为k的位置插入当前人
    }
    return queue;
};



参考&感谢各路大神

posted @ 2025-06-25 09:58  安静的嘶吼  阅读(4)  评论(0)    收藏  举报