欢迎关注更多精彩
关注我,学习常用算法与数据结构,一题多解,降维打击。

文章目录

  • 一、简介
  • 二、解题步骤
  • 三、作用
  • 四、经典算法介绍
    • 判断一个串是否为回文串(单次查询)
      • 普通情况
      • 判断指定字符
    • 多次子串查询
      • 动态规划法O(n^2)
      • 中心扩展法O(n^2)
      • Manacher(马拉车算法) O(n)
  • 五、牛刀小试
    • 练习1 [最长回文子串](https://leetcode-cn.com/problems/longest-palindromic-substring/)
      • 题目大意
      • 题目解析
      • AC代码
    • 练习2 [验证回文串](https://leetcode-cn.com/problems/valid-palindrome/)
      • 题目大意
      • 题目解析
      • AC代码
    • 练习3 [构建回文串检测](https://leetcode-cn.com/problems/can-make-palindrome-from-substring/)
      • 题目大意
      • 题目解析
      • AC代码
    • 练习4 [回文素数](https://leetcode-cn.com/problems/prime-palindrome/)
      • 题目大意
      • 题目解析
      • AC代码
  • 六、代码模板
  • 七、总结
  • 八、实战训练
    • 代码基础训练题
    • AK leetcode
    • 大神进阶

一、简介

“回文串”是一个正读和反读都一样的字符串,比如“level”或者“noon”等等就是回文串。

其基本问题是判断是不是回文串以及判断子串是否为回文串。

在此基础上衍生出回文串计数,求最长回文子串,回文串构造,异型回文(需要对原串字符进行预处理或使用非常规判断规则)等问题。

此类题目的意思往往很容易理解,思路也很容易想到。但是如果没有掌握回文串基本问题的解法,往往是无法在规定时间内运行出结果。本文重点介绍O(n)复杂度下判断回文子串的算法。

对于判断回文串的方法,主要有单次判断和多次判断。

单次查询可以使用对撞指针法进行判断。多次查询的方法有双指针动态规划,中心扩展法,Manacher(马拉车)算法。

其中双指针动态规划,中心扩展法是O(n^2)的算法也比较好理解。Manacher算法需要做一些理论推导与证明,以便加深理解。

二、解题步骤

解题步骤

  1. 将原串进行预处理(如有必要)
  2. 计算子串是否为回文串
  3. 根据题意选取回文子串
  4. 进行回文计数,求最长回文串,回文构造等操作

三、作用

  1. 查询最长回文子串
  2. 统计回文子串
  3. 构造回文子串
  4. 判断回文串
  5. 异型回文子串问题(比如对原串的字符进行相应的变化,对回文规则进行改变或加限制,将数字作为字符串)

四、经典算法介绍

单次查询可以使用对撞指针法进行判断。多次查询的方法有双指针动态规划,中心扩展法,Manacher(马拉车)算法。

其中双指针动态规划,中心扩展法是O(n^2)的算法也比较好理解。Manacher算法需要做一些理论推导与证明,以便加深理解。

判断一个串是否为回文串(单次查询)

主要思想是利用对撞指针判断断头尾是否相等,依次往中间靠拢,直到相遇。可以参考数组反转

普通情况

func isPalindrome(s string) bool {for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { // 每判断完一个都往里移一个if s[i] != s[j] {return false}}return true
}

判断指定字符

func isNeedCheck(c byte) bool {}func isPalindromeSpecial(s string) bool {for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { // 每判断完一个都往里移一个for ; i < j && !isNeedCheck(s[i]); i++ {} // 查找到第1个需要比较的元素for ; i < j && !isNeedCheck(s[j]); j-- {} // 查找最后一个需要比较的元素if s[i] != s[j] {return false}}return true
}

以上操作复杂都是O(n)对于单次查询的场景都可满足时间要求。

多次子串查询

极端情况下需求查询所有的子串。

 for i := 0; i < len(s); i++ { // 每判断完一个都往里移一个for j:=i;j<len(s);j++ {isPalindrome(s[i:j+1]) // 调用判断方法}}

以上算法的复杂是度O(n^3),n的极限只能是100. 下面介绍2种O(n^2)的算法。

动态规划法O(n^2)

设dp[i][j]为s[i:j+1]是否为回文串。

dp[i][i] = true

dp[i][j] = dp[i+1][j-1] && s[i]==s[j] , 当2头相等并且中间是回文时,s[i:j+1]是回文,具体还有其他细节在代码中说明

通过以上转移方程先计算出所有子串的dp值,在判断时,直接访问即可。

func makeDp(s string) [][]bool {// 预申请内存dp := make([][]bool, len(s))for i := 0; i < len(dp); i++ {dp[i] = make([]bool, len(s))}/*i,j 的循环值一定要注意。要保证在计算dp[i][j]时,dp[i+1][j-1]已经被计算过了。思考一下如果i从0开始会怎么样。???*/for i := len(s) - 1; i >= 0; i-- {for j := i; j < len(s); j++ {dp[i][j] = false //默认为false// 只有一个字符是回文if i == j {dp[i][j] = truecontinue}dp[i][j] = s[i] == s[j] && (i+1 >= j-1 || dp[i+1][j-1]) // i+1 >= j-1 时说明 s[i+1:j] 只有一个字符,或者空。}}return dp
}

以上计算 dp值是一个双重循环,总体复杂度是O(n^2), 后续判断子串的复杂度为O(1)总体复杂度是O(n^2)。对于规模n=1000左右的数据是适用的。

中心扩展法O(n^2)

  • 算法过程介绍

指的是以某个字符为中心向2侧延伸,我们把一侧最长可延伸的字符个数(不包含中心字符)定义为’回文半径’。每个字符的回文半径形成了一个半径数组rad []。

考虑到中心是2个字符的情况,我们对原字符串作一个特殊处理,先在字符中间和2端插入一个特殊字符’#’,在首尾加入字符’(’, ‘)’。然后按照单字符为中心来处理。

注意:前后一定要加入不同的字符

注意:前后一定要加入不同的字符

注意:前后一定要加入不同的字符

例如,原串s aabbdeaca ->s’ (#a#a#b#b#d#e#a#c#a#​)

如上图,rad[3]是由#向2边扩展,最多可以扩展2个字符,rad[2]=2。

仔细观察rad数组可以发现

当s’[i]是普通字符时,说明原串能以s[i/2-1]为中心向2边展开得到一个rad[i]长度的回文串。

当s’[i]=’#'时,说明原串能以s[(i-1)/2-1]s[(i+1)/2-1]两个字符为中心向2边延伸得到一个rad[i]长度的回文串。由于这里i肯定是奇数,所以(i+1)/2-1 = (i-1)/2,相当于是2个连续的字符。

举例,rad[2] = 1,我们可以得到a 是一个长度为1的回文串。

rad[3]=2 ,我们可以得到aa 是一个长度为2的回文串。

rad[16]=3 ,我们可以得到aca 是一个长度为3的回文串。

为什么会有这个性质,我的个人理解是这样。这是因为我们加入的字符都是#号当一个字母对应上后,扩展时会自然会带上一个#号。所以半径就是回文串的长度。

  • 如何解决常规问题(求最长回文子串和判断回文子串)

1 对于求最长回文子串,由于rad是存储的以某个字符向外延伸的最长回文子串,只要遍历rad就可以得到最大值。

2 判断回文子串s[i:j+1]

看了解一下原串与新的下标对应关系,从图中可以看出 原串中的i对应到新串的2*i+2

判断子串 i到j 是不是回文串,只要找到新串的中心x,查看rad[x]>=j-i+1. x = ((2*i+2) + (2*j+2))/2 = i+j+2

type Palindrome struct {rad []intarr []bytes string
}func (p *Palindrome)Init(s string) {p.s=sp.makeRad(s)
}func (p *Palindrome)makeRad(s string) {arr := make([]byte, len(s)*2+3)// 构造新数组arr[0], arr[1], arr[len(arr)-1] = '(', '#', ')'for i, v := range s {arr[i*2+2], arr[i*2+3]= byte(v), '#'}p.arr = arr//fmt.Println(string(arr))rad := make([]int, len(arr))// 计算半径for i:=1;i<len(arr)-1 ;i++  {for ;arr[i-1-rad[i]]==arr[i+1+rad[i]];rad[i]++{}}p.rad = rad//fmt.Println(rad)
}// 检查s[i:j+1]是不是回文
func (p *Palindrome)Check(i, j int) bool {// 原来的i对应到新数组的i*2+2, 新数组中心点是(i*2+2 + j*2+2)/2 = i+j+2// 要判断半径是不是>= j-i+1mid := i+j+2return p.rad[mid]>=j-i+1
}

在上面代码中,求rad是双重循环,复杂度O(n^2)

Manacher(马拉车算法) O(n)

Manacher算法是在上面中心扩展算法的基础上,优化rad生成过程,从而得到O(n)复杂度。

  • 过程介绍

基本框架与中心扩展法一样,我们主要介绍计算半径的优化。

以b向外延伸,把这个延伸分成前一半和后一半

由回文性质可以知前一半和后一半是对称的。

由于我们rad[i]是从小到大计算的,当计算rad[i]时, x<i 的 rad[x]已经计算完毕,可以想办法把这部分信息用上。

那如果知道前一半里的某个rad[x] (i-rad[i]<=x<i),

是不是可以推测出后一半rad[j] (i<j<=i+rad[i])

大致可以分成3种情况:

  • 情况1 (i-rad[i]<i-k-rad[i-k] 可以直接赋值)

上图中i-rad[i]<i-k-rad[i-k], 相当于i-k 的扩展区域在rad[i]的管辖区域内,根据对称性,i+k的扩展区域也应该在rad[i]的管辖区域。所以rad[i+k]=rad[i-k]

  • 情况2 (i-rad[i]=i-k-rad[i-k] 无法确定)

上图看出i-rad[i]=i-k-rad[i-k] 时,有可能存在右边边缘可以继续扩展的情况,这是rad[i]无法管辖的,所以无法进行直接赋值。

  • 情况3 (i-rad[i]>i-k-rad[i-k] 可以确定)

上图展示了i-rad[i]>i-k-rad[i-k]情况下,rad[i+k]的取值情况。

左增量,右增量是超出rad[i]的管辖,同时说明左增量与右增量不是对称的。(反证,如果是的话,rad[i]肯定要增加)

由于rad[i-k]=5, 说明左边以a为中心最多可以向2边扩展5个字符,说明可以以a(下标i-k)为中心扩展 i-k - i-rad[i] = rad[i]-k个字符,对称到右边就是以a(下标i+k)为中心扩展 i+rad[i] - (i+k) = rad[i]-k 个字符

所以,右增量不能作为i+k字符的扩展范围,rad[i+k] 最多只能扩展到i+rad[i]。rad[i+k]=rad[i]-k。

结合以上3种情况,在i-rad[i]!=i-k-rad[i-k]的情况下,rad[i+k]可以直接确定,rad[i+k] = min(rad[i]-k, rad[i-k])

当i-rad[i]=i-k-rad[i-k]时,还要继续往外扩展。但是此时不用从0开始,可以直接从rad[i+k]=3开始(虚线部分)。

  • rad计算代码
func (p *Palindrome) makeRad(s string) {arr := make([]byte, len(s)*2+3)// 构造新数组arr[0], arr[1], arr[len(arr)-1] = '(', '#', ')'for i, v := range s {arr[i*2+2], arr[i*2+3] = byte(v), '#'}p.arr = arr//fmt.Println(string(arr))rad := make([]int, len(arr))// 计算半径for k, j, i := 0, 0, 1; i < len(arr)-1; i += k {for ; arr[i-1-j] == arr[i+1+j]; j++ {}rad[i] = j // 通过扩展得到当前字符回文半径//for k=1; k <= rad[i] && i-rad[i]!=i-k-rad[i-k]; k++ { // 通过移项可以得到下面简化条件for k = 1; k <= j && rad[i-k] != rad[i]-k; k++ {rad[i+k] = min(rad[i-k], rad[i]-k)}/*rad[i]=j上面退出循环2种情况1. k>rad[i], 说明k>j ==> j=02. rad[i-k]==rad[i]-k ==> j=j-k*/j = max(0, j-k) // j可以继续前面已经扩展的结果。}p.rad = rad//fmt.Println(rad)
}

上述代码的复杂度是O(n)

主要分析一下计算半径部分。

i+rad[i] 代表的是从i向外延伸的最右边界

当一个大循环结束后,有2种情况。

k>rad[i]时,j=0, i+k+j = i+rad[i]+1 > i+rad[i]

k<=rad[i]时, j=rad[i]-k, i+k+j = i+rad[i] .

说明, i+k+j 整体是一直是增大的且i+k+j<len(arr) 。

所以总体复杂度是O(n)。

  • 整体代码
func min(a, b int) int {if a < b {return a}return b
}func max(a, b int) int {if a > b {return a}return b
}type Palindrome struct {rad []intarr []bytes   string
}func (p *Palindrome) Init(s string) {p.s = sp.makeRad(s)
}func (p *Palindrome) makeRad(s string) {arr := make([]byte, len(s)*2+3)// 构造新数组arr[0], arr[1], arr[len(arr)-1] = '(', '#', ')'for i, v := range s {arr[i*2+2], arr[i*2+3] = byte(v), '#'}p.arr = arr//fmt.Println(string(arr))rad := make([]int, len(arr))// 计算半径for k, j, i := 0, 0, 1; i < len(arr)-1; i += k {for ; arr[i-1-j] == arr[i+1+j]; j++ {}rad[i] = j // 通过扩展得到当前字符回文半径//for k=1; k <= rad[i] && i-rad[i]!=i-k-rad[i-k]; k++ { // 通过移项可以得到下面简化条件for k = 1; k <= j && rad[i-k] != rad[i]-k; k++ {rad[i+k] = min(rad[i-k], rad[i]-k)}/*rad[i]=j上面退出循环2种情况1. k>rad[i], 说明k>j ==> j=02. rad[i-k]==rad[i]-k ==> j=j-k*/j = max(0, j-k) // j可以继承前面已经扩展的结果。}p.rad = rad//fmt.Println(rad)
}// 检查s[i:j+1]是不是回文
func (p *Palindrome) Check(i, j int) bool {// 原来的i对应到新数组的i*2+2, 新数组中心点是(i*2+2 + j*2+2)/2 = i+j+2// 要判断半径是不是>= j-i+1mid := i + j + 2return p.rad[mid] >= j-i+1
}

五、牛刀小试

练习1 最长回文子串

题目链接 https://leetcode-cn.com/problems/longest-palindromic-substring/

题目大意

给你一个字符串 s,找到 s 中最长的回文子串。

示例 1:

输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。
示例 2:

输入:s = “cbbd”
输出:“bb”
示例 3:

输入:s = “a”
输出:“a”
示例 4:

输入:s = “ac”
输出:“a”

提示:

1 <= s.length <= 1000
s 仅由数字和英文字母(大写和/或小写)组成

题目解析

题目数据范围是1000,枚举判断,动态规划,中心扩展,Manacher都可用。

枚举判断:枚举所有子串判断是否为回文子串,取最优。

动态规划:直接枚举子串,判断是否为回文串,取最长。

中心扩展:枚举中心,往外扩展,取最长子串。

Manacher:找到最长半径,然后去构造出子原串

AC代码

  • 枚举判断
func isPalindrome(s string) bool {for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { // 每判断完一个都往里移一个if s[i] != s[j] {return false}}return true
}func longestPalindrome(s string) string {res := ""for i:=0;i<len(s);i++ {for j:=i;j<len(s);j++ {if isPalindrome(s[i:j+1]) && j-i+1>len(res){res = s[i:j+1]}}}return res
}
  • 动态规划
func makeDp(s string) [][]bool {// 预申请内存dp := make([][]bool, len(s))for i := 0; i < len(dp); i++ {dp[i] = make([]bool, len(s))}/*i,j 的循环值一定要注意。要保证在计算dp[i][j]时,dp[i+1][j-1]已经被计算过了。思考一下如果i从0开始会怎么样。???*/for i := len(s) - 1; i >= 0; i-- {for j := i; j < len(s); j++ {dp[i][j] = false //默认为false// 只有一个字符是回文if i == j {dp[i][j] = truecontinue}dp[i][j] = s[i] == s[j] && (i+1 >= j-1 || dp[i+1][j-1]) // i+1 >= j-1 时说明 s[i+1:j] 只有一个字符,或者空。}}return dp
}func longestPalindrome(s string) string {res := ""dp := makeDp(s)for i:=0;i<len(s);i++ {for j:=i;j<len(s);j++ {if dp[i][j] && j-i+1>len(res){res = s[i:j+1]}}}return res
}
  • Manacher法
func min(a, b int) int {if a < b {return a}return b
}func max(a, b int) int {if a > b {return a}return b
}type Palindrome struct {rad []intarr []bytes   string
}func (p *Palindrome) Init(s string) {p.s = sp.makeRad(s)
}func (p *Palindrome) makeRad(s string) {arr := make([]byte, len(s)*2+3)// 构造新数组arr[0], arr[1], arr[len(arr)-1] = '(', '#', ')'for i, v := range s {arr[i*2+2], arr[i*2+3] = byte(v), '#'}p.arr = arr//fmt.Println(string(arr))rad := make([]int, len(arr))// 计算半径for k, j, i := 0, 0, 1; i < len(arr)-1; i += k {for ; arr[i-1-j] == arr[i+1+j]; j++ {}rad[i] = j // 通过扩展得到当前字符回文半径//for k=1; k <= rad[i] && i-rad[i]!=i-k-rad[i-k]; k++ { // 通过移项可以得到下面简化条件for k = 1; k <= j && rad[i-k] != rad[i]-k; k++ {rad[i+k] = min(rad[i-k], rad[i]-k)}/*rad[i]=j上面退出循环2种情况1. k>rad[i], 说明k>j ==> j=02. rad[i-k]==rad[i]-k ==> j=j-k*/j = max(0, j-k) // j可以继续前面已经扩展的结果。}p.rad = rad//fmt.Println(rad)
}// 检查s[i:j+1]是不是回文
func (p *Palindrome) Check(i, j int) bool {// 原来的i对应到新数组的i*2+2, 新数组中心点是(i*2+2 + j*2+2)/2 = i+j+2// 要判断半径是不是>= j-i+1mid := i + j + 2return p.rad[mid] >= j-i+1
}// 获取最长,去除所有#号即可
func (p *Palindrome) GetLongest() (string, int) {res := ""l :=0for i:=1;i<len(p.rad)-1;i++ {if p.rad[i]>l {l = p.rad[i]res = string(p.arr[i-l:i+l+1])}}return strings.Replace(res, "#", "", -1), l
}func longestPalindrome(s string) string {p := &Palindrome{}p.Init(s)res,_ :=p.GetLongest() return res
}

练习2 验证回文串

题目链接:https://leetcode-cn.com/problems/valid-palindrome/

题目大意

给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。

说明:本题中,我们将空字符串定义为有效的回文串。

示例 1:

输入: “A man, a plan, a canal: Panama”
输出: true
示例 2:

输入: “race a car”
输出: false

题目解析

先把大写转成小写,调用指定字符模板

AC代码

func isNeedCheck(c byte) bool {return ('a'<=c&& c<='z') || ('0'<=c && c<='9')
}func isPalindromeSpecial(s string) bool {for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { // 每判断完一个都往里移一个for ; i < j && !isNeedCheck(s[i]); i++ {} // 查找到第1个需要比较的元素for ; i < j && !isNeedCheck(s[j]); j-- {} // 查找最后一个需要比较的元素if s[i] != s[j] {return false}}return true
}func isPalindrome(s string) bool {return isPalindromeSpecial(strings.ToLower(s))
}

练习3 构建回文串检测

题目链接:https://leetcode-cn.com/problems/can-make-palindrome-from-substring/

题目大意

给你一个字符串 s,请你对 s 的子串进行检测。

每次检测,待检子串都可以表示为 queries[i] = [left, right, k]。我们可以 重新排列 子串 s[left], …, s[right],并从中选择 最多 k 项替换成任何小写英文字母。

如果在上述检测过程中,子串可以变成回文形式的字符串,那么检测结果为 true,否则结果为 false。

返回答案数组 answer[],其中 answer[i] 是第 i 个待检子串 queries[i] 的检测结果。

注意:在替换时,子串中的每个字母都必须作为 独立的 项进行计数,也就是说,如果 s[left…right] = “aaa” 且 k = 2,我们只能替换其中的两个字母。(另外,任何检测都不会修改原始字符串 s,可以认为每次检测都是独立的)

示例:

输入:s = “abcda”, queries = [[3,3,0],[1,2,0],[0,3,1],[0,3,2],[0,4,1]]
输出:[true,false,false,true,true]
解释:
queries[0] : 子串 = “d”,回文。
queries[1] : 子串 = “bc”,不是回文。
queries[2] : 子串 = “abcd”,只替换 1 个字符是变不成回文串的。
queries[3] : 子串 = “abcd”,可以变成回文的 “abba”。 也可以变成 “baab”,先重新排序变成 “bacd”,然后把 “cd” 替换为 “ab”。
queries[4] : 子串 = “abcda”,可以变成回文的 “abcba”。

提示:

1 <= s.length, queries.length <= 10^5
0 <= queries[i][0] <= queries[i][1] < s.length
0 <= queries[i][2] <= s.length
s 中只有小写英文字母

题目解析

根据题意,本质是给一个字符串问能不能变化k个字符,并且可以重排得到一个回文串。

由于可以重排,所以只跟每种字母个数相关。

比如,abcda ,a(2), b(1), c(1), d(1)根据贪心原理,回文串最多只能有1种字母是奇数个。所以需要对奇数个进行统计s,然后改变s/2个。

现在题目有多个查询。相当于是查询区间里各字母的个数。

可以使用preSum思想

preSum[a][i]代表从0到第i个 位置为止,a出现的次数。

那么从i到j, a 出现的次数就是preSum[a][j] - preSum[a][i-1] O(1)

总体复杂度O(26*n)

AC代码

func getCnt(s string) [26][]int {cnt := [26][]int{}for i := 0; i < 26; i++ {cnt[i] = make([]int, len(s)+1)}for key, value := range s {for i := int32(0); i < 26; i++ {cnt[i][key+1] = cnt[i][key]if value-'a' == i {cnt[i][key+1]++}}}return cnt
}func canMakePaliQueries(s string, queries [][]int) []bool {cnt :=getCnt(s)ans := make([]bool, len(queries))for i, q := range queries {total :=0for j:=0;j<26;j++ {total += (cnt[j][q[1]+1]-cnt[j][q[0]])&1 // 判断奇数}ans[i]=q[2]>=total /2}return ans
}

练习4 回文素数

题目链接:https://leetcode-cn.com/problems/prime-palindrome/

题目大意

求出大于或等于 N 的最小回文素数。

回顾一下,如果一个数大于 1,且其因数只有 1 和它自身,那么这个数是素数。

例如,2,3,5,7,11 以及 13 是素数。

回顾一下,如果一个数从左往右读与从右往左读是一样的,那么这个数是回文数。

例如,12321 是回文数。

示例 1:

输入:6
输出:7
示例 2:

输入:8
输出:11
示例 3:

输入:13
输出:101

提示:

1 <= N <= 10^8
答案肯定存在,且小于 2 * 10^8。

题目解析

答案肯定是在 2*10^8 相当于最多只有9位数,我们可以先去构造前缀,然后利用前缀构造出整数。

构造出整数后再去判断是不是素数。

枚举时要先考虑短位数,再考虑中间位增加。

复杂度10^5 * sqrt(n)

AC代码

func isP(n int) bool {if n<4{return true}for i := 2; i <= int(math.Sqrt(float64(n)))+1; i++ {//for i := 2; i <= n-1; i++ {if n%i == 0 {return false}}return true
}func getPalinNum(n int, mid int) int {ans := nif mid >= 0 {ans = ans*10 + mid}for ; n > 0; n /= 10 {ans = ans*10 + n%10}return ans
}func primePalindrome(N int) int {// 只有一位特殊判断for i := 2; i < 10; i++ {if i < N {continue}if isP(i) {return i}}// 枚举前缀长度for l := 1; l < 5; l++ {low, up := int(math.Pow10(l-1)), int(math.Pow10(l))// 枚举偶数位for i := low; i < up; i++ {n := getPalinNum(i, -1)if n < N {continue}if isP(n) {return n}}// 枚举奇数位for i := low; i < up; i++ {// 中间位从小到大for mid := 0; mid <= 9; mid++ {n := getPalinNum(i, mid)if n < N {continue}if isP(n) {return n}}}}return 0
}

六、代码模板

  • 动态规划
func makeDp(s string) [][]bool {// 预申请内存dp := make([][]bool, len(s))for i := 0; i < len(dp); i++ {dp[i] = make([]bool, len(s))}/*i,j 的循环值一定要注意。要保证在计算dp[i][j]时,dp[i+1][j-1]已经被计算过了。思考一下如果i从0开始会怎么样。???*/for i := len(s) - 1; i >= 0; i-- {for j := i; j < len(s); j++ {dp[i][j] = false //默认为false// 只有一个字符是回文if i == j {dp[i][j] = truecontinue}dp[i][j] = s[i] == s[j] && (i+1 >= j-1 || dp[i+1][j-1]) // i+1 >= j-1 时说明 s[i+1:j] 只有一个字符,或者空。}}return dp
}
  • 中心扩展法
type Palindrome struct {rad []intarr []bytes string
}func (p *Palindrome)Init(s string) {p.s=sp.makeRad(s)
}func (p *Palindrome)makeRad(s string) {arr := make([]byte, len(s)*2+3)// 构造新数组arr[0], arr[1], arr[len(arr)-1] = '(', '#', ')'for i, v := range s {arr[i*2+2], arr[i*2+3]= byte(v), '#'}p.arr = arr//fmt.Println(string(arr))rad := make([]int, len(arr))// 计算半径for i:=1;i<len(arr)-1 ;i++  {for ;arr[i-1-rad[i]]==arr[i+1+rad[i]];rad[i]++{}}p.rad = rad//fmt.Println(rad)
}// 检查s[i:j+1]是不是回文
func (p *Palindrome)Check(i, j int) bool {// 原来的i对应到新数组的i*2+2, 新数组中心点是(i*2+2 + j*2+2)/2 = i+j+2// 要判断半径是不是>= j-i+1mid := i+j+2return p.rad[mid]>=j-i+1
}
  • Manacher
func min(a, b int) int {if a < b {return a}return b
}func max(a, b int) int {if a > b {return a}return b
}type Palindrome struct {rad []intarr []bytes   string
}func (p *Palindrome) Init(s string) {p.s = sp.makeRad(s)
}func (p *Palindrome) makeRad(s string) {arr := make([]byte, len(s)*2+3)// 构造新数组arr[0], arr[1], arr[len(arr)-1] = '(', '#', ')'for i, v := range s {arr[i*2+2], arr[i*2+3] = byte(v), '#'}p.arr = arr//fmt.Println(string(arr))rad := make([]int, len(arr))// 计算半径for k, j, i := 0, 0, 1; i < len(arr)-1; i += k {for ; arr[i-1-j] == arr[i+1+j]; j++ {}rad[i] = j // 通过扩展得到当前字符回文半径//for k=1; k <= rad[i] && i-rad[i]!=i-k-rad[i-k]; k++ { // 通过移项可以得到下面简化条件for k = 1; k <= j && rad[i-k] != rad[i]-k; k++ {rad[i+k] = min(rad[i-k], rad[i]-k)}/*rad[i]=j上面退出循环2种情况1. k>rad[i], 说明k>j ==> j=02. rad[i-k]==rad[i]-k ==> j=j-k*/j = max(0, j-k) // j可以继续前面已经扩展的结果。}p.rad = rad//fmt.Println(rad)
}// 检查s[i:j+1]是不是回文
func (p *Palindrome) Check(i, j int) bool {// 原来的i对应到新数组的i*2+2, 新数组中心点是(i*2+2 + j*2+2)/2 = i+j+2// 要判断半径是不是>= j-i+1mid := i + j + 2return p.rad[mid] >= j-i+1
}

动态规划法代码比较少,逻辑简单,现场写起来比较快。

Manercher代码多些,复杂度优。解决问题能力更强。

七、总结

主要内容:

  1. 回文串问题出现非常频繁,掌握相关算法对解题非常有帮助。
  2. 介绍回文串相关算法的作用
  3. 介绍回文串相关的常用算法,其中动态规划和Manacher要重点理解。

回文串判断是回文问题中最基础的问题,构造回文也是经常遇到的题目。

笔者水平有限,有写得不对或者解释不清楚的地方还望大家指出,我会尽自己最大努力去完善。

下面我精心准备了几个流行网站上的题目(首先AK F.*ing leetcode)。给大家准备了一些题目,供大家练习参考。干他F.*ing (Fighting?)。

八、实战训练

代码基础训练题

光说不练假把式,学完了怎么也要实操一下吧,快快动用把刚才那4题A了。

  1. 最长回文子串 题目链接 https://leetcode-cn.com/problems/longest-palindromic-substring/
  2. 验证回文串 题目链接 https://leetcode-cn.com/problems/valid-palindrome/
  3. 构建回文串检测 题目链接 https://leetcode-cn.com/problems/can-make-palindrome-from-substring/
  4. 回文素数 题目链接 https://leetcode-cn.com/problems/prime-palindrome/

AK leetcode

leetcode相关题目都在下面了,拿起武器挨个点名呗。

5. 最长回文子串 https://leetcode-cn.com/problems/longest-palindromic-substring/ 扩展法 manacher dp

9. 回文数 https://leetcode-cn.com/problems/palindrome-number/ 转数组 判断

125. 验证回文串 https://leetcode-cn.com/problems/valid-palindrome/ 查找特殊字符 验证字符串

131. 分割回文串 https://leetcode-cn.com/problems/palindrome-partitioning/ 回文串 dp 枚举 dfs

132. 分割回文串 II https://leetcode-cn.com/problems/palindrome-partitioning-ii/ dp 枚举 回文串

214. 最短回文串 https://leetcode-cn.com/problems/shortest-palindrome/ 马拉车 枚举 贪心

234. 回文链表 https://leetcode-cn.com/problems/palindrome-linked-list/ 模拟 反转

336. 回文对 https://leetcode-cn.com/problems/palindrome-pairs/ 贪心 字符串查询

409. 最长回文串 https://leetcode-cn.com/problems/longest-palindrome/ 扩展法 manacher dp

479. 最大回文数乘积 https://leetcode-cn.com/problems/largest-palindrome-product/ 枚举 模拟

516. 最长回文子序列 https://leetcode-cn.com/problems/longest-palindromic-subsequence/ dp

564. 寻找最近的回文数 https://leetcode-cn.com/problems/find-the-closest-palindrome/ 贪心 构造

647. 回文子串 https://leetcode-cn.com/problems/palindromic-substrings/ 扩展法 manacher dp

680. 验证回文字符串 Ⅱ https://leetcode-cn.com/problems/valid-palindrome-ii/ 贪心 双指针

730. 统计不同回文子序列 https://leetcode-cn.com/problems/count-different-palindromic-subsequences/ 枚举 dp(判断)

866. 回文素数 https://leetcode-cn.com/problems/prime-palindrome/ 素数 回文判断

906. 超级回文数 https://leetcode-cn.com/problems/super-palindromes/ 构造

1147. 段式回文 https://leetcode-cn.com/problems/longest-chunked-palindrome-decomposition/ dp kmp

1177. 构建回文串检测 https://leetcode-cn.com/problems/can-make-palindrome-from-substring/ 贪心 区间求和

1278. 分割回文串 III https://leetcode-cn.com/problems/palindrome-partitioning-iii/ dp 模拟

1312. 让字符串成为回文串的最少插入次数 https://leetcode-cn.com/problems/minimum-insertion-steps-to-make-a-string-palindrome/ dp 模拟

1328. 破坏回文串 https://leetcode-cn.com/problems/break-a-palindrome/ 贪心 模拟

1332. 删除回文子序列 https://leetcode-cn.com/problems/remove-palindromic-subsequences

1400. 构造 K 个回文字符串 https://leetcode-cn.com/problems/construct-k-palindrome-strings/ hash 贪心

1457. 二叉树中的伪回文路径 https://leetcode-cn.com/problems/pseudo-palindromic-paths-in-a-binary-tree/ dfs hash 贪心

1616. 分割两个字符串得到回文串 https://leetcode-cn.com/problems/split-two-strings-to-make-palindrome/ 贪心

1745. 回文串分割 IV https://leetcode-cn.com/problems/palindrome-partitioning-iv/ 回文判断 枚举

面试题 01.04. 回文排列 https://leetcode-cn.com/problems/palindrome-permutation-lcci/ hash 贪心

面试题 02.06. 回文链表 https://leetcode-cn.com/problems/palindrome-linked-list-lcci/ 链表 模拟


以上题目太多,大家适当选择就行,下面还有进阶题目。

大神进阶

也可以去vjudge https://vjudge.net/problem 搜索相关题号

poj

http://poj.org/problem?id=1159 动态规划

hdu

http://acm.hdu.edu.cn/showproblem.php?pid=1597

以下将序号替换就是题目链接。

http://acm.hdu.edu.cn/showproblem.php?pid=3294 模拟,manacher算法

http://acm.hdu.edu.cn/showproblem.php?pid=3068 manacher算法

http://acm.hdu.edu.cn/showproblem.php?pid=4513 manacher算法

http://acm.hdu.edu.cn/showproblem.php?pid=3948 manacher算法

http://acm.hdu.edu.cn/showproblem.php?pid=1431 枚举

http://acm.hdu.edu.cn/showproblem.php?pid=4632

http://acm.hdu.edu.cn/showproblem.php?pid=6156

zoj

https://vjudge.net/problem/ZOJ-3661 manacher 前缀和

hihocoder

最长回文子串 http://hihocoder.com/problemset/problem/1032

回文字符序列 http://hihocoder.com/problemset/problem/1149

回文字符串 http://hihocoder.com/problemset/problem/1323

回文子串的数量 http://hihocoder.com/problemset/problem/1589

本质不同的回文子串的数量 http://hihocoder.com/problemset/problem/1602

回文字符串2 http://hihocoder.com/problemset/problem/1721

偶数长度回文子串 http://hihocoder.com/problemset/problem/1788


本人码农,希望通过自己的分享,让大家更容易学懂计算机知识。

AK F.*ing leetcode 流浪计划之回文串相关推荐

  1. AK F.*ing leetcode 流浪计划之线段树

    欢迎关注更多精彩 关注我,学习常用算法与数据结构,一题多解,降维打击. 文章目录 零.简介 一.算法原理 树的构建 更新 查询 二.数据结构及算法实现 数据结构 构建 更新 查询 复杂度分析 例题题解 ...

  2. AK F.*ing leetcode 流浪计划之取模

    欢迎关注更多精彩 关注我,学习常用算法与数据结构,一题多解,降维打击. 文章目录 一.简介 二.公式证明 公式 1 公式2 公式3 三.作用 四.注意事项及优化 加(减)法取模 乘法取模 五.牛刀小试 ...

  3. AK F.*ing leetcode 流浪计划之线段求交

    欢迎关注更多精彩 关注我,学习常用算法与数据结构,一题多解,降维打击. 本期话题:2条线段求交点 我有两种线段求交方法. 这两种方法在图形中窗口剪裁中应用. 分别是 Cohen-Suther land ...

  4. AK F.*ing leetcode 流浪计划之Bresenham画线

    欢迎关注更多精彩 关注我,学习常用算法与数据结构,一题多解,降维打击. 文章目录 基本变量定义 Bresenham 推导过程及实现 中点法推导 基本变量定义 P1, P2 为要画线段的起终点,P1 = ...

  5. leetcode题解132-分割回文串 II

    问题描述 给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文. 返回符合要求的 最少分割次数 . 示例 1: 输入:s = "aab" 输出:1 解释:只需一次分割 ...

  6. leetcode题解131-分割回文串

    问题描述 给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 .返回 s 所有可能的分割方案. 回文串 是正着读和反着读都一样的字符串. 示例 1: 输入:s = "aa ...

  7. LeetCode Palindrome Partitioning(dfs +回文串 )

    问题:给出一个字符串,输出回文串,使得拼接后为原来的字符串 思路:先通过动态规划得到所有的回文串,然后使用深度优先搜索得到所有的解 具体代码参考: https://github.com/wuli249 ...

  8. leetcode系列--125.验证回文串

    leetcode系列–第125题.验证回文串 给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写. 说明:本题中,我们将空字符串定义为有效的回文串. 输入: "A ...

  9. LeetCode贪心 最长回文串

    Given a string s which consists of lowercase or uppercase letters, return the length of the longest ...

最新文章

  1. java动态代理_Java动态代理
  2. 存储过程排版工具_安利一款比Evernote更为实用的云笔记工具,不容错过
  3. 内聚的极限: 软件开发的不确定性原理
  4. RPC协议与Web Service
  5. .Net Core DI,如何在程序集中注册所有现有的可分配类型
  6. 【java笔记】数组概念初始化及相关操作
  7. Android手机投屏后没有声音,安卓手机镜像投屏没有声音解决办法
  8. 华为y220t android版本升级,华为 Y220T 刷机详细图文教程
  9. opencv检测动态物体
  10. html背景左右渐变,css 背景 上下渐变 左右渐变
  11. 权重确定方法四:主成分分析法确定权重(PCA)
  12. 在Vue3项目中使用 Echarts 绘制股票图表的分享(二):绘制现价图+均价图
  13. 使用蓝牙模块和笔记本自带蓝牙通讯
  14. Java文件传输带宽控制_如何限制Java中的带宽?
  15. 开源WPF第三方库【Newbeecoder.UI】分页控件
  16. 找到自身管理创新支点 振兴物流业
  17. 如何在迪士尼+上启用和自定义字幕
  18. 配置mysql数据库端口号_数据库配置时要注意端口号
  19. ios版基于微软azure的文字转语音工具
  20. cropper.js 实现HTML5 裁剪上传头像

热门文章

  1. 2020 5G场景化应用案例TOP20
  2. DELL R410升级网卡驱动
  3. 两节锂电池充电芯片,和保护IC的接法
  4. 你不努力,有什么资格谈未来
  5. 微课设计教案 计算机,信息技术微课设计
  6. 2021年电工(初级)考试题及电工(初级)新版试题
  7. 计算机算法对程序设计的作用,浅谈对计算机程序设计的认识
  8. 网易大师邮件的撤回功能怎么用?
  9. 做个小程序需要多少钱?
  10. 学计算机心得体会50字,心得体会50字