【动态规划】——斐波那契数列模型

文章目录:

1. 动态规划

1.1:动态规划的基本步骤

2. 斐波那契数列模型例题

2.1:第n个泰波那契数

2.1.1:算法思想

2.1.2:空间优化

2.2:三步问题

2.2.1:算法思想

2.2.2:注意

2.3:使用最小花费爬楼梯

2.3.1:本题的小误区之楼梯顶在哪里?

2.3.2:算法思想

2.3.3:解法二

2.4:解码方法

2.4.1:算法思想

2.4.2:优化


1. 动态规划

动态规划(Dynamic Programming, DP)是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。它通常用于优化问题。

动态规划直接从定义等方面理解起来,或许会有些晦涩难懂,所以选择直接从题目入手,通过60道动态规划相关例题,由易到难,由浅入深的感受动态规划。

1.1:动态规划的基本步骤

        1. 状态表示:

                a. 创建一个dp表(通常是一个数组)

                b. 填满dp表,dp表的每一个值就是一个状态

                c. 定义状态表示的方法:

                        i. 题目要求

                        ii. 经验 + 题目要求:

                                1)以某一个位置 i 作为起始

                                2)以某一个位置 i 作为结束

                        iii. 分析问题的过程中,发现了重复子问题

        2. 状态转移方程:根据状态表示得出状态转移方程

                用之前或之后的状态推导出dp[ i ] 的值

                通常:根据最近的一步来划分问题

        3. 初始化:保证填表的时候不发生越界

                只需要初始化可能发生越界的状态

        4. 填表顺序:根据状态转移方程和边界条件,确定子问题的求解顺序

                通常是从下到上、从左到右(或称为从小到大)地计算状态值。

        5. 返回值

了解了动态规划的一般流程;

那么,现在开始,进入动态规划的第一个模型——斐波那契数列模型

注:以下题目全部取自力扣!!!

2. 斐波那契数列模型例题

2.1:第n个泰波那契数

2.1.1:算法思想

        1. 状态表示:

                a. 创建一个dp表:本题是一个一维数组

                b. 定义状态表示:

                        i. 本题用到 题目要求:dp[ i ]为第i个泰波那契数

        2. 状态转移方程:根据状态表示得出状态转移方程

                本题:由题目给出 dp[ i ] = dp[ i-1 ]+dp[ i-2 ]+dp[ i-3 ]

        3. 初始化:保证填表的时候不发生越界

                本题:dp[ 0 ]:代入方程会产生:dp[ -1 ]、dp[ -2 ]、dp[ -3 ]

                          dp[ 1 ]:dp[ -1 ]、dp[ -2 ]

                          dp[ 2 ]:dp[ -2 ]

                        只需要初始化dp[ 0 ]、dp[ 1 ]、dp[ 2 ]

        4. 填表顺序:

                为了填写当前状态时,所需的状态已经计算过了

                本题:从左到右

        5. 返回值:第n个泰波那契数:dp[ n ]

代码实现:

    public int tribonacci(int n){
        // 创建dp表
        //初始化
        //填表
        //返回值

        //处理边界情况
        if(n==0) return 0;
        if(n==1||n==2) return 1;

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

        //时间复杂度 O(N)
        //空间复杂度 O(N)

    }

2.1.2:空间优化

方法:滚动数组法:

因为处理过程中实际只用到了四个状态:dp[ i ]、dp[ i-1 ]、dp[ i-2 ]、dp[ i-3 ],所以可以用四个变量代替这四个状态,每一次循环得出dp[ i ] 后,用当前状态覆盖掉原先的状态,再进行下一轮循环

需要注意的是,四个变量之间相互赋值的顺序

滚动数组法的代码实现:

    public int tribonacci(int n) {
        //处理边界情况
        if(n==0) return 0;
        if(n==1||n==2) return 1;

        int a=0,b=1,c=1,d=0;
        for(int i=3;i<=n;i++){
            d=a+b+c;
            a=b;
            b=c;
            c=d;
        }
        return d;
    }

注意:滚动数组法只适合当前的斐波那契数列模型和未来的背包问题

2.2:三步问题

这里的1000000007 = 1e9+7,当数据很大时,都会把结果对这个数取模

2.2.1:算法思想

        1. 状态表示:

                a. 创建一个dp表(数组)

                b. 定义状态表示:

                        i. 本体用到 经验 + 题目要求

                                1)以某一个位置 i 作为结束:dp[i]表示到达某位置时,一共有多少种方法

        2. 状态转移方程:以i状态最近的状态来划分问题

                dp[ i ]:

                        从i-1位置到达i位置

                        从i-2位置到达i位置

                        从i-3位置到达i位置

                所以:dp[ i ] = dp[ i-1 ]+dp[ i-2 ]+dp[ i-3 ]

        3. 初始化:保证填表的时候不发生越界

                只需要初始化dp[ 1 ]、dp[ 2 ]、dp[ 3 ]

        4. 填表顺序:

                从左到右

        5. 返回值:dp[n]

代码实现:

    public int wayToStep(int n){
        // 创建dp表
        //初始化
        //填表
        //返回值

        //取模数
        int MOD = (int)1e9+7;

        if(n==1||n==2) return n;
        if(n==3) return 4;

        int[] dp = new int [n+1];
        dp[1]=1;
        dp[2]=2;
        dp[3]=4;

        for(int i=4;i<=n;i++){
            //每次加法运算都要模 MOD
            dp[i]=((dp[i-1]+dp[i-2])%MOD+dp[i-3])%MOD;
        }
        return dp[n];

    }

2.2.2:注意

        1. 一般将1e9+7提前定义为MOD

        2. 每次加法运算后都要取模

        3. 代码优化依旧是使用滚动数组法

2.3:使用最小花费爬楼梯

2.3.1:本题的小误区之楼梯顶在哪里?

**整个数组的下一个位置,才是楼梯顶部

2.3.2:算法思想

        1. 状态表示:

                a. 创建一个dp表(数组)

                b. 定义状态表示:

                        i. 本体用到 经验 + 题目要求

                                1)以某一个位置 i 作为结束:dp[i]表示到达i位置时的最小花费

        2. 状态转移方程:以i状态最近的状态来划分问题

                dp[ i ]:

                        先到达 i-1位置,支付 costi-1  的费用 走一步  ——> dp[ i-1 ] + cost[ i-1 ]             

                        先到达 i-2位置,支付 costi-2  的费用 走两步 ——> dp[ i-2 ] + cost[ i-2 ]

                所以:dp[ i ] = min(dp[ i-1]+cost[ i-1 ] ,dp[i-2]+cost[ i-2 ])

        3. 初始化:保证填表的时候不发生越界

                dp[ 0 ] = dp[ 1 ] =0

        4. 填表顺序:

                从左到右

        5. 返回值:dp[n]

代码实现:

    public int minCostClimbingStairs(int[] cost) {
        // 创建dp表
        //初始化
        //填表
        //返回值

        int n = cost.length;
        int[] dp = new int [n+1];
        for(int i =2;i<=n;i++){
            dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
        }
        return dp[n];
    }

2.3.3:解法二

        1. 状态表示:

                a. 创建一个dp表(数组)

                b. 定义状态表示:

                        i. 本体用到 经验 + 题目要求

                                1)以某一个位置 i 作为起始:dp[i]表示从i位置出发,到达楼顶的最小花费

        2. 状态转移方程:以i状态最近的状态来划分问题

                dp[ i ]:

                        支付cost[ i ] 往后走一步 从i+1的位置出发 :dp[i+1]+cost[ i ]            

                        支付cost[ i ] 往后走两步 从i+2的位置出发 :dp[i+2]+cost[ i ]

                所以:dp[ i ] = min(dp[ i+1]+cost[ i ] ,dp[i+2]+cost[ i ])

        3. 初始化:保证填表的时候不发生越界

                dp[n-1] = cost[ n-1 ]

                dp[n-2] = cost[ n-2 ]

        4. 填表顺序:

                从右到左

        5. 返回值:min(dp[0],dp[1])

代码实现:

    public int minCostClimbingStairs(int[] cost) {
        // 创建dp表
        //初始化
        //填表
        //返回值

        int n = cost.length;
        int[] dp = new int [n+1];
        dp[n-1]=cost[n-1];
        dp[n-2]=cost[n-2];
        for(int i =n-3;i>=0;i--){
            dp[i]=Math.min(dp[i+1],dp[i+2])+cost[i];
        }
        return Math.min(dp[0],dp[1]);
    }

2.4:解码方法

2.4.1:算法思想

      1. 状态表示:

                a. 创建一个dp表(数组)

                b. 定义状态表示:

                        i. 本体用到 经验 + 题目要求

                                1)以某一个位置 i 作为结束:dp[i]表示以i为结尾时,解码方法总数

        2. 状态转移方程:以i状态最近的状态来划分问题

                dp[ i ]:

                        i位置单独解码:

                                成功: 1<=a<=9

                                失败:0

                        i-1和i 位置结合解码:

                                成功: 1<=b*10+a<=26

                                失败:0

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

                注意:如果解码成功才加,不是直接加

        3. 初始化:保证填表的时候不发生越界

        4. 填表顺序:

                从左到右

        5. 返回值:dp[n-1]

代码实现:

    public int numDecoding(String ss) {
        // 创建dp表
        //初始化
        //填表
        //返回值

        int n = ss.length();
        char[] s=ss.toCharArray();
        int[] dp=new int[n];

        //初始化第一个位置
        if(s[0]!='0')
            dp[0]=1;
        if(n==1)
            return dp[0];//边界情况

        ///初始化第二个位置
        if(s[1]!='0'&&s[2]!='0')
            dp[1]+=1;
        int t=(s[0]-'0')*10+(s[10]-'0');

        if(t>=10&&t<=26)
            dp[1]+=1;

        for(int i=2;i<n;i++){
            //第一种情况
            if(s[1]!='0')
                dp[i]+=dp[i-1];
            //第二种情况
            int tt=(s[i-1]-'0')*10+(s[i]-'0');
            if(tt>=10&&tt<=26)
                dp[i]+=dp[i-2];
        }
        return dp[n-1];
    }

2.4.2:优化

可以发现,初始化部分和循环体内代码结构相似,考虑是否可以合并

处理边界问题和初始化问题的技巧:虚拟节点法

在dp数组中加一个虚拟节点

需要注意的是:虚拟节点的值要保证后面的填表时正确的 and 注意下标的映射关系

这里的虚拟节点dp[ 0 ] 的值需要填1:根据状态转移方程,当我们求dp[ 2 ]时用到了dp[ 0 ],

表示原始字符串第一个位置和第二个位置拼起来可以解码成功,此时需要加dp[ 0 ]的值,

可以看出因为解码成功才需要加dp[ 0 ]的值,所以dp[ 0 ]不可以为0

优化后代码实现:

    public int numDecoding2(String ss) {
        // 创建dp表
        //初始化
        //填表
        //返回值

        int n = ss.length();
        char[] s=ss.toCharArray();
        int[] dp=new int[n];

        dp[0]=1;//保证后续填表是正确的
        //注重下标映射的关系
        //因为多加了一位 每一位都要减去一位 所以写成1-1 (没有必要 只为看的明白)
        if(s[1-1]!='0')
            dp[1]=1;

        for(int i=2;i<=n;i++){
            //第一种情况
            if(s[i-1]!='0')
                dp[i]+=dp[i-1];
            //第二种情况
            int tt=(s[i-1-1]-'0')*10+(s[i-1]-'0');
            if(tt>=10 && tt<=26)
                dp[i]+=dp[i-2];
        }
        return dp[n];
    }

  • 8
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值