不要失去信心,只要坚持不懈,就终会有成果。——钱学森
dp动态规划题目详解--第二天
前言
由于上一期的动态规划我觉的太过于繁琐,所以这次简化一下操作,题目概念解析将不会再写,我直接写题目的意思和直接引导出做法,当然,其中我觉得很重要的地方会讲的深入一点,希望读者能够好好的理解。
1、 最长定差子序列
题目链接在这
题目解答: 这里的第一道题目相当于是抛砖引玉的作用,后面的几道题目就能感受到具体的作用了。
寻找最长的定差子序列。我知道,在上一篇文章已经讲过一遍了,但是当时没有多大的抛砖引玉的作用,这里作为第一题还是希望能够对下面集体能够有反思,并且能够理解优化和处理一些两个末尾位置有限制时候的操作。
详细过程参考
2、 最长等差数列
题目链接在这
题目解答: 一开始,我觉的这题和最长斐波那契的那道题目很相似,但是这题肯定有不一样的地方,就比如说斐波那契数是固定的递增的数组,还有一种可能就是说这个还可能是重复的,那重复的话还要找到离最后两个以及确定下的数的位置最近的一个数,因为找最近的情况下,才可能是得到最长的等差数列。除此之外,这题的没有顺序导致的会是有关下表问题的出现。所以说这里就不能够完全利用那道题目的方法来解决,所以需要考虑另外的方法,那应该怎么弄呢?由于斐波那契那道题目是递增的,所以说递增的条件之下,那道题目是不会有这道题的在意的地方,所以说这题才需要另外的考虑。
解题代码:
class Solution {
public:
int longestArithSeqLength(vector<int>& nums)
{
unordered_map<int,int>hash;
int ret=2;
int n=nums.size();
//hash[nums[0]]=0;
for(int i=0;i<n;i++)hash[nums[i]]=i;
vector<vector<int>>dp(n,vector<int>(n,2));
for(int i=1;i<n;i++)
{
for(int j=i+1;j<n;j++)
{
int a=2*nums[i]-nums[j];
if(hash.count(a))
dp[i][j]=dp[hash[a]][i]+1;
ret=max(ret,dp[i][j]);
}
//hash[nums[i]]=i;
}
return ret;
}
};
其中为了解决下表问题,我们更新二维dp表的时候是固定i的位置,让j从i+1开始,向后移动,这样的好处是能够处理的更加的“丝滑”。怎么个事呢?由于i的位置是不动的,所以说hash表的更新是较少的,较少是因为只有在i变化的时候hash才需要再次更新。这样的话既能够消除数组中存在多个相同数的影响,并且也能够实现找到最近的数的位置,两全其美,优化了复杂程度还能解决问题。
3、等差数列划分 II - 子序列
题目链接在这
题目解答: 很疑惑啊,很疑惑,一开始,我在想这题和上一个的最长等差数组有什么区别吗,只不过是变成了找个数而已啊!其实不然,对于这道题来说,这题的优化和上面两个的又不一样,上面的第二题是把倒数第三个目标数的存储是实时更新的,但是对于这题来说这里需要保存每一个数的位置,因为是等差的个数,而且不是最长的等差的个数。所以保存位置最适合之前处理不同的地方,很重要。
解题代码:
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& nums)
{
unordered_map<long long,vector<int>>hash;
int n=nums.size();
for(int i=0;i<n;i++)
{
hash[nums[i]].push_back(i);
}
vector<vector<int>>dp(n,vector<int>(n));
int ret=0;
for(int j=2;j<n;j++)
{
for(int i=1;i<j;i++)
{
long long a=(long long)nums[i]*2-nums[j];
if(hash.count(a))
{
for(auto k:hash[a])
{
if(k<i)dp[i][j]+=dp[k][i]+1;
else
break;
}
}
ret+=dp[i][j];
}
}
return ret;
}
};
4、回文子串
题目链接在这
题目解答:
三种普遍的算法解决回文子串问题。
其中的时间复杂度和空间复杂度都是基于本题来说
算法种类 | 时间复杂度 | 空间复杂度 |
---|---|---|
中心扩展算法 | O(N2) | O(1) |
马拉车算法 | O(N) | O(N) |
动态规划 | O(N2) | O(N2) |
其中,动态规划的思路是值得学习的,有时候还能将hard的题目直接变为easy的题目。
利用二维dp表,dp[i][j]表示起始位置为i,结束为止为j的字符串是否为回文子串。所以在循环的时候只需要循环dp表的上三角部分,就能够实现回文串个数的判断。
又由于特殊的时候就比如i= =j或i= =j-1的时候是需要特殊判定的,并且判定之后呢,会消除对于dp表中越界访问的情况。
由dp表的转移状态方程能够得到关键是,从左下角的二维数组的位置向上更新,所以在填表的顺序的时候就必须要使用从下向上的方法来实现。
解题代码:
中心扩展算法:
class Solution {
public:
int countSubstrings(string s)
{
int n=s.size();
int ret=0;
int left=0,right=0;
while(right<n)
{
int lefttmp=left,righttmp=right;
while(lefttmp>=0&&righttmp<n&&s[lefttmp--]==s[righttmp++])
ret++;
lefttmp=left-1,righttmp=right;
while(lefttmp>=0&&righttmp<n&&s[lefttmp--]==s[righttmp++])
ret++;
right++;
left++;
}
return ret;
}
};
其中的要点就是关于right当时所处在的位置的字符是作为偶数个数回文串的中心部位,还是奇数个数回文串的正中间的部分。这就是重点地方。
动态规划:
class Solution {
public:
int countSubstrings(string s)
{
int n=s.size();
vector<vector<bool>>dp(n,vector<bool>(n));
int ret=0;
for(int i=n-1;i>=0;i--)
{
for(int j=i;j<n;j++)
{
if(s[i]==s[j])
dp[i][j]=i+1<j?dp[i+1][j-1]:true;
if(dp[i][j]==true)ret++;
}
}
return ret;
}
};
5、总结
虽然题目不多,但是其中相对于上一篇文章中的利用dp解决问题的方法有稍微的提升,从一维逐渐向使用二维,从dp表的直接使用到利用hash优化程序。并且从原来的最后一位能够推出前面全部的定差数组,也变成了需要两个末了位置的数才能推导出前面的所有数的转变。同时方法也会改变,一些hash表的更新也同时尤为重要,究竟是一边跟随着循环的更新,还是在循环开始之前先遍历一遍都显得尤为重要,究竟是不断改变hash出现过数的下表还是存起来方便以后的问题的解决。
这些都是从这四道题目中选取出的知识点,有关于dp表使用方法的知识点。