leetcode-master/problems/动态规划理论基础.md

144 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<p align="center">
<a href="https://mp.weixin.qq.com/s/QVF6upVMSbgvZy8lHZS3CQ"><img src="https://img.shields.io/badge/知识星球-代码随想录-blue" alt=""></a>
<a href="https://mp.weixin.qq.com/s/b66DFkOp8OOxdZC_xLZxfw"><img src="https://img.shields.io/badge/刷题-微信群-green" alt=""></a>
<a href="https://img-blog.csdnimg.cn/20201210231711160.png"><img src="https://img.shields.io/badge/公众号-代码随想录-brightgreen" alt=""></a>
<a href="https://space.bilibili.com/525438321"><img src="https://img.shields.io/badge/B站-代码随想录-orange" alt=""></a>
</p>
<p align="center"><strong>欢迎大家参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!</strong></p>
## 什么是动态规划
动态规划英文Dynamic Programming简称DP如果某一问题有很多重叠子问题使用动态规划是最有效的。
所以动态规划中每一个状态一定是由上一个状态推导出来的,**这一点就区分于贪心**,贪心没有状态推导,而是从局部直接选最优的,
在[关于贪心算法,你该了解这些!](https://mp.weixin.qq.com/s/O935TaoHE9Eexwe_vSbRAg)中我举了一个背包问题的例子。
例如有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i]得到的价值是value[i] 。**每件物品只能用一次**,求解将哪些物品装入背包里物品价值总和最大。
动态规划中dp[j]是由dp[j-weight[i]]推导出来的然后取max(dp[j], dp[j - weight[i]] + value[i])。
但如果是贪心呢,每次拿物品选一个最大的或者最小的就完事了,和上一个状态没有关系。
所以贪心解决不了动态规划的问题。
**其实大家也不用死扣动规和贪心的理论区别,后面做做题目自然就知道了**
而且很多讲解动态规划的文章都会讲最优子结构啊和重叠子问题啊这些,这些东西都是教科书的上定义,晦涩难懂而且不实用。
大家知道动规是由前一个状态推导出来的,而贪心是局部直接选最优的,对于刷题来说就够用了。
上述提到的背包问题,后序会详细讲解。
## 动态规划的解题步骤
做动规题目的时候很多同学会陷入一个误区就是以为把状态转移公式背下来照葫芦画瓢改改就开始写代码甚至把题目AC之后都不太清楚dp[i]表示的是什么。
**这就是一种朦胧的状态,然后就把题给过了,遇到稍稍难一点的,可能直接就不会了,然后看题解,然后继续照葫芦画瓢陷入这种恶性循环中**
状态转移公式(递推公式)是很重要,但动规不仅仅只有递推公式。
**对于动态规划问题,我将拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!**
1. 确定dp数组dp table以及下标的含义
2. 确定递推公式
3. dp数组如何初始化
4. 确定遍历顺序
5. 举例推导dp数组
一些同学可能想为什么要先确定递推公式,然后在考虑初始化呢?
**因为一些情况是递推公式决定了dp数组要如何初始化**
后面的讲解中我都是围绕着这五点来进行讲解。
可能刷过动态规划题目的同学可能都知道递推公式的重要性,感觉确定了递推公式这道题目就解出来了。
其实 确定递推公式 仅仅是解题里的一步而已!
一些同学知道递推公式但搞不清楚dp数组应该如何初始化或者正确的遍历顺序以至于记下来公式但写的程序怎么改都通过不了。
后序的讲解的大家就会慢慢感受到这五步的重要性了。
## 动态规划应该如何debug
相信动规的题目,很大部分同学都是这样做的。
看一下题解,感觉看懂了,然后照葫芦画瓢,如果能正好画对了,万事大吉,一旦要是没通过,就怎么改都通过不了,对 dp数组的初始化递归公式遍历顺序处于一种黑盒的理解状态。
写动规题目,代码出问题很正常!
**找问题的最好方式就是把dp数组打印出来看看究竟是不是按照自己思路推导的**
一些同学对于dp的学习是黑盒的状态就是不清楚dp数组的含义不懂为什么这么初始化递推公式背下来了遍历顺序靠习惯就是这么写的然后一鼓作气写出代码如果代码能通过万事大吉通过不了的话就凭感觉改一改。
这是一个很不好的习惯!
**做动规的题目写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍心中有数确定最后推出的是想要的结果**
然后再写代码如果代码没通过就打印dp数组看看是不是和自己预先推导的哪里不一样。
如果打印出来和自己预先模拟推导是一样的,那么就是自己的递归公式、初始化或者遍历顺序有问题了。
如果和自己预先模拟推导的不一样,那么就是代码实现细节有问题。
**这样才是一个完整的思考过程,而不是一旦代码出问题,就毫无头绪的东改改西改改,最后过不了,或者说是稀里糊涂的过了**
这也是我为什么在动规五步曲里强调推导dp数组的重要性。
举个例子哈:在「代码随想录」刷题小分队微信群里,一些录友可能代码通过不了,会把代码抛到讨论群里问:我这里代码都已经和题解一模一样了,为什么通过不了呢?
发出这样的问题之前,其实可以自己先思考这三个问题:
* 这道题目我举例推导状态转移公式了么?
* 我打印dp数组的日志了么
* 打印出来了dp数组和我想的一样么
**如果这灵魂三问自己都做到了,基本上这道题目也就解决了**或者更清晰的知道自己究竟是哪一点不明白是状态转移不明白还是实现代码不知道该怎么写还是不理解遍历dp数组的顺序。
然后在问问题,目的性就很强了,群里的小伙伴也可以快速知道提问者的疑惑了。
**注意这里不是说不让大家问问题哈, 而是说问问题之前要有自己的思考,问题要问到点子上!**
**大家工作之后就会发现,特别是大厂,问问题是一个专业活,是的,问问题也要体现出专业!**
如果问同事很不专业的问题,同事们会懒的回答,领导也会认为你缺乏思考能力,这对职场发展是很不利的。
所以大家在刷题的时候,就锻炼自己养成专业提问的好习惯。
## 总结
这一篇是动态规划的整体概述讲解了什么是动态规划动态规划的解题步骤以及如何debug。
动态规划是一个很大的领域,今天这一篇讲解的内容是整个动态规划系列中都会使用到的一些理论基础。
在后序讲解中针对某一具体问题还会讲解其对应的理论基础例如背包问题中的01背包leetcode上的题目都是01背包的应用而没有纯01背包的问题那么就需要在把对应的理论知识讲解一下。
大家会发现,我讲解的理论基础并不是教科书上各种动态规划的定义,错综复杂的公式。
这里理论基础篇已经是非常偏实用的了,每个知识点都是在解题实战中非常有用的内容,大家要重视起来哈。
今天我们开始新的征程了,你准备好了么?
## 其他语言版本
Java
Python
Go
-----------------------
* 作者微信:[程序员Carl](https://mp.weixin.qq.com/s/b66DFkOp8OOxdZC_xLZxfw)
* B站视频[代码随想录](https://space.bilibili.com/525438321)
* 知识星球:[代码随想录](https://mp.weixin.qq.com/s/QVF6upVMSbgvZy8lHZS3CQ)
<div align="center"><img src=../pics/公众号.png width=450 alt=> </img></div>