详细总结【动态规划】的解题规律

本文详细总结了动态规划的解题规律,包括准备工作、基本的一维和进阶的二维动态规划问题。通过实例解析了确定dp数组含义、状态转移方程和初始状态的重要性,并给出了不同类型问题的典型题目和解题策略,如背包问题、字符串类型问题等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

动态规划

准备工作

首先,我们需要确定适用动态规划这种算法的题目特征,毕竟笔试题不会好心地在旁边标上动态规划的标签。

维基百科:动态规划在查找有很多重叠子问题的情况的最优解时有效……只能应用于有最优子结构的问题。听起来非常云里雾里(专业总结都是这么抽象)。我们先来理解一下最优子结构。最优子结构是局部最优解能决定全局最优解。也就是说,动态规划其实和分治方法非常类似,都是通过组合子问题的解来求解原问题,但是分治方法划分的子问题互不相交,而动态规划的子问题则互相重叠。为了减少复杂度,保证每个子问题只求解一次,我们利用**历史记录(备忘录)**来避免重复计算,其实就是所谓的dp数组(一维/二维/甚至有三维,不过一般会压缩到二维)

下面就正式进入求解三部曲:

  • 确定dp数组的含义:好的开始就是成功的一半!本人曾经多少次因为找不出dp数组的含义而默默流泪,举步维艰……其实数组的含义一般都不会太复杂,一般都直接和要求解的问题挂钩,背包问题就是最好的示例。
  • 找出状态转移方程:实战部分!需要我们深入分析每种可能的情况。
  • 确定初始状态:请不要忽略这一步!不正确的边界条件常常是一些奇怪bug的罪魁祸首,对细心和耐心程度要求很高!可以用极端的测试数据自己在纸上演练一遍。这里通常要注意dp数组是从0开始还是从1开始。

在求解出正确答案的前提下,我们还常常进行dp数组的压缩。这部分将在后面具体展开。

基本:一维

练手题:53(经典) 70 213 198 413(可以直接用数学知识解) 650(有坑,解法很巧妙)

  • 浅浅总结一下以上涉及到的dp数组的含义

    • Q:求在不触发机关的情况下最多可以抢劫这n个房子的多少钱

      dp[i]:抢劫到第 i 个房子时,可以抢劫的最大数量(return dp[n]

    • Q:求给定数组中连续且等差的子数组一共有多少个

      dp[i]:以第i个数组元素结尾的子数组数目(return accumulate(dp.begin(), dp.end(), 0))

    • Q:给定整数数组 nums ,请找出一个具有最大和的连续子数组(最少包含一个元素),返回其最大和

      dp[i]:dp[i]表示以i结尾的连续数组的最大值,最后返回其中的最大值

    我们发现,dp数组的含义大多和求解问题直接挂钩,当然后面会根据问题的不同需要部分变通

下面讲解一些个人认为比较有代表性的题:

  • 213打家劫舍 II:作为70题的plus版本,该题增加了一个限制:这个地方所有的房屋都 围成一圈 。这意味着第一个房屋和最后一个房屋是紧挨着的。本菜狗一开始一直试图通过改变dp数组的含义或者状态转移方程来解题,后面发现正确的思路应该是分治:分为不偷第一间和不偷最后一间的情况。**注意是不偷不是偷!**如果是偷的话就需要分三种情况了。想通后就豁然开朗了。

    class Solution {
    public:
    //不要想着一次性解决,将情况分为两种,最后取max就好
        int rob(vector<int>& nums) {
            if(nums.size()==1) return nums[0];
            //不偷第一间!分情况不是分成偷第一间和偷最后一间,这样会出来三种情况
            //动态规划中也是可以结合分治算法的
            int ppre=0,pre=0,cur1,cur2;
            for(int i=1;i<nums.size();++i){
                cur1=max(ppre+nums[i],pre);
                ppre=pre;
                pre=cur1;
            }
            //不偷最后一间
            ppre=pre=0;
            for(int i=0;i<nums.size()-1;++i){
                cur2=max(ppre+nums[i],pre);
                ppre=pre;
                pre=cur2;
            }
            return max(cur1,cur2);
        }
    };
    
  • 343整数拆分:给定正整数 n 将其拆分为 k正整数 的和( k >= 2 )并使这些整数的乘积最大化。

    特点:本身不难,但是有很多小细节,以及转移方程不一定是根据dp的历史结果

    int integerBreak(int n) {
        vector<int> dp(n+1,1);
        for(int i=2;i<=n;i++)
        for(int j=1;j<i;++j) //不能取i,不能sqrt,不用整除……天知道我踩了多少诡异的坑
            dp[i]=max(dp[i],max(j,dp[j])* max(i-j,dp[i-j]));
        return dp[n];
    }
    
  • 650:最初记事本上只有一个 ‘A’ ,可以通过Copy All(复制当前全部)和Paste(粘贴上一次复制的字符),求最少的操作次数使记事本上输出恰好 n 个 ‘A’ 。

    审题:是拷贝当前!的全部,粘贴也只能是上一次!复制的字符。不同于以往通过加减实现的动态规划,这里需要乘除法来计算位置,因为粘贴操作是倍数增加的。

    dp[i]:延展到长度 i 的最少操作次数。对于每个位置 j,如果 j 可以被 i 整除,那么长度 i 就可以由长度 j 操作得到,其操作次数等价于把一个长度为 1 的 A 延展到长度为 i/j。因此递推公式是 dp[i] = dp[j] + dp[i/j]

    注意,官方给的题解状态转移方程是min(dp[i/j]+j-1,dp[j]+i/j-1)</

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值