回文字符串: 正着读和倒过来读一样的字符串
子字符串: 字符串中的由连续字符组成的一个序列
子序列: 不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列
647. 回文子串
给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"
示例 2:
输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
1. 确定 dp
数组以及下标的含义
布尔类型的dp[i][j]
:表示区间范围 [i,j]
的子串是否是回文子串,如果是dp[i][j]
为true,否则为false。
2. 确定递推公式
整体上是两种,就是s[i]
与s[j]
相等,s[i]
与s[j]
不相等这两种。
当s[i]与s[j]不相等,dp[i][j]
一定是false
当s[i]与s[j]相等时,有如下三种情况
- 情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串
- 情况二:下标i 与 j相差为1,例如
aa
,也是文子串 - 情况三:下标:i 与 j相差大于1的时候,例如
cabac
,此时s[i]
与s[j]
已经相同了,我们看i到j区间是不是回文子串就看aba
是不是回文就可以了,那么aba
的区间就是i+1
与j-1
区间,这个区间是不是回文就看dp[i + 1][j - 1]
是否为true。
if (s[i] == s[j]) {
if (j - i <= 1) { // 情况一 和 情况二
result++;
dp[i][j] = true;
} else if (dp[i + 1][j - 1]) { // 情况三
result++;
dp[i][j] = true;
}
}
result
就是统计回文子串的数量。
因为在下面dp[i][j]
初始化的时候,初始为false,所以当s[i]
与s[j]
不相等的时候,不需要进行赋值。
3. dp
数组初始化
dp[i][j]
初始化为false
,即一开始全都不匹配。
4. 确定遍历顺序
从递推公式中可以看出,情况三是根据dp[i + 1][j - 1]
是否为true,在对dp[i][j]
进行赋值的。
从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]
都是经过计算的。
for (let i = s.length - 1; i >= 0; i--) { // 注意遍历顺序
for (let j = i; j < s.length; j++) {
if (s[i] == s[j]) {
if (j - i <= 1) { // 情况一 和 情况二
result++;
dp[i][j] = true;
} else if (dp[i + 1][j - 1]) { // 情况三
result++;
dp[i][j] = true;
}
}
}
}
完整代码
let countSubstrings = function(s) {
const len = s.length
let count = 0
const dp = Array.from(Array(len), () => Array(len).fill(false));
for (let j = 0; j < len; j++) {
for (let i = 0; i <= j; i++) {
if (s[i] === s[j]) {
if ((j - i) < 2) {
dp[i][j] = true
} else {
dp[i][j] = dp[i + 1][j - 1]
}
count += dp[i][j] ? 1 : 0
}
}
}
return count;
}
516. 最长回文子序列
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
示例 1:
输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。
示例 2:
输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。
1. 确定dp
数组以及下标的含义
dp[i][j]
:字符串s在 [i, j]
范围内最长的回文子序列的长度为dp[i][j]
。
2. 确定递推公式
关键逻辑就是看s[i]与s[j]是否相同
如果s[i]与s[j]相同,那么dp[i][j]
= dp[i + 1][j - 1] + 2
如果s[i]
与s[j]
不相同,说明s[i]
和s[j]
的同时加入并不能增加[i,j]
区间回文子串的长度,那么分别加入s[i]
、s[j]
看看哪一个可以组成最长的回文子序列。
加入s[j]
的回文子序列长度为dp[i + 1][j]
加入s[i]
的回文子序列长度为dp[i][j - 1]
dp[i][j]
一定是取最大的,即:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
3. dp
数组初始化
考虑当i 和j 相同的情况,从递推公式:dp[i][j] = dp[i + 1][j - 1] + 2
可以看出递推公式是计算不到 i 和j相同时候的情况。
所以需要手动初始化一下,当i与j相同,那么dp[i][j]
一定是等于1的,即:一个字符的回文子序列长度就是1。
其他情况dp[i][j]
初始为0就行,这样递推公式:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
中dp[i][j]
才不会被初始值覆盖。
for (let i = 0; i < s.length; i++) {
dp[i] = [];
for (let j = 0; j < s.length; j++) {
dp[i][j] = 0;
}
dp[i][i] = 1;
}
4. 确定遍历顺序
从递推公式dp[i][j] = dp[i + 1][j - 1] + 2
和 dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
可以看出,dp[i][j]
是依赖于dp[i + 1][j - 1]
和 dp[i + 1][j]
。
也就是从矩阵的角度来说,dp[i][j]
下一行的数据。 所以遍历i的时候一定要从下到上遍历,这样才能保证,下一行的数据是经过计算的。
for (let i = s.length - 1; i >= 0; i--) {
for (let j = i + 1; j < s.length; j++) {
if (s[i] == s[j]) {
dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
完整代码
var longestPalindromeSubseq = function (s) {
let dp = [];
for (let i = 0; i < s.length; i++) {
dp[i] = [];
for (let j = 0; j < s.length; j++) {
dp[i][j] = 0;
}
dp[i][i] = 1;
}
for (let i = s.length - 1; i >= 0; i--) {
for (let j = i + 1; j < s.length; j++) {
if (s[i] === s[j]) {
dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
return dp[0][s.length - 1];
}
5. 最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串。
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
示例 3:
输入:s = "a"
输出:"a"
示例 4:
输入:s = "ac"
输出:"a"
1. 确定dp
数组以及下标的含义
dp[i][j]
:字符串s在 [i, j]
范围内最长的回文子串的长度为dp[i][j]
。
2. 确定递推公式
看s[i]
与s[j]
相等,s[i]
与s[j]
不相等这两种情况。
当s[i]与s[j]不相等,dp[i][j]
一定是false
当s[i]与s[j]相等时,有如下三种情况
- 情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串
- 情况二:下标i 与 j相差为1,例如
aa
,也是文子串 - 情况三:下标:i 与 j相差大于1的时候,例如
cabac
,此时s[i]
与s[j]
已经相同了,我们看i到j区间是不是回文子串就看aba
是不是回文就可以了,那么aba
的区间就是i+1
与j-1
区间,这个区间是不是回文就看dp[i + 1][j - 1]
是否为true。
3. dp
数组初始化
dp[i][j]
初始化为0
,即一开始全都不匹配。
4. 确定遍历顺序
从递推公式中可以看出,情况三是根据dp[i + 1][j - 1]
是否为true,在对dp[i][j]
进行赋值的。
从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]
都是经过计算的。
在这个过程中不断比较已生成的回文字符串的长度,取最长的字符串。
完整代码
var longestPalindrome = function (s) {
let n = s.length;
let res = '';
let dp = Array.from(new Array(n), () => new Array(n).fill(0));
for (let i = n - 1; i >= 0; i--) {
for (let j = i; j < n; j++) {
dp[i][j] = s[i] === s[j] && (j - i < 2 || dp[i + 1][j - 1]);
if (dp[i][j] && j - i + 1 > res.length) {
res = s.substring(i, j + 1);
}
}
}
return res;
};