1. 前缀和
前缀和(Prefix Sum)是一种常用的算法技巧,用于优化区间求和问题。
它的核心思想是通过预先计算数组的累加和,从而使得之后的区间和查询可以在常数时间内完成,显著提高查询效率。
a. 前缀和(一维)
ⅰ. 定义
给定一个数组 A,前缀和数组 P 的第 i 个元素表示数组 A 中从第一个元素到第 i 个元素的和,即:
前缀和数组的第 0 个元素通常定义为 P[0] = 0,这样可以方便计算从数组开头到任意位置的区间和。
假设我们有一个数组 A,其大小为 n,前缀和数组 P 计算方法如下:
ⅱ. 前缀和 实现 区间求和:
前缀和的一个主要应用是快速求解任意区间 [L, R] 的和。
这意味着可以通过前缀和数组计算任意区间的和,时间复杂度为 O(1)。
假设我们有数组 A = [1, 2, 3, 4, 5],我们希望计算区间 [2, 4] 的和。
ⅲ. 例题
from itertools import accumulate
input()
print(*accumulate(map(int, input().split())))
#include <iostream>
using namespace std;
int N, A[10000], B[10000];
int main() {
cin >> N;
for (int i = 0; i < N; i++) {
cin >> A[i];
}
// 前缀和数组的第一项和原数组的第一项是相等的。
B[0] = A[0];
for (int i = 1; i < N; i++) {
// 前缀和数组的第 i 项 = 原数组的 0 到 i-1 项的和 + 原数组的第 i 项。
B[i] = B[i - 1] + A[i];
}
for (int i = 0; i < N; i++) {
cout << B[i] << " ";
}
return 0;
}
b. 二维/多维 前缀和:
ⅰ. 定义
ⅱ. 前缀和计算方法
1. 基于容斥定理
,这里 k 是数组维度,而 N 是给定数组大小。因此该算法不再适用。
#include <iostream>
#include <vector>
int main() {
// Input.
int N1, N2, N3;
std::cin >> N1 >> N2 >> N3;
std::vector<std::vector<std::vector<int>>> a(
N1 + 1, std::vector<std::vector<int>>(N2 + 1, std::vector<int>(N3 + 1)));
for (int i = 1; i <= N1; ++i)
for (int j = 1; j <= N2; ++j)
for (int k = 1; k <= N3; ++k) std::cin >> a[i][j][k];
// Copy.
auto ps = a;
// Prefix-sum for 3rd dimension.
for (int i = 1; i <= N1; ++i)
for (int j = 1; j <= N2; ++j)
for (int k = 1; k <= N3; ++k) ps[i][j][k] += ps[i][j][k - 1];
// Prefix-sum for 2nd dimension.
for (int i = 1; i <= N1; ++i)
for (int j = 1; j <= N2; ++j)
for (int k = 1; k <= N3; ++k) ps[i][j][k] += ps[i][j - 1][k];
// Prefix-sum for 1st dimension.
for (int i = 1; i <= N1; ++i)
for (int j = 1; j <= N2; ++j)
for (int k = 1; k <= N3; ++k) ps[i][j][k] += ps[i - 1][j][k];
// Output.
for (int i = 1; i <= N1; ++i) {
for (int j = 1; j <= N2; ++j) {
for (int k = 1; k <= N3; ++k) {
std::cout << ps[i][j][k] << ' ';
}
std::cout << '\n';
}
std::cout << '\n';
}
return 0;
}
2. 逐维前缀和
子集和 (SOS, Sum Over Subsets) 的问题
子集和 (SOS, Sum Over Subsets) 问题是一个在组合优化和动态规划领域中常见的问题,特别是在处理与集合相关的计数问题、求和问题和动态规划问题时。SOS问题主要关注的是对所有子集进行求和或计算其某些属性。
问题背景与描述:
假设我们有一个集合 ( S = { a_1, a_2, ..., a_n } ),目标是计算该集合的所有子集的某种特征的和。例如,求出每个子集的元素之和,或者每个子集中的元素满足某些条件时的和。
一般的SOS问题形式:
,子集和问题通常要求计算所有子集的某个特定数值的和。例如,我们可能希望计算所有子集的和,或者我们可能希望计算子集中元素满足某些条件(比如是否满足某个数值的和)时的和。
经典例子:
- 动态规划中的SOS问题:SOS问题经常应用于动态规划算法中,特别是通过动态转移来计算子集的某些特征的和。例如,我们可能希望计算子集的和满足某个特定条件的总数(如子集的和等于某个特定值)。这种问题常常涉及到如何高效地处理所有可能的子集。
SOS动态规划技巧:
SOS问题的一个常见技巧是在动态规划中,利用“状态压缩”或“状态转移”来避免直接枚举所有的子集,而是通过分解问题来高效地求解。通常,可以使用位运算或者其他技术来实现这一目标。
典型的应用包括:
- 子集和问题:例如,给定一组整数,判断是否存在一个子集使得它的元素和等于一个特定的值。这个问题是经典的 NP 完全问题,可以通过动态规划来求解。
- 背包问题:子集和问题的变体,如 0/1 背包问题,就是要求在一定容量下选择一组物品,使得它们的总重量最大或总价值最大。
计算方法:
- 暴力求解:列举所有的子集,然后对每个子集计算其元素的和。这种方法非常简单,但时间复杂度是 ( O(2^n) ),对于大规模问题不可行。
- 动态规划法:通过定义状态并利用子集的递推关系,逐步计算出每个子集的和,通常可以将时间复杂度减少到 ( O(n \cdot \text{target}) ) 或更低,其中 ( \text{target} ) 是我们关注的某个具体值。
SOS问题的常见变种:
- 带有限制条件的子集和问题:有些问题不仅要求计算所有子集的和,还附加了额外的限制条件(如子集大小、某些元素的选择限制等)。这些问题可以通过更复杂的动态规划或组合技术来求解。
- 求子集和的个数:在某些应用中,我们不只关心子集和的值本身,还关心满足某些条件的子集的个数。这类问题的求解方法也可以通过状态转移方法来实现。
总结:
SOS问题(Sum Over Subsets)是一类组合优化问题,涉及计算所有子集的某种和或者某种特征的求和问题。这类问题可以通过暴力方法求解,但对于较大规模的输入,通常会使用动态规划或者其他优化技巧来高效求解。
ⅲ. 例题
n, m = map(int, input().split())
a = [list(map(int, input().split())) for _ in range(n)]
b = [a[0]] + [[i[0]] + [0] * (m - 1) for i in a[1:]]
ans = 0
for i in range(1, n):
for j in range(1, m):
if a[i][j]:
b[i][j] = min(b[i - 1][j], b[i][j - 1], b[i - 1][j - 1]) + 1
ans = max(ans, b[i][j])
print(ans)
c. 应用场景
d. 树上前缀和
树上前缀和(Tree Prefix Sum)是一个在树结构中进行区间求和的问题,广泛应用于树的路径查询、动态树和树形动态规划等场景。它是对树上每个节点的子树或者路径进行求和的一种技术,通常用于求解树上某个节点到根节点、某个子树或者指定路径上的某种累积值。
树上前缀和问题描述:
给定一棵树,树的每个节点都有一个值,并且我们需要对树上某些路径或者子树进行前缀和的查询。具体来说,树上前缀和的目标是求解从根节点到某个指定节点,或者从任意节点到根节点之间的路径上的某种累积和(如节点的值的和)。此外,树上前缀和也可以用于计算子树的和,即计算从某个节点出发的子树的所有节点的值之和。
基本概念:
树上前缀和的应用场景:
- 树上路径求和:给定一棵树,查询从某个节点到另一个节点之间路径的节点值之和。例如,在树上查询从节点 ( u ) 到节点 ( v ) 之间的路径和。
- 子树和查询:对于每个节点,查询该节点的子树的所有节点的值之和。可以使用树的DFS(深度优先搜索)或者其他算法预处理,使得每个节点的子树和可以快速查询。
- 动态树问题:树上前缀和也经常用于动态树结构中,在树的节点上进行增删操作时,保持树的结构和前缀和值的一致性。
树上前缀和的计算方法:
方法1:深度优先搜索(DFS)和树的Euler Tour
方法2:树上前缀和的树状数组(Fenwick Tree)
树状数组(Fenwick Tree)可以用于处理树上的前缀和查询。通过树状数组,我们可以将树的每个节点视为一个“点”,并使用树状数组进行求和操作。在每次更新或者查询时,树状数组会对路径上或子树中的节点进行累加。
方法3:链式前缀和(Link-Cut Tree)
链式前缀和(Link-Cut Tree)是一种支持动态树操作的数据结构。它允许在树中动态增删节点,同时支持路径查询、子树查询等操作。通过链式前缀和,可以在树中维护前缀和信息,并支持快速查询和更新。
算法示例:
假设我们有一棵树,并且每个节点有一个值。我们可以通过DFS算法来计算每个节点的前缀和。
def dfs(tree, node, parent, prefix_sum, values):
# 计算当前节点的前缀和
prefix_sum[node] = prefix_sum[parent] + values[node]
# 遍历所有子节点
for child in tree[node]:
if child != parent:
dfs(tree, child, node, prefix_sum, values)
# 示例数据
n = 5 # 5个节点
values = [0, 3, 1, 4, 2] # 节点的值,假设根节点值为0
tree = {1: [2, 3], 2: [1, 4], 3: [1], 4: [2]} # 树的邻接表表示
prefix_sum = [0] * (n + 1) # 存储每个节点的前缀和
# 从根节点开始DFS
dfs(tree, 1, 0, prefix_sum, values)
# 输出从根节点到每个节点的前缀和
print(prefix_sum) # 例如,输出路径上的前缀和
总结:
树上前缀和是一个涉及树的路径查询和子树求和的技巧。在解决这类问题时,我们常用DFS遍历、树状数组、链式前缀和等数据结构和算法来高效地进行求解。通过这些技术,我们可以在树形结构中快速查询路径和、子树和等信息,并处理动态更新的情况。
2. 差分
它应用于区间的修改和询问问题。把给定的数据集 A 分成很多区间,对这些区间做多次操作,每次操作是对某个区间内的所有元素做相同的加减操作,若一个个修改区间内的每个元素很耗时,引入差分数组 D,当修改某个区间时,只需要修改这个区间的端点,就能记录整个区间的修改,而对端点的修改很容易为 O(1)。当所有的修改操作结束后,再利用差分数组计算出新的 A。
数组 A 可以是一维线性数组 a[]、二维矩阵 a[][]、三维立体 [][][]。
相应的,定义一维差分数组 D[]、二维差分数组 D[][]、三维差分数组 D[][][]。
a. 一维差分
ⅰ. 概述
(1)初始:给定一个长度为 n 的一维数组,数组内每个元素有初始值
(2)m 次修改:做 m 次区间修改,每次修改对区间内所有元素做相同的加减操作。
第 i 次修改,降数组区间 [Li ,Ri ] 内所有的元素加上 di 。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5+6;
int data[MAXN] = {};
int diff[MAXN] = {};
int main() {
int n,m;
scanf("%d%d", &n, &m);
int i;
for (i=1; i<=n; i++) {
scanf("%d", &data[i]);
diff[i] = data[i] - data[i-1];
}
for (i=0; i<m; i++) {
int l, r, c;
scanf("%d%d%d", &l, &r, &c);
diff[l] += c;
diff[r+1] -= c;
}
//输出
int ans=diff[0];
for (i=1; i<=n; i++) {
ans += diff[i];
printf("%d ", ans);
}
printf("\n");
return 0;
}
ⅱ. 一维差分局限性
利用差分数组 可以把区间修改 转化为 端点修改,提高了修改操作的效率。
但是,一次查询操作,需要使用差分数组 来 计算整个原数组,计算量为 O(n)
当题目中的查询操作不是仅一次时,而是 m 次查询,k 次修改时候,使用一维差分的复杂度为 O(m+kn)
而暴力法的复杂度为 O(mn+k),区别并不大有时候甚至暴力法更佳。
如上 的题型即“区间修改+单点查询”,差分数组往往不够用。因为差分数组对区间修改很高效,但单点查询并不高效,此时需要使用树状数组和线段树 (详见高级数据结构部分)来求解。
b. 二维差分
ⅰ. 例题(地毯)
ⅱ. 二维差分的定义
ⅲ. 二维差分的使用
(1)------------------------------------------
(2)------------------------------------------
(3)------------------------------------------
(4)------------------------------------------
ⅳ. 二维差分的计算
c. 三维差分
ⅰ. 前缀和
ⅱ. 差分的定义
b[i][j][k] =s[i][j][k] −s[i−1][j][k] −s[i][j−1][k] −s[i][j][k−1] +s[i−1][j−1][k]+s[i−1][j][k−1] +s[i][j−1][k−1] −s[i−1][j−1][k−1]
ⅲ. 区间修改
// 前面
b[x1][y1][z1] += c; // 坐标起点
b[x2 + 1][y1][z1] -= c; // 右下顶点的右边一个点
b[x1][y1][z2 + 1] -= c; // 左上顶点的上面一个点
b[x2 + 1][y1][z2 + 1] += c; // 右上顶点的斜右上方一个点
// 后面
b[x1][y2 + 1][z1] -= c; // 左下顶点的后面一个点
b[x2 + 1][y2 + 1][z1] += c; // 右下顶点的斜右后方一个点
b[x1][y2 + 1][z2 + 1] += c; // 左上顶点的斜后上方一个点
b[x2 + 1][y2 + 1][z2 + 1] -= c; // 右上顶点的斜右上后方一个点,即区间终点的后一个点
{0, 0, 0, 1} // x1, y1, z1
{0, 0, 1, -1} // x1, y1, z2 + 1
{0, 1, 0, -1} // x1, y2 + 1, z1
{0, 1, 1, 1} // x1, y2 + 1, z2 + 1
{1, 0, 0, -1} // x2 + 1, y1, z1
{1, 0, 1, 1} // x2 + 1, y1, z2 + 1
{1, 1, 0, 1} // x2 + 1, y2 + 1, z1
{1, 1, 1, -1} // x2 + 1, y2 + 1, z2 + 1
ⅳ. 例题(三体攻击)
d. 树上差分
ⅰ. 点差分
ⅱ. 边差分
3. 习题
- 洛谷 B3612【深进 1. 例 1】求区间和
- 洛谷 U69096 前缀和的逆
- AtCoder joi2007ho_a 最大の和
- 「USACO16JAN」子共七 Subsequences Summing to Sevens
- 「USACO05JAN」Moo Volume S
- HDU 6514 Monitor
- 洛谷 P1387 最大正方形
- 「HNOI2003」激光炸弹
- CF 165E Compatible Numbers
- CF 383E Vowels
- ARC 100C Or Plus Max