LeetCode1147:段式回文

38 阅读2分钟

题目描述

2023/4/13 每日一题 难度:困难

将字符串text分成k个子字符串 (subtext1, subtext2,…, subtextk),满足:

  1. subtext i非空
  2. 所有子字符串的连接等于text,即subtext1 + subtext2 + ... + subtextk == text
  3. 对于所有i ( 1 <= i <= k ) ,subtexti == subtextk - i + 1 均成立

返回k可能最大值

输入:text字符串,表示待分割文本
输出:k整型,表示分割后的子字符串个数

算法

总思想

  • 枚举前后缀长度1 ~ n/2,一旦找到相同的前后缀就直接将其切割出去,剩下的子字符串作为新的text送入函数中继续从1的前后缀长度开始切割,直到text长度为0,返回0;若遍历完1 ~ n/2仍未找到能分割的点,就将整个text作为一个子字符串,返回1。
  • 核心算法:如何判断两个子字符串是否相等?
  1. substring()截取子字符串,并使用equals()判断。时间复杂度:O(n^2),n个长度的子串比较n次;空间复杂度:O(n),截取后有拷贝。
  2. 滚动哈希:计算每个子字符串的hashcode,hashcode相等则字符串相等。时间复杂度:O(n),预处理后每次比较时不需要再遍历子字符串的每一个字符;空间复杂度:O(n),hashcode存储空间

HashCode

image.png

|S|:字符串中包含的字符种类数
base:大于等于|S|的整数
例如给定字符串 s = abca, 设base=3,则s可以看作(1231)3,三进制转换为十进制等于1×33+2×32+3×31+1×30=551 \times {3^{3} } + 2 \times {3^{2} } + 3 \times {3^{1} } + 1 \times {3^{0} } = 55
结论:两个字符串相等,当且仅当它们的长度相等且编码值相等。

取模:当字符串很长时,其hashcode也会很大,无法用long等整型存储编码值了,因此一般将编码值对一个数MOD取模,使其保持在整型范围内。但是又可能会造成取模后的编码值冲突,因此设置两套base和MOD,如果两套编码值相等,就可以判定两个字符串相等。

实现细节

long[] pow:保存base的i次方的值
long[] pre:保存字符串前缀,即包含0 ~ i元素的子字符串的编码值
getHashcode(l, r):计算任意子字符串的hashcode,下标为l,r且包含l,r

代码实现

方法一

public int longestDecomposition(String text) {
    // 分别枚举1 ~ n/2长度的前后缀
    int n = text.length();
    // 必须判断字符串长度为0时返回0.否则会返回1造成结果 + 1
    if(n == 0){
        return 0;
    }
    for(int i = 1; i <= n / 2; i++){
        if(text.substring(0, i).equals(text.substring(n - i, n))){
            return 2 + longestDecomposition(text.substring(i, n - i));
        }
    }
    return 1;
}

方法二

class Solution {
    static long[] pow1;
    static long[] pow2;
    static long[] pre1;
    static long[] pre2;
    static final int MOD1 = 1000000007;
    static final int MOD2 = 1000000009;
    static int base1;
    static int base2;
    static Random random = new Random();

    public static int longestDecomposition(String text) {
        init(text);
        int n = text.length();
        int ans = 0;
        int l = 0, r = n - 1;
        while(l <= r) {
            int len = 1;
            while(l + len - 1 < r - len + 1) {
                if(Arrays.equals(getHashcode(l, l + len - 1), getHashcode(r - len + 1, r))) {
                    ans += 2;
                    break;
                }else {
                    len++;
                }
            }
            // 如果是因为字符串中无法找到相同前后缀而跳出上一个循环,则将整个字符串无法分割,作为一个子字符串,ans + 1
            // l + len > r + len,外层循环也会跳出,程序结束
            // 不能在break前更新l和r,因为若更新后就无法判断循环是因l + len - 1 >= r - len + 1结束还是因为hashcode相等结束
            if(l + len - 1 >= r - len + 1) {
                ans++;
            }
            l += len;
            r -= len;
        }
        return ans;
    }

    public static void init(String text) {
        base1 = 1000000 + random.nextInt(9000000);
        base2 = 1000000 + random.nextInt(9000000);
        while(base2 == base1) {
            base2 = 1000000 + random.nextInt(9000000);
        }

        int n = text.length();
        pow1 = new long[n];
        pow2 = new long[n];
        pre1 = new long[n + 1]; 
        pre2 = new long[n + 1];

        pow1[0] = pow2[0] = 1;
        pre1[1] = pre2[1] = text.charAt(0);
        for(int i = 1; i < n; i++) {
            pow1[i] = (pow1[i - 1] * base1) % MOD1;
            pow2[i] = (pow2[i - 1] * base2) % MOD2;
            // 如果索引与pow一致,则pre的最后一个元素不会被遍历计算
            pre1[i + 1] = (pre1[i] * base1 + text.charAt(i)) % MOD1;
            pre2[i + 1] = (pre2[i] * base2 + text.charAt(i)) % MOD2;
        }
    }

// l - r子字符串的编码值,包括l和r
    public static long[] getHashcode(int l, int r) {
        // 不能使用pre1[l - 1],因为当l == 0时,pre[-1]造成下标越界
        // return new long[] {(pre1[r] - ((pre1[l - 1] * pow1[r - l + 1]) % MOD1) + MOD1), (pre2[r] - ((pre2[l - 1] * pow2[r - l + 1]) % MOD2) + MOD2)};
    // +MOD:两数相减有可能是负数,直接取模结果可能为负,加一个模数后再取模保证结果为正
        return new long[] {(pre1[r + 1] - ((pre1[l] * pow1[r - l + 1]) % MOD1) + MOD1) % MOD1, (pre2[r + 1] - ((pre2[l] * pow2[r - l + 1]) % MOD2) + MOD2) % MOD2};
    }
}