c++算法初级2——暴力枚举优化

枚举优化

在上面的做法中,我们给出的都是最简单直接的算法。但是当复杂度为 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{ 正方形个数 } 矩形个数 =(ni+1)(mj+1)正方形个数 =min(ni+1,mj+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 1i这个前缀所有元素的和。所以该数组的前缀和数组为:

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[i1]即可。
使用前缀和数组,区间和可以通过两个前缀和相减快速求出。这里我们可以拓展这个思路,预处理出三个前缀和数组:

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

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值