前言
回文串在刚学习数据结构的时候,老师就给过这样的题目:判断字符串是否为回文串。首先要明白回文串是什么?通俗的讲法就是正着读字符串和反着读字符串是一样的,也就是一个字符串与它的逆转字符串是相等的。
在刷leetcode算法题的过程中,遇到过回文串问题的不同演化版本。例如判断字符串是否为回文串,其中只把数字和字母视为有效字符。今天想要把关于动态规划的回文子串问题单独拿出来讲一下,一是为了总结动态规划思想在回文子串问题上的应用,二是为了联系关于字符串相关的题目。
问题
一 判断字符串是否为回文串
这个问题在面试中,面试官可能会让你手写代码。这个问题的解题思路就是利用双指针,初始化一个left和right指针,分别指向字符串的首和尾。然后同时向中间靠拢,在这个过程中不断判断left和right指向的字符是否相等,如果不等则字符串不是回文子串;循环跳出,则判断出字符串为回文子串。
private boolean judge(String s) {
char[] chars = s.toCharArray();
int left = 0, right = chars.length - 1;
while (left < right) {
if (chars[left] != chars[right])
return false;
left++;
right--;
}
return true;
}
代码很简单易懂。
二 回文子串问题
首先总结一下字符串中有多少回文子串这个问题。给定一个字符串,计算它有多少个是回文的子串个数。
这个问题有很多解法:暴力法、动态规划法、中心扩展法、Manacher 算法。
个人认为如果对于动态规划法不太熟悉的(像我自己这样的),可以先把暴力法的思路和代码写出来。
暴力法
这个题目其实就是判断子串是否为回文,然后计数。那么就需要把一个字符串的所有子串找出来,然后依次判断是否为回文即可。找字符串子串也是使用双指针,left确定子串开始的位置,right从起始位置不断向后移动直至末尾。每移动依次,就能得到一个子串。
下面是暴力法得代码,很好理解。
public int countSubstrings(String s) {
if (s.equals("") || s == null)
return 0;
char[] chars = s.toCharArray();
int result = chars.length;
for (int i = 0; i < chars.length - 1; i++) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(chars[i]);
for (int j = i + 1; j < chars.length; j++) {
if (judge(stringBuilder.append(chars[j]).toString()))
result++;
}
}
return result;
}
//判断字符串是否为回文
private boolean judge(String s) {
char[] chars = s.toCharArray();
int left = 0, right = chars.length - 1;
while (left < right) {
if (chars[left] != chars[right])
return false;
left++;
right--;
}
return true;
}
动态规划
动态规划的根本就是拿空间换时间,将暴力法过程中的一些重复计算用空间存储下来,避免不必要的计算。而且动态规划解决的问题都是存在子问题结构的,如果一个字符串是回文那么在它的两端加上相同的字符,那么新的字符串也必定是回文。相反,如果一个字符串是回文,将两端相同的字符去掉得到的子串肯定也是回文。
根据本人刷的动态规划问题的经验,一个题目是否可以利用动态规划思路,就必须看它是否能够将现有的问题拆分成子问题。这个思路就是自顶向下的思想。
回文子串的个数依然按照动态规划三部曲来逐步分析
一 动态规划数组
这个题目是求子串是否是回文,由于子串肯定是有起始位置和结束位置的,那么我们可以用两个变量i,j来分别代表子串的起始位置和结束位置。那么很容易就想到这个动态规划数组是个二维数组dp[i][j]。
下面就开始确定数组元素代表的实际意义是什么。根据前面的分析,我们只关注一个字符串是不是回文即可,所以确定dp[i][j]代表子串S[i:j]是否是回文,其中包含两端的字符。
例如 s = “aabbaa”,dp[0][1]就代表子串aa是否为回文,这里很明显dp[0][1] = true。
这样做的意义在哪?
本题是计算回文子串的个数,也就是我们只关注那些是回文的子串,当我们找到一个子串是回文,那么只需要关注它的首尾各加的字符是否相等即可。而单独加首或者尾都不需要再被计算了,因为肯定不是回文子串。反过来就是判断一个子串是否是回文,那么只需要判断去掉首尾字符后的子串是否是回文以及首尾字符是否相等即可。
二 初始条件
确定了动态规划数组的意义之后,就需要对这个二维数组进行初始化。不难得到对角线上的元素都为true。因为对角线上的i = j,代表着的是单个字符,单个字符一定为回文。
三 动态转移方程
动态转移方程跟动态规划数组的确定是相辅相成的,这一步也是最难的。其实这一步应该是解决动态规划问题的第一步。
1. if (i == j) dp[i][j] = true; //单个字符情况
2. if (j - i == 1) dp[i][j] = (s[i] == s[j]);//两个字符情况
3. if (j - i > 1) dp[i][j] = (s[i] == s[j]) && (dp[i + 1][j -1]);//多个字符情况
上面三步分析完成之后,就可以愉(悲)快(伤)的写代码啦。
public int countSubstringsDP(String s) {
int res = 0;
int len = s.length();
char[] chars = s.toCharArray();
boolean[][] dp = new boolean[len][len];
for (int j = 0; j < len; j++) {
for (int i = 0; i <= j; i++) {
if (j - i == 0) {
dp[i][j] = true;
res++;
} else if (j - i == 1) {
if (chars[i] == chars[j]) {
dp[i][j] = true;
res++;
}
} else if (j - i > 1 && dp[i + 1][j - 1] && chars[i] == chars[j]) {
dp[i][j] = true;
res++;
}
}
}
return res;
}
结束语
在动态规划面前,我永远是个弟弟。leetcode也刷了200多题了,动态规划也刷了几十题了。唯一的进步就是以前都不知道这个问题可以用动规解到现在知道一些题目可以用动规解了。可是,知道了之后常常卡在了动态转移方程这里,菜就完了。