前缀和与差分 学习笔记【C++ 算法竞赛】
前缀和
对于数组 \(a\)(通常大小为 \(n\)),其前缀和数组 \(s\) 满足以下性质:
通俗地说,前缀和数组 \(s[i]\) 表示原数组 \(a\) 中从第 \(1\) 项到第 \(i\) 项的元素之和。
重要约定: 为了简化边界处理,原数组 \(a\) 通常从索引 \(1\) 开始存储有效数据,索引 \(0\) 的位置可以置为 \(0\) 或忽略。
为什么这样约定?
让我们从前缀和数组的构造过程来理解:
构造前缀和数组
根据定义,我们可以得到递推关系式:
根据这个递推式,我们可以在 \(\mathcal{O}(n)\) 的时间复杂度内构造前缀和数组。
边界处理的考量: 当 \(i = 1\) 时,递推式变为 \(s[1] = s[0] + a[1]\)。如果我们将 \(a[1]\) 作为第一个有效元素,并预先定义 \(s[0] = 0\),那么这个递推式对 \(i = 1\) 依然成立且无需特殊处理。如果数组从 \(0\) 开始存储有效数据,处理 \(s[0]\)(即 \(a[0]\) 本身)时递推式 \(s[0] = s[-1] + a[0]\) 会导致下标 \(-1\) 越界,需要额外的边界判断。因此,从 \(1\) 开始存储数据并定义 \(s[0] = 0\) 是最简洁、通用的做法。
利用一维前缀和解决问题
一维前缀和的核心应用是快速求解原数组 \(a\) 上任意区间 \([l, r]\) 的和,尤其在需要处理多次区间求和查询时效率优势显著。
构造示例 (C++):
int a[102]; // 有效数据存储在 a[1] 到 a[100], a[0] 未使用或设为0
int s[102] = {0}; // 初始化s[0]=0, 其余可能自动初始化为0或手动初始化
// 构造前缀和数组 (i 从 1 到 n, 假设 n=100)
for (int i = 1; i <= 100; i++) { // 修正: 循环条件应为 i <= n (例如100), 不是 i <= 101
s[i] = s[i - 1] + a[i];
}
区间求和: 对于查询区间 \([l, r]\)(\(1 <= l <= r <= n\)),其和可以通过前缀和数组高效计算:
处理 q 次查询示例 (C++):
int q;
cin >> q;
while (q--) {
int l, r;
cin >> l >> r; // 假设输入保证 1 <= l <= r <= n
cout << s[r] - s[l - 1] << endl; // O(1) 时间完成一次查询
}
通过前缀和预处理,每次区间求和查询的复杂度降至 \(\mathcal{O}(1)\)。
二维前缀和
将前缀和的概念扩展到二维数组 \(a\)(\(m\) 行 \(n\) 列):
构造递推式:
s[i][j] = a[i][j] + s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1]
解释: \(s[i][j]\) 等于当前元素 \(a[i][j]\),加上上方矩形区域的和 \(s[i-1][j]\),加上左方矩形区域的和 \(s[i][j-1]\),再减去左上角重叠计算了两次的区域 \(s[i-1][j-1]\)。
子矩阵求和: 求以 \((x1, y1)\) 为左上角、\((x2, y2)\) 为右下角(\(1 <= x1 <= x2 <= m, 1 <= y1 <= y2 <= n\))的子矩阵和:
sum = s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1]
解释: 总和 \(s[x2][y2]\) 减去上方不需要的区域 \(s[x2][y1-1]\),减去左方不需要的区域 \(s[x1-1][y2]\),再加上被减了两次的左上角区域 \(s[x1-1][y1-1]\)。
差分
差分是前缀和的逆运算。对于原数组 \(a\),其差分数组 \(b\) 满足:\(a[i]\) 是 \(b\) 的前 \(i\) 项前缀和,即 \(a[i] = b[1] + b[2] + ... + b[i]\)。这意味着 \(b[i] = a[i] - a[i-1]\)(对于 \(i >= 2\)),且 \(b[1] = a[1]\)(当 \(a[0] = 0\) 时)。
差分的主要优势在于它能高效地对原数组进行区间修改。
一维差分
区间修改操作 (insert): 若想在原数组 \(a\) 的区间 \([l, r]\) 上统一加上一个值 \(c\),只需对差分数组 \(b\) 做两次操作:
void insert(int l, int r, int c) {
b[l] += c; // 影响从 l 开始的所有前缀和 (即 a[l], a[l+1], ..., a[n])
b[r + 1] -= c; // 抵消从 r+1 开始的前缀和受到的影响
}
还原修改后的数组 (\(a\)): 对差分数组 \(b\) 计算前缀和即可得到修改后的 \(a\):
// 假设 b 已初始化为 a 的差分 (b[1]=a[1], b[i]=a[i]-a[i-1] for i>1)
for (int i = 1; i <= n; i++) {
b[i] += b[i - 1]; // 计算 b 的前缀和,结果覆盖 b[i] 或存入 a[i]
cout << b[i] << ' '; // 输出修改后的 a[i]
}
二维差分
子矩阵修改操作 (insert): 若想在以 \((x1, y1)\) 为左上角、\((x2, y2)\) 为右下角的子矩阵上统一加上值 \(c\),对差分数组 \(b\) 进行四次操作:
void insert(int x1, int y1, int x2, int y2, int c) {
b[x1][y1] += c; // 影响 (x1, y1) 右下所有区域
b[x2 + 1][y1] -= c; // 抵消 (x2+1, y1) 右下区域的影响
b[x1][y2 + 1] -= c; // 抵消 (x1, y2+1) 右下区域的影响
b[x2 + 1][y2 + 1] += c; // 补偿被上面两步多减了一次的区域
}
还原修改后的数组 (\(a\)): 对二维差分数组 \(b\) 计算二维前缀和即可得到修改后的 \(a\):
// 假设 b 已初始化为 a 的二维差分
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
// 计算 b 的二维前缀和,结果覆盖 b[i][j] 或存入 a[i][j]
b[i][j] += b[i][j - 1] + b[i - 1][j] - b[i - 1][j - 1];
cout << b[i][j] << ' ';
}
cout << endl;
}

前缀和与差分简单笔记
浙公网安备 33010602011771号