• 算法五最长回文子串


    给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。

    示例 1:

    输入: "babad"
    输出: "bab"
    注意: "aba" 也是一个有效答案。

    示例 2:

    输入: "cbbd"
    输出: "bb"

    来源:力扣(LeetCode)
    链接:https://leetcode-cn.com/problems/longest-palindromic-substring
    著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

    这个问题有几种解法,一种是性能最差的,暴力破解。就是把所有的回文子串一个个的比较获取出来,然后找出一个最大的。第二种是找最长公共子串,就是把字符串倒过来,然后找这两个字符串的公共子串,这种方法求得公共子串并不一定是回文,需要再判断,并且效率没有提升太多。第三种方法是动态规则,就是用一个二维数组保存每一个字符与另外一个是否相同,生成数组后,根据数组判断最长的字符。这三种感觉既占了空间效率也没有明显改善,所以就没实现。

    我的解,中心扩散法,回文的特点是两边一样,所以可以根据这个判断,就是一个字符一个字符的遍历,然后判断当前字符与下一个和下下个字符是否相等,如果相等,就是回文字符,然后向两边延伸,直到不相等,与最大的判断,获取最长的位置。

    class Solution {
    public:
        string longestPalindrome(string s) {
            int sstart = 0;
            int send = 0;
            for(int i = 0; i < s.size(); i++)
            {
                int j = i + 1;
                if(j < s.size() && s[i] == s[j])
                {
                    int ti = i;
                    int tj = j;
                    while(ti >= 0 && tj < s.size())
                    {
                        if(s[ti] != s[tj])
                        {
                            break;
                        }
                        ti--;
                        tj++;
                    }
                    ti++;
                    tj--;
                    if(send - sstart < tj - ti)
                    {
                        sstart = ti;
                        send = tj;
                    }
                }
                j = j + 1;
                if(j < s.size() && s[i] == s[j])
                {
                    int ti = i;
                    int tj = j;
                    while(ti >= 0 && tj < s.size())
                    {
                        if(s[ti] != s[tj])
                        {
                            break;
                        }
                        ti--;
                        tj++;
                    }
                    ti++;
                    tj--;
                    if(send - sstart < tj - ti)
                    {
                        sstart = ti;
                        send = tj;
                    }
                }
            }
            return string(s, sstart, send - sstart + 1);
        }
    };

    这个方法是比较好理解的,并且空间复杂度只有o(1),时间复杂度虽然是o(n^2),但是子串判断的时候,一次跳两个位置,比上面的o(n^2)要少一些步骤。

    除了这个方法之外,还有一个求解最长子串的公认算法,就是Manacher。这个算法看了好几个题解,查了很多,最后找到 https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zhong-xin-kuo-san-dong-tai-gui-hua-by-liweiwei1419/ 这个题解讲的比较清楚。感觉作者说的挺对的,是对中心扩散法的修正,为什么呢?因为中心扩散法在判断的索引不断后移中,有很多一开始遍历过的结果又被重新查询了一边。

    比如下面的字符串

    当我们判断到e的时候,其实是知道当前的回文字符串是abcecba,如果我们判断e的下一个c的时候,我们又需要循环判断一下c作为回文字符串中心的情况,虽然这个例子中没有判断太多,但是实际上,因为是回文字符串,也就像镜子一样,并且c是包含在前面e为中心的回文字符串中,那么c的回文字符串的情况与e左边刚刚判断完的c的情况是一样的,至少有很大一部分是可以利用的。也就是e左边的c是不是回文字符串,字符串有多长,与e的右边的c是一样的,最起码在e的回文字符串里面是一样的,这样就可以利用并且较少判断。

    为了更方便计算,Manacher算法会在每一个字符串左右增加一个分隔符,比如'#',然后求一个数组p,p就是当前索引字符串在的位置的回文半径。这样求完之后,直接遍历p找到最大的就可以了。求数组p的时候,就用到了上面的思路,如果某个索引在某一个回文字符串内,那么就可以通过镜像找到对应位置的数据,然后根据边界做一些处理就可以了。我一直在想,不增加分隔符是不是也可以,这样就减少了插入数据的时间和空间消耗,有待测试。

    string addboundaries(string s)
    {
        string tmp;
        for (auto& iter : s)
        {
            tmp = tmp + "#" + iter;
        }
        tmp = tmp + "#";
        return tmp;
    }
    string longestPalindrome(string s)
    {
        string tmpstr = addboundaries(s);
        int slen = tmpstr.size();
        int *p = new int[slen];
        memset(p, 0, slen * sizeof(int));
        int maxright = 0;
        int center = 0;
        int maxlen = 1;
        int start = 0;
        for (int i = 0; i < slen; i++)
        {
            if (i < maxright)
            {
                int mirror = 2 * center - i;
                p[i] = min(maxright - i, p[mirror]);
            }
            int left = i - (1 + p[i]);
            int right = i + (1 + p[i]);
            while (left >= 0 && right < slen && tmpstr[left] == tmpstr[right])
            {
                p[i]++;
                left--;
                right++;
            }
            if (i + p[i] > maxright)
            {
                maxright = i + p[i];
                center = i;
            }
            if (p[i] > maxlen)
            {
                maxlen = p[i];
                start = (i - maxlen) / 2;
            }
        }
        return string(s, start, maxlen);
    }

    上面代码可以看出,

    插入分隔符,方便计算

    申请一个数组p,保存数据

    保存计算到最有边界的位置索引(针对插入分隔符之后的数组)maxright

    保存针对最有边界位置的回文字符串的中心位置center

    原字符串中最长回文字符串的长度maxlen

    原字符串中最长回文字符串的起始位置start

    开始遍历,如果i比maxright小,说明i的一部分数据已经可以通过镜像得到,不需要计算了。只需通过中心法判断基于当前位置i超过maxright的字符串,因为这部分本来也没计算过,所以并没有重复计算,如果满足了回文字符串,就把p[i]实时更新,直到不满足。这时判断最右边界,更新最右边界,中心位置,最大字串长度和起始位置。

    当时对于这个算法没仔细想过,一直担心会有逻辑错误,比如,

    • 在更新center右边的p数组时,会不会影响到左边的数组
    • 会不会有情况使得center左边的回文字符串的最右边界比maxright大
    • 如果在center右边的i位置,回文字符串最右边界大于maxright,那么从i到当前maxright之间的字符镜像的位置就会改变,会不会出现更新后镜像位置的数组长度不是最优的

    第一条,右边数组修改,并不影响左边,因为从左边遍历过来,每一个位置已经通过中心法找到了最长的位置,所以不管怎么修改其他的数组,如果已经遍历过,肯定是最长的,最优解

    第二条,还是上面的解释,因为每个遍历都是最优解,并且记录了遍历过的所有字符串最右边界,所以maxright是最右边的位置,不存在center左边的回文字符串长度超过maxright

    第三条,做了好几个符合条件的字符串,发现边界都被处理了,并不会出现矛盾的情况,也就是按照这个逻辑是可以覆盖所有情况,并且是正确的

     比如这个字符串,如果当前center是x,maxright是e,就是第二行浅灰色的表示,如果当前走到了i位置,也就是标红的w,那么可以以判断i的最右边界超过了第二行的标记,更新后是第一行的标记,center到i的位置,maxright在原来的基础上增加了1,那么原来的i的下一个字符e(棕色),原来针对x的对应是最左边的e,长度是4,现在更新后变成了w左边的e,长度是0,这样发现确实是0,是正确的,为什么呢,因为在求x的时候到了e的位置就结束了,也就是e的右边与x对称轴e的左边并不一样,所以也不可能是4.

    另外我也试着不增加分隔符重写这个编码,立马发现了分隔符的作用,统一计算,减少条件判断,分隔符就是为了把所有的回文字符串都改成奇数的,这样就可以确定center和maxright,如果没有分隔符,那么每一个子回文字符串都要额外判断偶数的情况,太复杂。

  • 相关阅读:
    REBOOT Reload 可安装在优盘的 Windows 和 DOS 启动盘
    给博客添加框架固定式背景音乐
    假期太长,很无聊
    CISCO NAT 配置
    网络地址转换全面通
    [转]drupal用程序创建node的方法
    drupal文件上传表单的例子
    drupal6重建menu遇到的问题
    运行效率与开发效率的随想
    joomla jSeblod的研究
  • 原文地址:https://www.cnblogs.com/studywithallofyou/p/12009079.html
Copyright © 2020-2023  润新知