知乎-前缀和
前缀和与差分(超详细!!!)
一维前缀和 S(i) 即前 i 或 i+1 个元素的和,视情况而定是否需要让 S(0) = 0
Eg: [1,2,3,4,5]
如果有 m 次,求 [l,r] 这个区间元素的和:如果每次都遍历则时间复杂度为 O(mn)
若只一次计算出前缀和 [0,1,3,6,10,15],之后求 [2,3] 区间元素的和即:S(r+1)-S(l),求其他区间同理,此时时间复杂度为 O(m+n)
前缀和和前缀异或是一样的思路,同时需要结合异或的特点,如果两者相同那么异或的结果就是 0。参考题目:
1177. Can Make Palindrome from Substring
1310. XOR Queries of a Subarray
1371. Find the Longest Substring Containing Vowels in Even Counts
1442. Count Triplets That Can Form Two Arrays of Equal XOR
读入 n 个整数的数列 a1,a2,…,an 和正整数 k(1<=k<=n),请输出连续排列的 k 个整数的和的最大值
class Solution:
def maxSum0(self, nums: List[int], k: int) -> int:
"""
已知 kadane 算法适用于 k 任意的场景,求和最大的子序列,时间复杂度为 O(N)
arr = [nums[0]] * length
for i in range(1,length):
arr[i] = max(arr[i]+nums[i], nums[i])
return max(arr)
我们还是先用暴力法来看下这个问题:此时时间复杂度为 O(N^2),空间复杂度为 O(1)
:param nums:
:param k:
:return:
"""
length = len(nums)
if k == length:
return sum(nums)
res = float('-inf')
for i in range(length - k + 1):
tmp = nums[i]
for j in range(i + 1, i + k):
tmp += nums[j]
res = max(res, tmp)
return res
def maxSum(self, nums: List[int], k: int) -> int:
"""
我们发现在计算的过程中其实重复了很多次求和操作,因此我们可以尝试使用前缀和来解决这个问题
时间复杂度为: O(N)
空间复杂度为: O(N)
:param nums:
:param k:
:return:
"""
summary = [0]
length = len(nums)
for i in range(length):
summary.append(summary[-1] + nums[i])
# print(summary)
res = float('-inf')
for i in range(k, length+1):
res = max(res, summary[i] - summary[i - k])
return res
一维差分 D(i) 即 i 和 i-1 元素的差值
Eg: [1,2,3,4,5],其差分数组即为 [1,1,1,1,1] 对 [2,3] 区间的元素增加一个值 3,原数组变成了
[1,2,6,7,5] 差分数组变成了 [1,1,4,3,-2] 即只有 l 和 r+1 发生了变化,如果 r+1 大于等于 length 则它不改动,即:
D[l] = D[l]+3
D[r+1] = D[r+1]-3
因此如果原数组均为 0,对原数组修改了 m 次,修改内容是对 [l,r] 区间的元素加某个值或减某个值,有 q 次,求任意个元素的值是多少
暴力解法是对原数组修改 m 次,每次修改 [l,r] 的内容,时间复杂度为 O(mn)
如果利用差分数组,m 次只改动 D[l] D[r+1] 然后拼出原数组即可,时间复杂度为 O(m+n)
for i in range(m):
D[l] += add_val
D[r+1] -= add_val
arr = [0] * n
arr[0] = D[0]
for i in range(1,n):
arr[i] = arr[i-1]+D[i]
铁路经过 N 个城市,每个城市都有一个站。不过,由于各个城市之间不能协调好,于是乘车每经过两个相邻的城市之间(方向不限),必须单独购买这一小段的车票。第 i 段铁路连接了城市 i 和城市i+1(1<=i
如果搭乘的比较远,需要购买多张车票。第i段铁路购买纸质单程票需要 Ai 博艾元。
虽然一些事情没有协调好,各段铁路公司也为了方便乘客,推出了 IC 卡。对于第i段铁路,需要花 Ci 博艾元的工本费购买一张 IC 卡,然后乘坐这段铁路一次就只要扣 Bi(BiUim 现在需要出差,要去 M 个城市,从城市 P1 出发分别按照 P1,P2,P3…PM 的顺序访问各个城市,可能会多次访问一个城市,
且相邻访问的城市位置不一定相邻,而且不会是同一个城市。
现在他希望知道,出差结束后,至少会花掉多少的钱,包括购买纸质车票、买卡和充值的总费用。
class Solution:
def undersea(self, A: List[int], B: List[int], C: List[int], N: int, M: int, P: List[int]) -> int:
"""
:param M: 标识要去的城市个数
:param P: 标识依次要去的城市标记 [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
:param N: 标识城市的总个数 9
:param A: 标识第 i 段铁路单程票价 [200, 300, 500, 345, 100, 600, 450, 2]
:param B: 标识使用 IC 卡后第 i 段铁路的票价 [100, 299, 200, 234, 50, 100, 400, 1]
:param C: 标识购买第 i 段铁路 IC 卡的工本费 [50, 100, 500, 123, 100, 1, 80, 10]
求最少要花多少钱
题目主体思路:计算每段路程要路过的次数,比较每段路程用 IC 卡便宜还是直接买票便宜
时间复杂度:O(N)
空间复杂度:O(N)
:return:
"""
num = [0] * (N + 1)
# 3 2 1
# 1 2 3 4
# 4 3 2 1
# 1 2 3 4 5
# 5 6 7 8 9
# 9 8 7 6 5 4 3 2
# 2 3 4 5 6
# 6 5
# 5 4 3
# 我们需要耗费 O(M*N) 的时间复杂度来获取到每段路程经过的次数,但我们在这个循环中可以发现我们其实一直在给 [start,end-1] 区间加一个值
# 已知差分数组的特点是,仅 l 改变+add_val 和 r+1 改变-add_val 所以我们可以使用差分数组
# for i in range(M - 1):
# start = min(P[i], P[i + 1])
# end = max(P[i], P[i + 1])
# for j in range(start, end):
# num[j] += 1
for i in range(M - 1):
start = min(P[i], P[i + 1])
end = max(P[i], P[i + 1])
num[start] += 1
num[end] -= 1
print("====>D", num)
# 差分数组的前缀和即为每段路走过的次数
for i in range(1, N):
num[i] = num[i] + num[i - 1]
res = 0
# # [0, 4, 6, 6, 4, 4, 2, 2, 2, -2]
print("====>S", num)
for i in range(1, N):
# 买票的钱:A[i-1]*num[i]
# 用 IC 卡 的钱: B[i-1]*num[i]+C[i-1]
res += min(A[i - 1] * num[i], B[i - 1] * num[i] + C[i - 1])
return res
题解:github repo
题解:github repo
题解:github repo
主要应用于求二维矩阵和,给定矩阵 arr[i][j]
, S 为矩阵和, S[i][j]
标识从 arr[0][0]
到arr[i][j]
组成的矩阵的和,其中 S[0][0]=0
S [ i ] [ j ] = S [ i − 1 ] [ j ] + S [ i ] [ j − 1 ] − S [ i − 1 ] [ j − 1 ] + a r r [ i ] [ j ] S[i][j] = S[i-1][j]+S[i][j-1]-S[i-1][j-1]+arr[i][j] S[i][j]=S[i−1][j]+S[i][j−1]−S[i−1][j−1]+arr[i][j]
if i == 0:
for j in range(1, width):
S[0][j] = S[0][j-1] + arr[0][j]
if j == 0:
for i in range(1, height):
S[i][0] = S[i-1][0] + arr[i][0]
Eg: 给定一个 n*m 大小的矩阵 a,有 q 次询问,每次询问给定 x1,y1,x2,y2 四个数,求以 (x1,y1) 为左上角坐标和 (x2,y2) 为右下角坐标的子矩阵的所有元素和。注意仍然包含左上角和右下角的元素。
暴力解法:每次都遍历则时间复杂度为 O(mnq)
若只一次计算出二维前缀和,之后求以 (x1,y1) 为左上角坐标和 (x2,y2) 为右下角区间元素的和即:
S = S [ x 2 ] [ y 2 ] − S [ x 2 ] [ y 1 − 1 ] − S [ x 1 − 1 ] [ y 2 ] + S [ x 1 − 1 ] [ y 1 − 1 ] S = S[x2][y2]-S[x2][y1-1]-S[x1-1][y2]+S[x1-1][y1-1] S=S[x2][y2]−S[x2][y1−1]−S[x1−1][y2]+S[x1−1][y1−1]
S(r+1)-S(l),求其他区间同理,此时时间复杂度为 O(mn+q)
# 已知二维数组
# 0 1 2 3
# 0 7 4 6 5
# 1 1 3 7 4
# 2 6 5 1 3
class Solution:
def summary(self, nums: List[List[int]], start: List[int], end: List[int]):
height = len(nums)
width = len(nums[0])
S = [[0] * (width + 1) for _ in range(height + 1)]
for i in range(height):
for j in range(width):
S[i+1][j+1] = S[i+1][j] + S[i][j+1] - S[i][j] + nums[i][j]
print('====>', S)
res = S[end[0]+1][end[1]+1] - S[end[0]+1][start[1]] - S[start[0]][end[1]+1] + S[start[0]][start[1]]
return res
注:二维数组如何拍平成一维数组求前缀和,我们可以用三层循环 m*m*n
,利用 m*m
确定矩形的上下边界,利用 n 遍历获取前缀和。(其中,m == len(nums), n==len(nums[0])
)这种方法看起来舍近求远,但在特殊场景下可以发挥作用,参考 363. Max Sum of Rectangle No Larger Than K
leetcode-最大子矩阵
二维差分数组的前缀和数组就是原数组本身,求二维差分数组需要注意,如果是第一行或者第一列则和一维差分一样,如果是其他列则可以使用 nums[i][j]+nums[i-1][j-1]-nums[i-1][j]-nums[i][j-1]
class Solution:
def difference(self, nums: List[List[int]]) -> List[List[int]]:
"""
求二维差分数组,如果是第一行或者第一列则和一维差分一样,如果是其他列则可以使用
nums[i][j]+nums[i-1][j-1]-nums[i-1][j]-nums[i][j-1]
:param nums:
:return:
"""
height = len(nums)
width = len(nums[0])
D = [[0] * width for _ in range(height)]
D[0][0] = nums[0][0]
for i in range(height):
for j in range(width):
if i == 0 and j == 0:
continue
if i == 0:
D[0][j] = nums[0][j] - nums[0][j-1]
elif j == 0:
D[i][0] = nums[i][0] - nums[i-1][0]
else:
D[i][j] = -nums[i][j - 1] - nums[i - 1][j] + nums[i][j] + nums[i - 1][j - 1]
return D
当给二维数组的某一区域都添加一个值后,二维差分数组是怎么变化的呢:
假如修改的是 [x1,y1] [x2,y2] 为左上角和右下角的数据,都加了 target,那么此时:
D[x1][y1] += target
D[x1][y2+1] -=target
D[x2+1][y1] -= target
D[x2+1][y2+1] += target
如果 x2+1 或 y2+1 超过边界那就不需要更改
class Solution:
def difference_plus(self, nums: List[List[int]], start: List[int], end: List[int], target: int) -> List[List[int]]:
height = len(nums)
width = len(nums[0])
D = [[0] * width for _ in range(height)]
D[0][0] = nums[0][0]
for i in range(height):
for j in range(width):
if i == 0 and j == 0:
continue
if i == 0:
D[0][j] = nums[0][j] - nums[0][j - 1]
elif j == 0:
D[i][0] = nums[i][0] - nums[i - 1][0]
else:
D[i][j] = -nums[i][j - 1] - nums[i - 1][j] + nums[i][j] + nums[i - 1][j - 1]
x1 = start[0]
y1 = start[1]
x2 = end[0]
x3 = x2 + 1
y2 = end[1]
y3 = y2 + 1
D[x1][y1] += target
if y3 < width:
D[x1][y3] -= target
if x3 < height:
D[x3][y1] -= target
if y3 < width and x3 < height:
D[x3][y3] += target
return D
前缀和通常被用于一些复杂问题的中间步骤,比如
首先审查题目是否符合上述条件,此时列出前缀和或差分数组然后进行运算。
时间和空间复杂度视具体题目确定。
这道题既考察对题目的理解也考察了对前缀和以及二分查找的应用,来实际操作一下吧~
github repo
这道题的考察范围很广,既包括分治,又包括前缀和还有双指针的应用
解题过程:已知题目和数组中连续元素的和有关,所以首先求前缀和,但不完全满足需求,因为要求前缀和的不同差值。
如果使用嵌套循环,时间复杂度较高,为了降低时间复杂度我们可以考虑两个有序数组求差值,参考:详解双指针法
此时剩下的问题是如何让数组有序,则使用归并排序即可。
github repo
github repo
- 连续元素的和一定用前缀和
- 想了各种优化方法都不行,那就是得考虑数学规律
github repo
github repo
前缀和和 dp 的完美结合。
解题思路:
以 [1,2,3,4,5,6,7] 为例,分成最多 4 部分
1 [2,3,4,5,6,7] 分成最多 3 部分
1,2 [3,4,5,6,7] 分成最多 3 部分
1,2,3 [4,5,6,7] 分成最多 3 部分
1,2,3,4 [5 6 7] 分成最多 3 部分
1,2,3,4,5 [6,7] 分成最多 3 不分已经没有意义,因为将 [6,7]分成最多 2 份已经包含在其他答案中,且 1 2 3 4 5 分成一份肯定是最小的,所以这里没有意义,即当剩下的元素小于 k-1 时不必再比较。
发现问题的最优解是由子问题的最优解组成,符合最优子结构性质,状态转移方程 F(0,N,k) = max(F(0,0,1)+F(1,N, k-1),…F(0,N+1-k,1)+F(N+2-k,N,k-1))
边界条件:F(x,y,1) = sum(nums[x:y])/(y-x+1))
以 [9, 1, 2, 3, 9] 为例,我们发现上述的解法重复计算了 k 为 1 的很多结果
比如将已知 k 为 1 的情况即为 △
res = max(△+F(1,4,2), △+F(2,4,2), △+F(3,4,2))
F(1,4,2)=max(△+F(2,4,1), △+F(3,4,1), △+F(4,4,1))
F(2,4,2)=max(△+F(3,4,1), △+F(4,4,1))
F(3,4,2)=max(△+F(4,4,1))
再来看一下为什么没写 △+F(4,4,2) 即 F(0,3,1)+F(4,4,2)
因为在其他情况中已经包含了:
F(0,0,1)+F(1,3,1)+F(4,4,1)
F(0,1,1)+F(2,3,1)+F(4,4,1)
(a+b+c+d)/4 势必比 a+(b+c+d)/3 (a+b)/2+(c+d)/2 要小,所以可以不予计算
所以我们用一个三维数组来存储问题的答案。
时间复杂度:O(kN^2)
空间复杂度:O(kN^2)
class Solution:
def largestSumOfAverages(self, nums: List[int], k: int) -> float:
length = len(nums)
summary = [0]
for i in range(length):
summary.append(summary[-1] + nums[i])
dp = [[[0] * (k + 1) for _ in range(length)] for _ in range(length)]
for z in range(1, k + 1):
for x in range(length - 1, -1, -1):
if length-x < z:
continue
if z == 1:
dp[x][length - 1][1] = (summary[length] - summary[x]) / (length - x)
else:
tmp = float('-inf')
for y in range(x + 1, length):
dp[x][y-1][1] = (summary[y] - summary[x]) / (y-x)
if length - y < z - 1:
break
else:
tmp = max(tmp, dp[x][y - 1][1] + dp[y][length - 1][z - 1])
dp[x][length - 1][z] = tmp
return dp[0][length - 1][k]
一位数组求最靠近 k 的和
class Solution:
def maxSum(self, nums: List[int], k: int) -> int:
"""
一维数组求最靠近 k 的和,时间复杂度 O(NlogN)
空间复杂度:O(N)
:param nums:
:param k:
:return:
"""
from sortedcontainers import SortedList
summary = 0
sl = SortedList()
sl.add(0)
res = float('-inf')
for i in range(len(nums)):
tmp = summary + nums[i]
difference = tmp - k
idx = sl.bisect_right(difference)
if idx - 1 >= 0 and sl[idx-1] == difference:
return k
if idx < len(sl):
res = max(res, tmp - sl[idx])
sl.add(tmp)
summary = tmp
return res
且看二维
class Solution:
def maxSumSubmatrix(self, matrix: List[List[int]], k: int) -> int:
"""
将二维数组如何降维成一维:可以把二维数组「压缩」即让纵坐标的值相加,然后左右的前缀和相减即得到某个矩形的大小
时间复杂度:O((m^2)nlogn)
空间复杂度:O(n)
比暴力法 m^2n^2 有所优化
:param matrix:
:param k:
:return:
"""
from sortedcontainers import SortedList
m = len(matrix)
n = len(matrix[0])
res = float('-inf')
for start in range(m):
x_arr = [0] * n
for end in range(start, m):
x_summary = 0
sl = SortedList()
sl.add(0)
for i in range(n):
# for j in range(start, end + 1):
# x_arr[i] += matrix[j][i]
x_arr[i] += matrix[end][i]
tmp = x_summary + x_arr[i]
diff = tmp - k
idx = sl.bisect_right(diff)
# print('====>', idx)
if idx - 1 >= 0 and sl[idx - 1] == diff:
return k
if idx < len(sl):
res = max(res, tmp - sl[idx])
sl.add(tmp)
x_summary = tmp
# print(x_arr,x_summary)
return res