动态规划
准备工作
首先,我们需要确定适用动态规划这种算法的题目特征,毕竟笔试题不会好心地在旁边标上动态规划的标签。
维基百科:动态规划在查找有很多重叠子问题的情况的最优解时有效……只能应用于有最优子结构的问题。听起来非常云里雾里(专业总结都是这么抽象)。我们先来理解一下最优子结构。最优子结构是局部最优解能决定全局最优解。也就是说,动态规划其实和分治方法非常类似,都是通过组合子问题的解来求解原问题,但是分治方法划分的子问题互不相交,而动态规划的子问题则互相重叠。为了减少复杂度,保证每个子问题只求解一次,我们利用**历史记录(备忘录)**来避免重复计算,其实就是所谓的dp
数组(一维/二维/甚至有三维,不过一般会压缩到二维)
下面就正式进入求解三部曲:
- 确定dp数组的含义:好的开始就是成功的一半!本人曾经多少次因为找不出dp数组的含义而默默流泪,举步维艰……其实数组的含义一般都不会太复杂,一般都直接和要求解的问题挂钩,背包问题就是最好的示例。
- 找出状态转移方程:实战部分!需要我们深入分析每种可能的情况。
- 确定初始状态:请不要忽略这一步!不正确的边界条件常常是一些奇怪bug的罪魁祸首,对细心和耐心程度要求很高!可以用极端的测试数据自己在纸上演练一遍。这里通常要注意dp数组是从0开始还是从1开始。
在求解出正确答案的前提下,我们还常常进行dp数组的压缩。这部分将在后面具体展开。
基本:一维
练手题:53(经典) 70 213 198 413(可以直接用数学知识解) 650(有坑,解法很巧妙)
-
浅浅总结一下以上涉及到的
dp
数组的含义-
Q:求在不触发机关的情况下最多可以抢劫这n个房子的多少钱
dp[i]
:抢劫到第 i 个房子时,可以抢劫的最大数量(return dp[n]
) -
Q:求给定数组中连续且等差的子数组一共有多少个
dp[i]
:以第i个数组元素结尾的子数组数目(return accumulate(dp.begin(), dp.end(), 0)
) -
Q:给定整数数组
nums
,请找出一个具有最大和的连续子数组(最少包含一个元素),返回其最大和dp[i]
:dp[i]表示以i结尾的连续数组的最大值,最后返回其中的最大值
我们发现,dp数组的含义大多和求解问题直接挂钩,当然后面会根据问题的不同需要部分变通
-
下面讲解一些个人认为比较有代表性的题:
-
213
打家劫舍 II:作为70题的plus版本,该题增加了一个限制:这个地方所有的房屋都 围成一圈 。这意味着第一个房屋和最后一个房屋是紧挨着的。本菜狗一开始一直试图通过改变dp数组的含义或者状态转移方程来解题,后面发现正确的思路应该是分治:分为不偷第一间和不偷最后一间的情况。**注意是不偷不是偷!**如果是偷的话就需要分三种情况了。想通后就豁然开朗了。class Solution { public: //不要想着一次性解决,将情况分为两种,最后取max就好 int rob(vector<int>& nums) { if(nums.size()==1) return nums[0]; //不偷第一间!分情况不是分成偷第一间和偷最后一间,这样会出来三种情况 //动态规划中也是可以结合分治算法的 int ppre=0,pre=0,cur1,cur2; for(int i=1;i<nums.size();++i){ cur1=max(ppre+nums[i],pre); ppre=pre; pre=cur1; } //不偷最后一间 ppre=pre=0; for(int i=0;i<nums.size()-1;++i){ cur2=max(ppre+nums[i],pre); ppre=pre; pre=cur2; } return max(cur1,cur2); } };
-
343
整数拆分:给定正整数n
将其拆分为k
个 正整数 的和(k >= 2
)并使这些整数的乘积最大化。特点:本身不难,但是有很多小细节,以及转移方程不一定是根据dp的历史结果
int integerBreak(int n) { vector<int> dp(n+1,1); for(int i=2;i<=n;i++) for(int j=1;j<i;++j) //不能取i,不能sqrt,不用整除……天知道我踩了多少诡异的坑 dp[i]=max(dp[i],max(j,dp[j])* max(i-j,dp[i-j])); return dp[n]; }
-
650
:最初记事本上只有一个 ‘A’ ,可以通过Copy All(复制当前全部)和Paste(粘贴上一次复制的字符),求最少的操作次数使记事本上输出恰好 n 个 ‘A’ 。审题:是拷贝当前!的全部,粘贴也只能是上一次!复制的字符。不同于以往通过加减实现的动态规划,这里需要乘除法来计算位置,因为粘贴操作是倍数增加的。
dp[i]:延展到长度 i 的最少操作次数。对于每个位置 j,如果 j 可以被 i 整除,那么长度 i 就可以由长度 j 操作得到,其操作次数等价于把一个长度为 1 的 A 延展到长度为 i/j。因此递推公式是 dp[i] = dp[j] + dp[i/j]。
注意,官方给的题解状态转移方程是min(dp[i/j]+j-1,dp[j]+i/j-1)</