[英雄星球六月集训LeetCode解题日报] 第八日 前缀和
一、 1838. 最高频元素的频数
链接: 1838. 最高频元素的频数
1. 题目描述
元素的 频数 是该元素在一个数组中出现的次数。
给你一个整数数组 nums 和一个整数 k 。在一步操作中,你可以选择 nums 的一个下标,并将该下标对应元素的值增加 1 。
执行最多 k 次操作后,返回数组中最高频元素的 最大可能频数 。
2. 思路分析
1.把每个数想象成柱状图,问题转化为,用最多k个格子,填一部分柱子,使这部分柱子等高,满足的话更新宽度。
2.排完序更好想。
3.那么实际上是求左上方三角形面积,用长方形-柱状图面积:s=r*(r-l+1)-sum_interval(l,r),需求s<=k,可以双指针。
4.按这个思路不用前缀和,直接排序+滑窗也可以。复杂度O(nlgn)。
3. 代码实现
class Solution:
def maxFrequency(self, nums: List[int], k: int) -> int:
nums.sort()
n = len(nums)
presum = list(accumulate(nums,initial=0))
def sum_interval(i,j):
return presum[j+1]-presum[i]
def calc_s(i,j):
return nums[j]*(j-i+1)-sum_interval(i,j)
ans = 1
l=r=0
while l<n and r<n:
s = calc_s(l,r)
if s <= k:
ans = max(ans,r-l+1)
r += 1
else:
l += 1
if l==r:
r += 1
return ans
"""
1.把每个数想象成柱状图,问题转化为,用最多k个格子,填一部分柱子,使这部分柱子等高,满足的话更新宽度。
2.排完序更好想。
3.那么实际上是求做上方三角形面积,用长方形-柱状图面积:s=r*(r-l+1)-sum_interval(l,r),需求s<=k,可以双指针。
4.按这个思路不用前缀和,直接排序+滑窗也可以。
"""
二、 1590. 使数组和能被 P 整除
1. 题目描述
给你一个正整数数组 nums,请你移除 最短 子数组(可以为 空),使得剩余元素的 和 能被 p 整除。 不允许 将整个数组都移除。
请你返回你需要移除的最短子数组的长度,如果无法满足题目要求,返回 -1 。
子数组 定义为原数组中连续的一组元素。
2. 思路分析
看题解才会做:
- 用哈希表维护每个前缀和模p结果的最近下标
- 那么每次都可以去查找一下当前模-mod对应的值是否存在,存在表示满足需求可以移除。
- 复杂度O(n)。
3. 代码实现
class Solution:
def minSubarray(self, nums: List[int], p: int) -> int:
n = len(nums)
total = sum(nums)
mod = total%p
if mod == 0:
return 0
elif n == 1:
return -1
# 如果某个子数组的和%p==mod,就可以移除。
# 计算前缀和的过程中,用字典记录前边每个和模p结果的最近下标,
# 如果当前模-mod存在,那么这个差就是这个区间的和,模==mod
ans = n # 不允许移除整个数组
presum = 0
pres = {0:-1}
for i in range(n):
presum += nums[i]
cur_mod = presum%p
need_mod = (cur_mod+p-mod)%p
# print(presum,cur_mod,need_mod)
need_idx = pres.get(need_mod,-2)
if need_idx != -2 :
ans = min(ans,i-need_idx)
if ans == 1:
return 1
pres[cur_mod] = i
if ans == n: # 不允许移除整个数组
return -1
return ans
三、 1589. 所有排列中的最大和
链接: 1589. 所有排列中的最大和
1. 题目描述
有一个整数数组 nums ,和一个查询数组 requests ,其中 requests[i] = [starti, endi] 。第 i 个查询求 nums[starti] + nums[starti + 1] + … + nums[endi - 1] + nums[endi] 的结果 ,starti 和 endi 数组索引都是 从 0 开始 的。
你可以任意排列 nums 中的数字,请你返回所有查询结果之和的最大值。
由于答案可能会很大,请你将它对 109 + 7 取余 后返回。
2. 思路分析
题意实际上是需要访问次数越大的位置,放个越大的值。
那么由于数据范围是10^5,我们可以用树状数组计算每个点的访问次数。
对访问次数和原数组都逆序排序,对应的位置放对应的数计算即可。
这个方法比较慢,前缀和的思路还没想出来,待补充。
复杂度O(nlgn)。
明白了前缀和了:
实际上树状数组的IUPQ模型是基于差分数组的:
- sum(l,r,v):对差分数组区间两端操作。
- get(index):相当于求差分数组截止index的前缀和。
树状数组是为了查询任意区间的,这题我们只需要前缀和,那么可以去掉树状数组的lgN。
直接展开差分数组做。
当然可以优化掉差分数组的那层空间,直接计算freq。
3. 代码实现
树状数组
class BinIndexTree:
def __init__(self, size):
self.size = size
self.bin_tree = [0 for _ in range(size+5)]
def add(self,i,v):
while i<=self.size :
self.bin_tree[i] += v
i += self.lowbit(i)
def update(self,i,v):
val = v - (self.sum(i)-self.sum(i-1))
self.add(i,val)
def sum(self,i):
s = 0
while i >= 1:
s += self.bin_tree[i]
i -= self.lowbit(i)
return s
def lowbit(self,x):
return x&-x
def _point_query(self,i):
return self.sum(i)
def _interval_add(self,l,r,v):
self.add(l,v)
self.add(r+1,-v)
class Solution:
def maxSumRangeQuery(self, nums: List[int], requests: List[List[int]]) -> int:
n = len(nums)
mod = 10**9+7
nums.sort(reverse=True)
tree = BinIndexTree(n+5)
for start,end in requests:
tree._interval_add(start+1,end+1,1)
freq = [0] * n
for i in range(n):
freq[i] = tree._point_query(i+1)
ans = 0
freq.sort(reverse=True)
for i in range(n):
if freq[i] ==0:
break
ans += freq[i]*nums[i]%mod
ans %= mod
return ans
差分数组
class Solution:
def maxSumRangeQuery(self, nums: List[int], requests: List[List[int]]) -> int:
n = len(nums)
mod = 10**9+7
nums.sort(reverse=True)
diff = [0]*(n+1)
for start,end in requests:
diff[start] += 1
diff[end+1] -= 1
freq = [0] * n
s = 0
for i in range(n):
s += diff[i]
freq[i] = s
ans = 0
freq.sort(reverse=True)
for i in range(n):
if freq[i] ==0:
break
ans += freq[i]*nums[i]%mod
ans %= mod
return ans
差分数组
省去空间开销
class Solution:
def maxSumRangeQuery(self, nums: List[int], requests: List[List[int]]) -> int:
n = len(nums)
mod = 10**9+7
nums.sort(reverse=True)
diff = [0]*(n+1)
for start,end in requests:
diff[start] += 1
diff[end+1] -= 1
freq = [0] * n
s = 0
for i in range(n):
s += diff[i]
freq[i] = s
ans = 0
freq.sort(reverse=True)
for i in range(n):
if freq[i] ==0:
break
ans += freq[i]*nums[i]%mod
ans %= mod
return ans
四、 1712. 将数组分成三个子数组的方案数
1. 题目描述
我们称一个分割整数数组的方案是 好的 ,当它满足:
数组被分成三个 非空 连续子数组,从左至右分别命名为 left , mid , right 。
left 中元素和小于等于 mid 中元素和,mid 中元素和小于等于 right 中元素和。
给你一个 非负 整数数组 nums ,请你返回 好的 分割 nums 方案数目。由于答案可能会很大,请你将结果对 109 + 7 取余后返回。
2. 思路分析
- 先计算前缀和使区间和消耗降为O(1)。
- 然后枚举中间数组的左端点,范围是[1,n-1]。
- 由于数据非负,我们发现左端点固定时,右端点向左移动会使mid变小、right变大,向右移动会使mid变大,right变小。
- 也就是说,左端点固定时,右端点是有个取值范围的[j_min,j_max],而这个范围可以二分。
- 二分效率较低O(nlgn),我们发现i,j_min,j_max一直是同向(右)移动的,因此可以三指针,复杂度降为O(n)。
3. 代码实现
二分
class Solution:
def waysToSplit(self, nums: List[int]) -> int:
n = len(nums)
mod = 10**9+7
presum = list(accumulate(nums,initial=0))
def sum_interval(i,j):
return presum[j+1]-presum[i]
ans = 0
# 这段4796 ms 5.09%,O(nlgn)
# 遍历mid左端点
for i in range(1,n-1):
left = sum_interval(0,i-1)
# 左端点固定,右端点向右移动时,mid不断变大,right不断变小
# 那么右端点最小值可以二分:这个区域的和要大于等于left
j_min = bisect_left(range(n-1),left,lo=i,key=lambda j:sum_interval(i,j))
# 如果找不到,j_min==n-1,则这个不能当左端点;如果以这个位置分割,right<mid,也不可以。
if j_min >= n-1 or sum_interval(j_min+1,n-1) < sum_interval(i,j_min):
continue
# 右端点最大值必须满足:right-mid>=0,即mid-right<=0;随着j增加mid-right增加;
# 由于上边排除了,因此一定能找到j_max
j_max = bisect_right(range(n-1),0,lo=j_min,key=lambda j:sum_interval(i,j)-sum_interval(j+1,n-1))-1
ans += j_max-j_min+1
ans %= mod
return ans
三指针
class Solution:
def waysToSplit(self, nums: List[int]) -> int:
n = len(nums)
mod = 10**9+7
presum = list(accumulate(nums,initial=0))
def sum_interval(i,j):
return presum[j+1]-presum[i]
ans = 0
# 随着i右移,j_min是右移的,j_max右移的,所以可以三指针寻找。O(n)
j_min,j_max = 1,1
for i in range(1,n-1):
left = sum_interval(0,i-1)
j_min = max(j_min,i)
while j_min<n-1 and sum_interval(i,j_min) < left:
j_min += 1
# print(i,j_min,j_max,ans,left,sum_interval(i,j_min),sum_interval(j_min+1,n-1))
if j_min >= n-1 :
return ans
if sum_interval(j_min+1,n-1) < sum_interval(i,j_min):
continue
while j_max+1<n-1 and sum_interval(i,j_max+1) <= sum_interval(j_max+2,n-1):
j_max += 1
ans += j_max-j_min+1
ans %= mod
return ans
五、 附每日一题 1037. 有效的回旋镖
链接: 1037. 有效的回旋镖
1. 题目描述
给定一个数组 points ,其中 points[i] = [xi, yi] 表示 X-Y 平面上的一个点,如果这些点构成一个 回旋镖 则返回 true 。
回旋镖 定义为一组三个点,这些点 各不相同 且 不在一条直线上 。
2. 思路分析
水题直接模拟。
- 数组长度是3。
- 每个点不同。
- 两个斜率不同。
3. 代码实现
模拟
class Solution:
def isBoomerang(self, points: List[List[int]]) -> bool:
n = len(points)
if n != 3:
return False
p1,p2,p3 = (points[0][0],points[0][1]),(points[1][0],points[1][1]),(points[2][0],points[2][1])
if p1 == p2 or p1 == p3 or p2==p3:
return False
y1 = p1[1]-p2[1]
y2 = p1[1]-p3[1]
x1 = p1[0]-p2[0]
x2 = p1[0]-p3[0]
if x1*y2==x2*y1:
return False
return True