《力扣题库(C):第5题 (最长回文子串)》

第五题:最长回文子串

  • 给你一个字符串 s,找到 s 中最长的回文子串
  • 提示:回文串(palindromic string)是指这个字符串无论从左读还是从右读,所读的顺序是一样的;简而言之,回文串是左右对称的

示例 1:

  • 输入:s = "babad"
  • 输出:"bab"
  • 解释:"aba" 同样是符合题意的答案

示例 2:

  • 输入:s = "cbbd"
  • 输出:"bb"

示例 3:

  • 输入:s = "a"
  • 输出:"a"

示例 4:

  • 输入:s = "ac"
  • 输出:"a"

提示:

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

引入Manacher算法

博主也不知道Manacher 算法,所以参考了几篇文章,比较好的是:https://www.jianshu.com/p/eb1bfcb0d9e2

维基百科中对于 Manacher 算法是这样描述的:

[Manacher(1975)] 发现了一种线性时间算法,可以在列出给定字符串中从字符串头部开始的所有回文。并且,Apostolico, Breslauer & Galil (1995) 发现,同样的算法也可以在任意位置查找全部最大回文子串,并且时间复杂度是线性的。因此,他们提供了一种时间复杂度为线性的最长回文子串解法。替代性的线性时间解决 Jeuring (1994), Gusfield (1997)提供的,基于后缀树(suffix trees)。也存在已知的高效并行算法。

在还没有实现算法之前,我们先要弄清楚算法的运行流程,即给我们一个具体的字符串,我们通过稿纸演算的方式,应该如何得到给定字符串的最长子回文串。

理解 Manacher 算法最好的办法,其实是根据一些关于 Manacher 算法的文章,自己写写画画,最好能产生一些输出,画画图,举一些具体的例子,这样 Manacher 算法就不难搞懂了。

Manacher 算法本质上还是中心扩散法,只不过它使用了类似 KMP 算法的技巧,充分挖掘了已经进行回文判定的子串的特点,使得算法高效。

回文串可分为奇数回文串和偶数回文串,它们的区别是:奇数回文串关于它的“中点”满足“中心对称”,偶数回文串关于它“中间的两个点”满足“中心对称”。我们在具体判定一个字符串是否是回文串的时候,常常会不自觉地考虑到它们之间的这个小的差别。

第 1 步:预处理,添加分隔符

我们先给出具体的例子,看看如何添加分隔符。

例1:给字符串 "bob" 添加分隔符 "#"

答:"bob" 添加分隔符 "#" 以后得到:"#b#o#b#"

再看一个例子:

例2:给 "noon" 添加分隔符 "#"

答:"noon" 添加分隔符 "#" 以后得到:"#n#o#o#n#"

我想你已经看出来分隔符是如何添加的,下面是 2 点说明。

1、分隔符是字符串中没有出现过的字符,这个分隔符的种类只有一个,即你不能同时添加 "#""?" 作为分隔符;

2、在字符串的首位置、尾位置和每个字符的“中间”都添加 1个这个分隔符,可以很容易知道,如果这个字符串的长度是 len,那么添加的分隔符的个数就是 len + 1,得到的新的字符串的长度就是 2len + 1,显然它一定是奇数。

为什么要添加分隔符?

1、首先是正确性:添加了分隔符以后的字符串的回文性质与原始字符串是一样的。

2、其实是避免奇偶数讨论,对于使用“中心扩散法”判定回文串的时候,长度为奇数和偶数的判定是不同的,添加分隔符可以避免对奇偶性的讨论。

对于一个串str长度为n,有n-1个空格,首位有两个,对这些空处理,长度变成2n+1 (也就是不管奇偶扩展后肯定是奇数)

第 2 步:得到 p 数组

首先,我们先来看一下如何填表。以字符串 "abbabb" 为例,说明如何手动计算得到 p 数组。假设我们要填的就是下面这张表。

char#a#b#b#a#b#b#
index0123456789101112
p12125
p-1

第 1 行 char 数组:这个数组就是待检测字符串加上分隔符以后的字符构成的数组。

第 2 行 index 数组:这个数组是索引数组,我们后面要利用到它,填写即索引从 0 开始写就好了。

下面我们来看看 p 数组应该如何填写。首先我们定义回文半径。

回文半径:以 char[i] 作为回文中心,同时向左边、向右边进行扩散,直到不能构成回文串或者触碰到边界为止,能扩散的步数 + 1 ,即定义为 p 数组索引的值,也称之为回文半径。

以上面的例子,我们首先填。p[0],以 char[0] = '#'为中心,同时向左边向右扩散,走 1 步就碰到边界了,因此“能扩散的步数”为0,“能扩散的步数 + 1 = 1”,因此 p[0] = 1

char#a#b#b#a#b#b#
index0123456789101112
p1
p-1

下面填写 p[1] ,以 char[1] = 'a' 为中心,同时向左边向右扩散,走 1 步,左右都是 "#",构成回文子串,于是继续同时向左边向右边扩散,左边就碰到边界了,因此“能扩散的步数”为1,“能扩散的步数 + 1 = 2”,因此 p[1] = 2

char#a#b#b#a#b#b#
index0123456789101112
p12
p-1

下面填写 p[2] ,以 char[2] = '#' 为中心,同时向左边向右扩散,走 1 步,左边是 "a",右边是 "b",不匹配,因此“能扩散的步数”为 ,“能扩散的步数 + 1 = 1”,因此 p[2] = 1

char#a#b#b#a#b#b#
index0123456789101112
p121
p-1

下面填写 p[3],以 char[3] = 'b' 为中心,同时向左边向右扩散,走 1 步,左右两边都是 “#”,构成回文子串,继续同时向左边向右扩散,左边是 "a",右边是 "b",不匹配,因此“能扩散的步数”为1,“能扩散的步数 + 1 = 2”,因此 p[3] = 2

char#a#b#b#a#b#b#
index0123456789101112
p1212
p-1

下面填写 p[4],以 char[4]='#' 为中心,同时向左边向右扩散,可以知道可以同时走 4 步,左边到达边界,因此“能扩散的步数”为4,“能扩散的步数 + 1 = 5”,因此 p[4] = 5

char#a#b#b#a#b#b#
index0123456789101112
p12125
p-1

分析到这里,后面的数字不难填出,最后写成如下表格:

char#a#b#b#a#b#b#
index0123456789101112
p1212521612321
p-1

p-1 数组很简单了,把 p 数组的数 -1 就行了。实际上直接把能走的步数记录下来就好了。不过就是为了给“回文半径”一个定义而已。

于是我们得到如下表格:

char#a#b#b#a#b#b#
index0123456789101112
p1212521612321
p-10101410501210

于是:数组 p -1 的最大值就是最长的回文子串,可以在得到 p 数组的过程中记录这个最大值,并且记录最长回文子串

解题:

/**
 * @file 5.cpp
 * @author HarkerYX
 * @brief  最长回文子串
 * @version 0.1
 * @date 2021-04-27
 * 
 * @copyright Copyright (c) 2021
 * 
 */


/*
第五题:最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串


解题思路:
Manacher 算法解决 奇偶回文的不确定性,对于一个串str长度为n,有n-1个空格,首位有两个,对这些空处理,长度变成2n+1
再者就是知道了扩展后的中心位置如何推算出原始位置
*/

#include <stdio.h>
#include <windows.h>

char * createManacherStr(char *s)
{
    int len = strlen(s);

    //不去判断长度了,就当不是空字符串

    char *newstr = (char *)malloc(len * 2 + 2);
    char *p = newstr;

    *newstr++ = '#';

    while(*s != NULL){

        *newstr++ = *s;
        *newstr++ = '#';
        s++;
    }

    *newstr = '\0';
    return p;
}



/**
 * @brief 
 * 
 * @param org 
 * @return char* 
 */
char * longestPalindrome(char* org){


    char *s = NULL;

    //创建扩展字符串
    s = createManacherStr(org);   

    printf("newsStr = %s ,len = %d \n",s,strlen(s));
    //记录最终最长回文长度
    int maxPalindromeLen = 0;
    //Manacher扩展后的长度
    int len = strlen(s);

    //记录找到的最大回文的移动位置
    int maxPos = 0;

    //每次位移时候算出来的最大回文长度
    int tmpMaxlen = 0;

    for (int i = 0; i<len ;i++){

        tmpMaxlen = 0;

        // 判断长度中心点,位置在左边的情况时,左边扩展肯定要小于右边的长度,那就看左边就行
        if(i <= (len/2+1))
        {
            //当前的位置能往左移动的步数
            int MaxleftPos = i;

            for (int j = 0; j <= MaxleftPos; j++)
            {
                // if(i==4){
                //     printf("%c,%c\n",s[i - j],s[i + j]);     

                // }
                
                if(s[i-j] != s[i+j])
                    break;

                tmpMaxlen++;
            }
        }
        else if(i > (len/2+1)){

            //当前的位置能往右移动的步数
            int MaxRightPos = len - i;      

            for (int j = 0; j <= MaxRightPos; j++)
            {
                if(s[i-j] != s[i+j])
                    break;

                tmpMaxlen++;
            }
        }

        // 每次移动判断出来的最大回文判断后给最终回文
        maxPalindromeLen = maxPalindromeLen > tmpMaxlen ? maxPalindromeLen : tmpMaxlen;

        // 如果 if(tmpMaxlen > maxPalindromeLen) 就得到第一个最大位置 ,>=就得到最后的一个最大位置
        if(tmpMaxlen >= maxPalindromeLen)
            maxPos = i;
    }

    //为什么 -1 ,看Manacher介绍最后就知道了
    maxPalindromeLen -= 1;
    printf("最大回文长度 = %d \n",maxPalindromeLen);
    printf("Manacher扩展后的字符串中的位置: %d\n",maxPos);


    // 得到最大位置在 maxPos ,结合最大的回文推算出 扩展字符串从什么位置开始
    int org_pos  = (maxPos - 1) / 2;
    int beginPos = org_pos - (maxPalindromeLen - 1) / 2;
    printf("原字符串的位置: %d \n", beginPos);

    char *tmpS = (char *)malloc(maxPalindromeLen * sizeof(char) + 1);
    memcpy(tmpS, org + beginPos, maxPalindromeLen);
    tmpS[maxPalindromeLen] = '\0';
    printf("最大回文原字符串: %s \n", tmpS);

    return tmpS;
}


int main(void)
{
    // char *s ="a";
    // char *s ="ac";
    // char *s ="cbbd"; 
    char *s = "babad";

    longestPalindrome(s);

    return 0;
}

newsStr = #b#a#b#a#d# ,len = 11 
最大回文长度 = 3 
Manacher扩展后的字符串中的位置: 5
原字符串的位置: 1 
最大回文原字符串: aba 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HarkerYX

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值