前缀和&差分

1. 前缀和

前缀和可以简单理解为「数列的前 n 项的和」,是一种重要的预处理方式,能大大降低查询的时间复杂度。

二维/多维前缀和:

常见的多维前缀和的求解方法有两种。

基于容斥原理:
这种方法多用于二维前缀和的情形。给定大小为 m × n m\times n m×n 的二维数组 A,要求出其前缀和 S。那么,S 同样是大小为 m × n m\times n m×n 的二维数组,且

S i , j = ∑ i ′ ≤ i ∑ j ′ ≤ j A i ′ , j ′ S_{i,j} = \sum_{i'\le i}\sum_{j'\le j}A_{i',j'} Si,j=iijjAi,j

类比一维的情形, S i , j S_{i,j} Si,j 应该可以基于 S i − 1 , j S_{i-1,j} Si1,j S i , j − 1 S_{i,j-1} Si,j1 计算,从而避免重复计算前面若干项的和。但是,如果直接将 S i − 1 , j S_{i-1,j} Si1,j S i , j − 1 S_{i,j-1} Si,j1 相加,再加上 A i , j A_{i,j} Ai,j,会导致重复计算 S i − 1 , j − 1 S_{i-1,j-1} Si1,j1 这一重叠部分的前缀和,所以还需要再将这部分减掉。这就是 容斥原理。由此得到如下递推关系:

在这里插入图片描述

实现时,直接遍历 ( i , j ) (i,j) (i,j) 求和即可。

在这里插入图片描述
同样的道理,在已经预处理出二位前缀和后,要查询左上角为 ( i 1 , j 1 ) (i_1,j_1) (i1,j1)、右下角为 ( i 2 , j 2 ) (i_2,j_2) (i2,j2) 的子矩阵的和,可以计算
在这里插入图片描述
这可以在 O(1) 时间内完成。

在二维的情形,以上算法的时间复杂度可以简单认为是 O(mn),即与给定数组的大小成线性关系。但是,当维度 k 增大时,由于容斥原理涉及的项数以指数级的速度增长,时间复杂度会成为 O ( 2 k N ) O(2^kN) O(2kN),这里 k k k 是数组维度,而 N N N 是给定数组大小。因此,该算法不再适用。

逐维前缀和:

对于一般的情形,给定 k k k 维数组 A A A,大小为 N N N,同样要求得其前缀和 S S S。这里,

在这里插入图片描述
从上式可以看出, k k k 维前缀和就等于 k k k 次求和。所以,一个显然的算法是,每次只考虑一个维度,固定所有其它维度,然后求若干个一维前缀和,这样对所有 k k k 个维度分别求和之后,得到的就是 k k k 维前缀和。

三维前缀和的参考实现:

N1, N2, N3 = map(int,input().split())

a = [[[0 for _ in range(N3+1)] for _ in range(N2+1)] for _ in range(N1+1)]

for i in range(1, N1+1):
    for j in range(1, N2+1):
        for k in range(1, N3+1):
            a[i][j][k] = int(input())

ps = [list(map(list, x)) for x in a]

for i in range(1, N1+1):
    for j in range(1, N2+1):
        for k in range(1, N3+1):
            ps[i][j][k] += ps[i][j][k-1]

for i in range(1, N1+1):
    for j in range(1, N2+1):
        for k in range(1, N3+1):
            ps[i][j][k] += ps[i][j-1][k]

for i in range(1, N1+1):
    for j in range(1, N2+1):
        for k in range(1, N3+1):
            ps[i][j][k] += ps[i-1][j][k]

for i in range(1, N1+1):
    for j in range(1, N2+1):
        for k in range(1, N3+1):
            print(ps[i][j][k], end=' ')
        print()
    print()

2. 差分

一维差分:

差分思想和前缀和是相反的。

首先我们先定义数组a, 其中a[1], a[2] … a[n]作为前缀和。

然后构造数组b,b[1], b[2] … b[n]为差分数组。其中通过差分数组的前缀和来表示a数组,即a[n] = b[1] + b[2]+…+b[n]。

一维差分数组的构造也很简单,即a[1] = b[1], b[2] = a[2] - a[1], b[n] = a[n] - a[n-1];

在做初始化的时候现将a,b全部初始化为0,然后可以按照下面的方式流式进行:

//eg:对于b[1]:
b[1] = 0 + a[1]
b[2] = 0 - a[1] 	# 最终:b[1] = a[1]
//对于b[2]:
b[2] = b[2] + a[2]  # ==> 最终:b[2] = a[2] - a[1]
b[3] = b[3] - a[2]

差分数组的好处是可以简化运算,例如想要给一个区间 [l,r] 上的数组加一个常数c,原始的方法是依次加上c,这样的时间复杂度是O(n)的。但是如果采用差分数组的话,可以大大降低时间复杂度到O(1)。

由于a[n] = b[1] + b[2]+…+b[n],因此只需要将b[l] = b[l] + c 即可,这样l之后的数字会依次加上常数c,而在 b[r]处,将b[r+1] = b[r+1] - c ,这样r之后的数组又会恢复原值,仅需要处理这两个边界的差分数组即可,时间复杂度大大降低。

在这里插入图片描述

这里不是说真的变成O(1)了,而是说如果多次给一个序列上的某些区间上的数组加一个常数,可以先在这个序列的差分数组上先给相应位置l和r+1进行操作,多次操作后再对整个差分数组求前缀和就可以得到结果了。比如给元素组的一些区间加了m次常数,原数组的长度为n,则相比在元素组上操作的时间复杂度为m*n,在它的差分数组上操作的时间复杂度为n。

例题:差分

输入一个长度为 n 的整数序列。

接下来输入 m 个操作,每个操作包含三个整数 l,r,c,表示将序列中 [l,r] 之间的每个数加上 c。

请你输出进行完所有操作后的序列。

输入格式

第一行包含两个整数 n 和 m。

第二行包含 n 个整数,表示整数序列。

接下来 m 行,每行包含三个整数 l,r,c表示一个操作。

输出格式

共一行,包含 n 个整数,表示最终序列。

数据范围

1≤n,m≤100000,
1≤l≤r≤n,
−1000≤c≤1000,
−1000≤整数序列中元素的值≤1000
N = 100010

m,n = 0,0
a = [0]*N
b = [0]*N

def insert(l, r , c):
    b[l] += c
    b[r+1] -= c

n, m = map(int,input().split())
a = list(map(int,input().split()))
for i in range(1, n+1): 
    insert(i, i, a[i-1])

for _ in range(m):
    l, r ,c = map(int,input().split())
    insert(l, r, c)
    
for i in range(1, n+1): 
    b[i] += b[i - 1]

for i in range(1, n+1): 
    print(b[i], end=" ")

参考1:差分算法及模板详解

参考2:前缀和 & 差分

`倍增`、`ST表`、`前缀和` 和 `差分` 是算法竞赛高效编程中常用的四种基础技术,它们各自解决不同类别的问题,但在实际应用中经常结合使用。下面我将逐一详细讲解其原理、实现方式、应用场景,并给出 C++ 代码示例。 --- ## 一、前缀和(Prefix Sum) ### ✅ 定义: 前缀和是数组的一个预处理技巧,用于**快速计算区间和**。 设原数组为 `a[0..n-1]`,定义前缀和数组 `prefix[i] = a[0] + a[1] + ... + a[i-1]`,则: > 区间 `[l, r]` 的和为:`sum(l, r) = prefix[r+1] - prefix[l]` ### ⏱ 时间复杂度: - 预处理:O(n) - 查询:O(1) ### 💡 应用场景: - 多次查询子数组和 - 子数组和为某值的问题(配合哈希表) ### ✅ C++ 实现: ```cpp #include <iostream> #include <vector> using namespace std; int main() { vector<int> a = {1, 2, 3, 4, 5}; int n = a.size(); // 构建前缀和(prefix[0]=0, prefix[i] 表示前 i 个元素的和) vector<int> prefix(n + 1, 0); for (int i = 0; i < n; ++i) { prefix[i + 1] = prefix[i] + a[i]; } // 查询 [1,3] 的和:a[1]+a[2]+a[3] = 2+3+4=9 int l = 1, r = 3; cout << "Sum from [" << l << "," << r << "] is: " << prefix[r + 1] - prefix[l] << endl; return 0; } ``` --- ## 二、差分(Difference Array) ### ✅ 定义: 差分前缀和“逆运算”,用于**高效进行多次区间修改**。 构造差分数组 `d`,使得对 `d` 做前缀和还原出原数组。 若想对原数组 `[l, r]` 加上 `x`,只需: ```cpp d[l] += x; d[r+1] -= x; // 如果 r+1 < n ``` ### ⏱ 时间复杂度: - 单次修改:O(1) - 最终还原:O(n) ### 💡 应用场景: - 多次区间加法操作 - 配合前缀和还原结果 ### ✅ C++ 实现: ```cpp #include <iostream> #include <vector> using namespace std; int main() { vector<int> a = {1, 2, 3, 4, 5}; int n = a.size(); // 构造差分数组 d vector<int> d(n + 1, 0); for (int i = 0; i < n; ++i) { d[i] = a[i] - (i > 0 ? a[i-1] : 0); } // 对 [1,3] 加 2 int l = 1, r = 3, x = 2; d[l] += x; d[r + 1] -= x; // 还原数组 vector<int> res(n); res[0] = d[0]; for (int i = 1; i < n; ++i) { res[i] = res[i-1] + d[i]; } cout << "After update: "; for (int v : res) cout << v << " "; cout << endl; return 0; } ``` --- ## 三、倍增(Binary Lifting / Doubling) ### ✅ 定义: 倍增是一种通过 **预处理 $ 2^k $ 步信息** 来加速查询的技术。 最典型应用:**快速求树上两个节点的 LCA(最近公共祖先)** 核心思想:每个节点记录它的第 $ 2^k $ 级祖先,从而可以在 $ O(\log n) $ 内跳转任意距离。 ### 🔧 应用场景: - 树上 LCA 查询 - 快速跳跃指针(如并查集优化、字符串匹配) - RMQ 问题的一种解法(替代 ST 表) ### ✅ C++ 实现(简化版倍增跳父亲): ```cpp #include <iostream> #include <vector> #include <cmath> using namespace std; const int MAXN = 1e5 + 5; const int LOG = 20; vector<int> tree[MAXN]; int parent[MAXN][LOG]; int depth[MAXN]; // DFS 预处理深度和直接父节点 void dfs(int u, int p) { parent[u][0] = p; depth[u] = depth[p] + 1; for (int i = 1; i < LOG; ++i) { if (parent[u][i-1] != -1) { parent[u][i] = parent[parent[u][i-1]][i-1]; } else { parent[u][i] = -1; } } for (int v : tree[u]) { if (v != p) { dfs(v, u); } } } // 求 LCA int lca(int u, int v) { if (depth[u] < depth[v]) swap(u, v); // 让 u 上升到和 v 同一层 int diff = depth[u] - depth[v]; for (int i = 0; i < LOG; ++i) { if (diff & (1 << i)) { u = parent[u][i]; } } if (u == v) return u; for (int i = LOG - 1; i >= 0; --i) { if (parent[u][i] != parent[v][i]) { u = parent[u][i]; v = parent[v][i]; } } return parent[u][0]; } ``` --- ## 四、ST表(Sparse Table)——静态RMQ结构 ### ✅ 定义: ST 表(Sparse Table)是一种基于**倍增思想**的数据结构,用于解决**静态区间最值查询**(Range Minimum/Maximum Query, RMQ),不能处理修改。 > 支持 O(1) 查询任意区间的最小值或最大值,预处理时间 O(n log n) ### 🧠 原理: 构建二维数组 `st[i][k]` 表示从位置 `i` 开始长度为 $ 2^k $ 的区间的最值。 转移方程: ```cpp st[i][k] = max(st[i][k-1], st[i + (1<<(k-1))][k-1]) ``` 查询 `[l, r]` 最值时: - 找最大的 $ k $ 使得 $ 2^k \leq r-l+1 $ - 分两段覆盖:`[l, l+2^k-1]` 和 `[r-2^k+1, r]` ### ✅ C++ 实现(静态区间最大值): ```cpp #include <iostream> #include <vector> #include <cmath> using namespace std; const int MAXN = 1e5 + 5; const int LOG = 20; int st[MAXN][LOG]; int log_table[MAXN]; // 预处理 log2 值 // 预处理 log 值(避免重复调用 log2 函数) void build_log_table(int n) { log_table[1] = 0; for (int i = 2; i <= n; ++i) { log_table[i] = log_table[i / 2] + 1; } } // 构建 ST 表(最大值) void build_st(vector<int>& arr, int n) { for (int i = 0; i < n; ++i) { st[i][0] = arr[i]; } for (int k = 1; (1 << k) <= n; ++k) { for (int i = 0; i + (1 << k) <= n; ++i) { st[i][k] = max(st[i][k-1], st[i + (1 << (k-1))][k-1]); } } } // 查询 [l, r] 的最大值 int query_max(int l, int r) { int len = r - l + 1; int k = log_table[len]; return max(st[l][k], st[r - (1 << k) + 1][k]); } // 测试 int main() { vector<int> arr = {1, 3, 2, 7, 5, 4}; int n = arr.size(); build_log_table(n); build_st(arr, n); cout << "Max in [1,4]: " << query_max(1, 4) << endl; // 输出 7 cout << "Max in [2,5]: " << query_max(2, 5) << endl; // 输出 5 return 0; } ``` --- ### ✅ 四者对比总结: | 技术 | 功能 | 修改支持 | 查询效率 | 预处理 | 典型用途 | |------|------|-----------|------------|----------|-------------| | **前缀和** | 快速区间求和 | ❌ 不支持 | O(1) | O(n) | 统计、子数组和 | | **差分** | 快速区间加法 | ✅ 支持批量加 | O(n)还原 | O(1) per op | 批量更新数组 | | **倍增** | 快速跳 $2^k$ 步 | ❌(静态) | O(log n) | O(n log n) | LCA、跳跃指针 | | **ST表** | 静态区间最值查询 | ❌ | O(1) | O(n log n) | RMQ 问题 | ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

comli_cn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值