枚举优化
在上面的做法中,我们给出的都是最简单直接的算法。但是当复杂度为 O ( n 2 ) O(n^2) O(n2)时,个人电脑在1s之内可能只能处理最多 1 0 4 10^4 104的数据规模,而当算法复杂度到 O ( n 4 ) O(n^4) O(n4)时,可能数据规模最多只能是 1 0 2 10^2 102 ,所以非常低效。所以,下面我们给出一些枚举算法的优化思路:
能算则算
可以通过必要的计算规避一些不必要的枚举。
统计矩形
给定 n × m n \times m n×m的网格图,求该网格图有多少长方形(长和宽不等),以及多少正方形。
比如在上面的统计矩形的例子中,我们枚举左上角之后,长方形和正方形满足条件的右下角个数可以通过计算得出。
所以,我们可以通过以下计算来统计(i, j)左上角对应对长方形和正方形的贡献
矩形个数 = ( n − i + 1 ) ∗ ( m − j + 1 ) 正方形个数 = min ( n − i + 1 , m − j + 1 ) 长方形个数 = 矩形个数 − 正方形个数 \text{矩形个数 }= (n - i + 1) * (m - j + 1) \\ \text{正方形个数 }= \min(n - i + 1, m - j + 1) \\ \text{长方形个数 }= \text{ 矩形个数 } - \text{ 正方形个数 } 矩形个数 =(n−i+1)∗(m−j+1)正方形个数 =min(n−i+1,m−j+1)长方形个数 = 矩形个数 − 正方形个数
能存则存
可以通过储存更多的信息来避免重复计算。
要想枚举满足条件的所有区间,最常见的枚举方法就是分别枚举区间的左右端点。
举例: 序列染色
有连续N个格子。起初每个格子分别被染成了R(红色)G(绿色)B(蓝色)三种不同颜色,问最少改变多少个格子的颜色,使得这N个格子可以被分成R、G、B的三段,且每一段长度不为空。
如下图的例子,第一行的格子通过将第三个涂成红色,第六个涂成蓝色,变成了一行RGB的形式。
思路
因为满足RGB条件的格子染色方案之间,区别在于位于中间的绿色区间的位置。
我们可以设计如下算法:
- 枚举所有可能的绿色区间的位置[i, j]。
- 计算从原序列到目标序列需要重新涂色的格子个数 C o u n t i , j Count_{i, j} Counti,j
- 输出所有 C o u n t i , j Count_{i, j} Counti,j中最小的一个。
在上述思路中,我们枚举的对象是“绿色区间的位置”,需要检查的条件是“需要修改的格子数是否为目前最少的”。
下面是实现该算法的伪代码:
ans <- n; // 最多修改不会超过n个格子
for i <- 2 to n - 1 do
for j <- i to n - 1 do
cnt <- 绿色区间为[i, j]时需要重新涂颜色的格子数
if cnt < ans then ans <- cnt
输出 ans
复杂度
因为“cnt <- 绿色区间为[i, j]时需要重新涂颜色的格子数”是一个子过程,并且该子过程被运行了
O
(
n
2
)
O(n^2)
O(n2)
)次,所以,整个算法的复杂度为
T
(
n
)
=
O
(
n
2
×
T
(
统计需要修改的格子数
)
)
T(n) = O(n^2 \times T(统计需要修改的格子数))
T(n)=O(n2×T(统计需要修改的格子数))
所以,一个高效的统计方法会降低整个算法的运行时间。目前,我们可以用最简单的方法:
将整个序列扫描一遍,如果当前格子的颜色和当前枚举的答案序列不一样,就让统计数值+1。
那么该子过程的复杂度是O(n),而整个算法的复杂度是 O ( n 3 ) O(n^3) O(n3)
后面,我们会介绍用前缀和数组优化该算法的做法。
在上面的序列染色的例子中,算法的瓶颈在于对于一个确定的绿颜色区间,如何快速计算需要修改的颜色个数。考虑到该步骤是在询问一个区间上的信息。
【前缀和优化】
什么是前缀?原数组的第i个前缀指的是第1个到第i个的一段。比如原数组为
(1, 2, 3, 4, 5, 6, 7)
(1,2,3,4,5,6,7)
则该数组的8个前缀分别为
( ) ( 1 ) ( 1 , 2 ) ( 1 , 2 , 3 ) ( 1 , 2 , 3 , 4 ) ( 1 , 2 , 3 , 4 , 5 ) ( 1 , 2 , 3 , 4 , 5 , 6 ) ( 1 , 2 , 3 , 4 , 5 , 6 , 7 ) ()\\ (1)\\ (1, 2)\\ (1, 2, 3)\\ (1, 2, 3, 4)\\ (1, 2, 3, 4, 5)\\ (1, 2, 3, 4, 5, 6)\\ (1, 2, 3, 4, 5, 6, 7) ()(1)(1,2)(1,2,3)(1,2,3,4)(1,2,3,4,5)(1,2,3,4,5,6)(1,2,3,4,5,6,7)
什么是前缀和数组?仍然假设原数组为
int a[] = {1, 2, 3, 4, 5, 6, 7};
那么,对于前缀和数组的第i个元素,它的值就是原数组 1 ∼ i 1\sim i 1∼i这个前缀所有元素的和。所以该数组的前缀和数组为:
int sum[] = {1, 3, 6, 10, 15, 21, 28};
前缀和数组有什么作用?可以看到,如果我们想求原数组第i个元素到第j个元素的和时,只需输出
s
u
m
[
j
]
−
s
u
m
[
i
−
1
]
sum[j] - sum[i - 1]
sum[j]−sum[i−1]即可。
使用前缀和数组,区间和可以通过两个前缀和相减快速求出。这里我们可以拓展这个思路,预处理出三个前缀和数组:
int a[N]; // 原序列
int not_R[N]; // 前i个格子里不是红色的格子个数
int not_G[N]; // 前i个格子里不是绿色的格子个数
int not_B[N]; // 前i个格子里不是蓝色的格子个数
for (int i = 1; i <= n; ++i) {
not_R[i] = not_R[i - 1] + (a[i] != 'R');
not_G[i] = not_G[i - 1] + (a[i] != 'G');
not_B[i] = not_B[i - 1] + (a[i] != 'B');
}
这样,对于一个绿色区间[i, j],总需要修改的格子数为三个颜色区间里,不等于各自颜色的格子数量求和:
n_change = not_R[i - 1] + (not_G[j] - not_G[i - 1]) + (not_B[n] - not_B[j]);
注:内容来源DataWhale