前缀和

  • 简介:前缀和算法是一种常见的数组处理算法,通过预处理出数组的前缀和(Prefix Sum),可以在$O(1)$时间内快速求出任意区间的和。
  • 计算方式:对于一个长度为n的数组a,其前缀和数组prefix_sum的第i个元素表示a数组中前i-1个元素的和,计算公式为prefix_sum[i+1] = prefix_sum[i] + a[i]。初始化prefix_sum=[0]*(len(a)+1)
  • 使用前缀和算法求解区间和:假设要求区间[l,r]的和,可以通过计算prefix_sum[r+1]-prefix_sum[l]来得到区间和。这是因为prefix_sum[r+1]表示a数组前r个元素的和,prefix_sum[l]`表示a数组前l-1个元素的和,两者相减即可得到区间[l,r]的和。
  • 应用场景:处理数组序列问题,如求解子数组的和、平均数、中位数等。也可以用于处理二维矩阵中的子矩阵问题。
  • 时间复杂度:预处理时间复杂度为$O(n)$,求解区间和的时间复杂度为$O(1)$,因此总的时间复杂度为$O(n)$。

算法框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 判断数组内是否有等于k的连续子数组,或者有几个
def prefix_search(self, k: int, nums: List[int]) :
# 计算前缀和数组,有时可以边遍历边计算,不用单独先计算前缀和
pre_sum = [0] * (len(nums) + 1)
for i in range(len(nums)):
pre_sum[i+1] = pre_sum[i] + nums[i]
for i in range(len(nums) + 1):
# 记录子前缀和信息
dict[pre_sum[i]] += 1
# 边遍历边记录前缀和
# pre_sum += nums[i]
# 转换问题pre_sum[i] - pre_sum[j] = k
target = pre_sum[i] - k
# 判断target是否满足什么条件,返回结果(通常通过字典查询)
if target in dict:
ans...

二维前缀和

1
2
3
4
5
6
7
8
9
10
11
12
13
class MatrixSum:
# s[i + 1][j + 1]表示左上角为a[0][0]右下角为a[i][j]的子矩阵元素和
def __init__(self, matrix):
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
self.s = s

# 返回左上角在 (r1,c1) 右下角在 (r2-1,c2-1) 的子矩阵元素和(类似前缀和的左闭右开)
def query(self, r1, c1, r2, c2):
return self.s[r2][c2] - self.s[r2][c1] - self.s[r1][c2] + self.s[r1][c1]

【图解】二维前缀和

力扣指南

前缀和
题目 技巧 难度
✅209. 长度最小的子数组 前缀和+二分bisect 🌟🌟
✅238. 除自身以外数组的乘积 分别计算前后缀乘积 🌟🌟
✅303. 区域和检索 - 数组不可变 一维前缀和 🌟
✅304. 二维区域和检索 - 矩阵不可变 二维前缀和 🌟🌟
❌363. 矩形区域不超过 K 的最大数值和 二分+有序集合 🌟🌟🌟
✅523. 连续的子数组和 sum[i]-sum[j]=n*k等效于sum[i]和sum[j]对k的余数相等,用哈希记录 🌟🌟
✅525. 连续数组 转换问题:求最长的连续子数组,其元素和为0 🌟🌟
✅560. 和为 K 的子数组 sum[i]-sum[j]=k转化成sum[j]=sum[i]-k,用哈希记录 🌟🌟
✅724. 寻找数组的中心下标 & 1991 🌟
✅930. 和相同的二元子数组 同974 🌟🌟
✅974. 和可被 K 整除的子数组 同523,需要初始化{0:1}来记录前缀总和的情况 🌟🌟
✅1177. 构建回文串检测 异或前缀和,计算奇偶数量 🌟🌟

前缀树

前缀树,也称为字典树(Trie)或键树,是一种用于有效存储和检索字符串集合的树形数据结构。前缀树的主要特点是将共同的前缀存储在树的相同深度上,从根节点到每个叶子节点都表示一个字符串。这使得前缀树非常适合用于字符串匹配、前缀匹配和字典查询等应用。

  • 基本思想:利用共享的前缀信息,将字符按照从根节点到叶子节点的路径组织起来。根节点表示空字符串,每个非根节点表示一个字符。从根节点到叶子节点的路径上,经过的字符连接起来就是一个完整的字符串
  • TrieNode节点本身只存储val字段,并没有一个字段来存储字符,字符是通过子节点在父节点的children数组中的索引确定的。
    形象理解就是,Trie 树用「树枝」存储字符串(键),用「节点」存储字符串(键)对应的数据(值)。

算法框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class Trie:
def __init__(self):
self.children = [None] * 26
self.isEnd = False

# 寻找前缀节点
def searchPrefix(self, prefix) -> Trie:
node = self
for ch in prefix:
ch = ord(ch) - ord('a')
if not node.children[ch]:
return None
node = node.children[ch]
return node

# 插入一个单词
def insert(self, word: str) -> None:
node = self
for ch in word:
ch = ord(ch) - ord('a')
if not node.children[ch]:
node.children[ch] = Trie()
node = node.children[ch]
node.isEnd = True

# 删除一个单词
def remove(self, word) -> None:


# 寻找指定word在前缀树中的前缀字符串
def searchWordPrefix(self, word) -> str:
node = self
prefix = ''
for ch in word:
idx = ord(ch) - ord('a')
if node.isEnd == True:
return prefix
if node.children[idx] is None:
return None
prefix += ch
node = node.children[idx]

# 判断前缀树中是否存在某个单词
def search(self, word: str) -> bool:
node = self.searchPrefix(word)
return node is not None and node.isEnd

# 判断前缀树中是否存在某个单词 .表示通配符
def search(self, word: str) -> bool:
def dfs(i, node):
if i == len(word):
return node.isEnd
if word[i] != '.':
ch = ord(word[i]) - ord('a')
if not node.children[ch]:
return False
return dfs(i + 1, node.children[ch])
else:
for child in node.children:
if child is not None and dfs(i + 1, child):
return True
return False
return dfs(0, self.trie)

# 判断前缀树中是否有某个前缀
def startsWith(self, prefix: str) -> bool:
return self.searchPrefix(prefix) is not None

力扣指南

前缀树
题目 技巧 难度
✅208. 实现 Trie (前缀树) TrieSet 🌟🌟
✅648. 单词替换 寻找前缀 🌟🌟
✅211. 添加与搜索单词 - 数据结构设计 带通配符的单词寻找 🌟🌟
✅677. 键值映射 TrieMap 🌟🌟
✅212. 单词搜索 II 前缀树+dfs回溯 🌟🌟🌟

差分数组

差分数组(Difference Array)是一种常用的数据结构和算法,用于高效地处理数组区间的修改操作。差分数组可以将原始数组的部分元素的差值存储在新的数组中,从而实现在常数时间内对原数组区间进行修改和查询。知识点:

  • 原理:通过记录相邻元素之间的差值,将原始数组的修改操作转化为对差分数组的修改操作。通过修改差分数组,可以实现在常数时间内对原数组的区间进行增减操作。差分数组的元素值表示原始数组对应位置的增减值。
  • 构建:遍历原始数组,计算相邻元素之间的差值,并将差值存储在差分数组中。时间复杂度为O(N),其中N是原始数组的长度。
  • 区间修改和查询:对原数组的区间[l, r]进行增加k的操作,只需要在差分数组中修改差分数组的[l][r+1]两个位置分别增加k和减去kdiff[l]+ k; diff[r+1]-=k。对原数组的区间进行查询,只需要查询差分数组的前缀和即可得到原数组的区间和。
  • 应用:频繁对原始数组的某个区间的元素进行增减。例如区间加减操作、区间查询等。常见的应用包括求解数组区间的前缀和、动态区间修改等。
  • 注意:使用差分数组时,需要注意边界条件和数组越界问题。在进行查询操作时,需要注意计算差分数组的前缀和时的下标范围。

算法框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 差分数组工具类
class Difference:
# 差分数组
def __init__(self, nums: List[int]):
assert len(nums) > 0
self.diff = [0] * len(nums)
# 根据初始数组构造差分数组
self.diff[0] = nums[0]
for i in range(1, len(nums)):
self.diff[i] = nums[i] - nums[i - 1]

# 给闭区间 [i, j] 增加 val(可以是负数)
def increment(self, i: int, j: int, val: int) -> None:
self.diff[i] += val
if j + 1 < len(self.diff):
self.diff[j + 1] -= val

# 返回结果数组
def result(self) -> List[int]:
res = [0] * len(self.diff)
# 根据差分数组构造结果数组
res[0] = self.diff[0]
for i in range(1, len(self.diff)):
res[i] = res[i - 1] + self.diff[i]
return res

力扣指南

差分数组
题目 技巧 难度
✅1109. 航班预订统计 🌟🌟
✅1094. 拼车 🌟🌟