5.5 KiB
什么是动态规划
动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。
所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心是局部直接选最优的,
在关于贪心算法,你该了解这些!中我举了一个背包问题的例子。
例如:有N件物品和一个最多能被重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
动态规划中dp[j]是又dp[j-weight[i]]推导出来的。
但如果是贪心呢,每次拿物品选一个最大的或者最小的就完事了。
所以贪心解决不了动态规划的问题,这也是最大的区别。
大家也不用死扣动规和贪心的理论区别,后面做做题目自然就知道了。
而且很多讲解动态规划的文章都会讲最优子结构啊和重叠子问题啊这些,这些东西都是教科书的上定义,晦涩难懂而且不实用。
大家知道动规是由前一个状态推导出来的,而贪心是局部直接选最优的,对于刷题来说就够用了。
上述提到的背包问题,后序会详细讲解。
动态规划的解题步骤
题目的时候,很多同学会陷入一个误区,就是以为把状态转移公式背下来,照葫芦画瓢改改,就开始写代码,甚至把题目AC之后,都不太清楚dp[i]表示的是什么。
这就是一种朦胧的状态,然后就把题给过了,遇到稍稍难一点的,可能直接就不会了,然后看题解,然后继续照葫芦画瓢陷入这种恶性循环中。
状态转移公式(递推公式)是很重要,但动规不仅仅只有递推公式。
对于动态规划问题,我将拆解为如下四步曲,这四步都搞清楚了,才能说把动态规划真的掌握了!
- 确定dp数组以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
一些同学可能想为什么要先确定递推公式,然后在考虑初始化呢?
因为一些情况是递推公式决定了dp数组要输入初始化!
后面的讲解中我都是围绕着这四个点来经行讲解。
可能刷过动态规划题目的同学可能都知道递推公式的重要性,感觉确定了递推公式这道题目就解出来了。
其实 确定递推公式 仅仅是解题里的一步而且,dp数组以及下标的含义, dp数组的初始化 以及确定遍历顺序,都非常重要。
后序的讲解的大家就会发现其重要性。
很多同学甚至知道递推公式,但搞不清楚dp数组应该如何初始化,或者正确的遍历顺序,以至于记下来公式,但写的程序怎么改都通过不了。
动态规划应该如何debug
平时我自己写的时候也经常出问题,找问题的最好方式就是把dp数组打印出来,看看究竟是不是按照自己思路推导的!
一些同学对于dp的学习是上来就是俯视类型的,总是想一下子全盘接纳,一鼓作气写出代码,如果代码能通过万事大吉,通过不了的话就凭感觉改一改。
这是一个很不好的习惯!
做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果。
然后在写代码,如果代码没通过就打印dp数组,看看是不是和自己推导的哪里不一样。
如果和自己模拟推导的一样,那么就是自己的递归公式有问题。
如果和自己模拟推导的不一样,那么就是代码实现细节有问题。
这样才是一个完整的思考过程,而不是一旦代码出问题,就毫无头绪的东改改西改改,最后过不了,或者说是稀里糊涂的过了。
我来举一个例子:一些同学可能代码通过不了,都会把代码抛到出来问:我这里代码都已经和题解一模一样的,为什么通过不了呢?
发出这样的问题之前,其实可以自己先思考这三个问题:
- 这道题目我推导状态转移公式了么?
- 我打印dp数组的日志了么?
- 打印出来了dp数组和我想的一样么?
如果这灵魂三问自己都做到了,基本上这道题目也就解决了,或者更清晰的知道自己究竟是哪一点不明白,是状态转移不明白,还是实现代码不知道该怎么写,还是不理解遍历dp数组的顺序。
然后在问问题,目的性就很强了,回答问题的同学也可以快速知道提问者的疑惑了。
总结
这一篇是动态规划的整体概述,讲解了什么是动态规划,动态规划的解题步骤,以及如何debug。
动态规划是一个很大的领域,今天这一篇讲解的内容是整个动态规划系列里都会使用到的一些理论基础。
在后序讲解中针对某一具体问题,还会讲解其对应的理论基础,例如背包问题中的01背包,leetcode上的题目都是01背包的应用,而没有纯01背包的问题,那么就需要在把对应的理论知识讲解一下。
一些同学可能着急想刷题,这个我很理解,我写的理论基础篇已经是非常偏实用的了,还是需要一点基础的。新加入的录友可能不了解,可以在「算法汇总」中看到每一个系列开始的时候都有对应的理论基础篇,都是特别实用的理论基础了。
今天我们开始新的征程了,你准备好了么?