题目链接:段式回文
PS:这道题的数据范围比较小,测试数据也比较弱,暴力枚举都能过。但分析的过程和优化的方法还是值得更深入理解的
题解:贪心 + 多项式哈希
- 贪心方法:在切割前后缀回文段时,优先切割最短的回文段,即对于原字符串,若同时存在AXA和ABYAB的情况(每个字母表示原串的一个子串),优先切割A而不是AB
- 贪心证明:
- 若B串长度大于A串
如上图所示,AB可以分为AXA,而对于AXA,我们可以切割出三组回文段,明显更优 - 若B串长度小于A串,且B > A / 2
如上图所示,A可以分为XYX,A本身可以被分为3组回文段,按我们的贪心策略,A会优先被切割,所以这种情况是不会存在的 - 若B串长度小于A串,且B < A / 2
如上图所示,不看最后的一段B,`A与A面临的情况是相同的,即保持B的长度不变,不断地从*A中分割出一个B,最后必定会得到B > *A / 2,也就是第2种情况。
由情况2可知:*A=XYX,B=YX,所以A划分的结果为:*ABBB...=XYX...XYX,与A同理,A本身可以被分为k组回文段,与贪心策略相悖,这种情况不存在
- 若B串长度大于A串
- 多项式哈希(基本知识见Oi WiKi的链接,讲解得比较详细):
- 按照原题的数据范围,暴力判断两子串是否相等也能过,时间复杂度大概是O(n^2),但如果n的范围扩大到1e5或者1e6,暴力的方法应该就不行了
- 这里我们可以用多项式哈希的方法预处理每位字符的哈希值,然后维护该哈希值的前缀,判断子串是否相等时我们可以通过前缀数组O(1)查询子串的哈希值
PS:前缀子串和后缀子串的哈希值本身相差b^(k),b是选择的随机值,k是两子串头部的距离,b^(k)可以在预处理每位字符的哈希值时记录下来 - 多项式哈希冲突的概率为(l为字符串的长度,M为选取的大质数)。一个大质数容易发生哈希冲突,一般选取两个大质数(冲突概率即为两个冲突概率的乘积)
这里简单估计一下冲突的概率:假设我们选取的大质数均是1e9级别的,字符串长度为1e5级别的,查询次数也为1e5级别的,那么保证不发生冲突的概率为:
代码示例:
class Solution {
public:
using ll = long long;
// 两个大质数减小冲突概率
const ll mod1 = 1e8 + 7, mod2 = 1e9 + 7;
// hash值前缀和
vector<ll> pre1, pre2;
vector<ll> bn1, bn2;
ll b;
string_view str;
int n;
void Init(vector<ll>& pre, vector<ll>& bn, ll mod) {
b = 2 + (rand() % mod);
pre.resize(n + 1, 0);
bn.resize(n + 1);
bn[0] = 1;
for (int i = 1; i <= n; i++) {
int ch = str[i - 1];
(pre[i] = ch * bn[i - 1] + pre[i - 1]) %= mod;
bn[i] = (bn[i - 1] * b) % mod;
}
}
// 判断子串是否相等
inline bool IsSame(int p, int q, int len, vector<ll>& pre, vector<ll>& bn, ll mod) {
++p, ++q;
ll v1 = (pre[p + len - 1] - pre[p - 1] + mod) % mod;
ll v2 = (pre[q] - pre[q - len] + mod) % mod;
return v1 * bn[q - len + 1 - p] % mod == v2;
}
int longestDecomposition(string text) {
srand(time(nullptr));
n = text.size();
str = text;
Init(pre1, bn1, mod1);
Init(pre2, bn2, mod2);
int ans = 0;
int l = 0, r = n - 1;
for (int len = 1; l + len - 1 < r - len + 1; len++) {
if (IsSame(l, r, len, pre1, bn1, mod1) && IsSame(l, r, len, pre2, bn2, mod2)) {
ans += 2;
l = l + len;
r = r - len;
len = 0;
}
}
if (l <= r) ++ans;
return ans;
}
};