力扣每日一题(三)划分题 + 思路题
目录
1. 划分题&记忆化搜索 @cache
3003. 执行操作后的最大分割数量 -- 记忆化搜索
3144. 分割字符频率相等的最少子字符串 -- 记忆化搜索
698. 划分为k个相等的子集
3117. 划分数组得到最小的值之和
2963. 统计好分割方案的数目
2. 执行操作 思路题
3347. 执行操作后元素的最高频率 II
3397. 执行操作后不同元素的最大数量
2273. 移除字母异位词后的结果数组 -- 快慢指针
56. 合并区间
1553. 吃掉 N 个橘子的最少天数
1. 划分题&记忆化搜索 @cache
3003. 执行操作后的最大分割数量 -- 记忆化搜索
对字符串s 可以最多修改一个字符,然后进行规则划分(前缀不能超过k个不同字符)

记忆化搜索:( i, mask, changed)
现在到第几个字符;这一段前面出现了哪些字符 用位运算进行mask;有没有changed过。
mask : 26个字母是否出现;
不修改:加上 i ,mask中的数量超过 k,就要在 i-1 分割,分割+1;否则接着mask。
如果 changed 可以修改,枚举改成哪个字母,找 max。
class Solution:def maxPartitionsAfterOperations(self, s: str, k: int) -> int:@cachedef dfs(i: int, mask: int, changed: bool) -> int:if i == len(s):return 1# 不改 s[i]bit = 1 << (ord(s[i]) - ord('a'))new_mask = mask | bitif new_mask.bit_count() > k:# 分割出一个子串,这个子串的最后一个字母在 i-1# s[i] 作为下一段的第一个字母,也就是 bit 作为下一段的 mask 的初始值res = dfs(i + 1, bit, changed) + 1else: # 不分割res = dfs(i + 1, new_mask, changed)if changed:return res# 枚举把 s[i] 改成 a,b,c,...,zfor j in range(26):new_mask = mask | (1 << j)if new_mask.bit_count() > k:# 分割出一个子串,这个子串的最后一个字母在 i-1# j 作为下一段的第一个字母,也就是 1<<j 作为下一段的 mask 的初始值res = max(res, dfs(i + 1, 1 << j, True) + 1)else: # 不分割res = max(res, dfs(i + 1, new_mask, True))return resreturn dfs(0, 0, False)
3144. 分割字符频率相等的最少子字符串 -- 记忆化搜索
对每个位置 i,往后枚举找可划分区间 (i,j)
在循环找末端点位置 j 的时候,用 Counter 计数,并用 all 判断是否次数都同。
class Solution:def minimumSubstringsInPartition(self, s: str) -> int:n = len(s)@cachedef dfs(i:int)->int:if i==n:return 0# Counter 进行计数c=Counter()ans=inffor j in range(i,n):c[s[j]]+=1# 优化不用判断,字母种类不是字母数目的因数的话 不可能都相等了if (j-i+1)%len(c)!=0:continuecc=c[s[j]]# 可划分就 dfsif all(cc==ac for ac in c.values()):ans=min(ans,dfs(j+1)+1)return ansreturn dfs(0)
698. 划分为k个相等的子集
记忆化搜索:
判断能不能把 n 个数 划分为 k 个相等的子集。 先看因数和最大值判断。
二进制 mask 表示有没有使用过。 从 2^n-1 -> 0
dfs (s, p) 代表对当前状态 s,当前余数 p,是否可以继续成功划分。
nums.sort() 从小到大,如果 nums[i] 塞到 p 会爆,后面更大的也会爆(减少判断次数)。
class Solution:def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:all = sum(nums)if all % k:return Falseper = all // knums.sort() # 方便下面剪枝if nums[-1] > per:return Falsen = len(nums)@cachedef dfs(s, p):if s == 0:return Truefor i in range(n):if nums[i] + p > per:breakif s >> i & 1 and dfs(s ^ (1 << i), (p + nums[i]) % per): # p + nums[i] 等于 per 时置为 0return Truereturn Falsereturn dfs((1 << n) - 1, 0)
动态规划 dp[s] 存状态 s 当前的余数。
(因为不管什么顺序放,最后剩下来的一个桶(余数)是一定的)
没塞过,且不爆的 -> 转换为可达状态。
class Solution:def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:all = sum(nums)if all % k:return Falseper = all // knums.sort()if nums[-1] > per:return Falsen = len(nums)dp = [-1] * (1 << n)dp[0] = 0for i in range(0, 1 << n):if dp[i] == -1:continuefor j in range(n):if dp[i] + nums[j] > per:breakif (i >> j & 1) == 0:next = i | (1 << j)if dp[next] == -1:dp[next] = (dp[i] + nums[j]) % perreturn dp[(1 << n) - 1] != -1
3117. 划分数组得到最小的值之和
目标划分为 每一段的& 和给出的第二个数组 对应相等。 数组的“值”定义为 数组最后一个元素。
dfs(i, j, _and) 现在在位置 i,正在划分第 j 段,第 j 段累积的&值为 _and。
当前 _and 与对应值相等则可以进行划分。
class Solution:def minimumValueSum(self, nums: List[int], andValues: List[int]) -> int:n, m = len(nums), len(andValues)@cachedef dfs(i: int, j: int, and_: int) -> int:if n - i < m - j: # 剩余元素不足return infif j == m: # 分了 m 段return 0 if i == n else infand_ &= nums[i]res = dfs(i + 1, j, and_) # 不划分if and_ == andValues[j]: # 划分,nums[i] 是这一段的最后一个数res = min(res, dfs(i + 1, j + 1, -1) + nums[i])return resans = dfs(0, 0, -1)return ans if ans < inf else -1
2963. 统计好分割方案的数目
所有相同数字必须出现在一段的分割。分割的方案数。
先看哪些位置是必须划分在一起的,如果 m 段,中间有 m-1 个位置是可拼接的,结果即为 2^(m-1)

出现过的数都要被包含,要到最右端 max_r。
先第一遍循环,使用覆盖,得到 r[x] 为 x 出现的最右边位置。
class Solution:def numberOfGoodPartitions(self, nums: List[int]) -> int:r = {}for i, x in enumerate(nums):r[x] = im = max_r = 0for i, x in enumerate(nums):max_r = max(max_r, r[x])if max_r == i: # 区间无法延长m += 1return pow(2, m - 1, 1_000_000_007)
2. 执行操作 思路题
3347. 执行操作后元素的最高频率 II
可以对 numOperations 个元素调整 [-k,k],最多可以多少相同的数。
nums[i] 可以变成 nums[i] - k ~ nums[i] + k 区间里的数,只需要知道哪个位置被最多的区间包含。
区间+1用差分实现,后一轮统计时 sum += diff
还有修改元素个数条件,用 cnt[x] 记录 x 本身有多少个。上界是 cnt[x] + numOperations。
所以为 min(sum_x,cnt[x] + numOperations)
由于这个值 sum_x 只受diff影响,后一项只受 cnt[x] 影响。
所以最后结果的最大值位置 只可能出现在 x,x-k,x+k+1。只需要记录、比较这些位置。
class Solution:def maxFrequency(self, nums: List[int], k: int, numOperations: int) -> int:cnt = defaultdict(int)diff = defaultdict(int)for x in nums:cnt[x] += 1diff[x] # 把 x 插入 diff,以保证下面能遍历到 xdiff[x - k] += 1 # 把 [x-k,x+k] 中的每个整数的出现次数都加一diff[x + k + 1] -= 1ans = sum_d = 0for x, d in sorted(diff.items()):sum_d += dans = max(ans, min(sum_d, cnt[x] + numOperations))return ans
3397. 执行操作后不同元素的最大数量
可以把每个数上下调整k,问最多可以有多少不同的数。
可以建模为军训站队,这个区域最多可以站多少人?最左边的同学会移动到最左边,方便右边的同学好站。
把数从小到大安排,每次安排到能放到的(x-k,x+k)区间,最左边的空位(前一个人pre 之后)。
class Solution:def maxDistinctElements(self, nums: List[int], k: int) -> int:nums.sort()ans = 0pre = -inf # 记录每个人左边的人的位置for x in nums:x = min(max(x - k, pre + 1), x + k)if x > pre:ans += 1pre = xreturn ans
2273. 移除字母异位词后的结果数组 -- 快慢指针
一个词如果和前一个词异位(同样的单词组成 只是顺序不同)就删掉。
快慢指针的思想:快指针遍历,慢指针把要保留的单词存下来。(实现原空间覆盖)
class Solution:def removeAnagrams(self, words: List[str]) -> List[str]:k = 1for s, t in pairwise(words): # 快指针遍历 pairwise取连续两个词if sorted(s) != sorted(t): # 慢指针存words[k] = tk += 1 del words[k:] # 删掉后面的return words
56. 合并区间
开始位置从前到后排序,和前一个区间不重叠就添加,重叠就通过max结束位置进行合并。
class Solution:def merge(self, intervals: List[List[int]]) -> List[List[int]]:intervals.sort(key=lambda x: x[0])ans = []for interval in intervals:# 不重叠 添加新区间if not ans or ans[-1][1] < interval[0]:ans.append(interval)# 与最后一个区间合并else:ans[-1][1] = max(ans[-1][1], interval[1])return ans
1553. 吃掉 N 个橘子的最少天数

只吃一个橘子,在 n 比较大的时候一定是比较劣的选择。
实际从 除以2和除以3 中选,吃一个只是为了吃掉对应的余数。
开记忆化 + 函数自我调用。
class Solution:@cachedef minDays(self, n: int) -> int:if n<=1:return nreturn min(n%2+1+self.minDays(n//2), n%3+1+self.minDays(n//3))
