跟着灵神一起学习~
(一)贪心算法
一、策略
两种基本贪心策略:
- 从最小/最大开始贪心,优先考虑最小/最大的数,从小到大/从大到小贪心。在此基础上衍生出了反悔贪心。
- 从最左/最右开始贪心,思考第一个数/最后一个数的贪心策略,把n个数的原问题转换为n-1个数(或更少)的子问题。
1、从最小/最大开始贪心
(1)重新分装苹果(3074)
class Solution:
def minimumBoxes(self, apple: List[int], capacity: List[int]) -> int:
capacity.sort(reverse=True)
n = len(capacity)
apple_sum = sum(apple)
ans = 0
for i in range(n):
if capacity[i] < apple_sum:
ans += 1
apple_sum -= capacity[i]
else:
ans += 1
break
return ans
- 时间复杂度:O(m+nlogn),m为apple的长度,n为capacity的长度
- 空间复杂度:O(1)
(2)装满石头的背包的最大数量(2279)
class Solution:
def maximumBags(self, capacity: List[int], rocks: List[int], additionalRocks: int) -> int:
n = len(capacity)
remains = [0] * n
for i in range(n):
remains[i] = capacity[i] - rocks[i]
remains.sort()
ans = 0
for i in range(n):
if remains[i] != 0 and additionalRocks >= remains[i]:
ans += 1
additionalRocks -= remains[i]
elif remains[i] == 0:
ans += 1
return ans
- 时间复杂度:O(n+nlogn)
- 空间复杂度:O(n)
(3)雪糕的最大数量(1833)
class Solution:
def maxIceCream(self, costs: List[int], coins: int) -> int:
costs.sort()
ans = 0
n = len(costs)
for i in range(n):
if coins >= costs[i]:
coins -= costs[i]
ans += 1
else:
break
return ans
- 时间复杂度:O(n+nlogn)
- 空间复杂度:O(1)
(4)K次取反后最大化的数组和(1005)
class Solution:
def largestSumAfterKNegations(self, nums: List[int], k: int) -> int:
nums.sort()
n = len(nums)
sum = 0
for i in range(n):
if nums[i] < 0 and k > 0:
nums[i] = -nums[i]
k -= 1
sum += nums[i]
nums.sort()
//如果k=0,表示所有的负数都变成正数,直接返回sum
//如果k还剩但为偶数,直接抵销就行,因为负数已经全部转换完了,如果再变列表中的正数,sum只会越来越小
if k == 0 or (k > 0 and k % 2 == 0):
return sum
else:
//如果k还剩但为奇数,那说明肯定会有至少一个数变成负数,那就直接变最小的那个数让sum尽可能大
return sum - 2 * nums[0]
- 时间复杂度:O(n+nlogn)
- 空间复杂度:O(1)
(5)不同整数的最少数目(1481)
class Solution:
def findLeastNumOfUniqueInts(self, arr: List[int], k: int) -> int:
kinds = collections.Counter(arr)
kinds = sorted(kinds.items(),key=lambda x:x[1])
n = len(kinds)
ans = n
for key,val in kinds:
if val <= k:
ans -= 1
k -= val
else:
break
return ans
- 时间复杂度:O(m+nlogn),m是arr的长度,n是kinds的长度
- 空间复杂度:O(n)
(6)非递增顺序的最小子序列(1403)
class Solution:
def minSubsequence(self, nums: List[int]) -> List[int]:
nums.sort(reverse=True)
total_sum = sum(nums)
cur_sum = 0
ans = []
for num in nums:
cur_sum += num
ans.append(num)
if cur_sum > total_sum - cur_sum:
break
return ans
- 时间复杂度:O(n+nlogn)
- 空间复杂度:O(n)
(7)将数组分成最小总代价的子数组I(3010)
这道题我只能说非常的脑筋急转弯==
开头一个子数组的代价绝对是nums[0],其他两个子数组的代价找剩余数组中最小的两个元素就可以了
class Solution:
def minimumCost(self, nums: List[int]) -> int:
n = len(nums)
if n == 3:
return sum(nums)
ans = nums[0]
nums = nums[1:]
nums.sort()
ans = ans + nums[0] + nums[1]
return ans
- 时间复杂度:O(nlogn)
- 空间复杂度:O(n)
(8)数组大小减半(1338)
class Solution:
def minSetSize(self, arr: List[int]) -> int:
kinds = collections.Counter(arr)
kinds = sorted(kinds.items(),key=lambda x:x[1],reverse=True)
total_size = len(arr)
current_size = 0
ans = 0
for _,val in kinds:
current_size += val
ans += 1
if current_size >= total_size // 2:
return ans
- 时间复杂度:O(m+nlogn),n是arr长度,m是kinds长度
- 空间复杂度:O(m)
(9)卡车上的最大单元数(1710)
class Solution:
def maximumUnits(self, boxTypes: List[List[int]], truckSize: int) -> int:
boxTypes.sort(key=lambda x:x[1],reverse=True)
ans = 0
for numberOfBoxes,numberOfUnitsPerBox in boxTypes:
if truckSize <= 0:
break
number = min(numberOfBoxes,truckSize)
ans += number * numberOfUnitsPerBox
truckSize -= number
return ans
- 时间复杂度:O(n+nlogn)
- 空间复杂度:O(n)
(10)幸福值最大化的选择方案(3075)
class Solution:
def maximumHappinessSum(self, happiness: List[int], k: int) -> int:
#每次选幸福值最大的孩子
happiness.sort(reverse=True)
n = len(happiness)
ans = happiness[0]
i = 1
times = 1
while i < k:
happiness[i] -= times
if happiness[i] < 0:
happiness[i] = 0
ans += happiness[i]
times += 1
i += 1
return ans
- 时间复杂度:O(k+nlogn)
- 空间复杂度:O(1)
(11)从一个范围内选择最多整数I(2554)
class Solution:
def maxCount(self, banned: List[int], n: int, maxSum: int) -> int:
ans = 0
cur_sum = 0
visited = set(banned)
for i in range(1,n+1):
if i not in visited:
if cur_sum + i <= maxSum:
cur_sum += i
ans += 1
else:
break
return ans
- 时间复杂度:O(n)
- 空间复杂度:O(n)
(12)摧毁小行星(2126)
class Solution:
def asteroidsDestroyed(self, mass: int, asteroids: List[int]) -> bool:
asteroids.sort()
n = len(asteroids)
i = 0
if mass < asteroids[0]:
return False
cur_mass = mass
for i in range(n-1):
cur_mass += asteroids[i]
if cur_mass < asteroids[i+1]:
return False
return True
- 时间复杂度:O(n+nlogn)
- 空间复杂度:O(1)
(13)重排数组以得到最大前缀分数(2587)
class Solution:
def maxScore(self, nums: List[int]) -> int:
nums.sort(reverse=True)
ans = 0
n = len(nums)
prefix = [0] * n
prefix[0] = nums[0]
if prefix[0] > 0:
ans = 1
else:
return 0
for i in range(1,n):
prefix[i] = prefix[i-1] + nums[i]
if prefix[i] > 0:
ans += 1
else:
break
return ans
- 时间复杂度:O(n+logn)
- 空间复杂度:O(n)
(14)三角形的最大周长(976)
- 时间复杂度:O(n+nlogn)
- 空间复杂度:O(1)
(15)减小和重新排列数组后的最大元素(1856)
class Solution:
def maximumElementAfterDecrementingAndRearranging(self, arr: List[int]) -> int:
arr.sort()
arr[0] = 1
n = len(arr)
for i in range(1,n):
if abs(arr[i] - arr[i-1]) > 1:
arr[i] = arr[i - 1] + 1
return max(arr)
- 时间复杂度:O(n+nlogn)
- 空间复杂度:O(1)
(16)使数组唯一的最小增量(945)【值得刷】
- 情况一:num >= next_free
说明当前num已经是唯一的了,不需要增加,更新next_free为num+1,表示下一个可用数字为num+1 - 情况二:num < next_free
说明当前num已被使用过,需要将其增加到next_free,更新next_free为next_free+1
class Solution:
def minIncrementForUnique(self, nums: List[int]) -> int:
nums.sort()
next_free = nums[0]
ans = 0
for num in nums:
if num < next_free:
ans += next_free - num
next_free += 1
else:
next_free = num + 1
return ans
- 时间复杂度:O(n+nlogn)
- 空间复杂度:O(1)
(17)高度互不相同的最大塔高和(3301)
相当舒服,跟上一道题一样的思路
class Solution:
def maximumTotalSum(self, maximumHeight: List[int]) -> int:
maximumHeight.sort(reverse=True)
next_free = maximumHeight[0]
for i in range(len(maximumHeight)):
if maximumHeight[i] > next_free:
maximumHeight[i] = next_free
next_free -= 1
else:
next_free = maximumHeight[i] - 1
if next_free < 0:
return -1
return sum(maximumHeight)
- 时间复杂度:O(n+nlogn)
- 空间复杂度:O(1)
(18)字符频次唯一的最小删除次数(1647)
class Solution:
def minDeletions(self, s: str) -> int:
kinds = collections.Counter(s)
kinds = sorted(kinds.items(),key=lambda x:x[1],reverse=True)
next_free = kinds[0][1]
ans = 0
for i in range(len(kinds)):
if kinds[i][1] > next_free:
if next_free >= 1:
ans += kinds[i][1] - next_free
next_free -= 1
else:
ans += kinds[i][1]
else:
next_free = kinds[i][1] - 1
return ans
- 时间复杂度:O(n+nlogn)
- 空间复杂度:O(n)
(19)找到最大周长的多边形(2971)
class Solution:
def largestPerimeter(self, nums: List[int]) -> int:
nums.sort(reverse=True)
n = len(nums)
cur_edge = sum(nums)
ans = 0
if n == 3:
if nums[0] >= sum(nums[1:]):
return -1
else:
return sum(nums)
for i in range(n-1):#7,5,1,1
cur_edge -= nums[i]
if cur_edge <= nums[i]:
continue
else:
ans = cur_edge + nums[i]
break
if ans <= 0:
ans = -1
return ans
- 时间复杂度:O(n+nlogn)
- 空间复杂度:O(1)
(20)拆分成最多数目的正偶数之和
class Solution:
def maximumEvenSplit(self, finalSum: int) -> List[int]:
if finalSum % 2:
return []
i = 2
ans = []
while i <= finalSum:
ans.append(i)
finalSum -= i
i += 2
ans[-1] += finalSum
return ans
2、单序列配对
从最小/最大的元素开始贪心。
(1)打折购买糖果的最小开销(2144)
class Solution:
def minimumCost(self, cost: List[int]) -> int:
n = len(cost)
if n == 2:
return sum(cost)
cost.sort(reverse=True)
ans = 0
while n >= 3:
ans += cost[0] + cost[1]
cost = cost[3:]
n = len(cost)
if n > 0:
ans += sum(cost)
return ans
- 时间复杂度:O(n+nlogn)
- 空间复杂度:O(1)
(2)数组拆分(561)
class Solution:
def arrayPairSum(self, nums: List[int]) -> int:
n = len(nums)
pairs_num = n // 2
nums.sort(reverse=True)
ans = 0
i = 1
while i < n:
ans += nums[i]
i += 2
return ans
- 时间复杂度:O(n+nlogn)
- 空间复杂度:O(1)
(3)数组中最大数对和的最小值(1877)
class Solution:
def minPairSum(self, nums: List[int]) -> int:
#最大的数和最小的数组一对
ans = 0
nums.sort()
n = len(nums)
left,right = 0,n-1
while left < right:
ans = max(ans,nums[left] + nums[right])
left += 1
right -= 1
return ans
- 时间复杂度:O(n+nlogn)
- 空间复杂度:O(1)
(4)救生艇(881)
class Solution:
def numRescueBoats(self, people: List[int], limit: int) -> int:
n = len(people)
if n == 2 and sum(people) <= limit:
return 1
people.sort()
def dfs(left:int,right:int)->int:
ans = 0
if left > right:
return ans
if people[left] + people[right] <= limit:
ans += 1
return ans + dfs(left+1,right-1)
else:
ans += 1
return ans + dfs(left,right-1)
return ans
return dfs(0,n-1)
- 时间复杂度:O(n+nlogn)
- 空间复杂度:O(1)
(5)最大化数组的伟大值【值得刷】
class Solution:
def maximizeGreatness(self, nums: List[int]) -> int:
nums.sort()
i = 0
for num in nums:
if num > nums[i]:
i += 1
return i
- 时间复杂度:O(nlogn)
- 空间复杂度:O(1)
(6)求出最多标记下标(2576)
class Solution:
def maxNumOfMarkedIndices(self, nums: List[int]) -> int:
nums.sort()
n = len(nums)
count = 0
left,right = 0, n // 2
while left < n // 2 and right < n:
if 2 * nums[left] <= nums[right]:
count += 1
left += 1
right += 1
else:
right += 1
return 2 * count
- 时间复杂度:O(n+nlogn)
- 空间复杂度:O(1)
3、双序列配对
从最小/最大的元素开始贪心。
(1)使每位学生都有座位的最少移动次数(2037)
class Solution:
def minMovesToSeat(self, seats: List[int], students: List[int]) -> int:
seats.sort()
students.sort()
n = len(seats)
ans = 0
for i in range(n):
ans += abs(seats[i] - students[i])
return ans
- 时间复杂度:O(nlogn)
- 空间复杂度:O(1)
(2)分发饼干(455)
class Solution:
def findContentChildren(self, g: List[int], s: List[int]) -> int:
g.sort()
s.sort()
n = len(s)
i = 0
for j in range(n):
if i < len(g) and g[i] <= s[j]:
i += 1
return i
- 时间复杂度:O(n+nlogn)
- 空间复杂度:O(1)
(3)运动员和训练师的最大匹配数
class Solution:
def matchPlayersAndTrainers(self, players: List[int], trainers: List[int]) -> int:
players.sort()
trainers.sort()
i = 0
n = len(trainers)
for j in range(n):
if i < len(players) and players[i] <= trainers[j]:
i += 1
return i
- 时间复杂度:O(n+nlogn)
- 空间复杂度:O(1)
(4)检查一个字符串是否可以打破另一个字符串(1433)
class Solution:
def checkIfCanBreak(self, s1: str, s2: str) -> bool:
s1_list = sorted(s1)
s2_list = sorted(s2)
n = len(s1)
flag1 = 1
flag2 = 1
for i in range(n):
if s1_list[i] < s2_list[i]:
flag1 = 0
if s2_list[i] < s1_list[i]:
flag2 = 0
return True if flag1 or flag2 else False
- 时间复杂度:O(n+nlogn)
- 空间复杂度:O(1)
(5)优势洗牌(870)【值得刷】
class Solution:
def advantageCount(self, nums1: List[int], nums2: List[int]) -> List[int]:
location = defaultdict(list)
n = len(nums1)
for i in range(n):
location[nums2[i]].append(i)
ans = [0] * n
nums1.sort()
nums2.sort()
i,j = 0,n-1
remaining = []
for num in nums1:
if num > nums2[i]:
ans[location[nums2[i]].pop()] = num
i += 1
else:
remaining.append(num)
for num in remaining:
ans[location[nums2[j]].pop()] = num
j -= 1
return ans
- 时间复杂度:O(n+nlogn)
- 空间复杂度:O(n)
(6)安排工作以达到最大收益(826)【值得刷】
class Solution:
def maxProfitAssignment(self, difficulty: List[int], profit: List[int], worker: List[int]) -> int:
jobs = sorted(zip(difficulty,profit))
worker.sort()
ans = j = max_profit = 0
for w in worker:
while j < len(jobs) and jobs[j][0] <= w:
max_profit = max(max_profit,jobs[j][1])
j += 1
ans += max_profit
return ans
- 时间复杂度:O(nlogn+mlogm),n为difficulty长度,m为worker长度
- 空间复杂度:O(n)
(7)使数组相似的最少操作次数(2449)【值得刷】
这个区分偶数和奇数的方法真的太妙了!!!
def f(nums:List[int]):
for i,x in enumerate(nums):
if x % 2:
nums[i] = -x
nums.sort()
class Solution:
def makeSimilar(self, nums: List[int], target: List[int]) -> int:
f(nums)
f(target)
return sum(abs(x-y) for x,y in zip(nums,target)) // 4
- 时间复杂度:O(nlogn)
- 空间复杂度:O(1)
(8)装包裹的最小浪费空间【值得刷】
贪心+前缀和+二分查找
class Solution:
def minWastedSpace(self, packages: List[int], boxes: List[List[int]]) -> int:
MOD = 10 ** 9 + 7
packages.sort()
n = len(packages)
prefix_sum = [0] * (n+1)
for i in range(n):
prefix_sum[i + 1] = prefix_sum[i] + packages[i]
min_waste = float('inf')
for box in boxes:
box.sort()
if box[-1] <packages[-1]:
continue
waste = 0
prev = 0
for size in box:
idx = bisect.bisect_right(packages,size,prev)
total = prefix_sum[idx] - prefix_sum[prev]
waste += (idx-prev) * size - total
prev = idx
if prev == n:
break
if prev == n:
min_waste = min(min_waste,waste)
return min_waste % MOD if min_waste != float('inf') else -1
(9)重排水果(2561)【值得刷】
这不经过训练谁想得到==
class Solution:
def minCost(self, basket1: List[int], basket2: List[int]) -> int:
count1 = collections.Counter(basket1)
count2 = collections.Counter(basket2)
combined = count1 + count2
for k,v in combined.items():
if v % 2:
return -1
extra1,extra2 = [],[]
for item in combined:
diff = count1[item] - count2[item]
if diff > 0:
extra1.extend([item] * (diff // 2))
elif diff <0:
extra2.extend([item] * (-diff // 2))
if len(extra1) != len(extra2):
return -1
extra1.sort()
extra2.sort(reverse=True)
min_element = min(basket1 + basket2)
ans = 0
for x,y in zip(extra1,extra2):
ans += min(x,y,min_element * 2)
return ans
- 时间复杂度:O(nlogn)
- 空间复杂度:O(n)
4、从最左/最右开始贪心
对于无法排序的题目,尝试从左到右/从右到左贪心。思考第一个数/最后一个数的贪心策略,把n个数的原问题转换成n-1个数(或更少)的子问题。
(1)使二进制数组全部等于1的最少操作次数I(3191)
class Solution:
def minOperations(self, nums: List[int]) -> int:
ans = 0
for i in range(len(nums) - 2):
if nums[i] == 0:
nums[i+1] ^= 1
nums[i+2] ^= 1
ans += 1
return ans if nums[-2] and nums[-1] else -1
- 时间复杂度:O(nk),n为nums长度,k=3为每次操作反转的元素个数
- 空间复杂度:O(1)
(2)最少操作使数组递增(1827)
class Solution:
def minOperations(self, nums: List[int]) -> int:
n = len(nums)
if n == 1:
return 0
ans = 0
for i in range(n-1):
if nums[i] >= nums[i+1]:
ans += nums[i] - nums[i+1] + 1
nums[i + 1] = nums[i] + 1
return ans
- 时间复杂度:O(n)
- 空间复杂度:O(1)
(3)转换字符串的最少操作次数(2027)
这道题和3191的区别就是只会从’X’变成’0’,而不会从’0’变成’X’,所以需要注意最后两位的情况
class Solution:
def minimumMoves(self, s: str) -> int:
ans = 0
n = len(s)
ch = list(s)
for i in range(n-2):
if ch[i] == 'X':
ch[i+1] = '0'
ch[i+2] = '0'
ans += 1
if ch[-1] == 'X' or ch[-2] == 'X':
ans += 1
return ans
- 时间复杂度:O(n)
- 空间复杂度:O(n)
(4)种花问题(605)
class Solution:
def canPlaceFlowers(self, flowerbed: List[int], n: int) -> bool:
flowerbed = [0] + flowerbed + [0]
m = len(flowerbed)
for i in range(1,m-1):
if flowerbed[i] == 0 and flowerbed[i-1] == 0 and flowerbed[i + 1] == 0:
flowerbed[i] = 1
n -= 1
return n <= 0
- 时间复杂度:O(n)
- 空间复杂度:O(1)
(5)覆盖所有点的最少矩形数目(3111)
class Solution:
def minRectanglesToCoverPoints(self, points: List[List[int]], w: int) -> int:
points = sorted(points,key=lambda x:x[0])
n = len(points)
i = 0
ans = 1
for j in range(1,n):
if points[i][0] + w >= points[j][0]:
continue
else:
ans += 1
i = j
return ans
- 时间复杂度:O(n+nlogn)
- 空间复杂度:O(1)
(6)覆盖所有点的最少矩形数目(3111)
class Solution:
def minRectanglesToCoverPoints(self, points: List[List[int]], w: int) -> int:
points = sorted(points,key=lambda x:x[0])
n = len(points)
i = 0
ans = 1
for j in range(1,n):
if points[i][0] + w >= points[j][0]:
continue
else:
ans += 1
i = j
return ans
- 时间复杂度:O(n+nlogn)
- 空间复杂度:O(1)
(7)消除相邻近似相等字符
class Solution:
def removeAlmostEqualCharacters(self, word: str) -> int:
ans = 0
i = 1
n = len(word)
while i < n:
if abs(ord(word[i]) - ord(word[i-1])) <= 1:
ans += 1
i += 2
else:
i += 1
- 时间复杂度:O(N)
- 空间复杂度:O(1)
(8)使二进制数组全部等于1的最少操作次数II(3192)
class Solution:
def minOperations(self, nums: List[int]) -> int:
k = 0
for x in nums:
if (x == 0 and k % 2) or (x == 1 and k % 2 == 0):
continue
else:
k += 1
x ^= 1
return k
- 时间复杂度:O(n)
- 空间复杂度:O(1)
(9)合并后数组中的最大元素(2789)
class Solution:
def maxArrayValue(self, nums: List[int]) -> int:
s = nums[-1]
n = len(nums)
for i in range(n-2,-1,-1):
s = s + nums[i] if nums[i] <= s else nums[i]
return s
- 时间复杂度:O(n)
- 空间复杂度:O(1)
(10)最少得后缀翻转次数(1529)
class Solution:
def minFlips(self, target: str) -> int:
ans = 0
flip = 0 #0表示未翻转,1表示已翻转
for ch in target:
cur = flip % 2 #当前s的状态
tar = int(ch)
if cur != tar:
flip += 1
ans += 1
return ans
- 时间复杂度:O(n)
- 空间复杂度:O(1)
(11)递减元素使数组呈锯齿状
为了使操作次数尽量小,nums[i]不断减小到比左右相邻数字都小,就立刻停止。所以nums[i]要修改成m=min(nums[i-1],nums[i+1])-1,修改次数为nums[i]-m,如果nums[i]本来就不超过m,无需修改。
因此nums[i]的修改次数为
m
a
x
(
n
u
m
s
[
i
]
−
m
i
n
(
n
u
m
s
[
i
−
1
]
,
n
u
m
s
[
i
+
1
]
)
+
1
,
0
)
max(nums[i]-min(nums[i-1],nums[i+1])+1,0)
max(nums[i]−min(nums[i−1],nums[i+1])+1,0)
若i-1或i+1下标越界,则对应的数字视作无穷大。
最后,把偶数和奇数下标对应的修改次数分别累加,结果分别设为s0和s1,那么答案就是min(s0,s1)
class Solution:
def movesToMakeZigzag(self, nums: List[int]) -> int:
s = [0] * 2
for i,x in enumerate(nums):
left = nums[i-1] if i-1 >= 0 else inf
right = nums[i+1] if i+1 < len(nums) else inf
s[i % 2] += max(nums[i] - min(left,right)+1,0)
return min(s)
- 时间复杂度:O(N)
- 空间复杂度:O(1)
(12)将 1 移动到末尾的最大操作次数(3228)
class Solution:
def maxOperations(self, s: str) -> int:
ans = cnt = 0
for i,c in enumerate(s):
if c == '1':
cnt += 1
elif i and s[i-1] == '1':
ans += cnt
return ans
- 时间复杂度:O(n)
- 空间复杂度:O(1)
(13)喂食仓鼠的最小食物桶数(2086)
class Solution:
def minimumBuckets(self, hamsters: str) -> int:
n = len(hamsters)
i,ans = 0,0
while i < n:
if hamsters[i] == 'H':
if i+1 < n and hamsters[i+1] == '.':
ans += 1
i += 2
elif i-1 >= 0 and hamsters[i-1] =='.':
ans += 1
else:
return -1
i += 1
return ans
- 时间复杂度:O(n)
- 空间复杂度:O(1)
(14)将整数减少到零需要的最少操作数(2571)
把n看成二进制数,那么更高位的比特1是会收到更低位的比特1的加减影响的,但是最小的比特1没有这个约束。
那么考虑优先消除最小的比特1,设它对应的数字为lowbit。
消除方法只能是加上lowbit或减去lowbit。
贪心策略:若有多个连续1,则采用加法更优,可一次消除多个1;否则对于单个1,减法更优。
class Solution:
def minOperations(self, n: int) -> int:
ans = 1
while n & (n-1):
lb = n & -n
if n & (lb << 1):
n += lb
else:
n -= lb
ans += 1
return ans
-
ans:初始化为1,因为若n已经是2的幂,则只需一次操作。
-
while n & (n-1):用来检查n是否是2的幂,因为若n是2的幂,n的二进制形式只包含一个1,在这种情况下,n&(n-1)的结果为0。
-
lb = n & -n:提取n的最低位的1,即n二进制形式中的最右边的1位。
-
n & (lb << 1):将lb左移一位,若n在该位置也有1,则n有连续的1位。
-
时间复杂度:O(n)
-
空间复杂度:O(1)
(15)使所有字符相等的最小成本(1791)
class Solution:
def minimumCost(self, s: str) -> int:
n = len(s)
ans = 0
for i in range(1,n):
if s[i] != s[i-1]:
ans += min(i,n-i)
return ans
- 时间复杂度:O(n)
- 空间复杂度:O(1)
(16)使二叉树所有路径值相等的最小代价(2673)
class Solution:
def minIncrements(self, n: int, cost: List[int]) -> int:
ans = 0
for i in range(n-2,0,-2):
ans += abs(cost[i] - cost[i+1])
cost[i // 2] += max(cost[i],cost[i+1])
return ans
- 时间复杂度:O(n)
- 空间复杂度:O(1)
5、划分型贪心
把数组/字符串划分成满足要求的若干段,最小化/最大化划分的段数。
思考方法同上,尝试从左到右/从右到左贪心。
6、先枚举,再贪心
枚举题目的其中一个变量,将其视作已知条件,然后在此基础上贪心。
也可以枚举答案,检查是否可以满足要求。(类似二分答案)
(1)枚举最后袋子中魔法豆的数目(2171)【值得刷】
拿出魔法豆+剩余魔法豆=初始魔法豆之和,可以考虑最多剩余多少魔法豆,从而计算出最少能拿出多少个魔法豆。
class Solution:
def minimumRemoval(self, beans: List[int]) -> int:
beans.sort()
return sum(beans) - max((len(beans) - i) * v for i,v in enumerate(beans))
- 时间复杂度:O(nlogn)
- 空间复杂度:O(1)
(2)成为K特殊字符串需要删除的最少字符数(3085)
统计word中每个字母的出现次数,记录一个数组cnt中。
枚举i作为出现次数最小的字母,为保留尽量多的字母,字母i肯定不需要删除。此外,出现次数最多的字母,其出现次数不能超过cnt[i]+k。
分类讨论:
- 出现次数小于cnt[i]的字母,全部删除
- 出现次数大于等于cnt[i]的字母j,保留min(cnt[j],cnt[i]+k)个。累加保留的字母个数,更新最多保留的字母个数maxSave的最大值。
class Solution:
def minimumDeletions(self, word: str, k: int) -> int:
cnt = sorted(Counter(word).values())
max_save = max(sum(min(c,base + k) for c in cnt[i:]) for i,base in enumerate(cnt))
return len(word) - max_save
7、交换论证法
- 对于题目,猜想按照某种顺序处理数据,可以得到最优解;
- 交换顺序中的两个元素ai和aj,计算交换后的答案;
- 对比交换前后的答案。如果交换后,答案没有变得更优,则说明猜想成立。
也可以不用猜想,而是计算先ai后aj和先aj后ai对应的答案,通过比较两个答案谁更优,来确定按照何种顺序处理数据。
(1)切蛋糕的最小总开销II(3219)
每条水平线和垂直线,最终都要全部切完。
- 水平线(横切)开销horizontalCut[i]对答案的贡献,等于horizontalCut[i]乘以横切次数(经过多少块蛋糕),即在此之前的竖切次数+1。
- 垂直线(竖切)开销verticalCut[i]对答案的贡献,等于verticalCut[i]乘以竖切次数(经过多少块蛋糕),即在此之前的横切次数+1。
以示例1为例,其操作序列为
竖切
0
,横切
0
,横切
1
竖切0,横切0,横切1
竖切0,横切0,横切1
最小总开销为
v
e
r
t
i
c
a
l
C
u
t
[
0
]
∗
1
+
h
o
r
i
z
o
n
t
a
l
C
u
t
[
0
]
∗
2
+
h
o
r
i
z
o
n
t
a
l
C
u
t
[
1
]
∗
2
verticalCut[0] * 1 + horizontalCut[0] * 2 + horizontalCut[1]*2
verticalCut[0]∗1+horizontalCut[0]∗2+horizontalCut[1]∗2
设横切的开销为h,若先横切,设需要横切cntH次。
设竖切的开销为v,若先竖切,设需要竖切cntV次。
- 先横切,再竖切,那么竖切的次数(这一刀经过蛋糕的次数)要多1,开销为
h ∗ c n t H + v ∗ ( c n t V + 1 ) h * cntH + v * (cntV+1) h∗cntH+v∗(cntV+1) - 先竖切,再横切,那么横切的次数(这一刀经过蛋糕的次数)要多1,开销为
v ∗ c n t V + h ∗ ( c n t H + 1 ) v * cntV + h * (cntH+1) v∗cntV+h∗(cntH+1)
若先横再竖开销更小,则有
h ∗ c n t H + v ∗ ( c n t V + 1 ) < v ∗ c n t V + h ∗ ( c n t H + 1 ) h * cntH + v * (cntV+1) < v * cntV + h * (cntH+1) h∗cntH+v∗(cntV+1)<v∗cntV+h∗(cntH+1)
化简为
h > v h > v h>v
这意味着,谁的开销更大,就先切谁,并且这个先后顺序与cntH和cntV无关。
做法: - 把horizontalCut和verticalCut从大到小排序;
- 初始化cntH=1,cntV=1,i=0,j=0;
- 双指针遍历horizontalCut和verticalCut;
- 若horizontalCut[i]>verticalCut[j],则优先横切,把horizontalCut[i] * cntH假如答案,i+1,然后需要竖切的次数增加,即cntV+1;否则优先竖切,把verticalCut[j]*cntV加入答案,j+1,然后需要横切的次数增加,即cntH+1.
- 循环直到两个数组都遍历完;
- 返回答案。
class Solution:
def minimumCost(self, m: int, n: int, horizontalCut: List[int], verticalCut: List[int]) -> int:
horizontalCut.sort(reverse=True)
verticalCut.sort(reverse=True)
ans = 0
cntH,cntV = 1,1
i,j = 0,0
while i < m - 1 or j < n - 1:
if j == n - 1 or i < m - 1 and horizontalCut[i] > verticalCut[j]:
ans += horizontalCut[i] * cntH
i += 1
cntV += 1
else:
ans += verticalCut[j] * cntV
j += 1
cntH += 1
return ans
- 时间复杂度:O(mlogm+nlogn)
- 空间复杂度:O(1)
(2)对Bob造成的最少伤害(3273)【值得刷】
首先,一直攻击同一个敌人,相比来回攻击多个敌人(雨露均沾)更好,因为这样我们被敌人攻击的次数更少。
从特殊到一般,如果只有两个敌人A和B,我们应该先攻击谁?
消灭A需要攻击的次数:
同理可得消灭B需要的攻击次数,记作kB。
若先消灭A,再消灭B,那么受到的伤害总和为
k
A
×
d
a
m
a
g
e
A
+
(
k
A
+
k
B
)
×
d
a
m
a
g
e
B
k_A \times damage_A + (k_A+k_B) \times damage_B
kA×damageA+(kA+kB)×damageB
若先消灭B,再消灭A,那么受到的伤害总和为
k
B
×
d
a
m
a
g
e
B
+
(
k
A
+
k
B
)
×
d
a
m
a
g
e
A
k_B \times damage_B + (k_A+k_B) \times damage_A
kB×damageB+(kA+kB)×damageA
如果先消灭A,再消灭B更好,则有
k A × d a m a g e A + ( k A + k B ) × d a m a g e B < k B × d a m a g e B + ( k A + k B ) × d a m a g e A k_A \times damage_A + (k_A+k_B) \times damage_B < k_B \times damage_B + (k_A+k_B) \times damage_A kA×damageA+(kA+kB)×damageB<kB×damageB+(kA+kB)×damageA
化简得
k
A
×
d
a
m
a
g
e
B
<
k
B
×
d
a
m
g
e
A
k_A \times damage_B < k_B \times damge_A
kA×damageB<kB×damgeA
k
A
/
d
a
m
g
e
A
<
k
B
/
d
a
m
a
g
e
B
k_A / damge_A < k_B / damage_B
kA/damgeA<kB/damageB
即优先消灭k/damage更小的敌人。
class Solution:
def minDamage(self, power: int, damage: List[int], health: List[int]) -> int:
n = len(damage)
for i in range(n):
health[i] = (health[i] - 1) // power + 1
ans = s = 0
for i in sorted(range(n),key=lambda i:health[i]/damage[i]):
s += health[i]
ans += s * damage[i]
return ans
- 时间复杂度:O(nlogn)
- 空间复杂度:O(1)
(3)全部开花的最早一天(2136)
(4)最大数(179)
这里的排序使用了 key=lambda x: x * 10,表示将每个字符串扩展为自身重复 10 次,然后按降序排序。
具体原因是,我们需要确保两数之间组合的最大效果。例如,对于 3 和 30,我们比较 330 和 303,显然 330 更大,因此 3 应该在 30 前。
通过 x * 10 将每个数字字符串扩展 10 次,例如 “3” * 10 = “3333333333”,“30” * 10 = “3030303030”,这样做可以确保在字符串组合时按正确顺序排列。
class Solution:
def largestNumber(self, nums: List[int]) -> str:
nums = list(map(str,nums))
nums.sort(key=lambda x:x*10,reverse=True)
ans = ''.join(nums)
return ans if ans[0] != '0' else '0'
- 时间复杂度:O(nlogn)
- 空间复杂度O(n)
(5)连接二进制表示可形成的最大数值(3309)
class Solution:
def maxGoodNumber(self, nums: List[int]) -> int:
s = []
for x in nums:
x = format(x,'b')
s.append(x)
s.sort(key=lambda x:x * 10,reverse=True)
ans = ''.join(s)
return int(ans,2)
- 时间复杂度:O(nlogn)
- 空间复杂度:O(n)
8、相邻不同
给定正整数数组,每次操作,把数组中的两个数各减一,并去掉变成0的数。目标:使最后剩下的数最小,或最大化操作次数。
由于每次操作的都是两个下标不同的数,把这些下标按顺序拼接,可以构造出一个相邻元素不同的序列。
(1)重构字符串
class Solution:
def reorganizeString(self, s: str) -> str:
kinds = collections.Counter(s)
kinds = sorted(kinds.items(),key=lambda x:x[1],reverse=True)
max_freq = kinds[0][1]
if max_freq > (len(s) + 1) // 2:
return ""
ans = [""] * len(s)
index = 0
for char,freq in kinds:
for _ in range(freq):
ans[index] = char
index += 2
if index >= len(s):
index = 1
return "".join(ans)
- 时间复杂度:O(nlogn)
- 空间复杂度:O(n)
(2)删除数对后的最小数组长度(2856)
假设x出现次数最多,其出现次数为maxCnt。
分类讨论:
- 若maxCnt * 2 > n,其余所有n-maxCnt个数都要与x消除,所以最后剩下maxCnt*2-n个数;
- 若maxCnt*2 ≤ n且n为偶数,那么可以把其余数消除至剩下maxCnt个数,然后再和x消除,最后剩下0个数;
- 如果maxCnt*2≤n且n为奇数,同上,最后剩下1个数。
由于nums是有序的,如果maxCnt超过数组长度的一半,那么nums[n/2]一定是出现次数最多的那个数。
bisect_left返回x第一次出现的索引,bisect_right返回x最后出现位置的下一个索引。
class Solution:
def minLengthAfterRemovals(self, nums: List[int]) -> int:
n = len(nums)
x = nums[n // 2]
max_cnt = bisect_right(nums,x) - bisect_left(nums,x)
return max(max_cnt * 2 - n,n % 2)
- 时间复杂度:O(N)
- 空间复杂度:O(1)
(3)你可以工作的最大周数(1953)
class Solution:
def numberOfWeeks(self, milestones: List[int]) -> int:
total = sum(milestones)
max_stone = max(milestones)
if total - max_stone >= max_stone:
return total
else:
return 2 * (total - max_stone) + 1
- 时间复杂度:O(N)
- 空间复杂度:O(1)
(4)不含 AAA 或 BBB 的字符串(984)
class Solution:
def strWithout3a3b(self, a: int, b: int) -> str:
res = []
while a > 0 or b > 0:
if len(res) >= 2 and res[-1] == res[-2]:
add_a = res[-1] == 'b'
else:
add_a = a >= b
if add_a:
res.append('a')
a -= 1
else:
res.append('b')
b -= 1
return "".join(res)
- 时间复杂度:O(n)
- 空间复杂度:O(n)
(5)最长快乐字符串(1405)
- 时间复杂度:O(N)
- 空间复杂度:O(n)
9、反悔贪心
一定要用到堆。
(1)魔塔游戏(LCP30)
首先,若nums的元素和小于0,那么即使把所有负数都移到末尾,也无法访问所有房间,返回-1。
否则遍历数组,能加血就尽管加血,要扣血就直接扣血,但如果血量小于1,我们就反悔:从前面的扣血中,拿出一个扣血量最大的数(最小的负数),移到数组的末尾,把之前扣掉的血重新加回来。
具体来说:
①初始化血量hp=1
②从左到右遍历数组,把小于0的数丢到一个小根堆中。
3.遍历的同时,把nums[i]加到hp中。如果hp<1,则弹出堆顶,hp减去堆顶,相当于把之前扣掉的血重新加回来。同时把调整次数+1。如果hp<1,那么必然是由当前这个小于0的nums[i]导致的,这一保证了此时堆不为空,二保证了hp减去堆顶后必然可以恢复成整数,因为堆顶不会比nums[i]还大。
class Solution:
def magicTower(self, nums: List[int]) -> int:
if sum(nums) < 0:
return -1
hp = 1
h = []
ans = 0
for x in nums:
if x < 0:
heappush(h,x)
hp += x
if hp < 1:
hp -= heappop(h)
ans += 1
return ans
- 时间复杂度:O(nlogn)
- 空间复杂度:O(n)
(2)可以到达的最远建筑(1642)
class Solution:
def furthestBuilding(self, heights: List[int], bricks: int, ladders: int) -> int:
n = len(heights)
h = []
for i in range(1,n):
diff = heights[i]-heights[i-1]
if diff <= 0:
continue
heappush(h,diff)
if len(h) > ladders:
bricks -= heappop(h)
if bricks < 0:
return i - 1
return n - 1
- 时间复杂度:O(nlogn)
- 空间复杂度:O(n)
(3)课程表III(630)
class Solution:
def scheduleCourse(self, courses: List[List[int]]) -> int:
h = []
total_time = 0
courses = sorted(courses,key=lambda x:x[1])
for duration,lastday in courses:
heappush(h,-duration)
total_time += duration
if total_time > lastday:
total_time += heappop(h)
return len(h)
- 时间复杂度:O(nlogn)
- 空间复杂度:O(n)
(4)最低加油次数(871)
class Solution:
def minRefuelStops(self, target: int, startFuel: int, stations: List[List[int]]) -> int:
stations.append((target,0))
ans = 0
cur_fuel = startFuel
pre_position = 0
fuel_heap = []
for position,fuel in stations:
cur_fuel -= position - pre_position
while fuel_heap and cur_fuel < 0:
cur_fuel -= heappop(fuel_heap)
ans += 1
if cur_fuel < 0:
return -1
heappush(fuel_heap,-fuel)
pre_position = position
return ans
- 时间复杂度:O(nlogn)
- 空间复杂度:O(n)
(5)子序列最大优雅度(2813)
class Solution:
def findMaximumElegance(self, items: List[List[int]], k: int) -> int:
items = sorted(items,key=lambda x:x[0],reverse=True)
vis = set()
total_profit = ans = 0
duplicate = []
for i,(profit,category) in enumerate(items):
if i < k:
total_profit += profit
if category not in vis:
vis.add(category)
else:
duplicate.append(profit)
else:
if duplicate and category not in vis:
total_profit -= duplicate.pop()
total_profit += profit
vis.add(category)
ans = max(ans,total_profit + len(vis) ** 2)
return ans
- 时间复杂度:O(nlogn)
- 空间复杂度:O(n)
(6)标记所有下标的最早秒数 II
二分答案+反悔贪心
翻译题目:
我们有n门课程,每门课程需要nums[i]天来复习。在某些特定的天数(通过changeIndices[i]给定的天数),可以快速复习某门课程。我们要找到一个最小的天数mx,使得所有课程的复习和考试都可以在mx天内完成。
解题思路:
- 二分查找:
- 可以使用二分查找来确定最早的天数mx,在这个天数内所有的可成都能完成复习和考试。
- 通过check(mx)函数,判断是否可以在mx天内完成任务。如果可以,则以为可能的答案更小,我们继续缩小范围;反之,我们需要增加天数。
- 快速复习和慢速复习
- 慢速复习:每天复习一门课程
- 快速复习:在changeIndices[i]对应的天数,可以快速复习某门课程,将其复习时间直接设为0。
- 小根堆和反悔机制
- 反悔目的:将原本计划慢速复习的课程,改为快速复习,以节省时间。快速复习会导致某些课程的复习时间被提前完成,腾出更多时间来安排其他课程。
- 选择较短复习时间的课程:因为它们对总时间的影响较小。
class Solution:
def earliestSecondToMarkIndices(self, nums: List[int], changeIndices: List[int]) -> int:
n = len(nums)
m = len(changeIndices)
total = n + sum(nums)
first_t = [-1] * n
for t in range(m - 1,-1,-1):
first_t[changeIndices[t] - 1] = t
def check(mx:int)->bool:
slow = total
cnt = 0
h = []
for t in range(mx - 1,-1,-1):
i = changeIndices[t] - 1
v = nums[i]
if v <= 1 or t != first_t[i]:
cnt += 1
continue
if cnt == 0:
if not h or h[0] > v:
cnt += 1
continue
slow += heappop(h) + 1
cnt += 2
slow -= v + 1
cnt -= 1
heappush(h,v)
return cnt >= slow
ans = n + bisect_left(range(n,m+1),True,key=check)
return -1 if ans > m else ans
- 时间复杂度:O(mlogmlogn)
- 空间复杂度:O(n)
(7)最小移动总距离
记忆化搜索
对机器人和工厂按照位置从小到大排序,那么每个工厂修复的机器人就是连续的一段了。
定义f(i,j)表示用第i个及其右侧的工厂,修理第j个及其右侧的机器人,机器人移动的最小总距离。
枚举第i个工厂修理了k个机器人,则有f(i,j)=min(k)f(i+1,j+k) + cost(i,j,k)
cost(i,j,k)表示第i个工厂修理第j个到第j+k-1个机器人。
class Solution:
def minimumTotalDistance(self, robot: List[int], factory: List[List[int]]) -> int:
n = len(robot)
m = len(factory)
robot.sort()
factory = sorted(factory,key=lambda x:x[0])
@cache
def f(i:int,j:int)->int:
if j == n:
return 0
if i == m - 1:
if n - j > factory[i][1]:
return inf
return sum(abs(x - factory[i][0]) for x in robot[j:])
res = f(i+1,j)
s,k = 0,1
while k <= factory[i][1] and j + k - 1 < m:
s += abs(factory[i][0] - robot[j + k - 1])
res = min(res,s + f(i + 1,j + k))
k += 1
return res
return f(0,0)
- 时间复杂度:O(mn²)
- 空间复杂度:O(mn)