LeetCode 134.加油站:
思路:
- 暴力
遍历每个站点作为开始站点,模拟是否能跑完一圈。
模拟的过程为,使用rest记录剩余油量,即从当前站点跑到下一个站点剩下的油量,index为下一个站点。
(会超时)
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
n = len(gas)
for i in range(n):
rest = gas[i] - cost[i] # 使用rest判断能否到达下一站点
index = (i + 1) % n # index为下一站点
while rest > 0 and index != i:
rest += gas[index] - cost[index]
index = (index + 1) % n
if rest >= 0 and index == i: # 找到了符合条件的起始站点
return i
return -1
- 贪心算法1
直接全局贪心,从站点0作为出发站点开始遍历,curSum记录剩余油量(和暴力中的rest一样),minN记录最小的curSum。跑完一圈后的结果如下:
1)sum(gas) < sum(cost),无论从哪个站点出发都不能跑完一圈,即curSum < 0。
2)从站点0出发,跑完一圈过程中没有断油,那么站点0即为正确的出发站点编号。即minN >= 0
3)从站点0出发,跑完一圈过程中断了油,那么从后向前开始填补minN,直到将minN填平的站点即为新的出发站点
以下图为例解释:
① 从0累加到3,得到min(curSum),即minN
② 又sum(gas) >= sum(cost),因此后面能够将minN补上,即可以将所有按照按照顺序分为两部分,即下图蓝色和绿色部分。
③ 蓝色部分为从0累加到3站点,curSum < 0,即从0开始到3站点时断油,且缺少的油量为minN
④ 绿色部分是能将蓝色部分断的油补上,因此3站点之后的站点的rest >= 0,以绿色部分的某个站点作为开始站点(即从后向前能补上minN的站点),先累加abs(minN),从而遍历到3站点时剩余油量 > 0,那么也就能遍历完一圈。
也就是遍历完一圈的过程为:从开始站点到0站点前填补剩余油量为 >= abs(minN),再从0站点到3站点使得剩余油量curSum >= 0,再遍历剩下的站点
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
curSum = 0
minN = float('inf') # 遍历过程中的最小curSum
n = len(gas)
for i in range(n):
rest = gas[i] - cost[i]
curSum += rest
if minN > curSum:
minN = curSum
if curSum < 0:
return -1 # sum(gas) < sum(cost),从任何地点出发都跑不了一圈
if minN >= 0:
return 0 # 从0站点出发,一圈过程中没断过油
# 从0站点出现,中间有断过油,因此从后向前找能填平断油的站点
for i in range(n - 1, 0, -1):
rest = gas[i] - cost[i]
minN += rest
if minN >= 0:
return i
return -1
- 贪心算法2
如果sum(gas) < sum(cost),那么一定不能跑完一圈。如果要跑完一圈的话,各站点的rest[i] = gas[i] - cost[i]之和大于0。
可以 i 从0开始累积rest,curSum为累加和。
如果第 i 站点curSum < 0,可以说明[0, i]区间的站点都不能作为起始位置,因为在这个区间选择任何一个位置作为起始站点,到 i 都会断油。因此新的起始站点从 i + 1开始,再从0计算curSum。
假设[0, i]中有站点 j 作为起始站点累加到 i 的新curSum > 0
curSum = pre_curSum + j_curSum ,因为curSum < 0且j_curSum > 0
因此pre_curSum < 0,从而在遍历到 j 前就已经更新了起始站点了。
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
n = len(gas)
curSum, totalSum = 0, 0
start = 0
for i in range(n):
rest = gas[i] - cost[i]
curSum += rest
totalSum += rest
if curSum < 0: # 一旦curSum < 0,以i + 1为新的开始站点
start = i + 1
curSum = 0
if totalSum >= 0:
return start
return -1 # 怎么都不能遍历完一圈
感悟:
局部最优与全局最优
LeetCode 135.分发糖果:
思路:
本题的关键在于先处理一边,再处理另外一边。(因为相邻两个孩子评分更高的孩子会获得更多的糖果)。
1)先处理右孩子评分 > 左孩子评分的情况。
从前往后遍历,遍历的当前节点为右孩子(增加糖果不会导致某个孩子所分到的糖果为0),如果rating[i] > rating[i - 1],candy[i] = candy[i - 1] + 1。
局部最优,右边评分 > 左边,右边比左边多一个糖果。全局最优,相邻的孩子中,右边评分更高的孩子会获得更多的糖果
2)再处理左孩子评分 > 右孩子的情况。
同样处理的是左孩子,也就是增加糖果的那一个。当期孩子为 i,其相邻右孩子为 i + 1,因为处理左孩子 i 要用到 i + 1的信息,因此从后往前遍历。
那么如果rating[i] > rating[i + 1],需要对candy[i]进行更新,由于前面得到的candy[i]是满足了rating[i] > rating[i - 1]信息的糖果数,因此rating[i] > rating[i - 1]和rating[i] > rating[i + 1]需要同时满足,因此candy[i] = max(candy[i], candy[i + 1] + 1)。
局部最优,取candy[i + 1]和candy[i]最大的糖果数,保证第 i 个小孩的糖果数量同时大于左边和右边的。全局最优,相邻孩子中,评分高的孩子获得更多的糖果。
class Solution:
def candy(self, ratings: List[int]) -> int:
childNum = len(ratings)
candy = [1] * childNum
# 从前往后
for i in range(1, childNum):
if ratings[i] > ratings[i - 1]: # 如果右边 > 左边
candy[i] = candy[i - 1] + 1
# 从后往前
for i in range(childNum - 2, -1, -1):
if ratings[i] > ratings[i + 1]: # 左边 > 右边
candy[i] = max(candy[i], candy[i + 1] + 1)
# 统计需要的最少糖果数目
result = sum(candy)
return result
感悟:
局部最优和全局最优是相邻两个孩子评分更高的孩子获得更多的糖果数
LeetCode 860.柠檬水找零:
思路:
需要注意的是找零只能用5,10,20找零,而不是当前零钱数量 > 需要找零的数量就可以找零。
顾客支付的只有5,10,20。分情况进行探讨
1)支付5,不需要找零
2)支付10,找零5
3)支付20,找零10和5,或者找零三个5。
第三个部分可以用到贪心,只要能找零10和5,就不找零三个5,因为相对10而言,5能找零的地方更多更万能
class Solution:
def lemonadeChange(self, bills: List[int]) -> bool:
five, ten, twenty = 0, 0, 0
for i in range(len(bills)):
if bills[i] == 5:
five += 1
elif bills[i] == 10:
ten += 1
if five <= 0:
return False
five -= 1
else:
twenty += 1 # 其实不用记录20,因为找零不用20
if ten > 0 and five > 0: # 找零10 + 5
ten -= 1
five -= 1
elif five >= 3: # 找零 5+5+5
five -= 3
else: # 不能找零
return False
return True
感悟:
找零只能用顾客给5,10进行找零。顾客支付20时,优先找零为10和5
LeetCode 406.根据身高重建队列:
思路:
本题也是两个维度的信息,身高h和人数k。同样需要先确定一个后再确定另一个。
此处采用先确定 h,后再确定k来实现。(因为k是前面身高 >= hi的人数)
1)因为题目中k 为身高 >= hi,因此首先对身高从大到小排序,身高相同时k小的在前面。
2)现在确定了h,再来确定k。每次将people[i]插入到下标为people[i][1]的位置。
因为身高是按照从高到低排序的,因此插入的people[i][0] 比之前插入的矮,不会影响前面的结果,同时插入到people[i][1]位置保证前面有ki个身高大于等于hi的人。
局部最优:优先按照身高高的人的k来插入。插入操作后的people满足队列属性
全局最优:people数组全部插入操作完成后,整个队列满足题目队列属性
class Solution:
def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]:
que = list()
people.sort(key=lambda x: (-x[0], x[1])) # 按照身高进行排序,身高相同k小的站前面
# 逐个插入新队列中,插入下标为k
for p in people:
que.insert(p[1], p)
return que
时间复杂度O(nlogn + n^2)
因为list底层实现是数组,插入元素时会判断数组空间是否足够,不够的话会申请两倍的空间再将元素复制到新空间中。
学习收获:
涉及多维度的贪心问题,首先分清楚都有什么维度,再根据之间的关系先处理一边再处理另外一边。
贪心的局部最优和全局最优比较难找和分辨