Given a string, your task is to count how many palindromic substrings in this string.
The substrings with different start indexes or end indexes are counted as different substrings even they consist of same characters.
Example 1:
Input: "abc" Output: 3 Explanation: Three palindromic strings: "a", "b", "c".
Example 2:
Input: "aaa" Output: 6 Explanation: Six palindromic strings: "a", "a", "a", "aa", "aa", "aaa".
Note:
- The input string length won't exceed 1000.
分析:翻译一下:要求找字符串s中回文字串的个数,追忆这里回文串只要开始位置或结束位置不同都视为不同,哪怕内容相同。
第一个思路:首先想到判断回文串的方法:给一个字符串t,从0——t,.length()-1,从两端向中间判断。然后两层循环从s的0位置搜索到最后的位置,直觉感觉O(n^3)时间复杂度,代码如下:
1 class Solution { 2 public int countSubstrings(String s) { 3 char[] array = s.toCharArray(); 4 int count = 0; 5 for ( int i = 0 ; i < array.length ; i ++ ){ 6 for ( int j = i ; j < array.length ; j ++ ){ 7 if ( isPalindromic(array,i,j) ) count++; 8 } 9 } 10 return count; 11 } 12 private boolean isPalindromic(char[] array, int low, int high){ 13 while ( low < high ){ 14 if ( array[low++] != array[high--] ) return false; 15 } 16 return true; 17 } 18 }
提交虽然AC了,但是运行时间65ms,超过了15%的提交,显然是个辣鸡算法。下面就是想办法改进了。
第二个思路:想到判断回文串有两种情况:第一种是长度是奇数,例如aba,那么就从中间向两边判断,同时再判断的同时也可以计数;第二种是长度是偶数,例如aa,这种的话就比较两个位置的元素是否相等,然后向两边延展。
举个例子:aabaa
首先我们比较以每个元素为中心,长度为奇数的情况:
0位置:a
1位置:a
2位置:b、aba、aabaa
3位置:a
4位置a
然后我们比较两个元素为中心,长度为偶数的情况:
0、1位置:aa
1、2位置:ab(不是回文)
2、3位置:ba(不是回文)
3、4位置:aa
按照上面的思路,代码如下:
1 class Solution { 2 int count = 0; 3 public int countSubstrings(String s) { 4 char[] array = s.toCharArray(); 5 for ( int i = 0 ; i < array.length ; i ++ ) isPalindromic(array,i,i); //以i为中心,长度为奇数 6 for ( int i = 0 ; i < array.length - 1 ; i ++ ) isPalindromic(array,i,i+1); //以i,i+1为中心,长度为偶数 7 return count; 8 } 9 private void isPalindromic(char[] array, int low, int high){ 10 while ( low >= 0 && high <= array.length-1 ){ 11 if ( array[low--] == array[high++] ) count++; 12 else break; 13 } 14 } 15 }
这个思路非常好,因此总结一下回文的处理一定要从中间到两边,分长度为奇数和偶数情况讨论!
运行时间6ms,击败100%提交。
第三个思路,其实我本意是想学习一下动态规划的,下面简要总结一下我使用动态规划的一些想法。
首先,我们要维护一个dp数组,关于dp[i]的含义我有两个理解:第一个是用来记录以第i个位置开头的元素构成的这么多字串里面回文字串的个数,但是没有找到其中必然的关联。第二个是用来记录以某个元素开头的第i个位置有几个回文串,但是好像也没找到状态转移方程。很尴尬,因此参考了一下网上的代码,整理思路如下:
维持一个boolean数组dp,这里dp[i][j]表示从位置j到位置i的字串是否是一个回文串。这一步很关键,下面说明这个设计的巧妙之处。那么状态转移方程,乍一看肯定是:dp[i][j] = array[i] == array[j] && dp[i-1][j+1]
,也就是当前状态取决于i和j字符是否相等 && 中间字符串是否是个字串,但是这么有个问题,没有考虑初始情况,修改状态转移方程:dp[i][j] = array[i] == array[j] && (i-j<3 || dp[i-1][j+1]),这里一定注意i-j<3放在前面,这个状态转移方程就包含了初始情况。
代码如下:
1 class Solution { 2 public int countSubstrings(String s) { 3 char[] array = s.toCharArray(); 4 int n = array.length; 5 boolean[][] dp = new boolean[n][n]; //dp[i][j]表示从j到i位置的字串是否是回文串 6 int res = 0; 7 for ( int i = 0 ; i < n ; i ++ ){ 8 for ( int j = i ; j >= 0 ; j -- ){ 9 dp[i][j] = array[i] == array[j] && (i-j<3 || dp[i-1][j+1]); //这里考虑到刚开始的时候,也就是长度为1、2、3的时候,只要最外面的两个相等就可以了。这个状态转移方程找的非常巧妙 10 if ( dp[i][j] ) res++; 11 } 12 } 13 return res; 14 } 15 }
运行时间11ms。虽然也是用到了两层循环,但是显然使用动态规划方法比第一种每次都全部计算更加省时间,因为用动态规划保留了中间结果。其实用动态规划方法应该是从1延申出来的,这里要注意需要保存中间结果的情况,一定要想到使用动态规划。
动态规划的核心两个点:1、dp数组表示的含义。2、状态转移方程。
目前做的题目dp数组有一维的,有二维的。一般一维的比较好找,二维的还需要多练习。