Day29-贪心算法,leetcode134,135,860,406
- 加油站
- 在一条环路上有 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;
};
- 分发糖果
-
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);
};
- 柠檬水找零
-
在柠檬水摊上,每一杯柠檬水的售价为 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
};
- 根据身高重建队列
-
假设有打乱顺序的一群人站成一个队列,数组 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;
};
参考&感谢各路大神
宝剑锋从磨砺出,梅花香自苦寒来。