动态规划题目难度比较高,要完全掌握不是一件容易的事情,因此先从一些经典题型开始,刷的次数多了,慢慢就能抓住动态规划的牛鼻子了,话休絮烦,直接开冲!
问题描述
问类似 “cbaab”这样一个字符串,最长的回文子串是哪个?
思考过程
很容易想到一种O(n2)的解法,逐个遍历所有的子串,挨个判定是否回文。毫无疑问会超时,超时的原因是重复判断了很多子串是否是回文,例如当判断子串 “aa” 的时候,我们对其做出了判断是回文串,然而当轮到子串 “baab” 的时候,利用双指针方法判断时,左右字符相同,因此接着判定“aa”,这样又对“aa”做了一次重复判定,浪费了效率。因此我们需要做的就是直接利用一些稍短子串的判断结果,辅助判断较长的子串。
1. 定义状态
设定二维数组dp[i][j],表示子串s[i,…,j]是否是回文串
2. 状态转移
这部分是动态规划的核心,也是最难想的部分,一言以蔽之就是如何将一个现在的问题,转移到我们之前已经解决的一个子问题上。直接上结论:
dp[i][j] = (s[i]==s[j]) && dp[i+1][j-1]
上面转移公式的可以这么解读:一个子串 s[i…j],如果首尾两个字符不等,肯定不是回文串,如果首尾相等,就判定除去首尾两个字符剩下的子串,若是回文串,则整个s[i…j]就是回文串。
3. 确定初始状态
以 cbaab 为例,输入数组长度为5,则5×5的二维数组dp,我们只需要填充对角线上方的表格内容(因为dp[i][j必须满足i<=j),并且对角线dp[i][i]都为真
\ | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
0 | T | ||||
1 | T | ||||
2 | T | ||||
3 | T | ||||
4 | T |
4. 填表(输出)
这一步的难点在于,我们要按照怎样的顺序填表,一开始的想法是逐行去填,但是当判定dp[0][3]的时候,dp[1][2]还没有填到,无法辅助我们填表。这里正确的做法应该是按照子串的长度来填,联想到第三步中,我们把所有的对角线初始化为真,其实这一步就是把所有的长度为1的子串判定,因此接下来就要判定所有的长度为2,3…m的子串。
- 判定所有长度为2的子串,
\ | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
0 | T | F | |||
1 | T | F | |||
2 | T | T | |||
3 | T | F | |||
4 | T |
- 判定所有长度为3的子串,
\ | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
0 | T | F | F | ||
1 | T | F | F | ||
2 | T | T | F | ||
3 | T | F | |||
4 | T |
- 判定所有长度为4的子串,
\ | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
0 | T | F | F | F | |
1 | T | F | F | T | |
2 | T | T | F | ||
3 | T | F | |||
4 | T |
- 判定所有长度为5的子串,
\ | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
0 | T | F | F | F | F |
1 | T | F | F | T | |
2 | T | T | F | ||
3 | T | F | |||
4 | T |
5. 得出结论
填表的时候,我们只需记录回文串的最终长度和开始序号就行了,例如这里是长度为4,从1开始的子串 "baab"
最后附上代码
class Solution {
public:
string longestPalindrome(string s)
{
int m = s.size();
if( m <= 1) return s;
vector<vector<int>> dp(m, vector<int>(m, 0));
// 确定初始状态,所有的单个字符都是回文字串
for(int i = 0; i < m; i++)
dp[i][i] = 1;
int begin = 0, ans_len = 1;
// 判断所有长度2到m的字串
for(int len = 2; len <= m; len++)
{
for(int i = 0; i + len <= m; i++)
{
if(len <= 3) // 小于等于3的子串,直接判断首尾是否想等
dp[i][i + len - 1] = s[i] == s[i + len-1] ? 1 : 0;
else // 长度大于3的子串,判断首尾以及dp[i+1][j-1]
dp[i][i + len - 1] = s[i] == s[i + len - 1] && dp[i + 1][i + len - 2] ? 1 : 0;
if(dp[i][i + len - 1] && len > ans_len)
{
begin = i;
ans_len = len;
}
}
}
return s.substr(begin, ans_len);
}
};
这里的时间和空间复杂度是O(n2),也并非是ac的最优解,重要的是动态规划的思考过程!