1.线性动态规划
线性动态规划的特点是:状态推导按照i从小到大进行推导。
大规模的解答依靠小规模的解答。
解决动态规划问题大体上分为:
- 问题的Base Case情况(最简单的情况)
- 问题的状态有哪些
- 什么选择问题的状态可能会改变
- 如何定义dp数组表达状态和选择(状态转移)
线性动态规划状态定义一般为:
dp[n]:0~n符合题干的答案
状态转移一般有两种情况:
1. 依赖比 i 小的 O(1) 个子问题
dp[n] = f(dp[n-1], ..., dp[0]);
其中f一般为最大值和最小值函数。具体情况具体分析
2. 依赖比 i 小的 O(n) 个子问题
此时dp[n] 与此前的更小规模的所有子问题 dp[n - 1], dp[n - 2], …, dp[1] 都可能有关系
for(int i=1;i<n;i++){
for(int j=1;j<i-1;j++){
dp[i]=f(dp[i],f(dp[j]))
}
}
其中f一般为最大值和最小值函数。具体情况具体分析
线性动态规划分为:单串,双串,矩阵问题等,这里先练习单串最经典的LIS问题。
2.例题练习
最长递增子序列
LeetCode原题链接
思路:输入是一个单串,首先思考单串问题中设计状态
- dp[i]数组定义:代表数组下标为i时最长子序列。包括数组下标为i的元素。
- 初始状态:当i=0时,数组只有一个元素,所以dp[0]=1。
- 每次选择是将数组的下标+1,代表检测过这个元素了。这个选择会影响最长子序列。
- 状态转化:因为dp[i]是数组下标为i时最长子序列,但不一定dp[i-1]一定小于dp[i]。所以这个状态转化为dp[i]=max(dp[i],(dp[0]…dp[i-1])+1)。
- 需要注意,更新dp数组的时机,当num[0]…num[i-1]之间有元素<num[i]才需要更新dp[i]。
- 最后的值是dp数组中最大的值,所以需要记录最大值作为返回值。
eg:
nums[j]<nums[i](0<= j <=i-1)
此时的dp[i]=max(dp[i],dp[j]+1);
需要遍历0~i-1次dp数组才可以判断dp[i]的值。
这个状态转移是第二类。依赖比 i 小的 O(n) 个子问题
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
//dp[i]代表数组下标为i时最长子序列
vector<int>dp(nums.size(),1);
dp[0]=1;
int ret=1;
for(int i=1;i<nums.size();i++){
for(int j=0;j<i;j++){
//因为dp[i]最大值不一定是最后值,所以每次都需要遍历一遍dp数组找最大值
if(nums[j]<nums[i]){
dp[i]=max(dp[j]+1,dp[i]);
}
}
ret=max(ret,dp[i]);
}
return ret;
}
};
最长递增子序列的个数
LeetCode原题链接
这个题的思路与上题相同,但是需要返回最长递增的子序列个数。
首先,我们是按照递增的方式找的子序列,所以数列一定是递增的。
我们定义一个数组count,count[i]代表nums[i]元素之前的递增序列长度有多少个。
最后答案为count数组中长度是最长子序列的所有数组元素和。
int longest;//最长递增子序列
int ans;//最后答案
for(int i=0;i<count.size();i++){
if (dp[i] == longest) {
ans += counts[i];
}
}
之所以这么设,是因为最长递增子序列可能在dp数组的任何一个位置。
其次在一问中我们可以知道
当dp[i]>dp[j]需要更新dp数组
dp[i]=max(dp[i],(dp[0]…dp[i-1])+1)
for(int i=1;i<nums.size();i++){
for(int j=0;j<i;j++){
//因为dp[i]最大值不一定是最后值,所以每次都需要遍历一遍dp数组找最大值
if(nums[j]<nums[i]){
//dp[i]=max(dp[j]+1,dp[i]);
if(dp[j]>=dp[i]){
dp[i]=dp[j]+1 ;
count[i]=count[j];//最长递增子序列发生改变,此时count[i]为最长递增子序列,其值和发生改变的上一个位置相同。
}
else if(dp[j] + 1 == dp[i]){
//最长递增子序列长度没变化,但是序列发生变化,有count[j] 个额外的序列
count[i]+=count[j];
}
}
}
ret=max(ret,dp[i]);
}
- 如果这些序列比 length[i] 长,那么我们就知道我们有count[j] 个长度为 length 的序列。
- length[j] + 1 = length[i]说明在某个最长递增序列中,num[j]正好是num[i]的前一个元素。那么num[j]所对应的count[j]应该要累加到num[i]所对应的count[i]上
class Solution {
public:
int findNumberOfLIS(vector<int>& nums) {
int size=nums.size();
if(size==0||size==1){
return size;
}
vector<int>dp(size,1);
vector<int>count(size,1);
dp[0]=1;
count[0]=1;
int maxlen=1;
for(int i=0;i<size;i++){
for(int j=0;j<i;j++){
if(nums[i]>nums[j]){
//dp[i]=max(dp[j]+1,dp[i]);
if(dp[j]+1==dp[i]){
count[i]+=count[j];
}
else if(dp[j]+1>=dp[i]){
count[i]=count[j];
dp[i]=dp[j]+1;
}
}
}
maxlen=max(maxlen,dp[i]);
}
int ans=0;
for(int i=0;i<count.size();i++){
if(dp[i]==maxlen){
ans+=count[i];
}
}
return ans;
}
};