记忆化搜索以及记忆数组大小的影响
一 问题的引入
小编在回顾欧拉计划时遇到记忆化搜索问题,略有感触,感觉有必要和大家分享。
话不多说咱们直接上截图:
问题大意:
一个数,若为偶数,则它下一次将变化为n / 2;若为奇数,则将变化为3 * n + 1;这样变化最后一定会以1结尾,为此,我们得到一个转换序列。
现在我们要做的就是在1~1000000之间,找到一个数,该数字对应的转换序列的长度最长。
二 代码解决
2.1 首先我们最先想到的办法就是直接递归求解,代码如下 (有错) :
#include <iostream>
using namespace std;
#define MAX_N 1000000
//得到数字n对应的序列长度
int get_len(int n) {
if (n == 1) return 1;
else if (n & 1) return get_len(3 * n + 1) + 1; //n为奇数
else return get_len(n >> 1) + 1; //n为偶数
}
int main() {
int num = 0, len = 0;
for (int i = 1; i <= MAX_N; i++) {
int tmp = get_len(i);
if (len >= tmp) continue;
num = i; len = tmp;
}
printf("num = %d, len = %d\n", num, len);
return 0;
}
但是运行程序时出现segment fault 也就是段错误,为此我们对程序进行分析
首先出现segment fault大致是因为出现以下四种错误:
1. 访问下标超过数组的大小,从而进行了非法访问或者是非法修改
2. 野指针或者是空指针的乱用
3. 变量大小超过变量类型所能表示的范围,像常见的爆int等等
4. 递归函数 递归的层数过深,报栈
分析程序我们不难发现,get_len函数中,当n为奇数时,下一次的变化为:3n + 1,也就变为偶数(原来的n为奇数,我们设为其2m + 1,所以变化后为3(2m + 1) + 1 = 6m + 4 = 2(3m + 2),所以一定为偶数),再下一次的变换为 (3n + 1) / 2,此时我们无法判度 (3n + 1) / 2 的奇偶性质,但可以确定的是 (3n + 1) / 2 相对于原来的n一定是变大的,若此时 (3n + 1) / 2为奇数,那么下一次的变化它将会再一次的乘3加1;如此的循环,可能某一个数字会变的越来越大,最后超过整形int所能表示的范围。
为此我们对程序进行修改,仅需将get_len()函数中参数的类型,修改成long long 即可,改动很小,因此就不再粘贴代码,对应运行结果截图如下:
2.2 记忆化搜索
我们现在求解数字13对应的序列长度,假设当数字变化到10时,若数字10对应的变换序列长度已知,那我们就不需要再向下求解了,直接返回结果,回到递归的上一层即可。
对应的代码为:
#include <iostream>
using namespace std;
#define MAX_N 1000000
typedef long long ll;
int keep[MAX_N + 5] = {0}; //记忆化数组,keep[i]表示数字i对应序列的长度
//得到数组n对应的序列长度
int get_len(ll n) {
if (n == 1) return 1;
if (n <= MAX_N && keep[n]) return keep[n];
int tmp = 0;
if (n & 1) tmp = get_len(3 * n + 1) + 1;
else tmp = get_len(n >> 1) + 1;
if (n <= MAX_N) keep[n] = tmp;
return tmp;
}
int main() {
int num = 0, len = 0;
for (int i = 1; i <= MAX_N; i++) {
int tmp = get_len(i);
if (len >= tmp) continue;
num = i; len = tmp;
}
printf("num = %d, len = %d\n", num, len);
return 0;
}
对应的结果显示如下:
显然在结果相同的情况下,采用记忆化搜索,程序运行的时间大大降低。
但是在这里,小编还是想说一说编程时遇到的BUG。
在程序的第12行,原来小编直接写的是if (keep[n]) return keep[n]; 第16行,直接写的是keep[n] = tmp; 此时程序报错还是segment fault。 仔细观察发现,这是因为数组的越界非法访问造成的
(虽然说起来不值得一提,但是小编当时就是没想到想到这一点,找了好久。。。。。)
三 记忆化数组大小对程序运行速率的影响
上面我们说到,增加记忆化数组后,程序运行的速率大大提高,但是程序运行的效率和记忆化数组的大小是否有关系呢?
先不急着下结论,我们试验便知。
我们在程序中添加宏定义#define arraySize 100 也就是先将数组的大小变化为100,此时我们看程序运行的速度:
#include <iostream>
using namespace std;
#define arraySize 100
#define MAX_N 1000000
typedef long long ll;
int keep[arraySize + 5] = {0}; //记忆化数组,keep[i]表示数字i对应序列的长度
//得到数组n对应的序列长度
int get_len(ll n) {
if (n == 1) return 1;
if (n <= arraySize && keep[n]) return keep[n];
int tmp = 0;
if (n & 1) tmp = get_len(3 * n + 1) + 1;
else tmp = get_len(n >> 1) + 1;
if (n <= arraySize) keep[n] = tmp;
return tmp;
}
int main() {
int num = 0, len = 0;
for (int i = 1; i <= MAX_N; i++) {
int tmp = get_len(i);
if (len >= tmp) continue;
num = i; len = tmp;
}
printf("num = %d, len = %d\n", num, len);
return 0;
}
对应运行的速率为(arraySize = 100):
此时我们增加数组的大小,将其修改为1000 (arraySize = 1000),对应的运行效率为:
这样一直增加数组的大小,当arraySize = 1000000,此时程序运行速率为:
观察我们可以发现:当记忆化数组的大小越大,记录的有效信息越多,此时程序运行的效率也就越快。但真的是这样吗????
此时我们再次增加数组的大小 arraySize = 10000000, 此时程序运行效率如下:
此时我们会发现,程序运行的速率不仅没有增加,反而下降了,这是为何????
其实这涉及到计算机操作系统的底层 (重要!!!)
我们都知道,我们的程序不论是变量还是函数,都在内存中,而我们计算机的中央处理单元是CPU,CPU处理的数据来自于内存,但是内存和CPU的速率是不匹配的,为了弥补这种不匹配,我们引入了cache缓存 了解cache概念,请点击我 ,其中cache一般分为三级缓存,从第一级到第三级,速度越来越小,容量越来越大,但即使是最大的三级缓存容量也为8MB(不同计算机不一定相同)在本程序中,若数组太大,当从内存加载到cache中时,此时会存在页面置换 (一个页面无法包含所有的数组元素,若当前cache中没有我要的数据,命中失败,此时只好将当前页调出,将新的页面置换进来)(内存到cache中的页面置换和硬盘到内存中的页面置换是类似的 有:FIFO先来先服务,clock时间片轮转法,Optimal优先级算法等等),这样的页面置换,反而会浪费计算机的运行时间。
因此数组大到一定程度,程序运行的速率反而会下降。
四 题外话
因此,学好操作系统和计算机组成原理是多么的重要,只有理解了程序在计算机底层是如何运行的,我们才能够写出高效并且稳定的代码。
加油! 路漫漫其修远兮,吾将天天敲代码!