滚动数组-动态规划(跳跃训练-费波那契数列)

跳跃训练:

今天的有氧运动训练内容是在一个长条形的平台上跳跃。平台有 num 个小格子,每次可以选择跳 一个格子 或者 两个格子。请返回在训练过程中,学员们共有多少种不同的跳跃方式。

结果可能过大,因此结果需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

示例 1:

输入:n = 2
输出:2

示例 2:

输入:n = 5
输出:8

提示:

  • 0 <= n <= 100

我的解法:动态规划

这个问题和典型的“爬楼梯”问题非常相似。每次可以选择跳一个格子或两个格子,因此,求解的方式可以通过动态规划来解决。

假设:

  • dp[i] 表示跳到第 i 个格子的不同跳跃方式数。
  • 如果我们站在第 i 个格子,可以从第 i-1 个格子跳一步到达,或者从第 i-2 个格子跳两步到达。因此,状态转移方程为:
  • dp[i]=dp[i−1]+dp[i−2]
  • 其中 dp[1] = 1(只有一种方式跳到第一个格子),dp[2] = 2(跳到第二个格子可以选择跳一步+一步,或者直接跳两步)。

为了避免结果过大,题目要求我们在每次计算时对结果取模 1e9+7

class Solution {
    public int trainWays(int num) {
        int MOD = 1000000007; // 定义取模数
        if(num==0){
            return 1;
        }
        
        if (num == 1) {
            return 1;
        }
        if (num == 2) {
            return 2;
        }
        
        int[] dp = new int[num]; // 动态规划数组
        dp[0] = 1;
        dp[1] = 2;
        
        for (int i = 2; i < num; i++) {
            dp[i] = (dp[i - 1] + dp[i - 2]) % MOD; // 每次计算都要取模
        }
        
        return dp[num - 1]; // 返回最终结果
    }
}

滚动数组解法:

class Solution {
    public int trainWays(int num) {
        int MOD = 1000000007;
        
        // 基本情况
        if (num == 1) {
            return 1;
        }
        if (num == 2) {
            return 2;
        }
        
        // 动态规划初始状态
        int prev2 = 1;  // 相当于 dp[1]
        int prev1 = 2;  // 相当于 dp[2]
        
        // 从第3个格子开始递推
        for (int i = 3; i <= num; i++) {
            int current = (prev1 + prev2) % MOD;  // dp[i] = dp[i-1] + dp[i-2],取模
            prev2 = prev1;  // 更新 prev2
            prev1 = current;  // 更新 prev1
        }
        
        // 返回 dp[num],即 prev1 的值
        return prev1;
    }
}

滚动数组解法动态规划解法的核心思想相同,都是通过记录子问题的解来求解更大规模的问题。它们之间的主要区别在于 空间优化。让我们详细讨论它们的区别和滚动数组解法的局限性。

1. 动态规划解法

动态规划通常通过构建一个数组 dp[] 来存储中间状态。每个 dp[i] 代表到达第 i 个状态的解法数,最终我们通过递推得到 dp[num] 的解。

优点:
  • 逻辑简单,易于理解和实现。
  • 可以轻松处理复杂的问题状态和多维状态。
缺点:
  • 空间复杂度高:通常会使用一个大小为 n 的数组来存储中间结果(比如跳格子问题中的 dp[num]),即使有些中间状态在后续计算中不再需要,仍然会占用内存。

2. 滚动数组解法

滚动数组解法是动态规划的一种 空间优化 技巧。它的基本思想是,由于状态转移方程通常只依赖于前几项(例如斐波那契数列只依赖于前两项),所以我们不需要维护整个数组,只需要记录当前状态和前一个(或前两个)状态即可。

滚动数组解法的步骤:


当问题的状态转移方程简单且依赖的状态较少时,滚动数组解法非常适合。然而,对于依赖更多前置状态或复杂多维问题,滚动数组解法的应用局限性会显现出来。

  • 只保存几个必要的状态变量,省去不再使用的状态。
  • 比如在跳格子问题中,状态转移方程 dp[i] = dp[i-1] + dp[i-2] 只依赖于 dp[i-1]dp[i-2],所以我们只需要保存这两个值。
优点:
  • 空间复杂度低:滚动数组解法将空间复杂度从 O(n) 降低为 O(1),因为只存储了固定数量的状态,而不是所有的中间结果。
缺点(局限性):
  • 只适用于状态转移依赖较少的情况
  • 滚动数组只能优化那些状态转移方程依赖较少前置项的问题(如仅依赖 dp[i-1]dp[i-2])。
  • 如果问题的状态转移方程依赖于多个前置状态(如 dp[i] 可能依赖 dp[i-1], dp[i-2], dp[i-3] 等等),滚动数组的空间优化效果就会变差。
  • 不适用于复杂的多维动态规划问题
  • 如果是二维或多维动态规划问题(比如矩阵路径问题等),很难通过滚动数组来实现空间优化。因为每个状态的依赖项不仅限于前一行或者前一列,还可能是多个其他维度的数据,这种情况下,通常需要完整地保留整个状态数组。
  • 可读性稍差
  • 对于某些较为复杂的问题,滚动数组解法由于不断更新有限的几个状态变量,可能会使代码的可读性下降,因为我们需要时刻追踪变量之间的更新关系,而不像动态规划解法那样直观地维护一个数组。

总结

解法空间复杂度适用场景优点缺点
动态规划(DP)O(n)大多数动态规划问题逻辑清晰,易于理解,适用于多维问题空间复杂度高
滚动数组(空间优化)O(1)状态转移依赖少的动态规划问题(如斐波那契数列)空间复杂度低,节省内存只适用于状态依赖较少的情况,不适用复杂的多维问题

 题目链接:

斐波那契数列:

. - 力扣(LeetCode). - 备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。icon-default.png?t=O83Ahttps://leetcode.cn/problems/fibonacci-number/description/

跳跃训练:. - 力扣(LeetCode). - 备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。icon-default.png?t=O83Ahttps://leetcode.cn/problems/qing-wa-tiao-tai-jie-wen-ti-lcof/description/?envType=problem-list-v2&envId=59jEaTgw&difficulty=EASY

### 滚动数组动态规划中的应用及其实现方式 滚动数组是一种用于优化动态规划的空间复杂度的技术。其核心思想是利用当前状态仅依赖于有限数量的前序状态这一特性,从而避免存储整个 DP 数组,而只需保留必要的部分即可完成计算。 #### 1. **基本原理** 在许多动态规划问题中,`dp[i]` 的值通常只取决于 `dp[i-1]`, `dp[i-2]` 或更少的状态。因此,没有必要维护完整的 DP 数组,而是可以通过覆盖旧的状态来节省内存空间。这种技术被称为滚动数组[^2]。 --- #### 2. **实现方式** 以下是几种常见的滚动数组实现方式: ##### 方法一:使用布尔取模操作 (`&`) 这种方法适用于只需要两个连续状态的情况。通过位运算符 `&` 来交替更新两个变量的位置。例如,在 Fibonacci 序列中,可以用如下代码实现: ```c++ #define FIBONACCI_MAX 2 int dp[FIBONACCI_MAX] = {0}; int fibonacci(int n) { dp[0] = 1; dp[1] = 1; for (int i = 2; i <= n; ++i) { dp[i & 1] = dp[(i - 1) & 1] + dp[(i - 2) & 1]; } return dp[n & 1]; } ``` 上述代码中,`dp[i & 1]` 和 `dp[(i - 1) & 1]` 始终指向两个不同的位置,实现了对两个状态的循环更新[^3]。 --- ##### 方法二:单变量替换 如果每次迭代只需要上一次的结果,则可以进一步简化为单变量替代的方式。例如,Fibonacci 计算也可以这样实现: ```python def fibonacci(n): a, b = 1, 1 for _ in range(2, n + 1): a, b = b, a + b return b ``` 这种方式完全不需要额外的数组,直接用两个临时变量保存最近两次的结果[^4]。 --- ##### 方法三:多维度滚动数组 对于二维动态规划问题(如矩阵路径),同样可以采用滚动数组的思想。假设原问题是基于二维数组 `dp[m][n]` 完成的,但如果每一行的状态只依赖于上一行的状态,则可以将其压缩为两行或者一行的一维数组。例如,LeetCode 上的经典问题——最大子数组乘积,可以这样处理: ```cpp #include <vector> using namespace std; int maxProduct(vector<int>& nums) { int prevMax = nums[0], prevMin = nums[0], result = nums[0]; for (size_t i = 1; i < nums.size(); ++i) { int currentMax = max({nums[i], prevMax * nums[i], prevMin * nums[i]}); int currentMin = min({nums[i], prevMax * nums[i], prevMin * nums[i]}); result = max(result, currentMax); prevMax = currentMax; prevMin = currentMin; } return result; } ``` 这里通过保持局部的最大值和最小值来进行状态转移,无需显式的二维数组[^5]。 --- ### 总结 滚动数组的核心在于识别并丢弃不再需要的历史状态,同时确保能够正确访问所需的前驱状态。它不仅减少了空间开销,还可能提高程序运行效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值