HOW - DP 动态规划系列(一)

一、介绍

动态规划(Dynamic Programming,简称 DP)是一种解决多阶段决策问题的数学方法。

在计算机科学中,动态规划常用于解决具有重叠子问题最优子结构性质的问题,通过将问题分解成相互重叠的子问题,并以自底向上或自顶向下的方式进行求解,从而避免重复计算,提高算法效率。

动态规划的核心思想:利用之前已经求解过的子问题的解来求解当前问题,从而将问题的规模不断缩小,直到求解最终问题。

动态规划通常包括以下五个步骤:

  1. 定义状态:将问题抽象成一个状态空间,定义状态表示问题的子问题解集合。

  2. 确定状态转移方程:建立状态之间的递推关系,确定不同状态之间的转移规则。

  3. 初始化边界条件:确定最小规模问题的解,即边界条件。

  4. 自底向上或自顶向下求解:根据状态转移方程和边界条件,采用自底向上或自顶向下的方式求解问题。

  5. 返回结果:根据问题的要求,返回最终问题的解。

动态规划常见的应用包括:

  • 计数问题:如组合数、排列数等。
  • 路径问题:如最短路径、最短编辑距离等。
  • 最优化问题:如最长递增子序列、0-1 背包问题等。
  • 概率问题:如期望值、方差等。

动态规划是一种十分重要的算法思想,在算法设计与解决实际问题中都有广泛的应用。

二、由斐波那契数列入门动态规划

斐波那契数列(Fibonacci Sequence)是一个经典的数学问题,也是动态规划中的一个常见例子。

斐波那契数列是一个递归序列,定义如下:

第一个和第二个数为 1;从第三个数开始,每个数都是前两个数之和。数学表示为:F(1) = 1, F(2) = 1, F(n) = F(n-1) + F(n-2) (n > 2)

斐波那契数列的前几个数是:1, 1, 2, 3, 5, 8, 13, 21, …

斐波那契数列的递推关系是其特点之一,这种递推关系可以用递归或动态规划来求解。

递归方法通常会存在重复计算的问题,而动态规划则可以通过保存中间结果来避免重复计算。

递归法

比较简单:

function fibonacci(n) {
	if (n <= 2) {
		return 1;
	}
  return fibonacci(n - 1) + fibonacci(n - 2);
}
// 示例用法
console.log(fibonacci(1)); // 输出:1
console.log(fibonacci(2)); // 输出:1
console.log(fibonacci(5)); // 输出:5
console.log(fibonacci(10)); // 输出:55

这是斐波那契数列的基本解法,但我们今天的主角是动态规划。请往下看。

动态规划法

  1. 第一步,确定 dp[i] 含义【定义状态】

在做动态规划类的题目,都要定义一个一维或者二维的用来做状态转移的数组。

在斐波那契数列里,dp[i] 表示第 i 个斐波那契数的值。

  1. 第二步,确定递推公式【确定状态转移方程】

dp[i] = dp[i-1] + dp[i-2]

  1. 第三步,初始化【初始化边界条件】

dp[0] = 1, dp[1] = 1

  1. 第四步,确定遍历顺序【自底向上或自顶向下求解】

对于斐波那契数列,遍历顺序为自前向后。

  1. 第五步,打印 dp[] 数组【返回结果】

主要用来 debug

function fibonacci(n) {
	// 1. 确定 dp[i] 含义
	// 3. 初始化
    let dp = [0, 1]; 
    // 4. 确定遍历顺序
    for(let i = 2; i <= n; i++) {
    	// 2. 确定递推公式
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    // 5. 打印 dp[] debug
    console.log(dp);
    return dp[n];
}
// 示例用法
console.log(fibonacci(1)); // 输出:1
console.log(fibonacci(2)); // 输出:1
console.log(fibonacci(5)); // 输出:5
console.log(fibonacci(10)); // 输出:55

在这个实现基础之上,我们还可以将空间复杂的从 O(n) 降低到 O(1)。

var fibonacci = function(n) {
    // 动规状态转移中,当前结果只依赖前两个元素的结果,所以只要两个变量代替dp数组记录状态过程,就将空间复杂度降到 O(1)
    let pre1 = 1;
    let pre2 = 0;
    let temp;
    if (n === 0) return 0;
    if (n === 1) return 1;
    for(let i = 2; i <= n; i++) {
        temp = pre1;
        pre1 = pre1 + pre2;
        pre2 = temp;
    }
    return pre1;
};

三、由爬楼梯进一步巩固

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。

  • 1 阶 + 1 阶
  • 2 阶

示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。

  • 1 阶 + 1 阶 + 1 阶
  • 1 阶 + 2 阶
  • 2 阶 + 1 阶
function climbStairs(n) {
	// 1. 确定 dp[i] 含义:层数为 i,有多少种方法可以爬到层数 i
	// 2. 确定递推公式:dp[i] = dp[i-1] + dp[i-2]
	// 3. 初始化
	// 4. 确定遍历顺序
	// 5. 打印 dp[]
	// let dp = [1, 1];
	// for(let i = 2; i <= n; i++) {
	// 其实 dp[0] 是没有意义的
	let dp[1] = 1;
	let dp[2] = 2
	for(let i = 3; i <= n; i++) {
		dp[i] = dp[i - 1] + dp[i - 2];
	}
	console.log(dp);
	return dp[n];
}

// 示例用法
console.log(climbStairs(1)); // 输出:1
console.log(climbStairs(2)); // 输出:2
console.log(climbStairs(3)); // 输出:3
console.log(climbStairs(4)); // 输出:5
console.log(climbStairs(5)); // 输出:8

这道题目还可以继续深化,就是一步一个台阶,两个台阶,三个台阶,直到 m 个台阶,有多少种方法爬到 n 阶楼顶。这又有难度了,这其实是一个完全背包问题,在后续系列关于背包问题再进行分析。

四、总结

这就是动态规划的介绍和解题基本步骤,在后面的系列我们将由浅及深学习更复杂的场景。

最后,讲一下动态规划和贪心的区别,其实大家不用太强调理论上的区别,做做题,就感受出来了。

动态规划(Dynamic Programming,DP)和贪心算法(Greedy Algorithm)都是常见的算法思想,但它们有一些重要的区别:

  1. 重叠子问题
  • 动态规划:子问题可能会重叠,即在求解问题的过程中会重复计算相同的子问题。
  • 贪心算法:通常不涉及重叠子问题,每一步的决策都是基于当前局部最优解。
  1. 最优子结构性质
  • 动态规划:问题具有最优子结构性质,即问题的最优解可以通过子问题的最优解来求解。
  • 贪心算法:问题具有局部最优解性质,即每一步的局部最优解能够导致全局最优解。
  1. 解决的问题类型
  • 动态规划:适用于求解具有最优子结构性质的问题,如最长递增子序列、0-1 背包问题等。
  • 贪心算法:适用于求解具有贪心选择性质的问题,例如某些排序问题、最小生成树问题、部分背包问题等。
  1. 复杂度分析
  • 动态规划:通常需要使用额外的空间来存储子问题的解,时间复杂度通常为 O(n^2) 或 O(n*m),其中 n 和 m 是问题的规模。
  • 贪心算法:通常不需要额外的空间,时间复杂度通常为 O(n*log(n)) 或 O(n),其中 n 是问题的规模。
  1. 是否能够得到全局最优解
  • 动态规划:由于具有最优子结构性质,能够得到问题的全局最优解。
  • 贪心算法:通常只能得到局部最优解,并不能保证得到问题的全局最优解。

总的来说,动态规划通常用于求解具有最优子结构性质且子问题重叠的问题,能够得到问题的全局最优解;而贪心算法则通常用于求解具有贪心选择性质的问题,得到的是局部最优解,并不保证全局最优。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值