leetcode_贪心算法_python
leetcode455.分发饼干
题目:假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
思路:将饼干大小和胃口大小分别排序后,遍历饼干,尽可能用最小的饼干满足孩子。易错点:如果用饼干进行遍历,注意判断孩子的索引是否会出界。
leetcode376.摆动序列
题目:如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。
思路:首先要单独处理空列表的情况,因为除了空列表外其他情况下至少有一个元素可以作为摆动序列(默认第一个元素可以列入序列,所以下面的考量都从第二个开始)。
方法一:利用一个特点:局部极大值和局部极小值的差最多为1.易错点:严格大或严格小时更新。
class Solution: def wiggleMaxLength(self, nums: List[int]) -> int: if len(nums) < 2: return len(nums) num_up = 1 num_down = 1 for i in range(1,len(nums)):#判断i是什么 if nums[i] - nums[i-1] >0: num_up = num_down+1 elif nums[i] - nums[i-1]<0: num_down = num_up+1
方法二:记录上一个峰值的属性,并且与现在的对比。易错点:注意pre_diff记录的是上一个峰值的属性,需要在if里更新。否则当遇到diff = 0时只要下一个非0就会更结果,但可能是连续上升的中间有一段平,非局部极值。
class Solution: def wiggleMaxLength(self, nums: List[int]) -> int: if not nums: return 0 res = 1 pre_diff = 0 for i in range(1, len(nums)): cur_diff = nums[i] - nums[i-1] if pre_diff>=0 and cur_diff < 0 or pre_diff <= 0 and cur_diff >0: res += 1 pre_diff = cur_diff#注意在if里更新 非外 return res
leetcode53.最大子序和
给定一个整数数组 nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
思路:类似问题关键点,更新最大值,计算当前和分开处理。这样做的好处时,计算当前和时可以直接计算,不需要考虑是否需要存下来,当前和只是一个参考值,结果在于更新最大值上。先计算本轮的和,更新最大值,如果本轮和小于0,则会“拖累”后面,所以清零当前和。注意,先更新最大值,再清零。
class Solution: def maxSubArray(self, nums: List[int]) -> int: max_val = float('-inf') sum_val = 0 for i in range(len(nums)):#一定要注意顺序 先算这一轮的和 再更新max,最后决定这一轮是否清0 sum_val = sum_val+nums[i] max_val = max(max_val,sum_val) if sum_val<0: sum_val = 0 return max_val
leetcode122.买卖股票的最佳时机2
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
思路:profit = price[i] - price[j] = price[i]-price[i-1]+price[i-1]-price[i-2]+...-price[j]则相当于每天的利润相加,当该日利润大于0则应当卖掉。所以应当计算单日利润,大于0的即为应当操作的地方。
leetcode55.跳跃游戏
给定一个非负整数数组 nums
,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标。
思路:在每个点更新最远能到达的位置,如果最后最远大于等于最后一个下标,则可以到达。注意在每轮循环中首先确定该位置i是否能到达。
leetcode45.跳跃游戏2
给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。你的目标是使用最少的跳跃次数到达数组的最后一个位置。
思路:记录当前的步数,和当前步数下最多可以到达的小标位置。记录比当前步数多一步可以到达的下标位置。每到一步首先判断该位置是否可以在当前步数下到达,如果可以则步数不变,不可以则步数增一并更新当前步数可以到达的位置。然后更新比当前步数多一步可以到达的位置。注意先判断当前位置是否可达,再更新比当前位置多一步可达的位置。如果当前位置不可达而更新的多一步可达的位置,则该位置实际上是多两步可达的位置(首先多一步到达该位置,然后多一步到达该位置可达的位置)。
class Solution: def jump(self, nums: List[int]) -> int: steps = 0 max_reach_currrent_steps = 0 max_reach_onemore_steps = nums[0] for i in range(1, len(nums)): if i > max_reach_currrent_steps: steps += 1 max_reach_currrent_steps = max_reach_onemore_steps max_reach_onemore_steps = max(i+nums[i], max_reach_onemore_steps) return steps
leetcode1005.K次取反后最大化数组的和
给定一个整数数组 A,我们只能用以下方法修改该数组:我们选择某个索引 i 并将 A[i] 替换为 -A[i],然后总共重复这个过程 K 次。(我们可以多次选择同一个索引 i。)以这种方式修改数组后,返回数组可能的最大和。
思路:方法一:从小到大升序排列,从左到右如果是负数则取反直至遍历完成或K用完。若K没用完,则返回总和减去两倍的最小值。
方法二:只用排一次顺序。按数字的绝对值降序排列,在K没有用完的情况下优先对绝对值大的负数取反。最后如果K没用完且剩余为奇数,则对最后一个(即绝对值最小的取反)再算总和。
leetcode134.加油站
在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。
说明: 如果题目有解,该答案即为唯一答案。输入数组均为非空数组,且长度相同。输入数组中的元素均为非负数。
思路:首先定义一个车在某站的油量cur不包括该站加的油。无所谓从哪里开始,因为相当于一个曲线上以不同的点为默认“0”,遍历了一下曲线,无论最小值的大小是多少,取到该值的点都是一样的。所以默认开始时的油量为0,路上记录油量的最小值和产生最小值的索引。注意:cur+gas[i]-cost[i]实际上是到达i+1个点的剩余油量,所以应当记为i+1。当站的索引为极限无法取到的len(gas)时,其实是第一个点即0。
class Solution: def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int: #净营收大于0才能跑完 在此情况下,即是面积的相加,无论怎么加,最后和都是一样的,只是要保证最低点大于0,相当于一个曲线随机开始点,并把整体移动使得第一个点为0.无论中间值是多少,相对大小不变,因此可以直接找到最小点,从该点开始。因为rest指的是到下一个的剩余油量,所以返回i+1 if sum(gas) < sum(cost): return -1 rest = 0 min_val = float('inf') for i in range(len(gas)): rest += gas[i]-cost[i] if rest < min_val: min_val = rest #注意更新min_val i_min = i+1 if i_min == len(gas): i_min = 0 return i_min
leetcode135.分发糖果
老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。你需要按照以下要求,帮助老师给这些孩子分发糖果:每个孩子至少分配到 1 个糖果。评分更高的孩子必须比他两侧的邻位孩子获得更多的糖果。那么这样下来,老师至少需要准备多少颗糖果呢?
思路:先使得1.右边孩子比左边评分高,则比左边孩子多一个糖。2.左边孩子比右边评分高,则左边孩子取(现在的糖,右边孩子糖量+1)的最大值。值得注意的是,加糖时不能破坏之前已经排好的糖量顺序,所以一个值应当先更新再被比。左右比较,若动作为改变右边的,则向右遍历,使得右边的值先更新后再被比;同理,若动作为改变左边的值,则向左遍历。
class Solution: def candy(self, ratings: List[int]) -> int: candies = [1 for i in range(len(ratings))] # 右>左 向右遍历 右=左+1 for i in range(1,len(candies)): if ratings[i]>ratings[i-1]: candies[i] = candies[i-1]+1 #左>右 向左遍历 左=max(now,右+1) for i in range(len(candies)-2,-1,-1): if ratings[i]>ratings[i+1]: candies[i] = max(candies[i],candies[i+1]+1) return sum(candies)
leetcode860.柠檬水找零
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。注意,一开始你手头没有任何零钱。如果你能给每位顾客正确找零,返回 true ,否则返回 false 。
思路:该问题的要点在于实际上只有三种货币:5,10,20.要注意20可以找10+5或者5*3。
leetcode406.根据身高重建队列
假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。
思路:将人按身高降序(优先),ki升序的顺序排列。新建一个结果,然后从左向右依次将人插入到第ki个位置。重点在于实现这一步,通过list的合并,注意每个元素要以列表的形式合并,res的部分肯定是列表,注意元素部分。
class Solution: def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]: people.sort(key = lambda x:(x[0],-x[1]), reverse = True) #注意写法 #按身高降序,人数升序排 然后从左到右插入人数位置 res = [] for i in range(len(people)): i_insert = people[i][1] res = res[:i_insert]+[people[i]]+res[i_insert:] #注意 return res
leetcode452.用最小数量的箭引爆气球
在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。
思路:在气球右边界放箭,则按右边界升序排列,若某气球左边界小于放箭点,则引爆;大于,则需要另外的箭,并更新放箭点。重点在于为什么按右边界升序:按右边界升序,则后面的元素的右边界必然大于等于该点右边界,则若元素的左边界小于该点右边界,放箭点必然在元素左右之间。核心是通过排序控制一边的相对位置,在判断时只用判断另一边。同理,按左边界升序排列也可以,此时倒序遍历,元素的左边界必然小于该点的左边界,若右边界大于该点左边界,则可放箭。总之,哪个边界放箭,哪个边界排序。
class Solution: def findMinArrowShots(self, points: List[List[int]]) -> int: #按右边界升序,需要判断气球的左边界是否小于维护边界的右边界,是则击中,不是则需要加一个箭 points.sort(key = lambda x:x[1]) if not points: return 0 end = points[0][1] res = 1 for i in range(1,len(points)): if points[i][0] > end: res += 1 end = points[i][1] return res
leetcode435.无重叠区间
给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
注意:可以认为区间的终点总是大于它的起点。区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
思路:按右边界升序排列,重叠则数量加一,不重叠则更新右界。因为右边界是升序的,则其留给右边的空间是多的,所以当重叠时优先留下留给空间多的,删除留给空间少的,即右边的选项。
class Solution: def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int: #注意题目要求的区别 需要移除区间的数量 if not intervals: return 0 intervals.sort(key = lambda x:x[1]) end = intervals[0][1] count = 0 for i in range(1,len(intervals)): if intervals[i][0] < end: count += 1 continue else: end = intervals[i][1] return count
leetcode736.划分字母区间
字符串 S
由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。
思路:首先确定每个字母最后出现的索引,用字典存下来。然后遍历字符串,判断在(i_start,i]之间的字母最后出现索引的最大值,当该值等于i时,则(i_start,i]区间外无该区间字母,符合条件。易错点:因为左边是半开区间,所以起始值应当为-1;另外注意题目对返回值的要求,返回值为列表长度,非列表区间。
class Solution: def partitionLabels(self, S: str) -> List[int]: max_index = 0 res = [] dict_last_index = {} for i in range(len(S)): #每个字母最后一次出现的位置 dict_last_index[S[i]] = i #(i_start,i]的字符串 i_start = -1 for i in range(len(S)): #找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点 if dict_last_index[S[i]]>max_index: max_index = dict_last_index[S[i]] if i == max_index: res.append(max_index-i_start) i_start = max_index return res
leetcode56.合并区间
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
思路:关键是判断区间是否在区间内,还需要合并区间,所以需要从最大的边界开始,因此按左边界升序排列。若两区间重叠,则扩大区间包含范围;若不重叠,则更换区间。如果某区间开始点小于等于当前区间结束点,则说明重叠,所以结束点更新为二者结束点中较大的一个;如果某区间的开始点大于当前区间的结束点,则说明区间不重叠,应当存储当前区间,并开启下一个区间。注意在循环结束时补上一个结果,因为最后一次的结果没有存储。
class Solution: def merge(self, intervals: List[List[int]]) -> List[List[int]]: intervals.sort() if not intervals: #注意 因为后面直接以0位置初始化,所以要保证0有值 return intervals start = intervals[0][0] end = intervals[0][1] res = [] for i in range(1,len(intervals)): if intervals[i][0] <= end: end = max(intervals[i][1],end) else: res.append([start,end]) start,end = intervals[i][0],intervals[i][1] res.append([start, end]) #注意 return res
leetcode738.单调递增的数字
给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。(当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。)
思路:对于两位数,如果左比右高,则左减一,并将右边置为9。对于多位的情况,我们实际上是要找最高位的“错误”,然后将后面改为9。但不能从左往右直接找,因为先更新后比较,如前面所分析的,需要改左边,则需要从右向左找,因此每一位如果左高右低则左减一继续比较,该操作并不在乎右边变成什么,因为后面统一置为9。
class Solution: def monotoneIncreasingDigits(self, N: int) -> int: #右往左 num = list(str(N)) index = len(num) for i in range(len(num)-2,-1,-1): if int(num[i])>int(num[i+1]): index = i+1 num[i] = str(int(num[i])-1) for i in range(index,len(num)): num[i] = '9' return int(''.join(num))
leetcode968.监控二叉树
给定一个二叉树,我们在树的节点上安装摄像头。节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。计算监控树的所有节点所需的最小摄像头数量。
思路:叶子节点不放摄像头,希望放在叶子节点的父节点,在此基础上放置更上层的摄像头,因此从底下向上放,即使用后序遍历。一个节点有三种可能:被覆盖,本身就有摄像头,和没被覆盖。因为不想在叶子节点安装摄像头,所以空节点返回被覆盖。当左右有未被覆盖的节点时,该节点返回摄像头,总数加一;有摄像头时,返回被覆盖;其他则为未被覆盖。返回值为传入节点的状态,注意最后根节点返回值在总函数内,如果根节点未被覆盖,则加一个摄像头。注意如果返回为摄像头,因为在递归时已经算入摄像头的总数,所以总数不要增加。
class Solution: def minCameraCover(self, root: TreeNode) -> int: res = 0 def traverse(root): #1:被覆盖 2:摄像头 3:未覆盖 nonlocal res if not root: #叶子节点不安摄像头,则空节点需要认为是被覆盖的 return 1 condition_left = traverse(root.left) condition_right = traverse(root.right) if condition_left ==3 or condition_right == 3: #如果有一个没覆盖 则需要摄像头 res += 1 return 2 if condition_left == 2 or condition_right == 2: #如果有一个有摄像头 即被覆盖 return 1 return 3 condition_root = traverse(root) if condition_root == 3: res += 1 return res