动态规划1:理论基础、斐波那契数、爬楼梯、使用最小花费爬楼梯、第一节总结

1. 动态规划理论基础

动态规划刷题大纲在这里插入图片描述

什么是动态规划

动态规划(Dynamic programming),简称DP。如果面对一个问题有很多重叠子问题,使用动规很有效。

所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。

举一个例子说明动规和贪心的区别。
例如:有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

动态规划中dp[j]是由dp[j-weight[i]]推导出来的,然后取max(dp[j], dp[j - weight[i]] + value[i])。

但如果是贪心呢,每次拿物品选一个最大的或者最小的就完事了,和上一个状态没有关系。

所以贪心解决不了动态规划的问题。

动规的解题步骤

1、确定dp数组(dp table)以及下标的含义
2、确定递推公式
3、dp数组如何初始化
4、确定遍历顺序
5、举例推导dp数组

一些同学可能想为什么要先确定递推公式,然后在考虑初始化呢?

因为一些情况是递推公式决定了dp数组要如何初始化!

其实 确定递推公式 仅仅是解题里的一步而已!

只知道递推公式,但搞不清楚dp数组应该如何初始化,或者正确的遍历顺序,以至于记下来公式,但写的程序怎么改都通过不了。

动态规划如何debug

看一下题解,感觉看懂了,然后照葫芦画瓢,如果能正好画对了,万事大吉,一旦要是没通过,就怎么改都通过不了,对 dp数组的初始化,递推公式,遍历顺序,处于一种黑盒的理解状态。

写动规题目,代码出问题很正常!
找问题的最好方式就是把dp数组打印出来,看看究竟是不是按照自己思路推导的!

一些同学对于dp的学习是黑盒的状态,就是不清楚dp数组的含义,不懂为什么这么初始化,递推公式背下来了,遍历顺序靠习惯就是这么写的,然后一鼓作气写出代码,如果代码能通过万事大吉,通过不了的话就凭感觉改一改。

这是一个很不好的习惯!

做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果。

然后再写代码,如果代码没通过就打印dp数组,看看是不是和自己预先推导的哪里不一样。

如果打印出来和自己预先模拟推导是一样的,那么就是自己的递归公式、初始化或者遍历顺序有问题了。

如果和自己预先模拟推导的不一样,那么就是代码实现细节有问题。
如果碰到背下代码但AC不过,自己先思考这三个问题:

  • 这道题目我举例推导状态转移公式了么?
  • 我打印dp数组的日志了么?
  • 打印出来了dp数组和我想的一样么?

如果这灵魂三问自己都做到了,基本上这道题目也就解决了,或者更清晰的知道自己究竟是哪一点不明白,是状态转移不明白,还是实现代码不知道该怎么写,还是不理解遍历dp数组的顺序。

总结

动态规划五步骤、如何debug非常重要。

2. 斐波那契数

例题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. 确定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],那么遍历的顺序一定是从前到后遍历的。
    举例推导dp数组

  5. 按照这个递推公式dp[i] = dp[i - 1] + dp[i - 2],我们来推导一下,当N为10的时候,dp数组应该是如下的数列:

0 1 1 2 3 5 8 13 21 34 55

//动规解法:时间复杂度O(n),空间复杂度O(n)
class Solution {
    public int fib(int n) {
        int[] dp=new int[31];
        dp[0]=0;
        dp[1]=1;
        for(int i=2;i<=n;i++){
            dp[i]=dp[i-1]+dp[i-2];
        }
        return  dp[n];
    }
}
//递归:时间复杂度O(2^n),空间复杂度O(n)
class Solution {
    public int fib(int n) {
        if(n==0) return 0;
        else if(n==1) return 1;
        else return fib(n-1)+fib(n-2);
    }
}

3. 爬楼梯

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

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

  1. 如何可以推出dp[i]呢?

从dp[i]的定义可以看出,dp[i] 可以有两个方向推出来。

首先是dp[i - 1],上i-1层楼梯,有dp[i - 1]种方法,那么再一步跳一个台阶不就是dp[i]了么。

还有就是dp[i - 2],上i-2层楼梯,有dp[i - 2]种方法,那么再一步跳两个台阶不就是dp[i]了么。

那么dp[i]就是 dp[i - 1]与dp[i - 2]之和!

所以dp[i] = dp[i - 1] + dp[i - 2] 。

  1. 那么,dp该怎么初始化呢?

再回顾一下dp[i]的定义:爬到第i层楼梯,有dp[i]种方法。

那么i为0,dp[i]应该是多少呢,这个可以有很多解释,但基本都是直接奔着答案去解释的。

例如强行安慰自己爬到第0层,也有一种方法,什么都不做也就是一种方法即:dp[0] = 1,相当于直接站在楼顶。

但总有点牵强的成分。

那还这么理解呢:我就认为跑到第0层,方法就是0啊,一步只能走一个台阶或者两个台阶,然而楼层是0,直接站楼顶上了,就是不用方法,dp[0]就应该是0.

其实这么争论下去没有意义,大部分解释说dp[0]应该为1的理由其实是因为dp[0]=1的话在递推的过程中i从2开始遍历本题就能过,然后就往结果上靠去解释dp[0] = 1。

从dp数组定义的角度上来说,dp[0] = 0 也能说得通。这时就要从i=3开始递推。

需要注意的是:题目中说了n是一个正整数,题目根本就没说n有为0的情况。

所以本题其实就不应该讨论dp[0]的初始化!

class Solution {
    public int climbStairs(int n) {
        int[] dp=new int[46];
        dp[0]=0;
        dp[1]=1;
        dp[2]=2;
        for(int i=3;i<=n;i++){
            dp[i]=dp[i-2]+dp[i-1];
        }
        return dp[n];
    }
}

这道题从递推公式来看发现与斐波那契相同,但是对于这道题如果从2开始递推,dp[0]需要初始化未1,而从3开始递推,dp[0]=1。并且这道题没有给出递推公式,需要分析,难度更大。

拓展

这道题目还可以深化,可以在单纯爬楼梯中考察为什么dp[0]=1。以及可以一步迈1个、2个…m个台阶,那么跑到第n阶需要多少步?这道题在leetcode上没有原题,是完全背包问题,考察算法能力。
在后续背包问题中讲解。

4. 使用最小花费爬楼梯

例题746:
给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。
在这里插入图片描述
从后往前不正确,从前往后怎么判断0和1两个位置?

题目中说 “你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯” 也就是相当于 跳到 下标 0 或者 下标 1 是不花费体力的, 从 下标 0 下标1 开始跳就要花费体力了。

1. 确定dp数组以及下标的含义(确定dp数组含义很重要)
使用动态规划,就要有一个数组来记录状态,本题只需要一个一维数组dp[i]就可以了。

dp[i]的定义:到达第i台阶所花费的最少体力为dp[i]。

对于dp数组的定义,大家一定要清晰!

2. 确定递推公式(递推公式很重要)
可以有两个途径得到dp[i],一个是dp[i-1] 一个是dp[i-2]。

dp[i - 1] 跳到 dp[i] 需要花费 dp[i - 1] + cost[i - 1]。

dp[i - 2] 跳到 dp[i] 需要花费 dp[i - 2] + cost[i - 2]。

那么究竟是选从dp[i - 1]跳还是从dp[i - 2]跳呢?

比较两者谁小就选谁!

  1. 如何初始化?
    已经知道到下标0和1的位置花费为0。
  2. 递归方向?
    后一步的最小代价取决于前两步的代价,所以是从前往后遍历。
  3. 根据递推公式推导
    根据例子[10,15,20]可以推导出到达最高阶的最小花费是15,符合递推公式。
时间复杂度为O(n),空间复杂度为O(n)
class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int[] dp=new int[cost.length+1];
        dp[0]=0;
        dp[1]=0;
        for(int i=2;i<=cost.length;i++){
            dp[i]=Math.min((dp[i-1]+cost[i-1]),dp[i-2]+cost[i-2]);
        }
        return dp[cost.length];
    }
}

如果面试需要优化空间复杂度,可以知道第i位是由前两位推出的,所以不用dp数组,直接用两个变量记录。

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int a=0;
        int b=0;
        int t=0;
        for(int i=2;i<=cost.length;i++){
            t=Math.min((a+cost[i-2]),b+cost[i-1]);
            a=b;
            b=t;
        }
        return t;
    }
}

5. 动态规划第一节总结

  1. 首先最重要的是动态规划的五步骤!
    在做题的时候,先确定dp数组的含义与下标,然后推导递推公式,再决定初始化条件,再看遍历方向,最后举例验证推导出来的公式。
    如果动规报错的话,可以将dp数组打印出来,看是否与推导的结果相同。

如果代码写出来了,一直不能AC的灵魂三问
①这道题分析推导状态转移公式了吗?
②打印dp数组了吗?
③dp数组结果与推导的一样吗?

  1. 斐波那契数
    这道题直接给出状态转移公式与初始化,是入手的简单题。
  2. 爬楼梯
    这道题虽然递推公式与斐波那契相同,但是对于dp[0]的定义不同。并且需要自己推导递推公式,相较于上一题更难。
  3. 最小花费爬楼梯
    这道题进一步提升难度,需要明白dp数组的含义,到前两步的花费为0。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值