动态规划第二篇:斐波那契数 + 爬楼梯 + 使用最小花费爬楼梯

190 阅读4分钟

文章目录

509. 斐波那契数

斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给你 n ,请计算 F(n) 。

示例 1:

输入:2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1
示例 2:

输入:3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2
示例 3:

输入:4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3

动态五部曲:

  1. 确定dp数组以及下标的含义
    dp[i]的定义为:第i个数的斐波那契数值是dp[i]
  2. 确定递推公式
    题目已经把递推公式直接给我们了:状态转移方程 dp[i] = dp[i - 1] + dp[i - 2];
  3. dp数组如何初始化
    题目中把如何初始化也直接给我们了,如下:
    dp[0] = 0;
    dp[1] = 1;
  4. 确定遍历顺序
    从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的
  5. 举例推导dp数组
    按照这个递推公式dp[i] = dp[i - 1] + dp[i - 2],
class Solution {
    public int fib(int n) {
        if(n==0) return 0;
        if(n==1) return 1;
        int[] dp = new int[n+1];
        dp[0] = 0;
        dp[1] = 1;
        for (int i = 2; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }
}

简化,第一,n<=1情况简化;第二,因为最后仅返回的数组的一个元素,不是整个dp数组,所以可以简化,去掉dp数组。

class Solution {
    public int fib(int n) {
        if(n<=1) return n;
        int dp1=0;
        int dp2=1;
        for (int i = 2; i <= n; i++) {
            int temp = dp2;
            dp2= dp1+dp2;  // dp2的数值发生改变
            dp1 = temp;  // dp1的数值发生改变
        }
        return dp2;  // 最后一个元素
    }
}

70. 爬楼梯(和 斐波那契数 的动态方程一模一样)

题目描述:

分析:
入门dp,状态转移方程为:初始赋值好后,dp[i]=dp[i-1]+dp[i-2];

 public int climbStairs(int n) {
     if(n<3)return n;  // 动态规划的初始值处理方法
	 int dp[]=new int[n+1]; // 因为dp[0]不用,用dp[1]表示一个台阶方案数,所以用dp[n]表示n个台阶方案数,所以新建数组长度为n+1
	 dp[1]=1;   // 从1开始,所以到n 如果从0开始,就是到n-1了
	 dp[2]=2;
	 for(int i=3;i<=n;i++)  // 遍历3到n
	 {
		 dp[i]=dp[i-1]+dp[i-2];
	 }
	 return dp[n];
 }

本题思想:
如果只有一个阶梯,输出方案为一种;
如果有两个阶梯,输出方案有两种 1阶+1阶 直接2个台阶;
如果有三个阶梯,输出方案有三种 1阶+1阶+1阶 2阶+1阶 1阶+2阶;
如果有三个阶梯,输出方案有五种,2阶 + 两个阶梯(两种),1阶 + 三个阶梯(三种);
核心在于:如果n阶梯, 2阶 + (n-2)个阶梯,1阶 + (n-1)个阶梯;

也可以用dp数组 从0到n-1

class Solution{
    public int climbStairs(int n){
        if(n<3)return n;  // 动态规划的初始值处理方法
        int dp[]=new int[n]; // 因为dp[0]不用,用dp[1]表示一个台阶方案数,所以用dp[n]表示n个台阶方案数,所以新建数组长度为n+1
        dp[0]=1;   // 从1开始,所以到n 如果从0开始,就是到n-1了
        dp[1]=2;
        for(int i=2;i<n;i++)  // 遍历3到n
        {
            dp[i]=dp[i-1]+dp[i-2];
        }
        return dp[n-1];
    }
}

另外,本题还可以使用两个变量替代数组去优化空间,将O(n)的空间复杂度都去掉,代码如下:

class Solution {
    public int climbStairs(int n) {
       if(n<=3) return n;  // 动态规划的初始值处理方法
       int dp1=1;
       int dp2=2;
       for(int i=3;i<=n;i++){
           int temp=dp2;
           dp2 = dp2 + dp1;
           dp1=temp;
       }
       return dp2;  // 最后只要返回一个int数字,所以可以去掉dp数组
    }
}

其实,这个temp暂存空间也不需要,如下:

class Solution {
    public int climbStairs(int n) {
       if(n<=3) return n;    // 动态规划的初始值处理方法
       int dp1=1;
       int dp2=2;
       for(int i=3;i<=n;i++){
           dp2 = dp2 + dp1;
           dp1 = dp2 - dp1;   // 修改后dp2减去dp1就得到原有的dp2,存放到dp1变量中
       }
       return dp2;  // 最后只要返回一个int数字,所以可以去掉dp数组
    }
}

746. 使用最小花费爬楼梯

这种题目中带一个 最 字的让人认为是贪心,其实这里是动态规划,理由是 你就可以选择向上爬一个阶梯或者爬两个阶梯

数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。

每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯(动态规划)

请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯(补充条件)

示例 1:

输入:cost = [10, 15, 20]
输出:15
解释:最低花费是从 cost[1] 开始,然后走两步即可到阶梯顶,一共花费 15 。
示例 2:

输入:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出:6
解释:最低花费方式是从 cost[0] 开始,逐个经过那些 1 ,跳过 cost[3] ,一共花费 6 。

现在的dp数组不是方案数,而是花费成本
dp[i] 表示到达第 i 个位置,需要花费的最小成本
dp[i-1] 表示到达第 i-1 个位置,需要花费的最小成本
dp[i-2] 表示到达第 i-2 个位置,需要花费的最小成本

dp[i] = Math.min(dp[i-1]+cost[i],dp[i-2]+cost[i])

dp[i] = Math.min(dp[i-1],dp[i-2])+cost[i]

注意这里为什么是加cost[i],而不是cost[i-1],cost[i-2]之类的,因为题目中说了:每当你爬上一个阶梯你都要花费对应的体力值。cost[i]是从 i-1 或者 i-2 达到 i 位置需要花费的成本,所以当然这里是加cost[i]

要到达 i 位置,只有两条路劲,要么经过 i-1 ,要么不经过 i-1 ,就是这样简单的一个 yes/no 的回答。

for循环顺序:因为是模拟台阶,而且dp[i]又dp[i-1]dp[i-2]推出,所以是从前到后遍历cost数组就可以了。

至于 “在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯” 这个怎么模拟?
这个不用模拟,就是跳一步或者跳两步的意思,从 -1 下标跳一步到 0 ,需要花费cost[0],从-1下标条两步到 1,需要花费 cost[1]。

动态规划代码:

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int[] dp=new int[cost.length];
        dp[0]=cost[0];dp[1]=cost[1];  // 初始化??
        for (int i=2;i<cost.length;i++){
            dp[i]=Math.min(dp[i-1],dp[i-2])+cost[i];
        }
        // 这里不会直接返回dp[cost.length-1],而是取倒数第一步,第二步的最少值
        return Math.min(dp[cost.length-1],dp[cost.length-2]);  
    }
}

因为cost数组是从0到n-1的,所以dp数组就跟着从0到n-1了