-
647回文子串
- 代码随想录 (programmercarl.com)
-
第一印象
- 首先解决如何判定是回文子串,可以利用双指针的办法,分别从首尾向内推进,出现不相同时则不是回文子串。
- 暴力的方法可以使用两个指针标记子串的遍历所有起始位置并判断,两层循环+判断。复杂度是O(n^3)。
-
讲解观后感
- 按照动态规划的固定套路。首先我们要确定dp数组的含义。首先这个题目是判断子序列属性的题目,可以想到类似于之间使用下标标记子序列最后一位,然后进行推导的方式。但是使用后我们会发现只用过子序列的最后一位很难推出相邻位置的回文状态。因此我们要改变策略。
- 这是我们要考虑回文子串的性质。观察下图:
- 当我们用i、j两个下标固定子序列时,可以发现能找到一种递归关系,也就是判断一个子字符串(字符串的下表范围[i,j])是否回文,依赖于,子字符串(下表范围[i + 1, j - 1])) 是否是回文。
- 所以为了明确这种递归关系,我们的dp数组是要定义成一位二维dp数组。
布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。 - 接下来确定递推公式。整体上是两种,就是s[i]与s[j]相等,s[i]与s[j]不相等这两种。
- 当s[i]与s[j]不相等,dp[i][j]一定是false。
- 当s[i]与s[j]相等时,有如下三种情况
- 情况一:下标i 与 j相同,同一个字符例如a,true
- 情况二:下标i 与 j相差为1,例如aa,也是true
- 情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,结果是dp[i+1][j-1]。
-
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 } }
- 注意初始化时,要让所有dp[i][j]为false
- 确定遍历顺序:
- 我们能发现递推公式是需要[i+1]和[j-1]位置的值来满足的。所以在i的序列上需要大的值来推小的值,也就是从下到上遍历。j的序列上则需要小的值,也就是从前往后递推。如下图
- 注意因为dp[i][j]的定义,所以j一定是大于等于i的,那么在填充dp[i][j]的时候一定是只填充右上半部分。我们需要在遍历的时候保证j>=i
-
解题代码
- 动态规划
-
func countSubstrings(s string) int { res:=0 dp:=make([][]bool,len(s)) for i:=0;i<len(s);i++{ dp[i]=make([]bool,len(s)) } for i:=len(s)-1;i>=0;i--{ for j:=i;j<len(s);j++{ if s[i]==s[j]{ if j-i<=1{ res++ dp[i][j]=true }else if dp[i+1][j-1]{ res++ dp[i][j]=true } } } } return res } -
双指针扩散
- 根据回文子串的性质,我们再是可以通过中心向外扩散的方法来判定的。这样可以比便利首尾的方式减少一层循环。
- 需要注意的是拥有子串长度为奇数和偶数两种情况,这两种情况分别对应了中心是一个字符还是两个。要分别统计这两种情况下存在的回文数量。
-
func countSubstrings(s string) int { result := 0 extend := func(s string, i int, j int, n int) int { res := 0 for i >= 0 && j <= n && s[i] == s[j] {//左闭右闭 i-- j++ res++ } return res } for i:=0;i<len(s);i++ { result += extend(s, i, i, len(s)-1) // 以i为中心 result += extend(s, i, i + 1, len(s)-1) //以i和i+1为中心 } return result } // func extend(string s, int i, int j, int n) int { // res := 0 // for i >= 0 && j <= n && s[i] == s[j] {//左闭右闭 // i-- // j++ // res++ // } // return res // }
-
516最长回文子序列
- 代码随想录 (programmercarl.com)
-
第一印象
- 我们刚刚做完回文子串这道题。本题和回文子串的区别就是回文子序列不需要连续了。因此在递推的时候就可以直接统计两个指针位置出现相等的次数了。
-
讲解观后感
- 大致的解题思路同回文子串是类似的。我们还是按照动态规划的解题套路走一遍。
- 先确定dp数组(dp table)以及下标的含义
dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]。 - 然后确定递推公式
在判断回文子串的题目中,关键逻辑就是看s[i]与s[j]是否相同。
如果s[i]与s[j]相同,那么dp[i][j] = dp[i + 1][j - 1] + 2
如果s[i]与s[j]不相同,则分别取i、j序列上的上一位置的值进行比较,即dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]) - 在dp数组初始化的时候我们要注意。
dp[i][j] = dp[i + 1][j - 1] + 2代表着全部初始化为0时,我们得不到初始值为1即i、j相等时的情况。所以我们要把i=j时的值初始为1。dp := make([][]int, len(s)) for i := 0; i < size; i++ { dp[i] = make([]int, len(s)) dp[i][i] = 1 } - 在遍历顺序上,本题同上面回文子串一样,要由下到上、由左向右来进行遍历。并且要记得保证
j>=i -
解题代码
-
func longestPalindromeSubseq(s string) int { size := len(s) max := func(a, b int) int { if a > b { return a } return b } dp := make([][]int, size) for i := 0; i < size; i++ { dp[i] = make([]int, size) dp[i][i] = 1 } for i := size - 1; i >= 0; i-- { for j := i + 1; j < size; j++ { if s[i] == s[j] { dp[i][j] = dp[i+1][j-1] + 2 } else { dp[i][j] = max(dp[i][j-1], dp[i+1][j]) } } } return dp[0][size-1] }