动态规划
回文天然具有「状态转移」性质:一个长度严格大于 22 的回文去掉头尾字符以后,剩下的部分依然是回文。反之,如果一个字符串头尾两个字符都不相等,那么这个字符串一定不是回文。「动态规划」的方法根据这样的性质得到。
第 1 步:定义状态
dp[i][j] 表示:子串 s[i..j] 是否为回文子串,这里子串 s[i..j] 定义为左闭右闭区间,即可以取到 s[i] 和 s[j]。
第 2 步:思考状态转移方程
根据头尾字符是否相等,需要分类讨论:
dp[i][j] = (s[i] == s[j]) and dp[i + 1][j - 1]
说明:
「动态规划」的「自底向上」求解问题的思路,很多时候是在填写一张二维表格。由于 s[i..j] 表示 s 的一个子串,因此 i 和 j 的关系是 i <= j,只需要填这张表格对角线以上的部分;
看到 dp[i + 1][j - 1] 就需要考虑特殊情况:如果去掉 s[i..j] 头尾两个字符子串 s[i + 1..j - 1] 的长度严格小于 22(不构成区间),即 j - 1 - (i + 1) + 1 < 2j−1−(i+1)+1<2 时,整理得 j - i < 3j−i<3,此时 s[i..j] 是否是回文只取决于 s[i] 与 s[j] 是否相等。结论也比较直观:j - i < 3j−i<3 等价于 j - i + 1 < 4j−i+1<4,即当子串 s[i..j]s[i..j] 的长度等于 22 或者等于 33 的时候,s[i..j] 是否是回文由 s[i] 与 s[j] 是否相等决定。
第 3 步:考虑初始化
单个字符一定是回文串,因此把对角线先初始化为 true,即 dp[i][i] = true。根据第 2 步的说明:当 s[i..j] 的长度为 22 时,只需要判断 s[i] 是否等于 s[j],所以二维表格对角线上的数值不会被参考。所以不设置 dp[i][i] = true 也能得到正确结论。
第 4 步:考虑输出
一旦得到 dp[i][j] = true,就记录子串的「长度」和「起始位置」。没有必要截取,这是因为截取字符串也有性能消耗。
第 5 步:考虑优化空间
下面给出的「参考代码」,在填表的过程中,只参考了左下方的数值。事实上可以优化,但是增加了代码编写和理解的难度,丢失了可读性和可解释性。在这里不做优化空间;
填表应该遵守这样的原则:总是先得到小子串是否是回文的结果,然后大子串才能参考小子串的判断结果,所以填表顺序很重要;
建议自己动手,画一下表格,相信会对「动态规划」作为一种「表格法」有更好的理解。
std::string longestPalindrome(std::string s)
{
if (s.length() < 2)
{
return s;
}
std::vector<std::vector<int>> dp(s.length(), std::vector<int>(s.length()));
for (int i = 0; i < s.length(); i++)
{
dp[i][i] = 1;
}
int nStart = 0;
int maxlen = 0;
for (int j = 1; j < s.length(); j++)
{
for (int i = 0; i < j; i++)
{
if (s[i] != s[j])
{
dp[i][j] = 0;
}
else
{
if (j - i < 3)
{
dp[i][j] = true;
}
else
{
dp[i][j] = dp[i+1][j - 1];
}
}
if (dp[i][j] && j - i + 1 > maxlen)
{
maxlen = j - i + 1;
nStart = i;
}
}
}
return s.substr(nStart, maxlen);
}
链接:https://leetcode.cn/problems/longest-palindromic-substring/solution/zhong-xin-kuo-san-dong-tai-gui-hua-by-liweiwei1419/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。