动态规划-Dynamic Programing-DP

文章目录

一、背景

牛客网noob类的题里目前涉及动态规划思想的有noob36 牛牛学数列5- 斐波那契数列

、noob39 牛牛学数列6---f(n)=f(n−1)+2f(n−2)+f(n−3),这个代表:的三阶线性递推模型。

这2个题目都可以通过多种做法实现,最优解就是动态规划dp解法。

这里用noob39来做示例。

题目如下:

这里提供三种解法:

法1:暴力递归,算法时间复杂度O(3^n)

法2:递归+dp(自上而下) 时间复杂度O(n)

法3:迭代dp:自下而上 时间复杂度O(n)

二、三种解法

代码:
gitee提交记录

直接git clone整个nkw项目即可。

java 复制代码
    /**
     * 法1:暴力递归
     * @param n
     * @return
     */
    static int getAnThreeRecursion(int n) {
        if (n == 1) {
            return 0;
        }
        if (n == 2 || n == 3) {
            return 1;
        }
        return getAnThreeRecursion(n - 3) + 2 * getAnThreeRecursion(n - 2) + getAnThreeRecursion(n - 1);
        /**
         * 总结:
         * 递归的问题在于每次要计算到底,存在大量重复计算
         * 比如:A15 =A12 +2*A13+A14
         * 那么:A17 = A13+ 2*A15 + A16
         * A13在计算A15的时候其实已经计算过了,但是在计算A17的时候还是会"递归到底(0 1 1)",当n很大这样浪费大量资源,且时间复杂度为 O(3^n),空间复杂度为:O(n)
         * 时间复杂度:衡量算法执行的总操作数(这里是递归调用的总次数);
         * 空间复杂度的核心是递归调用栈的深度
         */
    }


    /**
     * 法2:递归+dp(自上而下)
     * @param n
     * @param dp
     * @return
     */
    static int getAnThreeRecursionV2Up2DownDp(int n, Integer[] dp) {
        if (n == 1) {
            return 0;
        }
        if (n == 2 || n == 3) {
            return 1;
        }
        //状态记忆(备忘录)
        if (dp[n - 1] != null) {
            System.out.println("进入了状态记忆备忘录, n的值是:" + n + " 备忘录的值是:" + dp[n - 1]);
            return dp[n - 1];
        }
        int An = getAnThreeRecursionV2Up2DownDp(n - 3, dp) + 2 * getAnThreeRecursionV2Up2DownDp(n - 2, dp) + getAnThreeRecursionV2Up2DownDp(n - 1, dp);
        dp[n - 1] = An;
        return An;
    }

    /**
     * 法3:迭代版dp-自下而上。
     * 【最优解法】
     * 迭代版本DP,无递归,效率略高于备忘录版本的递归
     *
     * @param n
     * @param dp
     * @return
     */
    static int getAnThreeNoRecursionDown2UpDp(int n) {
        if (n == 1) {
            return 0;
        }
        if (n == 2 || n == 3) {
            return 1;
        }
        int[] dp = new int[n];
        dp[0] = 0;
        dp[1] = 1;
        dp[2] = 1;
        for (int i = 4; i <= n; i++) {
            //这里要注意 数组下标和计算实际的前三个数的关系,不要搞混淆了。 当然为了更好的和公式对应,我们数组就不要从0开始,从1开始就可以很好的对应起来。
            dp[i - 1] = dp[i - 4] + 2 * dp[i - 3] + dp[i - 2];
        }
        System.out.println("无递归版本迭代DP结果是:" + JSON.toJSONString(dp));
        return dp[n - 1];
    }

看下测试结果:

java 复制代码
  //        noob39(17); //三阶线性递推模型结果是:578949
        //输入:n=17  调用递归状态记忆-getAnThreeRecursionV2Up2DownDp   结果:三阶线性递推模型结果是:58425  状态记忆(备忘录):[0,1,1,3,6,13,28,60,129,277,595,1278,2745,5896,12664,
        // 27201,58425]
        /**
         * 把进入记忆备忘录的日志打印出来看下,在再本子上推算下看和预期是否相符?
         * 进入了状态记忆备忘录, n的值是:4 备忘录的值是:3
         * 进入了状态记忆备忘录, n的值是:5 备忘录的值是:6
         * 进入了状态记忆备忘录, n的值是:4 备忘录的值是:3
         * 进入了状态记忆备忘录, n的值是:5 备忘录的值是:6
         * 进入了状态记忆备忘录, n的值是:6 备忘录的值是:13
         * 进入了状态记忆备忘录, n的值是:6 备忘录的值是:13
         * 进入了状态记忆备忘录, n的值是:7 备忘录的值是:28
         * 进入了状态记忆备忘录, n的值是:8 备忘录的值是:60
         * 进入了状态记忆备忘录, n的值是:7 备忘录的值是:28
         * 进入了状态记忆备忘录, n的值是:8 备忘录的值是:60
         * 进入了状态记忆备忘录, n的值是:9 备忘录的值是:129
         * 进入了状态记忆备忘录, n的值是:9 备忘录的值是:129
         */

        int result = getAnThreeNoRecursionDown2UpDp(17);
        System.out.println("无递归版本迭代DP结果是:" + result);
        /**
         * 计算结果:
         * 无递归版本迭代DP结果是:[0,1,1,3,6,13,28,60,129,277,595,1278,2745,5896,12664,27201,58425]
         * 无递归版本迭代DP结果是:58425
         */

其中对于法2:getAnThreeRecursionV2Up2DownDp()的状态记忆输出日志做了详细研究,核对过程如图:

三、对比下算法复杂度

四、扩展(应用场景)

这个数列:f(n)=f(n−1)+2f(n−2)+f(n−3),这个代表:的三阶线性递推模型。

名号没有斐波那契数列响亮,不过应用还是蛮广泛的,比如:

  • 组合计数:路径 / 步数规划 数字信号处理
  • 三阶递归滤波(工程领域)
  • 生物种群增长建模(生态 / 农业领域)
相关推荐
闻缺陷则喜何志丹2 小时前
【前后缀分解】P9255 [PA 2022] Podwyżki|普及+
数据结构·c++·算法·前后缀分解
每天吃饭的羊2 小时前
时间复杂度
数据结构·算法·排序算法
ValhallaCoder3 小时前
hot100-堆
数据结构·python·算法·
小小小米粒3 小时前
函数式接口 + Lambda = 方法逻辑的 “插拔式解耦”
开发语言·python·算法
风吹乱了我的头发~3 小时前
Day31:2026年2月21日打卡
开发语言·c++·算法
望舒5134 小时前
代码随想录day33,动态规划part2
java·算法·leetcode·动态规划
那起舞的日子4 小时前
牛客网刷算法的启发
算法
追随者永远是胜利者4 小时前
(LeetCode-Hot100)169. 多数元素
java·算法·leetcode·go
s砚山s5 小时前
代码随想录刷题——二叉树篇(二十)
算法