第十七节:动态规划详解之斐波那契数列(递归、记忆搜索、动态规划、状态压缩)

一. 动态规划详解

1. 定义

    动态规划(英语:Dynamic programming,简称DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。

    动态规划也是互联网大厂和算法竞赛中非常喜欢考察的一类题目:因为通过动态规划可以很好的看出一个人的思考问题的能力、逻辑的强度、程序和算法的设计等等。

    那么通过学习动态规划,可以提高算法设计和分析的能力,为解决复杂问题提供强有力的工具和思路。

 

2. 解题步骤

   动态规划的核心思想是“将问题划分为若干个子问题,并在计算子问题的基础上,逐步构建出原问题的解”

  步骤1:定义状态。

    ✓ 将原问题划分为若干个子问题,定义状态表示子问题的解,通常使用一个数组或者矩阵来表示。 let dp:number[]=[];

  步骤2:确定状态转移方程。

    ✓ 在计算子问题的基础上,逐步构建出原问题的解。

    ✓ 这个过程通常使用“状态转移方程”来描述,表示从一个状态转移到另一个状态时的转移规则。

  步骤3:初始化状态。

  步骤4:计算原问题的解(最终答案)。

    ✓ 通过计算状态之间的转移,最终计算出原问题的解。

    ✓ 通常使用递归或者迭代(循环)的方式计算

 注:其中步骤2 和 步骤3 在代码中的位置可能颠倒

 

二. 斐波那契数列-递归

1. 题目说明

    第 0 个和第 1 个斐波那契数分别为0和1,即 F0 = 0, F1 = 1。   (也有一种说法,F0=1)

    从第 2 个数开始,每个斐波那契数都是它前面两个斐波那契数之和,即F2 = F0 + F1,F3 = F1 + F2,F4 = F2 + F3,以此类推。

    求第 n 个数列?

 

2. 方案1-递归求解

(1). 思路分析

    A. 确认递归结束的条件 n=0 或 1 结束

    B. 确认递归公式  fib(n)=fib(n-1)+fib(n-2)

function fib(n: number): number {
	// if (n === 0) return 0;
	// if (n === 1) return 1;

	//上述两句可以简洁写法
	if (n <= 1) return n;
	return fib(n - 1) + fib(n - 2);
}

//测试
// 0 1 1 2 3 5 8 13 21 34 55
{
	const startTime2 = performance.now();
	console.log(fib(10));
	const endTime2 = performance.now();
	console.log(`fib(10)消耗的时间:${endTime2 - startTime2}毫秒`);
}
{
	const startTime2 = performance.now();
	console.log(fib(50)); //很慢
	const endTime2 = performance.now();
	console.log(`fib(50)消耗的时间:${endTime2 - startTime2}毫秒`);
}

(2). 存在的问题

    存在大量的重复计算,比如 fib(9) 需要计算8和7; fib(8) 需要计算7和6,  很明显 7计算重复,随着数量越来越大,重复的数据也越来越多

(3). 性能测试

    fib(10)  2毫秒

    fib(50)  121651毫秒

 

三. 记忆搜索

1. 含义

  记忆化搜索(Memoization)的技巧,将已经计算过的结果保存下来,以便在后续的计算中直接使用。

  记忆化搜索可以极大地提高递归算法的效率,特别是对于有大量重复计算的问题,优化效果尤为明显。 这种解法也可以称之为自顶向下的解法。

 

2. 思路分析

  之前方案1中存在大量的重复计算,导致计算缓慢,所以这里采用记忆化搜索,

  在递归的基础上,将算好数列值存放到数组中,无须重复计算,直接从数组中获取即可

  核心代码:dp[n] = fib(n - 1, dp) + fib(n - 2, dp);  调用的时候必须传入dp,从而保证至始至终是同一个dp。


/**
 * 方案2
 * @param n 求n个数列
 * @param dp 存放的记忆数组
 * @returns 返回第n个数列的值
 */
function fib(n: number, dp: number[] = []): number {
	//上述两句可以简洁写法
	if (n <= 1) return n;
    
	//直接从dp中取值
	if (dp[n]) {
		return dp[n];
	}
	//dp数组中不存在,则递归计算
	dp[n] = fib(n - 1, dp) + fib(n - 2, dp);

	return dp[n];
}

//测试
// 0 1 1 2 3 5 8 13 21 34 55
{
	const startTime2 = performance.now();
	console.log(fib(10));
	const endTime2 = performance.now();
	console.log(`fib(10)消耗的时间:${endTime2 - startTime2}毫秒`);
}
{
	const startTime2 = performance.now();
	console.log(fib(50)); //很快   值:12586269025
	const endTime2 = performance.now();
	console.log(`fib(50)消耗的时间:${endTime2 - startTime2}毫秒`);
}

 

3. 性能测试

    fib(10)  1毫秒内

    fib(50)  1毫秒内

 

四. 动态规划

1. 步骤划分详细分析

(1).定义状态

   dp数组保留斐波那契数列中每一个位置对应的值(状态)

   dp[x]表示的就是x位置对应的值(状态)

(2).设置初始化状态

   dp[0]/dp[1]初始化状态

(3).状态转移方程

   dp[i] = dp[i-1] + dp[i-2];  状态转移方程一般情况都是写在循环(for/while)中

(4).计算最终的结果

function fib(n: number): number {
	//1.定义状态
	let dp: number[] = [];

	//2. 初始化状态
	dp[0] = 0;
	dp[1] = 1;

	//3. 确认状态转换方程
	for (let i = 2; i <= n; i++) {
		dp[i] = dp[i - 1] + dp[i - 2];
	}

	//4.求的最终结果
	return dp[n];
}

/**
 * 测试
 * 0 1 1 2 3 5 8 13 21 34 55
 */
{
	const startTime2 = performance.now();
	console.log(fib(10));
	const endTime2 = performance.now();
	console.log(`fib(10)消耗的时间:${endTime2 - startTime2}毫秒`);
}

{
	const startTime2 = performance.now();
	console.log(fib(50)); //很快   值:12586269025
	const endTime2 = performance.now();
	console.log(`fib(50)消耗的时间:${endTime2 - startTime2}毫秒`);
}

 

2. 性能测试

   fib(10)  1毫秒内

   fib(50)  1毫秒内

 

3. 总结

   动态规划算法可以看作是记忆化搜索的一种扩展,它通常采用自底向上的方式计算子问题的结果,并将结果保存下来以便后续的计算使用

   对于斐波那契数列问题来说,我们采用自底向上的方式计算子问题的结果,确保 dp[i-1] 和 dp[i-2] 的值已经计算出来了,才能计算 dp[i] 的值。

   可以继续优化的点:dp数组,如果n很大,dp中占用的内存空间则很大,能不能优化一下呢

 

五. 状态压缩

1. 含义

   在动态规划算法中,有一种常见的优化方法叫做状态压缩,可以将状态的存储空间从数组优化为一个常数.

2. 步骤分析

(1). 定义状态

    变量pre用来保存前一个值,current保存当前值

(2). 设置初始化状态

    pre 和 current 初始化值

(3). 状态转移方程

   let res = current + pre; 然后重新给 pre和current赋值

(4). 计算最终的结果

  current即为最终的值

function fib(n: number): number {
	if (n <= 1) return n;

	//1.定义状态 和 2. 初始化状态
	let pre = 0;
	let current = 1;

	//3. 确认状态转换方程
	for (let i = 2; i <= n; i++) {
		let res = current + pre;
		pre = current;
		current = res;
	}

	//4.求的最终结果
	return current;
}

3. 性能测试

   fib(10)  1毫秒内

   fib(50)  1毫秒内

 

4. 总结

  采用两个变量实现和前面的动态规划实现相比,减少了存储空间的使用,优化了空间复杂度

 

 

 

 

!

  • 作       者 : Yaopengfei(姚鹏飞)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
  • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2024-02-20 11:19  Yaopengfei  阅读(8)  评论(1编辑  收藏  举报