目录
一. 引入
谈到判断回文,我们可以想到:
1.前后对比
将第一个字符与最后一个字符位置进行比对,left++,right--,直至left>right结束,如果仍没有中途return false,那么它就是回文序列;
public static boolean isPalindrome(String str) { int start = 0; int end = str.length() - 1; while (start < end) { if (str.charAt(start) != str.charAt(end)) { return false; } start++; end--; } return true; }
2.反转整个字符串与原字符串进行对比。
public static boolean isPalindrome(String str) { String reversed = ""; int length = str.length(); for (int i = length - 1; i >= 0; i--) { reversed = reversed + str.charAt(i); }//springbuilder的reverse也行 return str.equals(reversed); }
3.中心扩散
public static boolean isPalindrome(String str) { int length = str.length(); for (int i = 0; i < length; i++) { if (!expandFromCenter(str, i, i)) // 奇数长度以当前字符为中心 return false; if (!expandFromCenter(str, i, i + 1)) // 偶数长度以当前字符和下一个字符之间为中心 return false; } return true; } private static boolean expandFromCenter(String str, int left, int right) { while (left >= 0 && right < str.length() && str.charAt(left) == str.charAt(right)) { left--; right++; } return right - left == 2; // 判断回文串长度是否为奇数 }
4.动态规划
图解
代码
//将[i,j]的是否是回文串的信息填入dp表里里面,用的时候直接查就好. public void f(String s) { int n = s.length(); boolean[][] dp = new boolean[n][n]; int ret = 0; for (int i = n - 1; i >= 0; i--) { for (int j = i; j < n; j++) { if (s.charAt(i) == s.charAt(j)) { dp[i][j] = i + 1 < j ? dp[i + 1][j - 1] : true; } } } }
dp[i][j]表示字符串[i,j]是否是回文子串;其中i<=j ✅s[i]!=s[j]——>false; ✅s[i]==s[j]: i==j true i+1=j true i+1<j dp[i + 1][j - 1] 其中dp[i][j]还依赖dp[i+1][j-1],所以i从下往上填,j从左往右填 if (s.charAt(i) == s.charAt(j)) { dp[i][j] = i + 1 < j ? dp[i + 1][j - 1] : true; } 👻👻👻对于是否需要初始化?eg:3*3宫格 dp[i + 1][j - 1]涉及到越界的位置是(0,0),(2,2) 但是这两点都满足i=j,上面代码写法会直接特判这种情况为true; 所以不需初始化,这样写避免了越界情况
这种思路可用于求字符串中回文子串的个数,字符串中最长的回文串(记录每次满足的begin和len进行迭代
if (dp[i][j]&&j-i+1>len) {
begin=i;
len=j-i+1;
},最后substring(i,i+len)即可),通常在一些中等题或者困难题最为一种优化而出现。
二. 扩展
1.变成回文的最少操作次数
(1)题目
给你一个字符串
s
,每一次操作你都可以在字符串的任意位置插入任意字符。请你返回让
s
成为回文串的 最少操作次数 。示例 1:
输入:s = "zzazz" 输出:0 解释:字符串 "zzazz" 已经是回文串了,所以不需要做任何插入操作。示例 2:
输入:s = "mbadm" 输出:2 解释:字符串可变为 "mbdadbm" 或者 "mdbabdm" 。子串就是子数组
(2)思路
- 状态表示:dp[ i ][ j ]表示让字符串s中 i 到 j 位置的子串变成回文穿的最少操作次数。
- 状态方程:
- 初始化:对应图看
💡当s[i]=s[j]时,dp[i+1][j-1]会越界,对应位置(0,0)和(2,2),但是这两点都在i=j的对角线上,已经进行过特判0。这点在引入的第四点已经提到过;
if (s.charAt(i) == s.charAt(j)) { dp[i][j] = i + 1 < j ? dp[i + 1][j - 1] : true; } 👻👻👻对于是否需要初始化?eg:3*3宫格 dp[i + 1][j - 1]涉及到越界的位置是(0,0),(2,2) 但是这两点都满足i=j,上面代码写法会直接特判这种情况为true; 所以不需初始化,这样写避免了越界情况
优化:将j从i+1位置开始,也就满足题意,且不用考虑越界进行的特判;
关于为啥将 j 初始化成 i+1?
首先dp表未初始化时对应的值就是0,所以i=j, 这对角线就不用管了。并且不需要对越界进行特判,减小代码量,算是一种优化吧。
💡当s[i]!=s[j]时,首先就不会在对角线i=j上,只会存在于i=j+1,以及之后的,在下图就是对应的那三个黑点会涉及不相等的情况,会依赖下方的值dp[i+1][j]以及左方的值dp[i][j-1],即蓝色的箭头,很明显不会越界
4. 填表顺序:dp[i][j]依赖dp[i+1][j-1],dp[i][j+1],dp[i+1][j]。所以 i 从下往上,j 从左往右。
5. 返回值:
字符串 ——————————————————— 0 n-1 return dp[0][n-1];
(3)代码
class Solution {
public int minInsertions(String s) {
int n=s.length();
int [][] dp=new int[n][n];
for(int i=n-1;i>=0;i--){
for(int j=i+1;j<n;j++){
if(s.charAt(i)==s.charAt(j)) dp[i][j]=dp[i+1][j-1];
else dp[i][j]=Math.min(dp[i][j-1],dp[i+1][j])+1;
}
}
return dp[0][n-1];
}
}
2. 最长回文子序列
(1)题目
给你一个字符串
s
,找出其中最长的回文子序列,并返回该序列的长度。子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
示例 1: 输入:s = "bbbab" 输出:4 解释:一个可能的最长回文子序列为 "bbbb" 。 示例 2: 输入:s = "cbbd" 输出:2 解释:一个可能的最长回文子序列为 "bb" 。
(2)思考过程
才开始的想法就是以i位置为结尾,即dp[i]表示以i位置为结尾的所有子序列中最长回文子序列的长度,i可以依赖...i-2,i-1,但是不能确定前面的某个使我们想要的满足回文关系的。所以用dp[i][j]表示在字符串[i,j]位置的所有子序列中最长回文子序列的长度。看到了子序列还去看了下子序列的引入题300. 最长递增子序列。300题是[j,i]表示其中0<=j<i,
写出状态转移方程,
考虑初始化,当s[i]=s[j],这种对i=j的情况进行特判就可以,就不会发生越界问题,也不用对dp表初始化,当s[i]!=s[j],(0,0)(2,2)位置危险,但是仍然属于i=j的范畴,然而i=j这样,只有一个字符,必然满足s[i]=s[j],所以越界考虑不成立
![]()
子序列的引入题300. 最长递增子序列。 class Solution { //dp[i]以i位置为结尾的所有子序列的最长递增子序列的长度; public int lengthOfLIS(int[] nums) { int n=nums.length; int[] dp=new int[n]; for(int i=0;i<n;i++) dp[i]=1; int ret=1;//最差就是1 for(int i=1;i<n;i++){ //for(int j=i-1;j>=0;j--){ for(int j=0;j<i;j++){ if(nums[i]>nums[j]) dp[i]=Math.max(dp[j]+1,dp[i]); } ret = Math.max(dp[i],ret); } return ret; } } //**💡dp[i]以i位置为结尾的所有子序列的最长递增子序列的长度;** // dp[i]长度为1; // dp[i]长度大于1; // ... i-1 , i // ... i-2 , i // ... i-3 , i // **💡0=<j<=i-1**MAX(dp[j]+1) // ``` // **前提**nums[j]<nums[i] // dp[j]+1~ // ```
(3)代码
class Solution { public int longestPalindromeSubseq(String s) { int n = s.length(); int[][] dp = new int[n][n]; for (int i = n - 1; i >= 0; i--) { dp[i][i] = 1;//i=j的情况 for (int j = i + 1; j < n; j++) { if (s.charAt(i) == s.charAt(j)) { dp[i][j] = dp[i + 1][j - 1] + 2;//合并i+1<=j的情况 } else { dp[i][j] = Math.max(dp[i][j - 1], dp[i + 1][j]); } } } return dp[0][n - 1]; } }