每日算法总结——Manacher算法详解、LeetCode459.重复的子字符串

Manacher算法是一种用于查找字符串中最长回文子串的线性时间复杂度算法。它通过插入辅助符号处理奇偶长度的回文串,利用回文半径、最右边界和中心点等概念进行优化。在Java实现中,通过回文半径数组和动态更新中心点与最右边界,避免了不必要的回文区域计算。此外,文章还提到了LeetCode459题的解法,包括错位匹配和KMP算法的应用。
摘要由CSDN通过智能技术生成

Manacher算法详解

Manacher算法解决的问题:字符串str中,最长回文子串的长度如何求解?如何做到时间复杂度 O ( N ) O(N) O(N)完成

经典解法:遍历str中的字符,以str[i]为中心,向左右两边扩,求出最长回文子串。

  • 但是这种方法无法找到长度为偶数的回文子串
  • 改进
    • 每个字符间隙都插入一个辅助符号。比如123232 → \to #1#2#3#2#3#2#
    • 修改后的字符向左右两边扩的时候,就可以找出长度为偶数的回文子串,复杂度为 O ( N 2 ) O(N^2) O(N2)
    • 辅助符号是否必须是原字符串中未曾出现的?
      • 不是,向左右两边扩的时候,辅助符号只会和辅助符号比较、而原字符也只会和原字符比较,所以不会有影响。
学习Manacher之前,先来了解几个概念:
  • 回文半径:对于一个回文字符串12321,其回文直径就是整个回文字符串的长度5,回文半径就是整个回文字符串一半的长度3.
  • 回文半径数组:在从左往右遍历数组的过程中,把以每个字符为中心的回文子串的回文半径记录下来。
  • 之前所扩的所有位置中所到达的最右回文右边界R
    • #1#2#3#2#3#2#为例,不论以哪个符号为中心向左右两边扩,只要扩出来的右边界大于R,则更新R
    • 初始时,R=-1
    • 1时,扩出来的右边界为2,R更新为2。
  • 最右边界中心点C
    • C记录了扩到当前最右边界的中心点,也就是说,如果R更新了,则C就更新为当前的中心符号下标。
Manacher详解:

Manacher整个算法会用到上面所说的四个概念。

指针i在从左往右遍历数组的过程中,有以下几种情况:

  1. i>R,此时无法优化,只能从i向左右两边扩,同时更新RC
    在这里插入图片描述

  2. C<i<R,以C为中心点的最大回文区域[L,R],所以i会有一个对称点i',又会分两种情况

    • 如果以i'为中心的最大回文区域也在[L,R],则i的最大回文区域和i'的最大回文区域对称

    在这里插入图片描述

    • 为什么i最大回文区域不可能比[z, p]更大呢?

      C的最大回文区域中,一定有x = p, y = z(因为是回文嘛),而前面已经说了[x, y]i'的最大回文区域,说明x!=y,所以p!=zi的最大回文区域不可能超过[z, p]

    • 这样,我们就不需要再去计算i的最大回文区域的半径了,直接取i'的即可。

    • 如果以i'为中心的最大回文区域越过了[L,R]的范围,则i的最大回文区域就是下图的[R', R]

      在这里插入图片描述

      为什么i的最大回文区域不能更大呢?

      • 首先在回文区域[L, R]中,L'R'是对称的,所以有L' = R'
      • 而在i'的最大回文区域[x, y]中,一定有L = L'
      • C为中心的最大回文区域为[L, R],所以L != R
      • 综上,R' != R,即i的最大回文区域就是[R', R]
    • 如果以i'为中心的最大回文区域恰好没超过[L, R](压线,即x = L),则i的最大回文区域的右边界至少为R,R往后的部分仍需判断。

JavaCode:

public class Manacher {
    /**
     * 求s的最大回文子串的半径长度
     */
    public static int maxLcpLength(String s) {
        if (s == null || s.length() == 0) {
            return 0;
        }
        char[] str = manacherString(s);
        // pArr: 回文半径数组
        int[] pArr = new int[str.length];
        // c: 中心, r: 回文右边界的再往右一个位置,最右的有效区是R-1位置
        int c = -1, r = -1;
        // max: 扩出来的最大值
        int max = Integer.MIN_VALUE;
        // 每一个位置都求回文半径
        for (int i = 0; i < str.length; i++) {
            /*
               i至少的回文半径,先赋给pArr[i]
               情况一:i在r外,此时str[i]本身就构成一个回文串,所以至少为1
               情况二:i在r内,i的对称点为i'
                    1. i'的最大回文区域在[l, r]内,i回文半径就是i'的回文半径
                        · i' = 2 * c - i
                        · 此时pArr[2 * c - i] > r - i
                    2. i'的最大回文区域超过了[l, r],此时r - i就是i的回文半径
                        · 此时pArr[2 * c - i] < r - i
                    3. i'的最大回文区域压线,此时pArr[2 * c - i] == r - i是i的回文半径
             */
            pArr[i] = r > i ? Math.min(pArr[2 * c - i], r - i) : 1;
            // 半径扩充操作,但实际上只有情况一、和情况二.3需要扩充操作,另外两个不会再扩了
            while (i + pArr[i] < str.length && i - pArr[i] > -1) {
                if (str[i + pArr[i]] == str[i - pArr[i]]) {
                    pArr[i]++;
                } else {
                    break;
                }
            }
            // 更新r, c
            if (i + pArr[i] > r) {
                r = i + pArr[i];
                c = i;
            }
            max = Math.max(max, pArr[i]);
        }
        return max - 1;
    }

    /**
     * 字符串填充: 1221 -> #1#2#2#1#
     */
    public static char[] manacherString(String str) {
        char[] charArr = str.toCharArray();
        char[] res = new char[str.length() * 2 + 1];
        int index = 0;
        for (int i = 0; i < res.length; i++) {
            res[i] = (i & 1) == 0 ? '#':charArr[index++];
        }
        return res;
    }
}

LeetCode459.重复的子字符串

原题:459. 重复的子字符串 - 力扣(LeetCode)

难度:Esay

解法一:错位匹配

  • 由重复子串构成的字符串一定可以实现如下的匹配:
    在这里插入图片描述

    错位移动的距离就是整个重复子串的长度,移动之后,通过判断重叠部分是否相同来判断是否是满足题意的字符串

  • 如何判断重复子串,即错位移动的距离?

    • 首先可以肯定,重复子串的首字符一定是整个字符串的首字符str[0],我们就可以根据str[0]来找到下一个与它相等的字符,这个字符就作为循环结束的标志,当然也不一定,比如abacabac,循环子串abac中出现了两次a,这时候就需要对做好判断
  • 复杂度为 O ( N 2 ) O(N^2) O(N2),边界条件比较麻烦。

class Solution {
    public boolean repeatedSubstringPattern(String s) {
        if (s.length() == 1) {
            return false;
        }
        char[] str = s.toCharArray();
        int i = 1, j;	// i: 用于找寻循环前缀
        while (i < str.length) {
            while (i < str.length && str[i] != str[0]) {
                i++;// 找到下一个与首字符相等的字符
            }
            int temp = i;// 记录当前的位置,此时str[i]=str[0]
            if (str.length - i < i) {
                return false;// 如果后面字符的长度小于循环前缀的长度,说明不可能有一个完整的循环前缀,直接返回false
            }
            // 开始错位匹配
            j = 0;
            int prex = i;
            while (i < str.length && str[i] == str[j]) {
                i++;
                j++;
            }
            // 匹配停止,如果成功遍历到字符串的末尾,并且剩余的长度是循环前缀的倍数,则表示是满足题意的字符串。
            if (i == str.length && str.length % prex == 0) {
                return true;
            }
            // 否则,继续寻找下一个等于str[0]的字符
            i = temp + 1;
        }
        return false;
    }
}

解法二、移动匹配

  • 对于一个字符串s,如果s是由循环子串构成的,则s+s的中部一定会包含一个s,如下图:在这里插入图片描述

  • 所以我们可以先破坏,前后两个S的完整性,即去掉s+s首尾两个字符,然后应用字符串匹配算法,判断剩余字符串中是否有s

class Solution {
   public boolean repeatedSubstringPattern(String s) {
        String str = s + s;
        return str.substring(1, str.length() - 1).contains(s);
    }
}

解法三、KMP

  • 实际上是对上述解法的优化,因为各类语言库函数中的字符串查找方法(java中是contains())一般都是复杂度较高的方法(暴力解法?),应用KMP能很好的降低复杂度
class Solution {
   public boolean repeatedSubstringPattern(String s) {
        String str = s + s;
        return getIndexOf(str.substring(1, str.length() - 1), s) != -1;
    }

    public static int getIndexOf(String s, String m) {
        if (s == null || m == null || m.length() < 1 || s.length() < m.length()) {
            return -1;
        }
        char[] str1 = s.toCharArray();
        char[] str2 = m.toCharArray();
        int i1 = 0, i2 = 0;
        // O(M)
        int[] next = getNext(str2);
        // O(N)
        while (i1 < str1.length && i2 < str2.length) {
            if (str1[i1] == str2[i2]) {
                i1++;
                i2++;
            } else if (i2 > 0) {
                i2 = next[i2];
            } else {
                i1++;
            }
        }
        return i2 == str2.length ? i1 - i2 : -1;
    }

    public static int[] getNext(char[] ms) {
        if (ms.length == 1) {
            return new int[]{-1};
        }
        int[] next = new int[ms.length];
        next[0] = -1;
        next[1] = 0;
        int i = 2;
        int cn = 0;
        while (i < next.length) {
            if (ms[i - 1] == ms[cn]) {
                next[i++] = ++cn;
            } else if (cn > 0) {
                cn = next[cn];
            } else {
                next[i++] = 0;
            }
        }
        return next;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值