前言
上一篇主要是基本的一维和二维动态规划,这次是一些奇奇怪怪的动态规划。分割类型题
leetcode 279
对于分割类型题,动态规划的状态转移方程通常并不依赖相邻的位置,而是依赖于满足分割条件的位置。我们定义一个一维矩阵dp,其中dp[i] 表示数字i 最少可以由几个完全平方数相加构成。在本题中,位置i 只依赖
i
−
k
2
i - k^2
i−k2 的位置,如i - 1、i - 4、i - 9 等等,才能满足完全平方分割的条件。因此dp[i] 可以取的最小值即为1 + min(dp[i-1], dp[i-4], dp[i-9] … )。
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n+1,INT_MAX);
dp[0]=0;
for(int i=1;i<=n;++i){
for(int j=1;j*j<=i;++j){
dp[i]=min(dp[i],dp[i-j*j]+1);
}
}
return dp[n];
}
};
leetcode 91
这是一道很经典的动态规划题,难度不大但是十分考验耐心。这是因为只有1-26 可以表示字母,因此对于一些特殊情况,比如数字0 或者当相邻两数字大于26 时,需要有不同的状态转移方程,详见如下代码。状态转移方程与爬楼梯类似。
特殊情况
- 第一个数字为0
- 出现00 30 40…
- 数字为10~29之间 10和20需要单独考虑
- 1~9 和大于26
class Solution {
public:
int numDecodings(string s) {
int n=s.size();
if(n==0) return 0;
int prev=s[0]-'0';
if(!prev) return 0;
if(n==1) return 1;
vector<int> dp(n+1,1);
for(int i=2;i<=n;++i){
int cur=s[i-1]-'0';
if((prev==0||prev>2)&&cur==0) return 0;
if(prev==1||(prev==2&&cur<7)){
if(cur){
dp[i] = dp[i-2] + dp[i-1];
}else{
dp[i]=dp[i-2];
}
}else{
dp[i]=dp[i-1];
}
prev=cur;
}
return dp[n];
}
};
leetcode 139
类似于完全平方数分割问题,这道题的分割条件由集合内的字符串决定,因此在考虑每个分割位置时,需要遍历字符串集合,以确定当前位置是否可以成功分割。注意对于位置0,需要初始化值为真,相当于空字符串和一个单词。
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
int n=s.size();
vector<bool> dp(n+1,false);
dp[0]=true;
for(int i=1;i<=n;++i){
for(const string& word:wordDict){
int len=word.size();
if(i>=len&&s.substr(i-len,len)==word)
dp[i]=dp[i]||dp[i-len];
}
}
return dp[n];
}
};
子序列问题
leetcode 300 Longest Increasing Subsequence (Medium)
注意按照LeetCode 的习惯,子序列(subsequence)不必连续,子数组(subarray)或子字符串(substring)必须连续。
对于子序列问题,第一种动态规划方法是,定义一个dp 数组,其中dp[i] 表示以i 结尾的子序列的性质。在处理好每个位置后,统计一遍各个位置的结果即可得到题目要求的结果。在本题中,dp[i] 可以表示以i 结尾的、最长子序列长度。对于每一个位置i,如果其之前的某个位置j 所对应的数字小于位置i 所对应的数字,则我们可以获得一个以i 结尾的、长度为dp[j]+ 1 的子序列。为了遍历所有情况,我们需要i 和j 进行两层循环,其时间复杂度为
O
(
n
2
)
O(n^2)
O(n2)。
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n=nums.size();
if(n<2) return n;
int max_len=0;
vector<int> dp(n,1);
for(int i=0;i<n;++i){
for(int j=0;j<i;++j){
if(nums[i]>nums[j]){
dp[i]=max(dp[i],dp[j]+1);
}
}
max_len=max(max_len,dp[i]);
}
return max_len;
}
};
本题还可以使用二分查找将时间复杂度降低为O(nlogn)。我们定义一个dp 数组,其中dp[k]存储长度为k+1 的最长递增子序列的最后一个数字。我们遍历每一个位置i,如果其对应的数字大于dp 数组中所有数字的值,那么我们把它放在dp 数组尾部,表示最长递增子序列长度加1;如果我们发现这个数字在dp 数组中比数字a 大、比数字b 小,则我们将b 更新为此数字,使得之后构成递增序列的可能性增大。以这种方式维护的dp 数组永远是递增的,因此可以用二分查找加速搜索。
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int> dp;
for(int x:nums){
auto it=lower_bound(dp.begin(),dp.end(),x);
//dp.end() 返回一个迭代器,它指向容器dp的最后一个元素的下一个位置
if(it==dp.end()){
dp.push_back(x);
}else{
*it=x;
}
}
return dp.size();
}
};
这个算法来源于Patience Sorting。希望自己也要多一点耐心,希望你能如烈火,我能像海洋。
Smaller card can be put on top of large cards, chose the leftmost one. ifnot possible, start a new pile.Each pile is sorted and all piles can bemerged to a sorted array. Number of piles = len(LIS)
时间复杂度:O(nlogn)
空间复杂度: O(n)
leetcode 1143
对于子序列问题,第二种动态规划方法是,定义一个dp 数组,其中dp[i] 表示到位置i 为止的子序列的性质,并不必须以i 结尾。这样dp 数组的最后一位结果即为题目所求,不需要再对每个位置进行统计。
在本题中,我们可以建立一个二维数组dp,其中dp[i][j] 表示到第一个字符串位置i 为止、到第二个字符串位置j 为止、最长的公共子序列长度。这样一来我们就可以很方便地分情况讨论这两个位置对应的字母相同与不同的情况了。
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m = text1.length(), n = text2.length();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (text1[i-1] == text2[j-1]) {
dp[i][j] = dp[i-1][j-1] + 1;
} else {
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
}
return dp[m][n];
}
};