最长快乐前缀
题目描述:
给定一个字符串 s,请找出 s 的 最长快乐前缀。
快乐前缀定义:一个既是 s 的 非空前缀 又是 s 的 非空后缀 的子字符串。
如果不存在满足条件的快乐前缀,则返回空字符串 ""。
示例 1:
输入:s = "level"
输出:"l"
解释:s 的所有快乐前缀包括 "l" 和 "level",最长的是 "l"。
示例 2:
输入:s = "ababab"
输出:"abab"
解释:"abab" 是前缀 "ababab" 和后缀 "ababab" 的共同部分。
示例 3:
输入:s = "a"
输出:""
解释:没有既是非空前缀又是非空后缀的子串(因为不能是字符串本身)。
解题思路
题目本质是寻找字符串的 最长相同前后缀(LPS 概念,但不包括整个字符串本身)。
一种高效的解法是使用 滚动哈希(Rabin-Karp 思想),在 O(n) 时间内完成。
滚动哈希核心思想:
将字符串视为一个多进制数,通过哈希值快速比较前缀和后缀是否相等。
- 从左到右计算前缀哈希值(正向哈希)
- 从右到左计算后缀哈希值(反向哈希,但注意方向匹配)
- 当某个长度的前缀哈希值等于相同长度的后缀哈希值时,说明找到了一个快乐前缀
详细步骤
步骤 1:理解“相同前后缀”的含义
对于字符串 s,长度为 n。
设长度 k 的子串 s[0:k] 是前缀,s[n-k:n] 是后缀。
当这两个子串相等时,就是一个长度为 k 的快乐前缀。
我们要找最大的 k(1 ≤ k < n)。
步骤 2:选择哈希函数
为了避免哈希冲突,可以使用双哈希(两个不同模数)。
常用模数:
- MOD1 = 10^9 + 7
- MOD2 = 10^9 + 9
进制基数 base 通常取一个质数,如 31、131 等。
定义:
hash1 = (hash1 * base + char_val) % MOD1
类似计算 hash2。
但这里我们不仅要算前缀哈希,还要快速得到后缀哈希。
技巧:
后缀 s[n-k:n] 的哈希可以通过正向哈希公式从 n-k 开始计算,但更简单的方法是:
在从左到右计算前缀哈希的同时,也计算“反向前缀哈希”(即把字符串反转后计算哈希)。但要注意后缀对应原始字符串的某段,我们可以用另一种方式:
用两个变量分别记录前缀哈希和后缀哈希的累积值,但后缀哈希是从右向左累积的。
具体做法:
- 从左到右遍历每个字符,计算前缀的哈希值(正常顺序)
- 同时,从右到左(从字符串开头开始模拟后缀累积)比较麻烦,我们可以这样等价:
比较 s[0:k] 和 s[n-k:n] 是否相等,相当于比较它们两个子串的哈希值。
我们可以用前缀哈希数组直接得到 s[0:k] 的哈希值,而 s[n-k:n] 的哈希值可以通过后缀哈希公式用前缀哈希的差值得到(即把整个字符串的前缀哈希数组算好,然后任意子串哈希 = (hash[r] - hash[l-1]*base^(r-l+1)) mod MOD)。
步骤 3:算法步骤
- 预处理前缀哈希数组 hash[i] 表示 s[0:i] 的哈希值(i 从 0 到 n)。
- 预处理 pow[i] 表示 base^i mod MOD。
- 对每个长度 len 从 n-1 递减到 1(找最大):
- 前缀子串 s[0:len] 的哈希值 h1 = hash[len]
- 后缀子串 s[n-len:n] 的哈希值 h2 = (hash[n] - hash[n-len] * pow[len]) mod MOD
- 如果 h1 等于 h2,再比较字符串是否真的相等(防止哈希冲突),如果相等则返回 s[0:len]。
- 如果循环结束没找到,返回空字符串。
注意:由于我们要找最长,所以从大到小枚举长度。
举例说明
s = "ababab"
base = 131, MOD = 10^9+7
n=6
前缀哈希(假设用整数表示,这里简化):
- hash[0]=0
- hash[1]='a'
- hash[2]="ab"
- ...
- hash[6]="ababab"
计算长度 len=4:
- 前缀 s[0:4]="abab" 哈希值 h1 = hash[4]
- 后缀 s[2:6]="abab" 索引 n-len=2, 到 6
- 用公式 h2 = (hash[6] - hash[2]*pow[4]) mod MOD
- 如果 h1==h2,再比较字符串,相等,则找到快乐前缀 "abab"。
因为 len=5 时前缀 "ababa" 不等于后缀 "babab",所以最长是 4。
时间复杂度
- 计算前缀哈希 O(n)
- 枚举长度 O(n)(但可以在比较时用 O(1) 时间比较哈希值)
- 总 O(n)
空间复杂度
O(n) 存储哈希数组。
最终实现要点
- 用双模数哈希降低冲突概率
- 从长到短枚举快乐前缀长度
- 验证时先比哈希值,再比字符串(可避免冲突)
这样就找到了最长的既是前缀又是后缀的子串。