【上交ACM-算法初级】枚举
-
枚举法是一种通过枚举所有可能解,检查该可能解是否符合要求,并将符合要求的解计入答案的方法。
-
在解决问题的过程中,我们需要枚举的对象有很多种,比如数值、区间、矩形、日期等等。
-
在设计枚举算法时,一些思路直接的算法虽然很容易理解,但是通常会导致高昂的时间代价。所以我们可以通过加入数学计算、并且存储尽可能多的信息的方法,来降低时间复杂度。
-
严谨描述一下枚举法的过程:
确定枚举对象、枚举范围和判定条件;
枚举可能的解,验证是否是问题的解。
在上面的做法中,我们给出的都是最简单直接的算法。但是当复杂度为O(n^2)O(n2)时,个人电脑在1s之内可能只能处理最多10^4104的数据规模,而当算法复杂度到O(n^4)O(n4)时,可能数据规模最多只能是10^2102,所以非常低效。所以,下面我们给出一些枚举算法的优化思路:
能算则算
可以通过必要的计算规避一些不必要的枚举。
比如在上面的统计矩形的例子中,我们枚举左上角之后,长方形和正方形满足条件的右下角个数可以通过计算得出。
所以,我们可以通过以下计算来统计(i, j)(i,j)左上角对应对长方形和正方形的贡献
能存则存
可以通过储存更多的信息来避免重复计算。
在上面的序列染色的例子中,算法的瓶颈在于对于一个确定的绿颜色区间,如何快速计算需要修改的颜色个数。考虑到该步骤是在询问一个区间上的信息。
【前缀和优化】
(1, 2, 3, 4, 5, 6, 7)
- 什么是前缀?原数组的第ii个前缀指的是第11个到第ii个的一段。比如原数组为
则该数组的8个前缀分别为
- 什么是前缀和数组?仍然假设原数组为
int a[] = {1, 2, 3, 4, 5, 6, 7};那么,对于前缀和数组的第ii个元素,它的值就是原数组 1~i 这个前缀所有元素的和。所以该数组的前缀和数组为:
int sum[] = {1, 3, 6, 10, 15, 21, 28};
- 前缀和数组有什么作用?可以看到,如果我们想求原数组第ii个元素到第jj个元素的和时,只需输出
sum[j] - sum[i - 1]
即可。
枚举区间
要想枚举满足条件的所有区间,最常见的枚举方法就是分别枚举区间的左右端点。
举例: 序列染色
有连续N个格子。起初每个格子分别被染成了R(红色)G(绿色)B(蓝色)三种不同颜色,问最少改变多少个格子的颜色,使得这N个格子可以被分成R、G、B的三段,且每一段长度不为空。
如下图的例子,第一行的格子通过将第三个涂成红色,第六个涂成蓝色,变成了一行RGB的形式。
思路
因为满足RGB条件的格子染色方案之间,区别在于位于中间的绿色区间的位置。
我们可以设计如下算法:
在上述思路中,我们枚举的对象是“绿色区间的位置”,需要检查的条件是“需要修改的格子数是否为目前最少的”。
下面是实现该算法的伪代码:
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)次,所以,整个算法的复杂度为
所以,一个高效的统计方法会降低整个算法的运行时间。目前,我们可以用最简单的方法:
将整个序列扫描一遍,如果当前格子的颜色和当前枚举的答案序列不一样,就让统计数值+1。
那么该子过程的复杂度是O(n)O(n),而整个算法的复杂度是O(n^3)。
格子染色-枚举优化算法
使用前缀和数组,区间和可以通过两个前缀和相减快速求出。这里我们可以拓展这个思路,预处理出三个前缀和数组:
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]);