记忆化搜索以及记忆数组大小的影响

记忆化搜索以及记忆数组大小的影响

一 问题的引入

小编在回顾欧拉计划时遇到记忆化搜索问题,略有感触,感觉有必要和大家分享。
话不多说咱们直接上截图:
欧拉计划第14题
问题大意:
一个数,若为偶数,则它下一次将变化为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):
数组大小为100
此时我们增加数组的大小,将其修改为1000 (arraySize = 1000),对应的运行效率为:
数组大小为1000
这样一直增加数组的大小,当arraySize = 1000000,此时程序运行速率为:
数组大小为1000000
观察我们可以发现:当记忆化数组的大小越大,记录的有效信息越多,此时程序运行的效率也就越快。但真的是这样吗????

此时我们再次增加数组的大小 arraySize = 10000000, 此时程序运行效率如下:
数组大小为10000000
此时我们会发现,程序运行的速率不仅没有增加,反而下降了,这是为何????

其实这涉及到计算机操作系统的底层 (重要!!!)
我们都知道,我们的程序不论是变量还是函数,都在内存中,而我们计算机的中央处理单元是CPU,CPU处理的数据来自于内存,但是内存和CPU的速率是不匹配的,为了弥补这种不匹配,我们引入了cache缓存 了解cache概念,请点击我 ,其中cache一般分为三级缓存,从第一级到第三级,速度越来越小,容量越来越大,但即使是最大的三级缓存容量也为8MB(不同计算机不一定相同)在本程序中,若数组太大,当从内存加载到cache中时,此时会存在页面置换 (一个页面无法包含所有的数组元素,若当前cache中没有我要的数据,命中失败,此时只好将当前页调出,将新的页面置换进来)(内存到cache中的页面置换和硬盘到内存中的页面置换是类似的 有:FIFO先来先服务,clock时间片轮转法,Optimal优先级算法等等),这样的页面置换,反而会浪费计算机的运行时间。
因此数组大到一定程度,程序运行的速率反而会下降。

四 题外话

因此,学好操作系统和计算机组成原理是多么的重要,只有理解了程序在计算机底层是如何运行的,我们才能够写出高效并且稳定的代码。

加油! 路漫漫其修远兮,吾将天天敲代码!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值