一维前缀和
1. 定义
一维前缀和是一种用于优化区间求和问题的算法。通过将一个数组转换成前缀和数组,能在常数时间o(1)内快速计算任意区间的和。
2. 构造
1. 方法一 : 动态规划思想构造一维前缀和
def prefix_sum_method5(arr):
n = len(arr)
prefix_sum = [0] * (n + 1) # 初始化前缀和数组
for i in range(1, n + 1):
prefix_sum[i] = prefix_sum[i - 1] + arr[i - 1] # 动态规划计算
return prefix_sum
# 示例
arr = [1, 2, 3, 4, 5]
prefix_sum = prefix_sum_method5(arr)
print("前缀和数组:", prefix_sum)
2. 方法二 : 递归构造前缀和(了解,用不上)
def prefix_sum_method4(arr, n, prefix_sum=None):
if prefix_sum is None:
prefix_sum = [0] * (n + 1) # 初始化前缀和数组
if n == 0:
return prefix_sum
prefix_sum[n] = prefix_sum[n - 1] + arr[n - 1] # 递归计算前缀和
return prefix_sum_method4(arr, n - 1, prefix_sum)
# 示例
arr = [1, 2, 3, 4, 5]
prefix_sum = prefix_sum_method4(arr, len(arr))
print("前缀和数组:", prefix_sum)
3. 方法三 : 累加构造
def prefix_sum_method2(arr):
n = len(arr)
# 使用原数组来存储前缀和
for i in range(1, n):
arr[i] += arr[i - 1] # 在原数组上进行累加
return arr
# 示例
arr = [1, 2, 3, 4, 5]
prefix_sum = prefix_sum_method2(arr)
print("前缀和数组:", prefix_sum)
PS:它跟方法一的区别是不需要额外开一个数组(可以省内存),但是仅在原数组没有用的情况下用比较好。
4. 方法四 : 使用 itertools.accumulate
函数(Python 内建工具)
import itertools
def prefix_sum_method3(arr):
return [0] + list(itertools.accumulate(arr)) # 前缀和数组,前面加一个 0 作为初始化
# 示例
arr = [1, 2, 3, 4, 5]
prefix_sum = prefix_sum_method3(arr)
print("前缀和数组:", prefix_sum)
# 输出结果: 前缀和数组: [0, 1, 3, 6, 10, 15]
3. 例题
题目描述
给定 n 个正整数组成的数列 a1,a2,⋯ ,an 和 m 个区间 [li,ri],分别求这 m 个区间的区间和。
对于所有测试数据,n,m≤10**5,ai≤10**4
输入格式
第一行,为一个正整数 n 。
第二行,为 n 个正整数 a1,a2,⋯ ,an
第三行,为一个正整数 m 。
接下来 m 行,每行为两个正整数 li,ri 满足1≤li≤ri≤n
输出格式
共 m 行。
第 i 行为第 i 组答案的询问。
输入输出样例
输入
4 4 3 2 1 2 1 4 2 3输出
10 5说明/提示
样例解释:第 1 到第 4 个数加起来和为 10。第 2 个数到第 3 个数加起来和为 5。
对于 50% 的数据:n,m≤1000
对于 100% 的数据:1≤n,m≤10**5 1≤ai≤10**4
答案解析
# 读入数据 n = int(input()) # 数组长度 arr = list(map(int, input().split())) # 数组元素 m = int(input()) # 查询的次数 # 计算前缀和 prefix_sum = [0] * (n + 1) # 初始化前缀和数组,长度为n+1 for i in range(1, n + 1): prefix_sum[i] = prefix_sum[i - 1] + arr[i - 1] # 构建前缀和 # 处理查询 for _ in range(m): l, r = map(int, input().split()) # 读入每个查询区间 # 计算区间和 result = prefix_sum[r] - prefix_sum[l - 1] print(result)
一维差分
1. 定义
一维差分(或差分数组)是一种用于处理区间更新问题的算法。它通过构造一个辅助数组(差分数组)来高效地对原数组进行区间加法操作(或其他线性操作)。
举例(例子来源):
考虑数组 a=[1,3,3,5,10],对其中的相邻元素两两作差(右边减左边)
得到数组:[2,0,2,5]
然后在开头补上 a[0],得到差分数组:
d=[1,2,0,2,5]这有什么用呢?
如果从左到右累加 d中的元素,我们就「还原」回了原数组 a=[1,3,3,5,10]这又有什么用呢?
a′=[1,13,13,15,10]
现在把连续子数组 a[1],a[2],a[3] 都加上 10,得到:再次两两作差,并在开头补上 a′[0],得到差分数组:
d′=[1,12,0,2,-5]对比 d 和 d′,你会发现,对 a中连续子数组的操作,可以转变成对差分数组 d中两个数的操作。
这两个数分别是d和d‘的第二位和最后一位。
性质 (性质来源):
a为原数组,d为a的差分数组
性质 1:从左到右累加 d 中的元素,可以得到数组 a。
性质 2:如下两个操作是等价的。
- 区间操作:把 a 的子数组 a[i],a[i+1],…,a[j] 都加上 x。
- 单点操作:把 d[i] 增加 x,把 d[j+1]减少 x。
特别地,如果 j+1=n,则只需把 d[i]增加 x。(n 为数组 a 的长度)利用 性质 2,我们只需要 O(1) 的时间就可以完成数组 a 上的区间操作。最后利用 性质 1 从差分数组复原出数组 a。
2. 构造
接下来的构造,主要是对上面的实现。
# 给定的数组 a
a = [1, 3, 3, 5, 10]
# 1. 写出差分数组
def get_diff_array(a):
n = len(a)
diff = [a[0]] # 差分数组的第一个元素就是原数组的第一个元素
for i in range(1, n):
diff.append(a[i] - a[i - 1]) # 相邻元素两两作差
return diff
# 获取差分数组
diff = get_diff_array(a)
print("差分数组:", diff) # 差分数组: [1, 2, 0, 2, 5]
# 2. 实现区间操作
def range_update(diff, i, j, x):
"""
在差分数组 diff 上执行区间加法操作:[i, j] 区间加上 x
"""
diff[i] += x
if j + 1 < len(diff):
diff[j + 1] -= x
# 对区间 [1, 3] 加 5
range_update(diff, 1, 3, 5)
print("差分数组(经过区间操作后):", diff) # 差分数组(经过区间操作后): [1, 7, 0, 2, 5]
# 3. 实现单点操作
def point_update(diff, i, x):
"""
对差分数组 diff 中的第 i 个元素执行单点加法操作
"""
diff[i] += x
# 对单点操作,只需在 diff[i] 增加 x
# 对第 2 个元素(索引 1)加 3
point_update(diff, 1, 3)
print("差分数组(经过单点操作后):", diff) # 差分数组(经过单点操作后): [1, 10, 0, 2, 5]
# 最后,通过累加差分数组来恢复原数组
def recover_array(diff):
a_new = [diff[0]] # 数组的第一个元素
for i in range(1, len(diff)):
a_new.append(a_new[i - 1] + diff[i]) # 从差分数组恢复
return a_new
# 恢复数组
recovered_a = recover_array(diff)
print("恢复的数组:", recovered_a) # 恢复的数组: [1, 11, 11, 13, 18]
3. 例题
给你一个下标从 0 开始的二维整数数组 nums 表示汽车停放在数轴上的坐标。对于任意下标 i,nums[i] = [starti, endi] ,其中 starti 是第 i 辆车的起点,endi 是第 i 辆车的终点。
返回数轴上被车 任意部分 覆盖的整数点的数目。
示例 1:
输入:
3
3 6
1 5
4 7
输出:7
解释:从 1 到 7 的所有点都至少与一辆车相交,因此答案为 7 。
示例 2:输入:
2
1 3
5 8
输出:7
解释:1、2、3、5、6、7、8 共计 7 个点满足至少与一辆车相交,因此答案为 7 。
提示:
1 <= nums.length <= 100
nums[i].length == 2
1 <= starti <= endi <= 100答案解析
from itertools import accumulate # 导入 accumulate 函数,用于累积求和 # 输入车辆数 n n = int(input()) # 初始化一个空的列表 nums,用于存储每辆车的区间 nums = [] # 输入每辆车的起始位置和终止位置,并将其加入 nums 列表 for _ in range(n): a, b = map(int, input().split()) nums.append([a, b]) # 找出所有区间的最大终点,用于确定 diff 数组的大小 max_end = max(end for _, end in nums) # 初始化差分数组 diff,大小为 max_end + 2,因为差分数组需要包括最大终点+1的位置 # 加 2 是为了避免在处理 diff[end + 1] 时数组越界 diff = [0] * (max_end + 2) # 对每个车的区间进行差分处理 for start, end in nums: diff[start] += 1 # 该区间的起点位置增加 1,表示从此位置有车开始覆盖 diff[end + 1] -= 1 # 该区间的终点的下一位置减去 1,表示从此位置开始不再覆盖 # 通过 accumulate 函数累加差分数组,得到每个位置上有多少辆车覆盖 # 然后统计覆盖次数大于 0 的位置,即被车覆盖的点数 print(sum(s > 0 for s in accumulate(diff)))
二维前缀和
1. 定义
给我的感觉是一维前缀和只有x轴,二维前缀和拥有x,y轴,但是原理相通,所以对比起来学学。
一维前缀和:
对于一个一维数组 arr
,一维前缀和数组 prefix_sum
的定义是:prefix_sum[i]
表示 arr
中从第 0 个元素到第 i 个元素的和。
prefix_sum[i]=arr[0]+arr[1]+⋯+arr[i]
有了前缀和数组 prefix_sum
,我们可以快速计算任意区间和。例如,从 arr[l]
到 arr[r]
的和,只需要:
sum(l,r)=prefix_sum[r]−prefix_sum[l−1]
二维前缀和:
二维前缀和是基于类似的思路,它适用于二维数组或矩阵 matrix
。假设 matrix
是一个 n x m
的矩阵,二维前缀和数组 prefix_sum
的定义是:
即,prefix_sum[i][j]
表示矩阵中从 (0, 0)
到 (i, j)
的所有元素之和。
二维前缀和数组的作用是帮助快速计算任意一个子矩阵的和。例如,想要计算矩阵中从 (x1, y1)
到 (x2, y2)
的子矩阵和,可以通过二维前缀和来快速得到结果:
sum(x1,y1,x2,y2)=prefix_sum[x2][y2]−prefix_sum[x1−1][y2]−prefix_sum[x2][y1−1]+prefix_sum[x1−1][y1−1]
2. 构造
那么具体如何实现呢?在这里我们定义matrix是原二维数组或者矩阵。
matrix = [[1,2,3],
[4,5,6],
[7,8,9]]
m, n = len(matrix), len(matrix[0])
s = [[0] * (n + 1) for _ in range(m + 1)]
for i, row in enumerate(matrix):
for j, x in enumerate(row):
s[i + 1][j + 1] = s[i + 1][j] + s[i][j + 1] - s[i][j] + x
'''
enumerate 是 Python 中一个非常有用的内置函数,它用来遍历可迭代对象(如列表、元组、字符串等)时,既可以获取元素的值,又可以获取元素的索引(位置)。它返回的是一个枚举对象,这个对象包含了可迭代对象的索引和值。
'''
m,n分别是获取matrix 的行数和列数
创建s数组用来作为二维前缀和数组,一开始s是这个样子的:
0 0 0 0
0 0 0 0
0 0 0 0
0 0 0 0
为什么会比matrix多出一行和一列呢?目的是为了处理边界条件和简化计算过程,避免额外的边界检查,写起来更方便。
对于s[i + 1][j + 1] = s[i + 1][j] + s[i][j + 1] - s[i][j] + x,通过下面这两个图(这两个图并没有创造新的行和列)来理解为什么这么写。
s[i + 1][j + 1] = s[i + 1][j] + s[i][j + 1] - s[i][j] + x可以翻译成:绿框 + 红框 - 重复的框 + 当前位置的值。
你可能会不理解那两个循环怎么回事,建议动手画一画,初始数组在下面,自己手动画一下循环过程就明白了。
matrix:
1 2 3
4 5 6
7 8 9
s :
0 0 0 0
0 0 0 0
0 0 0 0
0 0 0 0
如果不创造新的行和列的话代码会怎么变呢?建议也思考一下。
我的感觉是,可能代码一时看会了,但是时间一久可能又忘了不会了,可以记下这种模板,会用就可以。
拓展:有了二维前缀和数组,我们又该如何计算任意子矩阵的元素和?与上面画框的计算过程类似。
3. 例题
题目描述
为了更好的备战 NOIP2013,电脑组的几个女孩子 LYQ,ZSC,ZHQ 认为,我们不光需要机房,我们还需要运动,于是就决定找校长申请一块电脑组的课余运动场地,听说她们都是电脑组的高手,校长没有马上答应他们,而是先给她们出了一道数学题,并且告诉她们:你们能获得的运动场地的面积就是你们能找到的这个最大的数字。
校长先给他们一个 n×n 矩阵。要求矩阵中最大加权矩形,即矩阵的每一个元素都有一权值,权值定义在整数集上。从中找一矩形,矩形大小无限制,是其中包含的所有元素的和最大 。矩阵的每个元素属于 [−127,127] ,例如
0 –2 –7 0 9 2 –6 2 -4 1 –4 1 -1 8 0 –2
在左下角:
9 2 -4 1 -1 8
和为 15。
几个女孩子有点犯难了,于是就找到了电脑组精打细算的 HZH,TZY 小朋友帮忙计算,但是遗憾的是他们的答案都不一样,涉及土地的事情我们可不能含糊,你能帮忙计算出校长所给的矩形中加权和最大的矩形吗?
输入格式
第一行:n,接下来是 n 行 n 列的矩阵。
输出格式
最大矩形(子矩阵)的和。
输入
4 0 -2 -7 0 9 2 -6 2 -4 1 -4 1 -1 8 0 -2输出
15答案解析
n = int(input()) matrix = [] for _ in range(n): matrix.append(list(map(int,input().split()))) s = [[0] * (n + 1) for _ in range(n + 1)] for i, row in enumerate(matrix): for j, x in enumerate(row): s[i + 1][j + 1] = s[i + 1][j] + s[i][j + 1] - s[i][j] + x mx = float('-inf') # 以下这四个循环是为了枚举所有子矩阵用的。 for x1 in range(1, n + 1): for y1 in range(1, n + 1): for x2 in range(1, n + 1): for y2 in range(1, n + 1): if x2 < x1 or y2 < y1: continue current_sum = s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1] mx = max(mx, current_sum) print(mx)
二维差分
1. 定义
二维差分数组是用来处理二维矩阵区域更新问题的一个技巧,类似于一维差分数组的原理。其基本思想是通过维护一个差分矩阵,减少每次区域更新的时间复杂度,从而提高效率。
2. 构造
由于跟之前很类似所以不再写构造了。
在这里给大家一个图片,图片来源
3. 例题
给你一个正整数
n
,表示最初有一个n x n
、下标从 0 开始的整数矩阵mat
,矩阵中填满了 0 。另给你一个二维整数数组
query
。针对每个查询query[i] = [row1i, col1i, row2i, col2i]
,请你执行下述操作:
- 找出 左上角 为
(row1i, col1i)
且 右下角 为(row2i, col2i)
的子矩阵,将子矩阵中的 每个元素 加1
。也就是给所有满足row1i <= x <= row2i
和col1i <= y <= col2i
的mat[x][y]
加1
。返回执行完所有操作后得到的矩阵
mat
。示例 1:
输入:n = 3, queries = [[1,1,2,2],[0,0,1,1]] 输出:[[1,1,0],[1,2,1],[0,1,1]] 解释:上图所展示的分别是:初始矩阵、执行完第一个操作后的矩阵、执行完第二个操作后的矩阵。 - 第一个操作:将左上角为 (1, 1) 且右下角为 (2, 2) 的子矩阵中的每个元素加 1 。 - 第二个操作:将左上角为 (0, 0) 且右下角为 (1, 1) 的子矩阵中的每个元素加 1 。示例 2:
输入:n = 2, queries = [[0,0,1,1]] 输出:[[1,1],[1,1]] 解释:上图所展示的分别是:初始矩阵、执行完第一个操作后的矩阵。 - 第一个操作:将矩阵中的每个元素加 1 。提示:
1 <= n <= 500
1 <= queries.length <= 104
0 <= row1i <= row2i < n
0 <= col1i <= col2i < n
# 二维差分 diff = [[0] * (n + 2) for _ in range(n + 2)] for r1, c1, r2, c2 in queries: diff[r1 + 1][c1 + 1] += 1 diff[r1 + 1][c2 + 2] -= 1 diff[r2 + 2][c1 + 1] -= 1 diff[r2 + 2][c2 + 2] += 1 # 计算 diff 的二维前缀和(原地修改) for i in range(1, n + 1): for j in range(1, n + 1): diff[i][j] += diff[i][j - 1] + diff[i - 1][j] - diff[i - 1][j - 1] # 保留中间 n*n 的部分,即为答案 diff = diff[1:-1] for i, row in enumerate(diff): diff[i] = row[1:-1] print(diff)
PS:给的 n 和queries 没有给出输入格式化。
特别感谢:灵茶山艾府