Ⅰ 算法题套路总结(三)——动态规划
前两篇我总结了链表和二分查找题目的一些套路,这篇文章来讲讲动态规划。动态规划从我高中开始参加NOIP起就一直是令我比较害怕的题型,除了能一眼看出来转移方程的题目,大部分动态规划都不太会做。加上后来ACM更为令人头秃的动态规划,很多题解看了之后,我根本就不相信自己能够想出来这种解法,看着大佬们谈笑间还加一些常数优化,一度怀疑自己的智商。以前一直觉得动态规划是给大佬准备的,所以刻意地没有去攻克它,主要也是没有信心。但是后来慢慢的,我再做LC的时候,发现很多DP的题目我自己慢慢能够推出转移方程了,而且似乎也没那么难。我一直在思考,到底是我变强了,还是因为LC的题目相比ACM或者NOI太简单了。其实主要还是后者,但是同时我也发现,动态规划其实是有套路的,我以前方法不对,总结太少。
主要就是,站在出题人的角度,他几乎不太可能完全凭空想出一个新的DP模型,因为动态规划毕竟要满足:
因此,能够利用DP来解决的问题实际上是有限的,大部分题目都是针对现有的模型的一些变种,改改题目描述,或者加点限制条件。所以要想攻克DP题目,最根本的就是要充分理解几个常见的DP模型。而要充分理解常见经典DP模型,就需要通过大量的做题和总结,而且二者不可偏废。通过做题进行思考和量的积累,通过总结加深理解和融会贯通进而完成质的提升。
动态规划是求解一个最优化问题,而最核心的思想就是:
解一道DP题目,先问自己几个问题:
当然以上内容看起来比较抽象,虽然它深刻地揭露了动态规划的本质,但是如果临场要去想明白这些问题,还是有些难度。如果只是针对比赛和面试,就像前面说的,DP题型是有限的。只要刷的题目足够多,总结出几个经典模型,剩下的都是些变种+优化而已。
一般来说,动态规划可以分成4个大类:
线性DP就是阶段非常线性直观的模型,比如:最长(上升|下降)序列,最长公共子序列(LCS)等,也有一些简单的递推,甚至都算不上是 经典模型 。
最长上升序列是一个非常经典的线性模型。说它是个模型,是因为它是一类题的代表,很多题目都只是换个说法,或者要求在这基础上进一步优化而已。最长上升序列最基础的转移方程就是 f[i] = max{f[j]}+1 (a[i] > a[j]) , f[i] 表示一定要以 a[i] 结尾的序列,最长长度是多少。很显然就是在前面找到一个最大的 f[j] 同时满足 a[j]<a[i] 。因此是 N^2 的时间复杂度和N的空间复杂度。这种方法是最朴素直观的,一定要理解。它非常简单,因此很少有题目直接能够这么做。大部分相关题目需要进一步优化,也就是有名的单调队列优化,能够把复杂度优化到nlogn。
说单调队列优化之前必须明白一个贪心策略。因为要求的是最长上升序列,那么很显然长度为k的上升序列的最大值(最后一个数)越小越好,这样后面的数才有更大的概率比它大。如果我们记录下来不同长度的上升序列的最后一个数能达到的最小值,那么对于后续每个数t,它要么能放到某个长度为y的序列之后,组成长度为y+1的上升序列,要么放到某个长度为x的序列后面,把长度为x+1的序列的最大值替换成t。同时我们可以发现,如果x<y,那么长度为x序列的最后一个数一定比长度为y的序列最后一个数小。因此这个上升序列我们可以用一个数组来维护(所谓的单调队列),数组下标就代表序列长度。 opt[i]=t 表示长度为i的上升序列最后一个数最小是t。那么当我们在面对后续某个数x时,可以对单调队列opt进行二分,把它插到对应的位置。因此总体复杂度就是NlogN。
相关题目比如:
但是你可以发现,其实这个题型其实变种很有限,吃透了也就那么回事。所以一定要总结。
最长公共子序列也是线性DP中的一种比较常见的模型。说它是一种“模型”其实有点拔高了,其实它就是一类比较常见的题目。很多题目都是在LCS的基础上进行简单的扩展,或者仅仅就是换一个说法而已。
求两个数组的最长公共子序列,最直观地做法就是:设f[i][j]表示S[..i]和T[..j]的最长公共子序列,则有:
这个转移方程也非常好理解,时间复杂度是 N^2 ,空间复杂度也是 N^2 。不过仔细观察你可以发现,当我们计算第i行时只与i-1和i行有关。因此我们可以利用01滚动来优化空间复杂度为2N。
相关题目:
线性DP除了上述的两种常见题型,还有很多别的类型,包括背包。我们要努力去尝试理解这些题目的异同,它们的转移方程,以及思路,可能的变化,这样才能更好的应对未知的题目。以下是一些我总结的题型:
最终结果就是max(0, f[n][2]+f[n][4])。
不过实际上你可以发现,由于各个状态只和前一维有关,且只能由固定的一个状态转移过来,因此我们可以省掉一维,只用4个变量来存储:
剩下的,同123题类似,由于最多进行k次交易,那么一天就有2k个状态:第1次买/卖……第k次买/卖,结合123题的优化,我们只需要2k个变量就能存储这些状态。因此设f[i×2]为第i次买入的最优值,f[i×2+1]为第i次卖出的最优值:
以上都是对一些常见的线性DP的一些小结,实际上线性DP还有一个重要的题型就是背包。关于背包,有很多相关的讲解,我这里就不多说了,推荐大家看看 背包九讲 。下一章依然是DP专题,我讲总结一些区间DP的题型。大部分区间DP都是hard级的,对于希望提高自己水平的人来说,需要投入更多精力去理解。
Ⅱ 计算机算法分析考试:动态规划0-1背包问题,怎么算
问题描述:
给定n种物品和一背包,物品i的重量是wi,其价值为vi,背包的容量为C。问应如何选择装入背包的物品(物品不能分割),使得装入背包中物品的总价值最大?
抽象描述如下:
x[n]:表示物品的选择,x[i]=1表示选择放进物品i到背包中。
Ⅲ Python之动态规划算法
动态规划算法中是将复杂问题递归分解为子问题,通过解决这些子问题来解决复杂问题。与递归算法相比,动态编程减少了堆栈的使用,避免了重复的计算,效率得到显着提升。
先来看一个简单的例子,斐波那契数列.
斐波那契数列的定义如下。
斐波那契数列可以很容易地用递归算法实现:
上述代码,随着n的增加,计算量呈指数级增长,算法的时间复杂度是 。
采用动态规划算法,通过自下而上的计算数列的值,可以使算法复杂度减小到 ,代码如下。
下面我们再看一个复杂一些的例子。
这是小学奥数常见的硬币问题: 已知有1分,2分,5分三种硬币数量不限,用这些硬币凑成为n分钱,那么一共有多少种组合方法。
我们将硬币的种类用列表 coins 定义;
将问题定义为一个二维数组 dp,dp[amt][j] 是使用 coins 中前 j+1 种硬币( coins[0:j+1] )凑成总价amt的组合数。
例如: coins = [1,2,5]
dp[5][1] 就是使用前两种硬币 [1,2] 凑成总和为5的组合数。
对于所有的 dp[0][j] 来说,凑成总价为0的情况只有一种,就是所有的硬币数量都为0。所以对于在有效范围内任意的j,都有 dp[0][j] 为1。
对于 dp[amt][j] 的计算,也就是使用 coins[0:j+1] 硬币总价amt的组合数,包含两种情况计算:
1.当使用第j个硬币时,有 dp[amt-coins[j]][j] 种情况,即amt减去第j个硬币币值,使用前j+1种硬币的组合数;
2.当不使用第j个硬币时,有 dp[amt][j-1] 种情况,即使用前j种硬币凑成amt的组合数;
所以: dp[amt][j] = dp[amt - coins[j]][j]+dp[amt][j-1]
我们最终得到的结果是:dp[amount][-1]
上述分析省略了一些边界情况。
有了上述的分析,代码实现就比较简单了。
动态规划算法代码简洁,执行效率高。但是与递归算法相比,需要仔细考虑如何分解问题,动态规划代码与递归调用相比,较难理解。
我把递归算法实现的代码也附在下面。有兴趣的朋友可以比较一下两种算法的时间复杂度有多大差别。
上述代码在Python 3.7运行通过。
Ⅳ 动态规划怎样计算最优解和最优值
动态规划算法通常用于求解具有某种最优性质的问题,需要最优子结构性质来确定最优策略。
一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。
Ⅳ 动态规划
动态规划(Dynamic Programming,DP)是运筹学的一个分支,是求解 决策过程最优化 的过程。20世纪50年代初,美国数学家贝尔曼(R.Bellman)等人在研究多阶段决策过程的优化问题时,提出了着名的最优化原理,从而创立了动态规划。动态规划的应用极其广泛,包括工程技术、经济、工业生产、军事以及自动化控制等领域,并在背包问题、生产经营问题、资金管理问题、资源分配问题、最短路径问题和复杂系统可靠性问题等中取得了显着的效果。
虽然动态规划主要用于求解以时间划分阶段的动态过程的优化问题,但是一些与时间无关的静态规划(如 线性规划、非线性规划 ),只要人为地引进时间因素,把它视为多阶段决策过程,也可以用动态规划方法方便地求解。
在现实生活中,有一类活动的过程,由于它的特殊性,可将过程分成若干个互相联系的阶段,在它的每一阶段都需要作出决策,从而使整个过程达到最好的活动效果。因此各个阶段决策的选取不能任意确定, 它依赖于当前面临的状态,又影响以后的发展 。当各个阶段决策确定后,就组成一个决策序列,因而也就确定了整个过程的一条活动路线.这种把一个问题看作是一个 前后关联具有链状结构的多阶段过程 就称为多阶段决策过程,这种问题称为多阶段决策问题。在多阶段决策问题中,各个阶段采取的决策,一般来说是与时间有关的, 决策依赖于当前状态,又随即引起状态的转移 ,一个决策序列就是在变化的状态中产生出来的,故有“动态”的含义,称这种解决多阶段决策最优化的过程为动态规划方法
动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。 动态规划算法与分治法类似 ,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是, 适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的 。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。
以一个例子来说明动态规划的概念(leetcode第5题最长回文子串):
在这个例子中,一个字符串如果是回文子串,那么去掉头尾也照样是回文子串。而每一个字符都有可能是最长回文子串的一部分。
上面这个例子使用一个二维数组表示各个阶段的状态,这个二维数组的行是子串的起始位置,列是子串的结束位置。由于j>=i,所以只需要考虑二维数组的主对角线的上半部分,对角线上的值永远是true。用true表示这个子串是回文串,false不是回文串。那么对于某个固定位置的数组元素来说,它的值依赖于左下角的元素的值。进行填充的时候只能一列一列地进行填充,同一列的元素从上到下依次填充。