问题
给定一个字符串
,求最小的
,使得存在
,满足
均为回文串,且
依次连接后得到的字符串等于
。
暴力做法
考虑动态规划,记
表示
长度为
的前缀的最小划分数,转移只需要枚举以第
个字符结尾的所有回文串:
由于一个字符串最多会有
个回文子串,因此上述算法的时间复杂度为
。
显然不能接受,为了加速转移,首先给出一些引理。
引理与证明
知乎传公式太麻烦了,证明过程留作读者自行思考(误,可以自行翻阅参考资料或前往我的博客阅读 https:// xlor.cn/2019/11/mpf )
定义
记字符串
长度为
的前缀为
,长度为
的后缀为
。
周期:若
,
,就称
是
的周期。
border:若
,
,就称
是
的 border。
周期和 border 的关系
是
的 border,当且仅当
是
的周期。
证明:
若
是
的 border,那么
,因此
,所以
就是
的周期。
若
为
周期,则
,因此
,所以
是
的 border。
引理 1
是回文串
的后缀,
是
的 border 当且仅当
是回文串。
下图中,相同颜色的位置表示字符对应相同。
引理 2
是回文串
的 border (
),
是回文串当且仅当
是回文串。
引理 3
是字符串
的 border,则
是
的周期,
为
的最小周期,当且仅当
是
的最长回文真后缀。
引理 4
是一个回文串,
是
的最长回文真后缀,
是
的最长回文真后缀。令
分别为满足
的字符串,则有下面三条性质
-
;
- 如果
,那么;
- 如果
,那么。
推论
的所有回文后缀按照长度排序后,可以划分成
段等差数列。
做法
有了上述结论后,我们现在可以考虑如何优化 dp 的转移。
回文树上的每个节点
需要多维护两个信息,
和
。
表示节点
和
所代表的回文串的长度差,即
。
表示
一直沿着 fail 向上跳到第一个节点
,使得
,也就是
所在等差数列中长度最小的那个节点。
根据上面证明的结论,如果使用 slink 指针向上跳的话,每向后填加一个字符,只需要向上跳
次。因此,可以考虑将一个等差数列表示的所有回文串的 dp 值之和(在原问题中指
),记录到最长的那一个回文串对应节点上。
表示
所在等差数列的 dp 值之和,且
是这个等差数列中长度最长的节点,则
。
下面我们考虑如何更新 g 数组和 dp 数组。以下图为例,假设当前枚举到第
个字符,回文树上对应节点为
。
为橙色三个位置的 dp 值之和(最短的回文串
算在下一个等差数列中)。
上一次出现位置是
(在
处结束),
包含的
值是蓝色位置。因此,
实际上等于
和多出来一个位置的 dp 值之和,多出来的位置是
。最后再用
去更新
,这部分等差数列的贡献就计算完毕了,不断跳
,重复这个过程即可。具体实现方式可参考例题代码。
最后,上述做法的正确性依赖于:如果
和
属于同一个等差数列,那么
上一次出现位置是
。
Codeforces 932G Palindrome Partition
构造字符串
,问题等价于求
的偶回文划分方案数,把上面的转移方程改成求和形式并且只在偶数位置更新 dp 数组即可。
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod = 1e9 + 7;
const int maxn = 1000000 + 5;
inline int add(int x, int y) {
x += y;
return x >= mod ? x -= mod : x;
}
namespace pam {
int sz, tot, last;
int ch[maxn][26], len[maxn], fail[maxn];
int cnt[maxn], dep[maxn], dif[maxn], slink[maxn];
char s[maxn];
int node(int l) {
sz++;
memset(ch[sz], 0, sizeof(ch[sz]));
len[sz] = l;
fail[sz] = 0;
cnt[sz] = 0;
dep[sz] = 0;
return sz;
}
void clear() {
sz = -1; last = 0;
s[tot = 0] = '$';
node(0); node(-1);
fail[0] = 1;
}
int getfail(int x) {
while (s[tot - len[x] - 1] != s[tot]) x = fail[x];
return x;
}
void insert(char c) {
s[++tot] = c;
int now = getfail(last);
if (!ch[now][c - 'a']) {
int x = node(len[now] + 2);
fail[x] = ch[getfail(fail[now])][c - 'a'];
dep[x] = dep[fail[x]] + 1;
ch[now][c - 'a'] = x;
dif[x] = len[x] - len[fail[x]];
if (dif[x] == dif[fail[x]]) slink[x] = slink[fail[x]];
else slink[x] = fail[x];
}
last = ch[now][c - 'a'];
cnt[last]++;
}
}
using pam::len;
using pam::fail;
using pam::slink;
using pam::dif;
int n, dp[maxn], g[maxn]; char s[maxn], t[maxn];
int main() {
pam::clear();
scanf("%s", s + 1);
n = strlen(s + 1);
for (int i = 1, j = 0; i <= n; i++) t[++j] = s[i], t[++j] = s[n - i + 1];
dp[0] = 1;
for (int i = 1; i <= n; i++) {
pam::insert(t[i]);
for (int x = pam::last; x > 1; x = slink[x]) {
g[x] = dp[i - len[slink[x]] - dif[x]];
if (dif[x] == dif[fail[x]]) g[x] = add(g[x], g[fail[x]]);
if (i % 2 == 0) dp[i] = add(dp[i], g[x]);
}
}
printf("%d", dp[n]);
return 0;
}
参考资料
- EERTREE: An Efficient Data Structure for Processing Palindromes in Strings
- Palindromic tree
- 2017 年 IOI 国家候选队论文集 回文树及其应用 翁文涛
- 2019 年 IOI 国家候选队论文集 子串周期查询问题的相关算法及其应用 陈孙立
- 字符串算法选讲 金策
- A bit more about palindromes
- A Subquadratic Algorithm for Minimum Palindromic Factorization