【算法】小汉堡初试动态规划 体力值一进DP数组

1.前言

参考:代码随想录 (programmercarl.com)

原题链接:746. 使用最小花费爬楼梯 - 力扣(LeetCode)

在每周练习的时候接触到了动规题,就想系统地了解一下动态规划是什么,如何判断一道题是否可以用动态规划解决,以及解决是否存在一个固定的程序或模板。

这里不得不感谢代码随想录的网站,题目顺序设计难度梯度循序渐进,在动态规划这一章节,有一种拨云见日的感觉。

2.解:

动态规划(Dynamic Programming),本质为将一个问题分解为许多个重叠的子问题,这里我将重叠二字画上着重符号,是因为在动态规划中,每一个状态都由上一个状态推导出来。即许多教科书提到的无后效性

无后效性:某阶段的状态一旦确定,这个阶段以后的过程演变就和这个阶段以前的状态和决策无关了,只和这个阶段的状态有关,当前状态是此前历史的完整总结,此前的状态和决策只能通过当前状态去影响过程未来的演变。

故而,对于动态规划问题,一般来说,我们都可以得到一个dp数组,通过初始化dp数组,递推dp数组,来得到需求的答案。

(初学者看到这里可能会有很多疑惑,什么是dp数组,dp数组是怎么得到的?为什么要得到一个dp数组来求解?这些都将在以下内容中得到一部分解答。)

由此,可以得到求解动态规划题目的一般步骤,即动态规划五部曲!

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

暂且先随我看两道基础题,尝试一下如何使用五部曲解题。

Round One 斐波那契数

请添加图片描述

这道题想必各位Coder都已熟悉不过了,今天要抛开各位熟悉的递归等等方法,套一次动态规划的模板,一起尝试一下如何使用动规五部曲解题。

首先,确定dp数组以及下标的含义,dp数组想必已经十分明确,即:斐波那契数的数列,而其下标n则代表第n个斐波那契数。

而后,确定递推公式,此处可以直接借用题目中的公式,即dp[n] = dp[n - 1] + dp[n - 2] (n > 1)。

接着,确定如何初始化dp数组,题目中也已经给出了,即dp[0] = 0,dp[1] = 1

下一步,确定遍历顺序,因为斐波那契数列,后面一个数字dp[n]取决于dp[n - 1]和dp[n - 2]的和,故而遍历顺序为从前到后。

最后,举例推导dp数组,此举主要为了方便debug。

放上新鲜出炉的代码~

int fib(int n){
    if(n <= 1)
    return n;
    int *dp = (int *)malloc((n + 1) * sizeof(int));
    dp[0] = 0;
    dp[1] = 1;
    for(int i = 2; i <= n; i++)
    {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
}

状态压缩

如若建立一个dp数组,那么空间复杂度为O(N),由于dp数组的值仅仅与前两项有关,故而实际上只需保存三个变量即可,滚动存储下去,即可实现空间复杂度的降低。

int fib(int n) {
    int a = 0, b = 1, sum;
    for(int i = 0; i < n; i++){
        sum = a + b;
        a = b;
        b = sum;
    }
    return a;
}

Round Two 爬楼梯

请添加图片描述

再举一个栗子!闲话少说直接开始套公式。

  1. 确定dp数组(dp table)以及下标的含义

    dp数组的值表示爬到某一层楼梯方法的数量,下标则表示第几阶台阶。

  2. 确定递推公式

    因为一次可以爬1或2个台阶,所以显而易见dp[i]的值与dp[i - 1]和dp[i - 2]的和有关,故而递推公式可以确认,为

    dp[i] = dp[i - 1] + dp[i - 2];
    
  3. dp数组如何初始化

    因为不存在第零节台阶,故而dp数组从1开始,爬到第一节楼梯仅有一种方式,爬到第二节楼梯有两种方式(1+1 或 2步),且因为递推公式只需要前两个变量的值,所以只需初始化前两个值即可。

  4. 确定遍历顺序

    由递推公式可得,遍历顺序为从前往后。

  5. 举例推导dp数组

    同样为了方便debug,推导出的数组与运行结果一致即可。

    放上新鲜出炉的代码~

    int climbStairs(int n) {
        if(n <= 2)
        return n;
    
        int *dp = (int *)malloc((n + 1) * sizeof(int));
        dp[1] = 1;
        dp[2] = 2;
    
        for(int i = 3; i <= n; i++){
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }
    

    跟斐波那契数列的代码不能说一模一样…只能说…

Round Three 使用最小花费爬楼梯

请添加图片描述

  1. 确定dp数组(dp table)以及下标的含义

    dp数组中值的定义:到达某一台阶所花费的最少体力。

    下标的含义:第几节台阶

  2. 确定递推公式

    本题的递推公式显然比前两题要难上一些,但也可以推。不妨任意拿出一个dp[i],dp[i]的值既可以取决于dp[i - 1],也可以取决于dp[i - 2],而这两者的选取,则是依据cost的大小,显然,选取最小值是最优的。这里放上代码~

    dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
    
  3. dp数组如何初始化

    看一下递归公式,dp[i]由dp[i - 1],dp[i - 2]推出,故而只需要初始化前两个数组元素即可。

    此处再回归题目,题目中说可以选择从下标为0或1的台阶开始爬,故而初始化dp[0] = 0,dp[1] = 0;

  4. 确定遍历顺序

    由递推公式可以得出,遍历顺序为从前到后。

  5. 举例推导dp数组

    任意选取一个cost数组举例即可。

    此处放上不新鲜但也热气腾腾的代码~

    int minCostClimbingStairs(int* cost, int costSize) {
        int* dp = (int*)malloc(sizeof(int) * (costSize + 1));
        if(costSize == 2) return fmin(cost[0],cost[1]);
    
        //dp数组的初始化
        dp[0] = 0;
        dp[1] = 0;
    
        //递推
        int i = 0;
        for(i = 2;i <= costSize;i++){
            dp[i] = fmin(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
        }
        return dp[i - 1];
        
    }
    

3.结语:

关于爬楼梯问题,仍有深化空间,例如:将题目中的“每次跨一阶或两阶台阶”替换成,每次跨“1-m阶台阶”,此问题就变成一个背包问题了,这些问题将在之后一步步解决。

本篇博客仅仅是初窥门径,借用代码随想录的一句话,是学习动态规划的方法论,为日后研究背包问题,股票问题,子序列问题等等打好基础。

另,近期流感横行,笔者也不幸中招,在宿舍躺尸了好几天,垂死病中惊坐起,发现博客还没写,写一半又烧的要死要活,大家平时一定要多锻炼,o(╥﹏╥)o,最后保留节目,放一点《劝学》吧。

百发失一,不足谓善射;千里跬步不至,不足谓善御;伦类不通,仁义不一,不足谓善学。

学也者,固学一之也。

一出焉,一入焉,涂巷之人也;其善者少,不善者多,桀纣盗跖也;

全之尽之,然后学者也。

  • 30
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值