第一次接触动态规划解题

废话

一道力扣easy没做出来…我是fw, orz。看了题解觉得学到了很多,记录下来和大家分享一下。

题目

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:

输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。

方法1

一开始我直接计算奇偶组的大小比较的,看到了[2,1,1,2]这个例子我意识到我是个哈皮。
先说一个很巧妙的方法,跟动态规划没关系,纯粹是利用了这题不能连续偷相邻的两个房间的设定想出的解法,思路非常棒!

解题人:
在这里插入图片描述
解法:
这个条件如果精简掉其他内容,很容易让人联想到奇偶数。这个解法就是从这点出发。

设置两个变量,sumOdd 和 sumEven 分别对数组的奇数和偶数元素求和。
最后比较这两个和谁更大,谁就是最优解。
至少在下面这个例子里,这么做是成功的了。

Index [0] [1] [2] [3]
nums 1 2 3 4
sumEven 1 1 4 4
sumOdd 0 2 2 6
接下来要解决的就是最优解不是纯奇数和或者偶数和的情况。
这种情况下,最优解可能前半段出现在这边,后半段出现在另一边。
那么只要找到一个时机,当这一段的最优解没有另一边好时,就复制对面的最优解过来。

举个例子:

Index [0] [1] [2] [3] [4]
nums 1 3 1 3 100
sumEven 1 1 2 2 => 3
sumOdd 0 3 3
当偶数和(奇偶指的数组下标)加到第二个 1 之后,发现还不如奇数和一个 3 大,就应该将对面的3复制过来替换掉自己的 2。

Index [0] [1] [2] [3] [4]
nums 1 3 1 3 100
sumEven 1 1 2 3 103
sumOdd 0 3 3 6 6
继续计算后得到最优解。

代码:

class Solution {
public:
    int rob(vector<int>& nums) {
        int money1=0, money2=0;
        for(int i = 0; i < nums.size(); i++)
        {
            money1 += nums[i];
            money1 = (money1 > money2) ? money1 : money2;
            i++;
            if(i < nums.size())
            {
                money2 += nums[i];
                money2 = (money2 > money1) ? money2 : money1;
            }
        }
        return max(money1, money2);
    }
};

废话:
就在我自暴自弃想着要不直接把组合全算出来算了的时候,这个思路真是妙的我呱呱叫,怎么避免有几个房间钱特别多导致间隔不固定的麻烦呢?先分两路计算,奇偶数组直接加,我们记为一组和二组。当其中一个加的房间数量比另一个多但是钱反而没另一个多的时候,比如一组间隔地偷了三个屋子,发现还没二组投的两个屋子钱多的时候,那么一组就直接改变之前投的屋子,调整为和二组相同,然后再继续间隔地偷,这样既保证了钱是最多地,又避免了触发警报。两个路线一边加一边相互比较,自然而然最后的结果就是正解了。

方法2

动态规划解法
新知识,直接上解说。

解题人:
在这里插入图片描述

解法:
动态规划的的四个解题步骤是:

定义子问题
写出子问题的递推关系
确定 DP 数组的计算顺序
空间优化(可选)
下面我们一步一步地进行讲解。

步骤一:定义子问题
稍微接触过一点动态规划的朋友都知道动态规划有一个“子问题”的定义。什么是子问题?子问题是和原问题相似,但规模较小的问题。例如这道小偷问题,原问题是“从全部房子中能偷到的最大金额”,将问题的规模缩小,子问题就是“从 kk 个房子中能偷到的最大金额”,用 f(k)f(k) 表示。
在这里插入图片描述

可以看到,子问题是参数化的,我们定义的子问题中有参数 kk。假设一共有 nn 个房子的话,就一共有 nn 个子问题。动态规划实际上就是通过求这一堆子问题的解,来求出原问题的解。这要求子问题需要具备两个性质:

原问题要能由子问题表示。例如这道小偷问题中,k=nk=n 时实际上就是原问题。否则,解了半天子问题还是解不出原问题,那子问题岂不是白解了。
一个子问题的解要能通过其他子问题的解求出。例如这道小偷问题中,f(k)f(k) 可以由 f(k-1)f(k−1) 和 f(k-2)f(k−2) 求出,具体原理后面会解释。这个性质就是教科书中所说的“最优子结构”。如果定义不出这样的子问题,那么这道题实际上没法用动态规划解。
小偷问题由于比较简单,定义子问题实际上是很直观的。一些比较难的动态规划题目可能需要一些定义子问题的技巧。

步骤二:写出子问题的递推关系
这一步是求解动态规划问题最关键的一步。然而,这一步也是最无法在代码中体现出来的一步。在做题的时候,最好把这一步的思路用注释的形式写下来。做动态规划题目不要求快,而要确保无误。否则,写代码五分钟,找 bug 半小时,岂不美哉?

我们来分析一下这道小偷问题的递推关系:
在这里插入图片描述

假设一共有 k 个房子,每个房子的金额分别是 H0 ~ Hk-1 ,子问题 f(k)表示从前 k 个房子中能偷到的最大金额。那么,偷 k 个房子有两种偷法:
k 个房子中最后一个房子是 k-1。
如果不偷这个房子,那么问题就变成在前 k-1 个房子中偷到最大的金额,也就是子问题 f(k-1)。
如果偷这个房子,那么前一个房子k−2 显然不能偷,其他房子不受影响。那么问题就变成在前k−2 个房子中偷到的最大的金额。两种情况中,选择金额较大的一种结果。
f(k) = max { f(k-1), f(k-2)+Hk-1 }
在写递推关系的时候,要注意写上 k=0和 k=1的基本情况:

当 k=0 时,没有房子,所以f(0)=0。
当 k=1 时,只有一个房子,偷这个房子即可,所以 f(1) = H0。
这样才能构成完整的递推关系,后面写代码也不容易在边界条件上出错。

步骤三:确定 DP 数组的计算顺序
在确定了子问题的递推关系之后,下一步就是依次计算出这些子问题了。在很多教程中都会写,动态规划有两种计算顺序,一种是自顶向下的、使用备忘录的递归方法,一种是自底向上的、使用 dp 数组的循环方法。不过在普通的动态规划题目中,99% 的情况我们都不需要用到备忘录方法,所以我们最好坚持用自底向上的 dp 数组。

DP 数组也可以叫”子问题数组”,因为 DP 数组中的每一个元素都对应一个子问题。如下图所示,dp[k] 对应子问题 f(k),即偷前 k 间房子的最大金额。
在这里插入图片描述

那么,只要搞清楚了子问题的计算顺序,就可以确定 DP 数组的计算顺序。对于小偷问题,我们分析子问题的依赖关系,发现每个 f(k) 依赖 f(k−1) 和 f(k-2)。也就是说,dp[k] 依赖 dp[k-1] 和 dp[k-2],如下图所示。

在这里插入图片描述

那么,既然 DP 数组中的依赖关系都是向右指的,DP 数组的计算顺序就是从左向右。这样我们可以保证,计算一个子问题的时候,它所依赖的那些子问题已经计算出来了。

代码:

class Solution {
public:
    int rob(vector<int>& nums) {
        if(nums.size() == 0)
            return 0;
        vector<int> dp(nums.size()+1, 0);
        dp[0] = 0;
        dp[1] = nums[0];
        for(int i = 2; i <= nums.size(); i++)
            dp[i] = max(dp[i-1], dp[i-2] + nums[i-1]);
        return dp[nums.size()];
    }
};

废话:
学到了。感觉和数学里的数学归纳法很像啊,基本是同一个思想,理解起来应该没什么难度。也算是让我体验了一下学以致用的感觉,曾经学过的数学思想能在实际应用中有出色的发挥,这感觉很棒。让自己应试教育的学生生涯有了那么点色彩。


又遇到了一个可用动态规划解的题目,但我没想出来,我为什么这么蠢啊

题目
在这里插入图片描述
动态规划dp解法
正着想没法子,再逆着想试试

class Solution {
public:
    int numSquares(int n) {
        vector<int> dp(n+1, INT_MAX);
        dp[0] = 0;
        for(int i = 1; i*i <= n; i++) dp[i*i] = 1;
        for(int i = 2; i <= n; i++){
            for(int j = 1; j*j <= i; j++){
                dp[i] =min(dp[i], dp[i-j*j] + 1);
            }
        }
        return dp[n];
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值