今日题单概览
| 题目 | 核心技巧 | 难度 | 收获等级 |
|---|---|---|---|
| 幂次区间和查询 | 多维度前缀和预处理 | ⭐⭐⭐ | 💡💡💡 |
| 小郑的蓝桥平衡串 | 前缀和 + 哈希表 | ⭐⭐⭐ | 💡💡💡💡 |
| 统计子矩阵 | 二维前缀和 + 枚举优化 | ⭐⭐⭐⭐ | 💡💡💡💡💡 |
一、幂次区间和查询:前缀和的"多维"思维
题目回顾
给定数组 aaa 和 mmm 个查询,每个查询问 [l,r][l,r][l,r] 区间内所有元素的 kkk 次方和(k≤5k \leq 5k≤5),对 109+710^9+7109+7 取模。
数据规模 :n,m≤105n, m \leq 10^5n,m≤105,要求高效处理。
核心洞察:kkk 的范围很小!
kkk 只有 1~5 五种可能。这意味着我们可以预处理 5 个前缀和数组 ,查询时直接 O(1)O(1)O(1) 回答。
python
import sys
from itertools import accumulate
input = sys.stdin.readline
n, m = map(int, input().split())
a = list(map(int, input().split()))
mod = 10**9 + 7
def get_prefix_sum(arr):
"""构建前缀和数组,带取模防溢出"""
prefix = [0]
for x in arr:
prefix.append((prefix[-1] + x) % mod)
return prefix
def range_sum(prefix, l, r):
"""查询[l,r]区间和,0-indexed"""
return (prefix[r + 1] - prefix[l] + mod) % mod
# 预处理 k=1,2,3,4,5 的前缀和
prefix_powers = []
for k in range(1, 6):
powered = [pow(x, k, mod) for x in a] # 先取模防溢出!
prefix_powers.append(get_prefix_sum(powered))
# 处理查询
for _ in range(m):
l, r, k = map(int, input().split())
# 转为 0-indexed
ans = range_sum(prefix_powers[k - 1], l - 1, r - 1)
print(ans)
复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 预处理 | O(5⋅n)O(5 \cdot n)O(5⋅n) | 5次遍历,每次O(n) |
| 单次查询 | O(1)O(1)O(1) | 直接数组访问 |
| 总查询 | O(m)O(m)O(m) | m次O(1) |
| 空间 | O(5⋅n)O(5 \cdot n)O(5⋅n) | 5个前缀和数组 |
💡 关键细节 :
pow(x, k, mod)比x**k % mod更高效且不会溢出,这是 Python 的模幂优化。
思维跃迁:如果 kkk 很大怎么办?
如果 k≤109k \leq 10^9k≤109,预处理就不现实了。这时需要数学武器:
- 费马小定理 :若 ppp 是质数,ap−1≡1(modp)a^{p-1} \equiv 1 \pmod{p}ap−1≡1(modp),可降幂
- 欧拉定理:更一般的降幂公式
- 线段树 + 快速幂:单点修改+区间查询时用
但今天这道题告诉我们:看清数据范围的约束,往往比盲目上高级数据结构更重要。
二、小郑的蓝桥平衡串:前缀和 + 哈希表的黄金组合
题目回顾
只含 L 和 Q 的字符串,找最长的平衡子串(L 和 Q 数量相等)。
样例 :LQLL → 最长平衡串是 LQ,长度为 2。
暴力解法的问题
python
# O(n^2) 暴力------对于 len≤1000 可过,但不够优雅
for i in range(n):
for j in range(i, n):
if s[i:j+1].count('L') == s[i:j+1].count('Q'):
ans = max(ans, j - i + 1)
优雅解法:前缀和 + 哈希映射
核心转化 :把 L 看作 +1,Q 看作 -1。问题转化为:找最长的子数组,使其和为 0。
python
from itertools import accumulate
s = input()
n = len(s)
# 转化:L -> +1, Q -> -1
diff = [1 if c == 'L' else -1 for c in s]
# 前缀和
prefix = [0] + list(accumulate(diff))
# 关键:如果 prefix[j+1] == prefix[i],说明 diff[i:j+1] 的和为0
# 即 s[i:j+1] 中 L 和 Q 数量相等!
ans = 0
# 用哈希表记录每个前缀和值第一次出现的位置
first_occurrence = {0: 0} # prefix[0] = 0 出现在索引0
for i in range(1, n + 1):
if prefix[i] in first_occurrence:
# 找到了一个平衡子串!
length = i - first_occurrence[prefix[i]]
ans = max(ans, length)
else:
# 只记录第一次出现的位置,保证子串最长
first_occurrence[prefix[i]] = i
print(ans)
算法可视化
字符串: L Q L L
diff: 1 -1 1 1
prefix: 0 1 0 1 2
↑ ↑
位置0 位置2
prefix[0] == prefix[2] == 0
说明 diff[0:2] = [1, -1] 和为0,即 "LQ" 是平衡串,长度2
为什么哈希表要记录"第一次出现"?
因为我们要最长的 子串。如果同一个前缀和值在位置 iii 和 jjj(i<ji < ji<j)都出现,那么 [i,j−1][i, j-1][i,j−1] 就是一个平衡子串。记录第一次出现,就能保证 j−ij - ij−i 最大。
复杂度对比
| 解法 | 时间 | 空间 | 适用场景 |
|---|---|---|---|
| 暴力枚举 | O(n2)O(n^2)O(n2) | O(1)O(1)O(1) | n≤103n \leq 10^3n≤103 |
| 前缀和+哈希 | O(n)O(n)O(n) | O(n)O(n)O(n) | n≤106n \leq 10^6n≤106 |
| 前缀和+暴力找 | O(n2)O(n^2)O(n2) | O(n)O(n)O(n) | 不推荐 |
💡 模式识别 :"找和为0的最长子数组"是经典题型,前缀和+哈希表是标准解法。类似的还有"和为K的子数组个数"等。
三、统计子矩阵:二维前缀和的降维打击
题目回顾
给定 N×MN \times MN×M 矩阵,统计有多少个子矩阵的元素和 ≤K\leq K≤K。
数据规模 :N,M≤500N, M \leq 500N,M≤500,K≤2.5×108K \leq 2.5 \times 10^8K≤2.5×108。
核心武器:二维前缀和
首先复习二维前缀和的构建:
python
n, m, k = map(int, input().split())
# 下标从1开始,方便处理边界
a = [[0] * (m + 1) for _ in range(n + 1)]
prefix = [[0] * (m + 1) for _ in range(n + 1)]
# 输入
for i in range(1, n + 1):
row = list(map(int, input().split()))
for j in range(1, m + 1):
a[i][j] = row[j - 1]
# 构建二维前缀和
for i in range(1, n + 1):
for j in range(1, m + 1):
prefix[i][j] = (prefix[i-1][j] + prefix[i][j-1]
- prefix[i-1][j-1] + a[i][j])
子矩阵和查询公式:
(x1,y1) 到 (x2,y2) 的子矩阵和 =
prefix[x2][y2]
- prefix[x1-1][y2]
- prefix[x2][y1-1]
+ prefix[x1-1][y1-1]
图示理解:
+-----------+-----------+
| | |
| 区域A | 区域B |
| | |
+-----------+-----------+
| | |
| 区域C | 目标区域 |
| | (x2,y2) |
+-----------+-----------+
(x1,y1)
完整解法:枚举 + 二维前缀和
python
def get_submatrix_sum(x1, y1, x2, y2):
"""获取子矩阵和,1-indexed,包含边界"""
return (prefix[x2][y2]
- prefix[x1-1][y2]
- prefix[x2][y1-1]
+ prefix[x1-1][y1-1])
ans = 0
# 枚举左上角 (x1, y1)
for x1 in range(1, n + 1):
for y1 in range(1, m + 1):
# 枚举右下角 (x2, y2)
for x2 in range(x1, n + 1):
for y2 in range(y1, m + 1):
if get_submatrix_sum(x1, y1, x2, y2) <= k:
ans += 1
print(ans)
复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 构建前缀和 | O(N⋅M)O(N \cdot M)O(N⋅M) | 双重循环 |
| 枚举子矩阵 | O(N2⋅M2)O(N^2 \cdot M^2)O(N2⋅M2) | 四重循环 |
| 单次查询 | O(1)O(1)O(1) | 公式计算 |
| 总时间 | O(N2⋅M2)O(N^2 \cdot M^2)O(N2⋅M2) | 当 N=M=500N=M=500N=M=500 时,约 6.25×10106.25 \times 10^{10}6.25×1010 |
等等! 5004=6.25×1010500^4 = 6.25 \times 10^{10}5004=6.25×1010,这显然会超时!
优化思路:降维 + 双指针/二分
对于 N,M≤500N, M \leq 500N,M≤500,O(N2⋅M2)O(N^2 \cdot M^2)O(N2⋅M2) 确实太大。我们需要优化到 O(N2⋅M)O(N^2 \cdot M)O(N2⋅M) 或更优。
优化策略 :固定上下两行,转化为"一维数组中子数组和 ≤K\leq K≤K 的个数"问题。
python
ans = 0
# 枚举上边界 top
for top in range(1, n + 1):
# row_sum[j] 表示从 top 到 bottom 行,第 j 列的累加和
row_sum = [0] * (m + 1)
# 枚举下边界 bottom
for bottom in range(top, n + 1):
# 更新 row_sum
for j in range(1, m + 1):
row_sum[j] += a[bottom][j]
# 现在问题转化为:一维数组 row_sum[1..m] 中,
# 有多少个子数组的和 <= K?
# 可以用前缀和 + 有序集合/树状数组优化到 O(m log m)
# 或者用双指针(如果元素非负)优化到 O(m)
# 由于 a[i][j] >= 0,row_sum 也是非负的!
# 可以用双指针(滑动窗口)
prefix_1d = [0]
for j in range(1, m + 1):
prefix_1d.append(prefix_1d[-1] + row_sum[j])
# 双指针:找满足 prefix_1d[r] - prefix_1d[l] <= K 的 (l,r) 对数
left = 0
for right in range(1, m + 1):
while prefix_1d[right] - prefix_1d[left] > k:
left += 1
ans += right - left
print(ans)
优化后的复杂度:
- 枚举上下边界:O(N2)O(N^2)O(N2)
- 每次双指针:O(M)O(M)O(M)
- 总复杂度:O(N2⋅M)≈5002×500=1.25×108O(N^2 \cdot M) \approx 500^2 \times 500 = 1.25 \times 10^8O(N2⋅M)≈5002×500=1.25×108,可接受!
💡 关键观察 :题目中 Aij≥0A_{ij} \geq 0Aij≥0,这是能用双指针优化的前提条件 。如果允许负数,就需要用前缀和 + 有序集合(平衡树)或树状数组/线段树 来优化,复杂度为 O(N2⋅MlogM)O(N^2 \cdot M \log M)O(N2⋅MlogM)。
四、今日心法:前缀和的三重境界
第一重:一维前缀和
区间求和 → O(1) 查询
公式:sum[l,r] = prefix[r] - prefix[l-1]
第二重:前缀和 + 哈希表
子数组和为K / 最长平衡子串 → O(n) 搞定
关键:和值 -> 位置的映射
第三重:二维前缀和 + 降维优化
子矩阵统计 → 枚举 + 双指针 / 有序集合
关键:固定一维,转化为一维问题
五、代码模板速查
模板1:一维前缀和(带取模)
python
def build_prefix(a, mod=10**9+7):
prefix = [0]
for x in a:
prefix.append((prefix[-1] + x) % mod)
return prefix
def range_query(prefix, l, r, mod=10**9+7):
# [l, r] 都是 0-indexed,闭区间
return (prefix[r + 1] - prefix[l] + mod) % mod
模板2:二维前缀和
python
def build_2d_prefix(a, n, m):
prefix = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for j in range(1, m + 1):
prefix[i][j] = (prefix[i-1][j] + prefix[i][j-1]
- prefix[i-1][j-1] + a[i][j])
return prefix
def submatrix_sum(prefix, x1, y1, x2, y2):
# 1-indexed,包含边界
return (prefix[x2][y2] - prefix[x1-1][y2]
- prefix[x2][y1-1] + prefix[x1-1][y1-1])
模板3:前缀和 + 哈希表(最长平衡子串)
python
from itertools import accumulate
def longest_balanced(s, char_pos, char_neg):
diff = [1 if c == char_pos else -1 for c in s]
prefix = [0] + list(accumulate(diff))
first = {0: 0}
ans = 0
for i in range(1, len(prefix)):
if prefix[i] in first:
ans = max(ans, i - first[prefix[i]])
else:
first[prefix[i]] = i
return ans
写在最后
今天的刷题让我深刻体会到:前缀和不是技巧,而是一种思想。
它教会我们:
- 空间换时间 :用 O(n)O(n)O(n) 空间预处理,换取 O(1)O(1)O(1) 查询
- 转化思想:把"区间和"转化为"两点差值"
- 降维打击:二维问题固定一维,化为一维问题
"算法之美,在于发现隐藏的结构。前缀和,就是发现'累积'结构的艺术。"
如果你也在刷题路上,欢迎交流。记住:看懂题解只是开始,能写出模板、能变形应用、能在面试中讲清楚,才是真正的掌握。 🚀