#Java #动态规划
Feeling and experiences:
给你一个字符串 s
,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
输入:s = "abc" 输出:3 解释:三个回文子串: "a", "b", "c"
动态规划在 回文子串上的运用
之前做回文子串的问题,一般会用到 双指针 和 回溯
class Solution {
public int countSubstrings(String s) {
int n = s.length(); // 字符串的长度
int ans = 0; // 记录回文子串的个数
for (int i = 0; i < 2 * n - 1; ++i) {
int l = i / 2, r = i / 2 + i % 2; // 根据奇偶性确定回文中心的左右边界
while (l >= 0 && r < n && s.charAt(l) == s.charAt(r)) {
--l; // 向左扩展
++r; // 向右扩展
++ans; // 找到一个新的回文子串
}
}
return ans; // 返回最终的回文子串个数
}
}
双指针的 思路很简单:
就是遍历每个可能的中心点,然后我们从 中心点 用两个指针开始遍历元素,判断是否为回文
至于 中心点 为什么是 2*n - 1 个:
(来自力扣评论中的一条解释):
为什么是2n-1
个中心点?
如果回文串是奇数,我们把回文串中心的那个字符叫做中心点
,如果回文串是偶数我们就把中间的那两个字符叫做中心点
。
对于一个长度为n
的字符串,我们可以用它的任意一个字符当做中心点,所以中心点的个数是n
。我们还可以用它任意挨着的两个字符当做中心点,所以中心点是n-1
,总的中心点就是2*n-1
。
动态规划的做法:
class Solution {
public int countSubstrings(String s) {
int len = s.length();
boolean[][] dp = new boolean[len][len]; // dp[i][j] 表示从索引 i 到 j 的子串是否为回文
int result = 0; // 记录回文子串的个数
for (int i = len - 1; i >= 0; i--) {
for (int j = i; j < len; j++) {
if (s.charAt(i) == s.charAt(j)) {
if (j - i <= 1) {
result++; // 单个字符或相邻字符相同,是回文子串
dp[i][j] = true;
} else if (dp[i + 1][j - 1]) {
result++; // 利用动态规划,如果内部子串是回文,则整个子串也是回文
dp[i][j] = true;
}
}
}
}
return result;
}
}
使用动态规划数组 dp
,其中 dp[i][j]
表示从索引 i
到 j
的子串是否为回文。
从字符串的最后一个字符开始,逆序遍历到第一个字符。这样可以确保在计算 dp[i][j]
时,dp[i+1][j-1]
已经被计算过,以便利用动态规划的结果。
在每个位置 (i, j)
,检查字符 s.charAt(i)
和 s.charAt(j)
是否相同,如果相同,判断子串是否为回文:
j - i
小于等于 1,说明子串长度为 1 或 2,一定是回文,此时增加回文子串的计数。j - i
大于 1,利用动态规划的结果,如果内部子串是回文,则整个子串也是回文,同样增加回文子串的计数。给你一个字符串 s
,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
示例 1:
输入:s = "bbbab" 输出:4 解释:一个可能的最长回文子序列为 "bbbb" 。
该题和 子串问题差不多
class Solution {
public int longestPalindromeSubseq(String s) {
// 本题是子序列问题,和子串问题类似
// 创建dp数组,定义:区间[i,j] 左闭右闭 最大的回文子序列为dp[i][j]
int[][] dp = new int[s.length()][s.length()];
// 初始化为 java 默认为0
// 循环,遍历
for (int i = s.length() - 1; i >= 0; i--) {
for (int j = i; j < s.length(); j++) {
if (s.charAt(i) == s.charAt(j)) {
if (j - i <= 1) {
dp[i][j] = j - i + 1;
} else {
dp[i][j] = dp[i + 1][j - 1] + 2;
}
} else {
dp[i][j] = Math.max(dp[i + 1][j], Math.max(dp[i][j - 1], dp[i][j]));
}
}
}
return dp[0][s.length() - 1];
}
}
从字符串的末尾开始循环,外层循环变量为 i
,内层循环变量为 j
。这样可以确保在计算 dp[i][j]
时,dp[i+1][j-1]
已经被计算过,以便利用动态规划的结果。
对于每一对索引 (i, j)
,如果 s[i]
和 s[j]
相等,那么:
j - i
小于等于 1,说明子串长度为 1 或 2,直接赋值 dp[i][j] = j - i + 1
。dp[i][j] = dp[i + 1][j - 1] + 2
。如果 s[i]
和 s[j]
不相等,那么 dp[i][j]
取左边、上边和左上角三个位置的最大值,即 dp[i][j] = Math.max(dp[i + 1][j], Math.max(dp[i][j - 1], dp[i][j]))
。
风不定,人初静,明日落红应满径。
Fighting!