使用Java刷动态规划问题的经验总结

1、基调

动态规划很难,不像排序算法或者查找算法,只需要记住固定的代码,就可以使用在任何需要该算法的地方,动态规划只有套路,或者说框架,可能你做了几百道动态规划的题,思路一时间没转过弯来,leetcode中medium级别的题都做不出来

在这里插入图片描述

2、术语解释

动态规划有最优子结构、重叠子问题、状态转移方程、dp数组、base case、状态等术语。

看起来高深莫测,其实都是一些容易理解的东西,这里我就解释一下我感觉需要了解的术语:

状态

什么是状态,状态就是变量,与所求的结果密切相关的变量。

比如背包问题,要求背包能装物品的最大价值,就与当前背包的容量,和当前物品的重量密切相关;
比如爬楼梯问题,要求有多少种爬楼梯的方式,与选择爬多少阶楼梯密切相关。
比如凑零钱问题,要求最少的硬币数凑零钱,与选择的硬币的价值,和当前硬币的个数密切相关。

状态转移方程

通俗点,就是在已知dp[0],dp[1]…dp[i-1]的情况下,如何推导出dp[i]。没啥好说的,就这么简单。

dp table

也就是所谓的备忘录,并且数组相比较于List,Map之类的,具有天然的绝对优势,所以每次动态规划的题目都会首先定义一个dp数组,比如dp[0],dp[1]…dp[i-1],避免重复计算。

base case

也就是初始条件,都知道从dp[0],dp[1]…dp[i-1]推导出dp[i],那dp[0]咋办,难道还有dp[-1]来推导吗,所以就只能依靠手动初始化dp[0]这些数据,从而保证后面能够正常推导。

至于最优子结构和重叠子问题,主要用于从递归推导出动态规划,了解一下就行,在我的学习历程中,对于写出动态规划的题目,用处不大(个人想法)

3、套路/框架

在刚接触动态规划的题目的时候,许多博主都说了一个解决动态规划问题的流程:
递归的暴力解法 => 带备忘录的递归解法 => 非递归的动态规划解法

这对于新手理解来说,确实如此,但是对于熟悉写动态规划,感觉没啥用处,反而让我走了一些弯路。刚开始我按照这个流程,想办法用暴力解法来写,结果越写越懵,就算写出了暴力解法,又要去想方设法去想,如何增加备忘录,然后才是改造为动态规划解法,太繁琐了,我感觉比起直接用动态规划的写法来写,更难。

所以我按照我的学习经验,来谈一谈如何搞定动态规划。

好了,接下来就是具体的流程了:
1、确认状态(我感觉这一步也好难,和推导状态转移方程难度五五开),也就是不管啥题目,先假设使用二维数组dp[i][j]来推导,然后尝试用一句话来概括这个dp[i][j]的含义,比如背包问题,dp[i][j]代表的是对于前i个物品,当前背包的容量为j时的最大价值。比如凑零钱问题,dp[i][j]代表的是对于前i个硬币,当前的总价值为j时,所使用的最小硬币个数。但是如果是爬楼梯问题,如果使用dp[i][j]二维数组,貌似就有点难定义了,i,j要分别代表什么才能用一句概括dp[i][j]的含义?我做这题的时候,发现没有那么多的状态,就改变思路,选择用一维数组,也就是dp[i]代表的是前i阶楼梯的不同上楼方法的个数。或者两个状态不能够表达题意,也可以选择用三维数组dp[i][j][k]来表述,比如leetcode【474】一和零问题,就需要用三维数组:dp[i][j][k]代表的是前面i个字符串,有j个0,k个1,值为子集的长度。

2、写出状态转移方程:根据状态,以及你确认了使用几维数组,就可以尝试推导出状态转移方程了。这一步只能根据经验,只要牢记一点:在已知dp[0],dp[1]…dp[i-1]的情况下,如何推导出dp[i],可以从简单的第0个,第1个,第2个入手,然后推导出规律,也就是使用数学归纳法。

3、确定base case ,一般情况下一维数组就是手动求出dp[0],二维数组就是使用一两个for循环,手动求出dp[0][0],dp[1][0]…dp[i][0],或者手动求出dp[0][0],dp[0][1]…dp[0][j]。这需要看情况,比如爬楼梯用一维数组,就初始化个dp[0]就行,背包问题和凑零钱问题用二维数组,就是初始化dp[i][0],以及dp[0][j]。

4、使用for循环框架,这个框架符合绝大多数的动态规划问题(因为绝大多数的动态规划问题都是用二维数组),甚至有一些一维数组也需要用双重for循环才行。

for (int i = 1; i <= N; i++) {
        for (int j = 1; j <= W; j++) {
            if(某种情况){
            	状态转移方程
            }else{
			    状态转移方程或赋值
			}
        }
    }

就硬往这个框架套,至于N、W是啥,一般情况下是二维数组的两个长度,具体情况,就要根据题意了,有一些情况下,W等于i。情况多种多样,这也是一大难点,不过如果推导出了状态转移方程,这些边界条件,都比较容易推算出来。有些题目,如果你没思路,不知道到动态转移方程怎么写,可以先把大体解题框架写出来,比如定义dp,初始化,两个for循环。写完后,就有可能迎刃而解(亲测,好多题我都是写完大体流程才最终确定状态转移方程,因为写完大体框架能够帮助我们理解题目

为什么会有这个框架,因为一个点:所有的动态规划本质都是穷举(你穷举难道不得用for循环吗?)
在这里插入图片描述

动态规划相比较于递归之类的优势,就是不会重复计算,100种可能,动态规划就只需要穷举100种可能。而递归,就会重复穷举,100种可能,递归可能要穷举10000遍才能找出最优解。(天哪,我写了1700字)
在这里插入图片描述

posted @ 2022-06-14 08:49  道祖且长  阅读(70)  评论(0)    收藏  举报