动态规划整理之二
分割类子问题
leetcode 91解码方法
一条包含字母 A-Z 的消息通过以下映射进行了 编码 :
‘A’ -> 1 ‘B’ -> 2 … ‘Z’ -> 26
要 解码已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。
例如,“11106” 可以映射为: “AAJF” ,将消息分组为 (1 1 10 6) “KJF” ,将消息分组为 (11 10 6) 注意,消息不能分组为 (1 11 06) ,因为"06" 不能映射为 “F” ,这是由于 “6” 和 “06” 在映射中并不等价。
给你一个只含数字的 非空 字符串 s ,请计算并返回 解码方法的 总数 。题目数据保证答案肯定是一个 32 位 的整数。示例 1: 输入:s = “12” 输出:2 解释:它可以解码为 “AB”(1 2)或者 “L”(12)。
示例 2: 输入:s = “226” 输出:3 解释:它可以解码为 “BZ” (2 26), “VF” (22 6), 或者 “BBF” (2 2 6) 。
本题的一般思路是非常简单的,就是看看当前的字符能否和前面的搭配为可以映射的字符,同时也要考虑是否可以自己作为一个映射的字符。但是本题对于特殊情况的处理要求有一些细节。01,10,0以及这些搭配需要好好考虑.
比如这里考虑一个实验用例:2101 10011
class Solution {
public:
//实验用例 2101 10011
bool nums(char c1,char c2)
{
return (((c1-'0')*10+(c2-'0'))<=26)&&c1!='0';
}
int numDecodings(string s) {
//简单的分割问题,简单的动态规划
//采用压缩空间的方式来简化空间复杂度
int pre1 = 1,pre2 = 1,cur;
if(s[0]=='0') return 0;
cur = pre1;
for(int i = 1;i<s.length();++i)
{
//这里没有考虑到 10,20这种情况
//处理特殊情况
if(s[i]=='0')
{
if(s[i-1]>'2'||s[i-1]=='0')
return 0;
else
cur = pre2;
}
else
{
cur = pre1;
if(nums(s[i-1],s[i]))
cur += pre2;
}
//更新
pre2 = pre1;
pre1 = cur;
}
return cur;
}
};
leetcode 139单词拆分
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。 你可以假设字典中没有重复的单词。示例 1: 输入: s = “leetcode”, wordDict = [“leet”, “code”] 输出: true 解释: 返回
true 因为 “leetcode” 可以被拆分成 “leet code”。
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
//分割问题
//字符串和子字符串的分割,熟悉一些常用的函数
int n = s.length();
vector<bool>dp(n+1,false);
dp[0] = true;
for(int i = 1;i<=n;++i)
{
for(const string& word:wordDict)
{
int len = word.length();
if(i>=len&&s.substr(i-len,len) == word)
{
dp[i] = dp[i]||dp[i-len];
}
}
}
return dp[n];
}
};
在这里很重要的一个点就是我们不是简单从原来的(0,i)中渐进的取数来去找是否能够在字典串之中找到相应的字符串,而是直接从字典出发,看看能不能找到以当前的字符串为结尾的字符串组合.
子序列问题
子序列玩的就是一个数组的搭配,递增子序列,递减子序列,上下波动序列,很多,而且一般都不会要求你返回这种选择的路径,所以一般优先考虑使用动态规划来处理。
leetcode 300 最长递增子序列问题
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组[0,3,1,6,2,2,7] 的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
最容易想到的就是这种N2复杂度的,就是设置一组数组去记录以当前的位置为结尾的子数组的最大递增子序列,对于从0到n-1的遍历之中,我们不断的比较当前的位置的值和当前位置之前的值的大小,如果大,说明可以作为递增的序列的末尾,然后在比较的过程中不断的记录最大值就可以了
#define inf -10001
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int sum = 1;
vector<int>dp(nums.size(),1);
for(int i = 1;i<nums.size();i++)
{
for(int j = 0;j<i;j++)
{
if(nums[i]>nums[j])
dp[i] = max(dp[i],dp[j]+1);
}
sum = max(sum,dp[i]);
}
return sum;
}
};
但是题目要求的是是否能够达到NLOG(N)复杂度,看到这个就很容易想到估计要使用二分查找的知识来大大缩短遍历的区间,但是该如何入手呢,我也不太清楚,就参考了如下的代码,在这里记录一下,然后分享一下我对这个解法的理解,加深我自己的印象。
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
//因为是递增序列,所以需要考虑使用二分查找来减少问题的复杂度
int n = nums.size();
if(n==1) return n;
vector<int>dp;
dp.push_back(nums[0]);
for(int i = 1;i < n;i++)
{
if(dp.back()<nums[i])
{
dp.push_back(nums[i]);
}else
{
auto it = lower_bound(dp.begin(),dp.end(),nums[i]);
*it = nums[i];
}
}
return dp.size();
}
}
二分查找针对的是有序的数组,如何能够构成有序的数组,如何能够充分利用前缀的信息呢?那么既然这道题是以长度作为求解的问题,那么我们就可以考虑当前数组某长度下对应的最小的数字。
这里是按照一个老师的原话来描述:我们定义一个数组dp,其中dp[k]存储长度为k+1的最长递增子序列的最后一个元素。我们遍历每一个位置i,如果其对应的数字大于dp数组中所有的数字的值,,那么我们把它放在数组的末尾,表示最长递增子序列加1,如果我们发现这个数字在dp数组中比数字a大,比数字b小,那么我们更新b为此数字,使得之后构成递增子序列的可能性变大。这样构造的dp数组永远是递增的,因此可以使用二分查找。