题目描述
2023/4/13 每日一题 难度:困难
将字符串text
分成k
个子字符串 (subtext1, subtext2,…, subtextk),满足:
subtext i
非空- 所有子字符串的连接等于
text
,即subtext1 + subtext2 + ... + subtextk == text - 对于所有i ( 1 <= i <= k ) ,subtexti == subtextk - i + 1 均成立
返回k可能最大值
输入:text
字符串,表示待分割文本
输出:k
整型,表示分割后的子字符串个数
算法
总思想
- 枚举前后缀长度
1 ~ n/2
,一旦找到相同的前后缀就直接将其切割出去,剩下的子字符串作为新的text
送入函数中继续从1的前后缀长度开始切割,直到text
长度为0,返回0;若遍历完1 ~ n/2
仍未找到能分割的点,就将整个text
作为一个子字符串,返回1。 - 核心算法:如何判断两个子字符串是否相等?
- substring()截取子字符串,并使用equals()判断。时间复杂度:O(n^2),n个长度的子串比较n次;空间复杂度:O(n),截取后有拷贝。
- 滚动哈希:计算每个子字符串的hashcode,hashcode相等则字符串相等。时间复杂度:O(n),预处理后每次比较时不需要再遍历子字符串的每一个字符;空间复杂度:O(n),hashcode存储空间
HashCode
|S|:字符串中包含的字符种类数
base:大于等于|S|的整数
例如给定字符串 s = abca, 设base=3,则s可以看作(1231)3,三进制转换为十进制等于
结论:两个字符串相等,当且仅当它们的长度相等且编码值相等。
取模:当字符串很长时,其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};
}
}