动态规划回顾总结

前言:本周来进行一次过去写过的动态规划内容的回顾,还是先列出动规的五部曲

本文适合对基本的动规和背包问题有一定了解的用户理解

动态规划五部曲

一 明确dp数组下标以及其内值的含义

二 找出递推公式

三 如何对dp数组进行初始化

四 确定遍历顺序

五 打印dp数组找问题看是否符合预期

下面就直接开始了

63. 不同路径 II - 力扣(LeetCode)

相比于前置题目,该题目中增加了障碍,作为本体的核心

  1. 求解路径的数量,自然dp数组内存放值的含义就是路径数了,这道题目中是在一个二维地图中走,很明显ij就是x轴方向和y轴方向了
  2. 机器人每次只能向下或者向右走一步,那到达每一个格子的方式就是到达其上面和左边方式的加和了,故有递推公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
  3. 此题的地图中存在障碍,那显现障碍势必是初始化的一部分,按题目中要求将二维dp数组中相对应的各自初始化为0,即没有办法到达该格子,初始的dp[0][0]肯定是只有一种途径,即不动就可以到,所以要设为1,而在第一行和第一列的两组因为没有上面或者左边的途径,就只能一直向右或向下到达,所以要全部设为1
  4. 遍历的顺序其实无关紧要,换一下也是可行的,无非就是把地图转了90度进行操作
  5. 常规操作可以检查错误

注:在进行所有操作的时候都不要忘了障碍的存在,有可能障碍在起点或者终点,需要进行特判,在遍历dp数组时也不要忘记处理障碍

int uniquePathsWithObstacles(int** obstacleGrid, int obstacleGridSize, int* obstacleGridColSize) {
    int m = obstacleGridSize;
    int n = *obstacleGridColSize;
    if (obstacleGrid[0][0] == 1 || obstacleGrid[m - 1][n - 1] == 1) {
        return 0;
    }
    int i, j;
    int dp[m][n];
    memset(dp, 0, sizeof(dp));
    for (i = 0; i < m; i++ ) {
        if (obstacleGrid[i][0] == 0) {
            dp[i][0] = 1;
        }
        else {
            break;
        }
    }
    for (j = 0; j < n; j++ ) {
        if (obstacleGrid[0][j] == 0) {
            dp[0][j] = 1;
        }
        else {
            break;
        }
    }
    for (i = 1; i < m; i++ ) {
        for (j = 1; j < n; j++ ) {
            if(obstacleGrid[i][j] == 1) {
                dp[i][j] = 0;
            }
            else {
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
    }
    return dp[m - 1][n - 1];
}

122. 买卖股票的最佳时机 II - 力扣(LeetCode)

去年十二月贪心的那篇博客上我们已经见过这道题,但贪心的思路一般都不太好想,这里用动规的思路再进行解答

  1. 求解最大利润,则dp数组内放利润,题目中是根据天数进行操作的,遍历天数的责任落在了i身上,那j呢,天数和利润都名花有主了,剔除这两条,题目中还告诉我们可以在一天内选择出售或者购买股票,同时身上只能存在一张股票,好像依然没有 什么需要j进行遍历的,那就需要换个思路了,身上只能同时有一张股票,遍历天数的时候就要考虑到底买还是不买,当你看重了某一天的股票,此时就出现了两种状态,之前到底有没有买过股票,这件事情就需要交给j去做了,也不需要一个专门的变量,两个空间即可

  2. 由于j承担的职责只有两个空间,也就是说只需要遍历天数就可以了,j的问题将应有的循环写两个式子,即前一天有股票和没有股票两种情况,两个递推式,那我们这里假设前一天没有票的状态在dp[i][0]中,则dp[i][0] = fmax(dp[i - 1][0], dp[i - 1][1] + prices[i]),有票的状态在dp[i][1]中,则dp[i][1] = fmax(dp[i - 1][1], dp[i - 1][0] - prices[i]),递推关系式管的是买不买今天的票,j保存的是昨天有没有买票的情况,各位若是一下子没想通可以从逻辑上仔细看一看这两个递推关系式

  3. 初始化也是两部分,第一天没买票则初始化为0,买了票那此时是没有任何本钱的,但若执意要买减成负数其实也无关紧要,因为最终返回的结果一定是最后一天没有票,哪怕有票也要在最后一天卖了获取最大利润,返回的结果是dp[i][0],与dp[i][1]是没有关系的,所以初始化第一天时就按买了票减去相应的钱数即可

  4. 只有一层循环,此题不存在遍历顺序的问题

  5. 常规操作可以检查错误

    int maxProfit(int* prices, int pricesSize) {
        int n = pricesSize;
        int dp[n][2];
        dp[0][0] = 0, dp[0][1] = -prices[0];
        for (int i = 1; i < pricesSize; i++) {
            dp[i][0] = fmax(dp[i - 1][0], dp[i - 1][1] + prices[i]);
            dp[i][1] = fmax(dp[i - 1][1], dp[i - 1][0] - prices[i]);
        }
        return dp[n - 1][0];
    }
    

213. 打家劫舍 II - 力扣(LeetCode)

这道题比前置题目多了首尾房屋相连的条件,大致思路是差不错的,多了一个成环的处理

所以先展示未成环时的解决

  1. 看题可以知道dp数组内存放的应该是偷到的钱数,而i去遍历房屋,j似乎没有什么必要,用一维数组处理就好了
  2. 偷的关键在于偷不偷当前房间,如果不偷的话直接继承前一个房屋偷到的最大钱数即可,偷的话就不能偷前一个房间,继钱数只能从前前一个房间继承,则有dp[i] = fmax(dp[i - 1], dp[i - 2] + nums[i])
  3. 根据2中的分析,遍历得从第三个屋子开始,那就要对前两个房屋初始化,自然dp[0]要有最大值就是偷当前屋子的钱,即dp[0] = nums[0],而为了算出三号房屋能偷到的最多的钱,则dp[1] = fmax(nums[0], nums[1])
  4. 只有一层循环,此题不存在遍历顺序的问题
  5. 常规操作可以检查错误
int rob(int* nums, int numsSize) {
    if (numsSize == 0) return 0;
    if (numsSize == 1) return nums[0];
    int dp[110];
    dp[0] = nums[0];
    dp[1] = fmax(nums[0], nums[1]);
    for (int i = 2; i < numsSize; i++) {
        dp[i] = fmax(dp[i - 1], dp[i - 2] + nums[i]);
    }
    return dp[numsSize - 1];
}

基础思路有了,现在再加上成环的条件,其实关键的因素在于首尾房屋,不偷首尾,那思路直接转化为少了两个房屋的前置问题了

而不考虑尾部房屋的情况其实是包含在其他两种情况之中的

int robRange(int* nums, int start, int end) {
    int dp[200];
    dp[start] = nums[start];
    dp[start + 1] = fmax(nums[start], nums[start + 1]);
    for (int i = start + 2; i <= end; i++) {
        dp[i] = fmax(dp[i - 1], dp[i - 2] + nums[i]);
    }
    return dp[end];
}

int rob(int* nums, int numsSize) {
    if (numsSize == 1) {
        return nums[0];
    } else if (numsSize == 2) {
        return fmax(nums[0], nums[1]);
    }
    return fmax(robRange(nums, 0, numsSize - 2), robRange(nums, 1, numsSize - 1));
}

在解决这类问题时也不能忘记特判长度为1和2的情况,毕竟dp数组是从第三个房屋开始遍历的

1049. 最后一块石头的重量 II - 力扣(LeetCode)

这是本周博主才刷的题,看题面第一时间想到的应该是二分,排序加贪心,实现起来未免太过复杂

一般贪心的题都能用动规解,但这道用动规去看也很迷糊,如果一道能用动规去解的题目题面描述地完全想不到什么思路,那么它一定是要进行某种转化,转化成我们能看懂的样子

明明可以一块块石头选择碎还是不碎,这样子就很舒服了,但偏偏题目告诉我们要碎就两块一起碎,那能不能像上面股票那样分成两个状态,一次遍历石头总量,很显然不行,这两块石头是实际存在的,总不能爆掉第一块石头后去找第二块吧,这样就成暴力求解了

想不清这个问题,那我们先试一试动规五部曲(注:试错不一定所有地方都对)

    • 说到遍历石头,这件事就交给i去做了,再想,题目要求解剩余的最小质量,dp数组里装的自然是质量,那么j应该承担什么任务,不妨先进行初始化,有剩余就有原先,而碎掉石头后质量会减小,那么初始化的问题就解决了,即最开始的dp[0][0]应该装着这堆石块的质量总和
    • 现在第0号位置装着所有的质量,遍历时是一块一块石头减去的,选择碎还是不碎,这是两种状态,而01背包中恰好也有着选与不选两种状态(对01背包问题没有了解的朋友建议先去学习其思想),既然股票的思路行不通,来试一试套01背包的模板,那这个时候j理所应当地要去遍历每一块石头的重量了
    • 这个时候又有问题了,假设此题就是01背包的变式,01背包里面物品有体积和价值两个属性,而本体中只有质量一个属性,就算对应明显也是对应的体积,好像又碰壁了,博主在最开始突破此题的时候就卡死在这了,类比一下,01背包问的是价值,本体问的是质量,没错,本体中的质量同时充当了体积和价值两种角色,这就属于初见必死了,如果没见过一个属性充当两种效果,本题基本是做不出来的,还是要多见题
  • 下来只需要尝试将该问题转化为01背包的模板就行了,01背包是求解最大值,本题求解最小值,那可以先转化一下,求解一个可以碎掉的最大值,来获取剩余的最小值,这个思路转化了,那前面的分析基本可以宣告失败了,初始化需要重新考虑,每碎掉一个石头增加dp数组中的质量,那最初的dp[0][0]就应该初始化为0

  • 回到最开始就有的那个问题,一次碎要碎两块,套背包模板时每次加需要加两次相同的质量,属实是没有必要,让j在遍历质量时以最大质量的一半为极限即可,这个操作就相当于把石头自动分成了两堆,求解其中一堆能达到的最大质量就相当于求解出两堆一共能达到的最大质量,至于怎么分其实不需要考虑,在dp遍历的过程中会自己分好的

既然都是背包的模板了,动规五部曲就不进行了,有问题的可以去看一看01背包的具体解决,结合上面对问题的转换即可解决此题

int lastStoneWeightII(int* stones, int stonesSize) {
    int dp[15001];
    memset(dp, 0, sizeof(dp));//这里的初始化因为直接套了01背包的一维解决,因其后面遍历时的倒叙遍历,所有值都应该初始化为0
    int sum = 0;
    for (int i = 0; i < stonesSize; i++) {
        sum += stones[i];
    }
    int target = sum / 2;
    for (int i = 0; i < stonesSize; i++) {
        for (int j = target; j >= stones[i]; j--) {
            dp[j] = fmax(dp[j], dp[j - stones[i]] + stones[i]);
        }
    }
    return sum - dp[target] * 2;
}

动规的类型很多,解决此类问题的核心还是要多见题多做题,本文就到此结束了

  • 26
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
错题回顾功能测试用例的设计可以遵循以下步骤和方法: 1. 需求分析:首先,需要从需求文档中明确错题回顾功能的具体要求和功能。这包括用户可以查看错题的方式、展示形式、错题的分类和筛选功能等。 2. 测试计划:根据需求分析的结果,制定测试计划。明确测试的目标、范围和内容,并确定测试的开始和结束日期。 3. 测试设计:根据需求和测试计划,设计错题回顾功能的测试用例。测试用例应包括输入数据、预期结果和执行步骤。例如,可以设计测试用例来验证用户在查看错题时是否能够正确显示错题内容、错题的答案和解析等。 4. 系统测试:在系统测试阶段,执行设计好的测试用例来验证错题回顾功能是否按照需求正常工作。通过输入不同的测试数据,观察系统的反应和输出结果,与预期结果进行比较。 5. 回归测试:在软件的后续版本中,当对错题回顾功能进行修改或添加新功能时,需要进行回归测试,以确保修改后的功能不会对原有的功能产生不良影响。可以使用之前设计的测试用例进行回归测试。 在测试过程中,可以借鉴黑盒测试的优点,如不需要了解内部实现细节、能够测试整个系统等。同时,也要注意黑盒测试的缺点,如无法全面覆盖所有可能的情况、难以检测到代码错误等。 此外,还可以参考软件开发中的探索性测试和可用性测试的方法。探索性测试可以帮助发现系统中的潜在问题和不符合预期的行为。可用性测试可以评估错题回顾功能的易学性、易记性、容错性、交互效率和用户满意度等指标。 总结起来,设计错题回顾功能测试用例需要进行需求分析、测试计划制定、测试设计、系统测试和回归测试等步骤,并可以借鉴黑盒测试、探索性测试和可用性测试的方法。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [秋招笔试错题整理](https://blog.csdn.net/qq_43767234/article/details/121457239)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [敏捷ACP.知识总结.错题回顾](https://blog.csdn.net/u010025781/article/details/125473903)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值