前缀和、差分
前缀和与区间和
【前缀和】:
序列中第 1 项到第 i 项所有数据之和。
在构造前缀和数组的方式上,直接的可以使用 O() 的做法得到,但是这样的效率并不高效。
// prefix 前缀和数组,a 原始数组
for (int i = 1; i <= n; i++) {
int sum = 0;
for (int j = 1; j <= i; j++) {
sum += a[j];
}
prefix[i] = sum;
}
可以通过观察前一项(i−1 项)前缀和等价于
但实际上第 i 项前缀和等价于
仅仅只相差 a[j],j=i 这一项,所以可以将循环降低至 O(1) 时间复杂度。
// prefix 前缀和数组,a 原始数组
for (int i = 1; i <= n; i++) {
prefix[i] = prefix[i - 1] + a[i];
}
【区间和】:
序列中连续一段区间的数据之和。
例如区间 [3,5],区间和为 2+5+9=15
可以借助前缀和计算,将循环降低至 O(1) 时间复杂度。
前缀和第 R 项等价于
前缀和第 L−1 项等价于
将其两者相减可得区间和 [L,R]
亦可得 23−7=15
得到公式:prefix[R]-prefix[L-1]
或 prefix[R]-prefix[L]+a[L]
差分与区间修改
【差分】:
差分是相邻两个数据的差值,是前缀和的逆运算。
// 差分数组构造
// diff 差分数组,a 原始数组
for (int i = 1; i <= n; i++) {
diff[i] = a[i] - a[i - 1]; // 当前项 - 前一项
}
【区间修改】:
对序列中某区间 [L,R] 之间修改数据 c。
可以借助差分数组,差分数组实际保存的便是数据之间的差值,当修改某个位置时,会导致从该位置开始,其通过前缀和逆运算得到的数据正是原数据修改后的值。
那么则有:
若想在某段区间修改,而不是全部影响,需要在结束的地方后一个位置弥补修改的差值。
在这里 R+1 的位置便能够恢复正常,而不受前面修改的影响。
递归
递归是自己调用自己,往往将复杂的问题拆解为一个个规模更小、问题相似的子问题去解决。
递归需要理清楚的是两大要素:
- 递归式(如何解决这个问题的方法步骤)
- 边界条件(总会有最简单的情况作为结束传递的信号)
例如:汉诺塔
需要将 n 层移动到目标位置,可以每次都拆分成三大步来看。
- 将上面 n−1 层移动到不是目标位置,因为目标位置需要放最大的第 n 层
- 将这个最大的第 n 层移动到目标位置
- 将刚才 n−1 层移动过的在那个位置移动至目标位置,完成所有操作
记忆化递归
递归在解决问题时,往往代码十分精简,但很可能做的次数非常的多。
例如:求斐波那契第 n 项,以下递归解决的时间复杂度是 O(2n)。
long long fib(int n) {
if (n <= 2) return 1;
return fib(n - 1) + fib(n - 2);
}
可以将求过的数据存放在数组中,这样如果下一次遇到相同计算的问题时,可以直接返回,而不必重头再来一遍运算。这样所有的数据只求 1 次,因此是 O(n) 时间复杂度。
long long f[55]; // 记忆数组
long long fib(int n) {
if (n <= 2) return 1;
if (f[n]) return f[n];
return f[n] = fib(n - 1) + fib(n - 2); // 返回前先保存在 f 数组中
}
递推
核心:从已知条件根据数量关系(递推式)推往未知条件,这一点和动态规划十分相似,是一种特殊的动态规划问题,往往单一的状态转移就可以解决。
递推的题型往往涉及到组合数学的相关知识,如以下的加法原理与乘法原理。
加法原理
解决某一件事情共有 n 类方案,第 1 类有 a1 种方法,第 2 类有 a2 种方法,...,第 n 类有 an 种方法,那么解决这件事情的总方法数是 。值得注意的是,这里每种方法都可以直接的完成任务,而不依赖于其他事情。
例如:北京前往上海,可以选择汽车(私家车、DD顺风车、大巴,共 3 种),飞机(10 种航班),高铁(5 种列车)。总共 3+10+5=18 种方法。
乘法原理
解决某一件事情共有 n 步步骤,第 1 步有 a1 种方法,第 2 步有 a2 种方法,...,第 n 步有 an 种方法,那么解决这件事情的总方法数是 。值得注意的是,这里每个步骤必须都要选择某个方法完成,否则整件事情无法完成。
例如:出门前需要穿衣服,上衣(5 件),下衣(3 件),鞋子(2 双),不考虑穿搭美观,仅仅搭配数量为 5×3×2=30 种方案。