最长考拉兹序列

2024-06-24 05:12
文章标签 考拉 序列 最长

本文主要是介绍最长考拉兹序列,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

  • 题目: 

考虑如下定义在正整数集上的迭代规则: 

n  \rightarrow  n/2 (若n为偶数)

n  \rightarrow  3n+1 (若n为奇数)

从13开始,可以迭代生成如下的序列:

        13 \rightarrow 40 \rightarrow 20 \rightarrow 10 \rightarrow 5 \rightarrow 16 \rightarrow 8 \rightarrow 4 \rightarrow 2 \rightarrow 1

可以看出这个序列(从13开始到1结束)共有10项。 尽管还未被证明,但普遍认为,从任何数开始最终都能抵达1并结束, 这被称为 “考拉兹序列”。

在小于100万的数中,从哪个数开始迭代生成的序列最长?

注:在迭代过程中允许出现超过一百万的项(例如,题目中的序列,第二项40就比第一项13大)。

  • 题目分析:

根据题目描述,我们可以知道这是一道枚举类型的题,

  • 代码实现
//代码段1
#include <stdio.h>#define MAX_N 1000000int cal_len(int n){if(n == 1) return 1;if(n & 1) return cal_len(3*n +1)+1;return cal_len(n>>1)+1;    
}int main(){int ans = 0, len = 0;for(int i = 1;i <= MAX_N;i++){int temp_len = cal_len(i);if(temp_len <= len) continue;len = temp_len;ans = i;}printf("ans = %d, len = %d\n",ans, len);return 0;
}

小技巧:

在函数 int cal_len(long long n) 中,判断当前n值是否为偶数,没有使用模运算 if(n % 2 ) 而是使用了 位运算 if(n & 1 ) ,  这是因为位运算的运算效率更快 ,(n& 1) 为真值,说明n是奇数。 同样在,n/2 运算也写成了位运算 n >> 1.

  • 运行结果

  • 运行结果分析

程序报错崩溃了,这个报错结果表示“访问了非法内存”,造成这个报错的原因可以有以下几种情况:

1)访问未被分配或已释放的内存区域;

2)使用空指针或野指针进行读写操作;

3)数组越界访问,例如通过错误的下标访问数组元素;

4)非法指针操作,如未经授权的指针转换或解引用非合法的内存地址;

5)堆栈溢出,通常由于局部变量过大或递归调用过深导致;

6)多线程编程中使用了线程不安全的函数或未加锁保护共享资源;

7)对常量或只读内存区域的非法写入操作。

如何避免出现这个报错?

1)正确的使用指针;

确保所有指针在使用前都进行了初始化,并在不再需要时及时释放;避免使用野指针,特别是不要随意假设指针指向的内存是可用的。

2)数组的访问

不要出现数组的越界访问,例如通过错误的下标访问数组元素;

3)动态内存管理

在程序员自己进行内存分配时,确保内存分配后要有对应的释放操作,避免出现内存泄漏;

4)多线程环境

如果使用了多线程编程,要合理使用互斥锁或其他同步机制来保护共享资源。

经过上述分析,我们进一步分析代码段1出现崩溃的原因可能是什么,代码中使用了递归调用,会不会是递归调用的层数太深,栈中装不下了呢?但是在cal_len 函数中,即使递归层数比较深,每一次进行函数调用所需要的空间仅是一个整型值再加上一个函数指针的大小,所以,我们猜测他不是因为空间上存不下了,那会不会是函数本身陷入了死循环?这个函数如果想结束,需要满足条件 n==1, 如果这个条件不会出现呢?那就进入死循环。n在变化的过程中,超出了int 所能表示的范围,成为了负数,就会出现这个结果。我们来验证一下, n是否出现了负数。

//代码段2
#include <stdio.h>#define MAX_N 1000000int cal_len(int n){if(n == 1) return 1;if(n < 0) printf("n < 0 ,error");if(n & 1) return cal_len(3*n +1)+1;return cal_len(n>>1)+1;    
}int main(){int ans = 0, len = 0;for(int i = 1;i <= MAX_N;i++){int temp_len = cal_len(i);if(temp_len <= len) continue;len = temp_len;ans = i;}printf("ans = %d, len = %d\n",ans, len);return 0;
}

运行结果:

运行结果显示,果然出现了负数,说明数据溢出了,即当前的int类型存不下实际数据的长度,应该改为long long 类型。

//代码段3
#include <stdio.h>#define MAX_N 1000000int cal_len(long long n){if(n == 1) return 1;if(n & 1) return cal_len(3*n +1)+1;return cal_len(n>>1)+1;    
}int main(){int ans = 0, len = 0;for(int i = 1;i <= MAX_N;i++){int temp_len = cal_len(i);if(temp_len <= len) continue;len = temp_len;ans = i;}printf("ans = %d, len = %d\n",ans, len);return 0;
}

运行结果:

  • 代码优化:

代码段3运行时间:

现在将数据规模由100万增加到1000万:

结果显示,运行时间也增加了10倍多,可见这段代码的耗时是较长的,提高运行时间就是我们优化的目标,那么,这段这段代码为什么会如此耗时?

分析递归调用的过程:

如果当前遍历到 i = 10, 那么我们得到的迭代序列是:

10 \rightarrow 5 \rightarrow 16 \rightarrow 8 \rightarrow 4 \rightarrow 2 \rightarrow 1, len = 7;

如果当前遍历到 i = 13, 得到的迭代序列是:

13 \rightarrow 40 \rightarrow 20 \rightarrow 10 \rightarrow 5 \rightarrow 16 \rightarrow 8 \rightarrow 4 \rightarrow 2 \rightarrow 1, len = 10;

这两段迭代过程是有重复的,当i= 13 时,序列10 之后的迭代过程在 i = 10 时已经计算过了,所以如果i = 13时, len = 1 + 1+ 1 + 7 就可以得出结果,所需要的前期操作只是把 i=10 时的len值记录保存下来。

这个将重复计算过程值保存下来的思路,被称为记忆化

总结:

1)将中间的计算结果保存下来,减少后续计算中的重复计算;

2)这种技巧常常被用在搜索算法中,称为“记忆化搜索”。

例如: 

先前计算:10 \rightarrow 5 \rightarrow 16 \rightarrow 8 \rightarrow 4 \rightarrow 2 \rightarrow 1

记忆化:f(10) = 7, f(5) = 6, f(16) = 5, ... f(1) = 1;

之后计算:13 \rightarrow 40 \rightarrow 20 \rightarrow 10 \rightarrow 5 \rightarrow 16 \rightarrow 8 \rightarrow 4 \rightarrow 2 \rightarrow 1

直接得到:f(20) = 1+f(10) = 1+7 = 8; f(40) = 1+1+f(10) = 9, f(13) = 1+1+1+f(10) = 10, 少做了6次计算。

  • 代码优化思路

1. 定义一个数据结构 int keep[M] , 用于存储数据范围M之内的序列长度, 即keep[i]  =  i 的序列长度,(i<M);

2.  当遍历值 n < M 时, len(n) =  keep[n];

3.  当遍历值 n > M 时,len(n) = 1+1+ ... + len(i) ; i<M;

  •  代码优化实现:
//代码段4
#include <stdio.h>#define MAX_N 1000000
#define MAX_M 10000int keep[MAX_M+5] = {0};int cal_len(long long n){if(n == 1) return 1;if(n <= MAX_M && keep[n]) return keep[n];int ret = ((n & 1) ? cal_len(3*n+1):cal_len(n>>1))+1;if(n <= MAX_M) return keep[n];return ret;
}int main(){int ans = 0, len = 0;for(int i = 1;i< MAX_N;i++){int temp_len = cal_len(i);if(temp_len <= len) continue;ans = i;len = temp_len;}printf("ans = %d, len = %d\n",ans, len);return 0;
}

  运行结果:

通过计算结果,运行时间果然是缩短了, 目前代码中的keep数组大小是10000,现在将数据规模增加到1000万,keep数据的大小不变:

 可以看到运行时间原来的约13秒提高到约5秒。

  • 优化结果分析

那么keep数组的大小设计为多大合适,是不是越大越好呢?

设: MAX_N = 100 0000

测试1: MAX_M = 10 0000

运行时间由 0.367s 提高到 0.164 s

测试2:MAX_M = 100 0000

 运行时间由0.164s 提高到0.032s

测试3:MAX _M = 1000 0000

运行时间由0.032s 增加到 0.082s

结论:

增加keep数组的大小是可以提高程序的运行时间的,但keep数组不是越大越好,因为keep是用于搜索查询的,而数据在计算机中是按页存储的,,如果keep数组太大,那么将会开辟多个页用于存储数据,在搜素查询所需数据时,就会将时间用在页面切换上,反而增加了时间的开销。

  • 题目总结:

1. 在题目中出现迭代,我们可以考虑递归思想的应用;

2. "segmentation fault" 报错的原因分析。

3. 当出现大量重复计算的时候,考虑“记忆化” 这个优化技巧。

这篇关于最长考拉兹序列的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1089258

相关文章

时序预测 | MATLAB实现LSTM时间序列未来多步预测-递归预测

时序预测 | MATLAB实现LSTM时间序列未来多步预测-递归预测 目录 时序预测 | MATLAB实现LSTM时间序列未来多步预测-递归预测基本介绍程序设计参考资料 基本介绍 MATLAB实现LSTM时间序列未来多步预测-递归预测。LSTM是一种含有LSTM区块(blocks)或其他的一种类神经网络,文献或其他资料中LSTM区块可能被描述成智能网络单元,因为

代码随想录——摆动序列(Leetcode376)

题目链接 贪心 class Solution {public int wiggleMaxLength(int[] nums) {if(nums.length <= 1){return nums.length;}// 当前一对差值int cur = 0;// 前一对差值int pre = 0;// 峰值个数int res = 1;for(int i = 0; i < nums.length -

想让Python序列切片更高效?这些技巧你不可不知!

目录 1、自定义类实现切片 🍏 1.1 实现__getitem__方法 1.2 支持正负索引与步长 2、利用 collections.abc 模块 🧠 2.1 继承MutableSequence类 2.2 重写关键方法 3、使用标准库itertools.slice 🍲 3.1 itertools工具介绍 3.2 slice函数应用实例 4、通过生成器实现动态切片 🌀

leetcode刷题(97)——106. 从中序与后序遍历序列构造二叉树

根据一棵树的中序遍历与后序遍历构造二叉树。 注意: 你可以假设树中没有重复的元素。 例如,给出 中序遍历 inorder = [9,3,15,20,7]后序遍历 postorder = [9,15,7,20,3] 返回如下的二叉树: 3/ \9 20/ \15 7 看下后序和中序遍历的框架: void traverse(TreeNode root) {trave

leetcode刷题(97)——105. 从前序与中序遍历序列构造二叉树

根据一棵树的前序遍历与中序遍历构造二叉树。 注意: 你可以假设树中没有重复的元素。 例如,给出 前序遍历 preorder = [3,9,20,15,7]中序遍历 inorder = [9,3,15,20,7] 返回如下的二叉树: 3/ \9 20/ \15 7 1.先回顾前序遍历和中序遍历的框架: void traverse(TreeNode root) {//

根据序列:2/1,3/2,5/3,...生成前30项打印出并求和

根据斐波那契数列 def fab(max): def fib_loop_while(max):max = maxa, b = 0, 1while max &

biostar handbook(五)|序列从何而来和质量控制

测序仪 2017年一篇发表在Nature的综述"DNA sequencing at 40: past, present and future"介绍了DNA测序这40年的发展历程。1976年,Sanger和Coulson同时发表了2种方法用于对上百个DNA碱基进行解码,这就是第一代测序技术。到了2005年,罗氏的454平台揭开了高通量测序的序幕,后面则是SOLiD,454和Illumina三方对抗

day16--513.找树左下角的值+112. 路径总和+106.从中序与后序遍历序列构造二叉树

一、513.找树左下角的值 题目链接:https://leetcode.cn/problems/find-bottom-left-tree-value/ 文章讲解:https://programmercarl.com/0513.%E6%89%BE%E6%A0%91%E5%B7%A6%E4%B8%8B%E8%A7%92%E7%9A%84%E5%80%BC.html 视频讲解:https://www

c#编程:有一个分数序列,2/1,3/2,5/3,8/5,13/8,21/13....找出数列的规律并求出其前30项的和

using System;using System.Collections.Generic;using System.Linq;using System.Text;//有一个分数序列,2/1,3/2,5/3,8/5,13/8,21/13....找出数列的规律并求出其前30项的和namespace ans1{class Program{static void Main(string[]

“序列优化探究:最长上升子序列的算法发现与应用“

最长上升子序列 最长上升子序列是指在一个给定序列中,找到一个最长的子序列,使得子序列中的元素单调递增。例如,序列 [1, 3, 5, 4, 7] 的最长上升子序列是 [1, 3, 5, 7],长度为4。 这是一个经典的动态规划问题。 假设dp[i]表示以第i个元素为结尾的最长上升子序列的长度。 可以用一个嵌套循环来遍历所有的元素对,如果前一个元素小于后一个元素,则可以将后一个元素添加到