最长回文串_最小回文划分

问题

给定一个字符串

,求最小的
,使得存在
,满足
均为回文串,且
依次连接后得到的字符串等于

暴力做法

考虑动态规划,记

表示
长度为
的前缀的最小划分数,转移只需要枚举以第
个字符结尾的所有回文串:

由于一个字符串最多会有

个回文子串,因此上述算法的时间复杂度为

显然不能接受,为了加速转移,首先给出一些引理。

引理与证明

知乎传公式太麻烦了,证明过程留作读者自行思考(误,可以自行翻阅参考资料或前往我的博客阅读 https:// xlor.cn/2019/11/mpf )

定义

记字符串

长度为
的前缀为
,长度为
的后缀为

周期:若

,就称
的周期。

border:若

,就称
的 border。

周期和 border 的关系

的 border,当且仅当
的周期。

证明:

的 border,那么
,因此
,所以
就是
的周期。

周期,则
,因此
,所以
的 border。

引理 1

是回文串
的后缀,
的 border 当且仅当
是回文串。

下图中,相同颜色的位置表示字符对应相同。

e0cae78b40250ea97a6f07820bfa2620.png

引理 2

是回文串
的 border (
),
是回文串当且仅当
是回文串。

引理 3

是字符串
的 border,则
的周期,
的最小周期,当且仅当
的最长回文真后缀。

引理 4

是一个回文串,
的最长回文真后缀,
的最长回文真后缀。令
分别为满足
的字符串,则有下面三条性质
  1. 如果
    ,那么
  2. 如果
    ,那么

推论

的所有回文后缀按照长度排序后,可以划分成
段等差数列。

做法

有了上述结论后,我们现在可以考虑如何优化 dp 的转移。

回文树上的每个节点

需要多维护两个信息,
表示节点
所代表的回文串的长度差,即
表示
一直沿着 fail 向上跳到第一个节点
,使得
,也就是
所在等差数列中长度最小的那个节点。

根据上面证明的结论,如果使用 slink 指针向上跳的话,每向后填加一个字符,只需要向上跳

次。因此,可以考虑将一个等差数列表示的所有回文串的 dp 值之和(在原问题中指
),记录到最长的那一个回文串对应节点上。

表示
所在等差数列的 dp 值之和,且
是这个等差数列中长度最长的节点,则

下面我们考虑如何更新 g 数组和 dp 数组。以下图为例,假设当前枚举到第

个字符,回文树上对应节点为
为橙色三个位置的 dp 值之和(最短的回文串
算在下一个等差数列中)。
上一次出现位置是
(在
处结束),
包含的
值是蓝色位置。因此,
实际上等于
和多出来一个位置的 dp 值之和,多出来的位置是
。最后再用
去更新
,这部分等差数列的贡献就计算完毕了,不断跳
,重复这个过程即可。具体实现方式可参考例题代码。

9955d9c6d1dc6055941d56c66da66879.png

最后,上述做法的正确性依赖于:如果

属于同一个等差数列,那么
上一次出现位置是

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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值