典型的动态规划题,可以用递归、迭代两种形式来解。
首先引用一篇讲子序列问题的好文章:子序列问题通用思路|最长回文子序列
以下内容引自上文:
子序列问题是常见的算法问题,而且并不好解决。
首先,子序列问题本身就相对子串、子数组更困难一些,因为前者是不连续的序列,而后两者是连续的,就算穷举你都不一定会,更别说求解相关的算法问题了。
而且,子序列问题很可能涉及到两个字符串,比如前文「最长公共子序列」,如果没有一定的处理经验,真的不容易想出来。所以本文就来扒一扒子序列问题的套路,其实就有两种模板,相关问题只要往这两种思路上想,十拿九稳。
一般来说,这类问题都是让你求一个最长子序列,因为最短子序列就是一个字符嘛,没啥可问的。一旦涉及到子序列和最值,那几乎可以肯定,考察的是动态规划技巧,时间复杂度一般都是 O(n^2)。
原因很简单,你想想一个字符串,它的子序列有多少种可能?起码是指数级的吧,这种情况下,不用动态规划技巧,还想怎么着?
既然要用动态规划,那就要定义 dp 数组,找状态转移关系。我们说的两种思路模板,就是 dp 数组的定义思路。不同的问题可能需要不同的 dp 数组定义来解决。
解法一:用记忆化的递归求解。思路应该没问题,但是对于pair的hash一直报错,参数不匹配。这个问题暂时没有解决。
参考链接:自定义哈希函数
struct pair_hash{
template<class T1, class T2>
std::size_t operator() (const std::pair<T1, T2>& p) const{
auto h1 = std::hash<T1>{}(p.first);
auto h2 = std::hash<T2>{}(p.second);
return h1 ^ h2;
}
};
class Solution {
public:
int longestPalindromeSubseq(string s) {
if(s.length() < 2) return 0;
return curLongest(s, 0, s.length()-1);
}
private:
unordered_map <pair<int, int>, int, pair_hash> cache;
int curLongest(string& s, int left, int right) { //[left, right] 闭区间
if(left >= right) return 0;
if(cache.count(pair<left, right>)) {
return cache[pair<left, right>];
}
int longest;
if(s[left] == s[right]) {
longest = 2 + curLongest(left+1, right-1);
}
else {
longest = max(curLongest(left+1, right), curLongest(left, right+1));
}
cache.insert(pair<left, right>, longest);
return longest;
}
};
解法二:Bottom - Up 解法
注意:初始化是容易出错的地方,只有对角线应该初始化成1,对应的实际意义是自己到自己的闭区间,就是字符本身,形成的回文序列长度是1。
class Solution {
public:
int longestPalindromeSubseq(string s) {
if(s.length() < 1) return 0;
int n = s.length();
vector<vector<int>> dp (n, vector<int> (n, 0)); // n*n矩阵,初始化为0,初始化为1不对!
for (int i = n-1; i >= 0; i--) {
dp[i][i] = 1; //对角线元素为1,代表自身长度为1
for (int j = i+1; j < n; j++) { //i=n-1时,j=n,不会进入循环因此不会越界,因此不必padding
if(s[i] == s[j]) {
dp[i][j] = 2 + dp[i+1][j-1];
}
else {
dp[i][j] = max(dp[i+1][j], dp[i][j-1]);
}
}
}
return dp[0][n-1];
}
};