首页 > 其他分享 >「笔记」回文自动机

「笔记」回文自动机

时间:2023-11-20 11:33:21浏览次数:39  
标签:子串 状态 笔记 operatorname fail 自动机 PAM 回文

目录

写在前面

其实这东西学名叫 EER Tree,Palindromic Tree,直译是回文树,但本质上是一类有限状态自动机所以也可以叫 Palindromic Automaton,因为我很喜欢自动机所以以下都叫它回文自动机。

结构

类似后缀自动机的,回文自动机(以下简称 PAM)也是一类确定有限状态自动机。对于字符串 \(S\),它的回文自动机是由以下五部分构成的五元组:

  • 状态集合 \(Q\):每个状态对应 \(S\) 中的本质不同的回文子串。
  • 字符集 \(\Sigma\)。
  • 转移函数\(\delta\):有:\(Q\times \Sigma \rightarrow Q\),转移函数 \(\delta(u, c)\) 存在当且仅当在 \(u\) 对应的回文子串两端添加字符 \(c\) 得到的字符串也是 \(S\) 的一个子串。
  • 起始状态 \(start\),代表空串。
  • 接受状态集合 \(F\)。

特别地,对于每个状态定义 \(\operatorname{len}\) 表示该状态对应的回文串的长度,定义 \(\operatorname{fail}\) 指针指向该状态对应的回文串的最长的回文后缀。显然 \(\operatorname{fail}\) 指针只会连向长度严格小于当前状态的回文串对应的状态,则由各状态和 \(\operatorname{fail}\) 指针构成了一棵树,称为回文树。

看起来和 SAM 非常像,但需要注意的是回文串存在奇数和偶数长度的,按照上述定义的话转移和 \(\operatorname{fail}\) 均只能转移到与当前状态对应字符串长度奇偶性相同的状态,于是钦定了 PAM 中存在两个代表空串的初始状态,分别代表长度为 -1 和 0 的回文串,可以称它们为奇根,偶根,并且钦定偶根的 \(\operatorname{fail}\) 指针指向奇根,而我们并不关心奇根的 \(\operatorname{fail}\) 指针,因为奇根不可能失配(奇根转移出的下一个状态长度为 1,即单个字符。一定是回文子串)。

另外,PAM 比 SAM 更直观的一点是每个状态仅代表唯一的本质不同的回文子串。

构造

与 SAM 类似地,考虑使用增量法构建 PAM,即在 \(s[1:i-1]\) 的 PAM 基础上构造 \(s[1:i]\) 的 PAM。考虑维护一个 \(\operatorname{last}\) 指针指向 \(s[1:i-1]\) 的最长回文后缀,初始时 \(\operatorname{last}\) 指向偶根。然后对 \(\operatorname{last}\) 不断地跳 \(\operatorname{fail}\),即按长度递减不断枚举 \(s[1:i-1]\) 的所有回文后缀,直到满足 \(\operatorname{last}\) 对应的 \(s[1:i-1]\) 的回文后缀的前一个字符为 \(s_i\),则转移 \(\delta(\operatorname{last}, s_i)\) 转移到的状态即为 \(s[1:i]\) 的最长回文后缀,即 \(s[1:i]\) 的最长回文后缀。

若该转移存在则直接转移即可,否则考虑新建状态 \(x\),其长度为 \(\operatorname{len}(\operatorname{last}) + 2\),然后再对 \(\operatorname{last}\) 跳 \(\operatorname{fail}\) 直至找到满足上述条件的另一个回文后缀 \(\operatorname{last}'\),则 \(\operatorname{fail}(x) = \delta(last', s_i)\),然后再进行转移。

复杂度证明

详见 OI-wiki。

字符串 \(s\) 的本质不同回文子串的数量至多只有 \(|s|\) 个,则 PAM 的状态数个数是 \(O(n)\) 级别的。证明考虑数学归纳,可证明每增加一个字符本质不同的回文子串数至多增加一个。

在 PAM 中对于某个状态,通过转移可以使状态对应的回文子串的长度 \(+2\),通过跳 \(\operatorname{fail}\) 可以使状态对应的回文子串的长度至少 \(-1\),构造 PAM 过程中状态对应的子串长度只会增加 \(n\) 次,则跳 \(\operatorname{fail}\) 的过程至多只会进行 \(2\times n\) 次,则构建 PAM 的时间复杂度也是 \(O(n)\) 级别。

模板题

这题会比 P5496 【模板】回文自动机(PAM) 更好写一点所以把这题放这里了。

P3649 [APIO2014] 回文串
给定一仅由小写字母组成的字符串 \(s\),定义 \(s\) 的一个子串的存在值为这个子串在 \(s\) 中出现的次数乘以这个子串的长度,求 \(s\) 的所有回文子串的存在值的最大值。
\(1\le |s|\le 3\times 10^5\)。
1S,128MB。

考虑在构建 PAM 过程中对每个状态额外维护 \(\operatorname{cnt}\) 表示该状态对应的回文子串作为 \(s\) 的某前缀 \(s[1:i]\) 的最长回文后缀时的出现次数,构建完 PAM 后考虑对回文树上所有状态求子树 \(\operatorname{cnt}\) 之和即为该回文子串的实际出现次数。

上述过程没有必要通过 dfs 进行,直接倒序枚举所有状态 \(i\),不断地将 \(\operatorname{cnt}_i\) 累计到 \(\operatorname{cnt}_{\operatorname{fail}_i}\) 中即可。

答案即 \(\max_i (\operatorname{\operatorname{cnt}_i\times \operatorname{len}_i})\)。

代码

//P3649 [APIO2014] 回文串
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 3e5 + 10;
char s[kN];
int n;
//=============================================================
//=============================================================
inline int read() {
  int f = 1, w = 0; char ch = getchar();
  for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0'); 
  return f * w;
}
namespace PAM {
  const int kNode = kN << 1;
  int nown, nodenum, last, tr[kNode][26], len[kNode], fail[kNode];
  int cnt[kNode];
  char t[kN];
  int Newnode(int len_) {
    ++ nodenum;
    memset(tr[nodenum], 0, sizeof (tr[nodenum]));
    len[nodenum] = len_;
    fail[nodenum] = cnt[nodenum] = 0;
    return nodenum;
  }
  void Init() {
    nodenum = -1;
    last = 0;
    t[nown = 0] = '$';
    Newnode(0), Newnode(-1);
    fail[0] = 1;
  }
  int getfail(int x_) {
    while (t[nown - len[x_] - 1] != t[nown]) x_ = fail[x_];
    return x_;
  }
  void Insert(char ch_) {
    t[++ nown] = ch_;
    int now = getfail(last);
    if (!tr[now][ch_ - 'a']) {
      int x = Newnode(len[now] + 2);
      fail[x] = tr[getfail(fail[now])][ch_ - 'a'];
      tr[now][ch_ - 'a'] = x;
    }
    last = tr[now][ch_ - 'a'];
    ++ cnt[last];
  }
  LL Solve() {
    LL ans = 0;
    for (int i = nodenum; i >= 0; -- i) {
      cnt[fail[i]] += cnt[i];
    }
    for (int i = 1; i <= nodenum; ++ i) {
      ans = std::max(ans, 1ll * cnt[i] * len[i]);
    }
    return ans;
  }
}
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  scanf("%s", s + 1); n = strlen(s + 1);
  PAM::Init();
  for (int i = 1; i <= n; ++ i) PAM::Insert(s[i]);
  printf("%lld\n", PAM::Solve());
  return 0;
}

写在最后

参考:

学习耗时:3 分钟。

OI-wiki 上直接就“类似后缀自动机”给我笑烂了,幸亏狠狠地学习过 SAM 让发现 PAM 好傻逼。

话说 PAM 居然是 15 年才被发明的,居然有幸学到了本世纪的新知识,令人感慨。

另外本文居然是本博客的第 400 篇可见随笔,四年两个月前为了存点代码的建的小破站到现在居然有 82k 的阅读量了,令人感慨。

标签:子串,状态,笔记,operatorname,fail,自动机,PAM,回文
From: https://www.cnblogs.com/luckyblock/p/17843578.html

相关文章

  • 笔记
    PL/SQL导入sql文件:1.点击新建2.命令窗口3.@+回车4.选择导入的sql文件Createtabletest_tableasselect*fromdev_table----复制一个临时表(并且复制表里的数据)Createtabletest_tableasselect*fromdev_tablewhere1=2----复制一个临时表(不复制表里的数据)insertinto......
  • sharding分表应用笔记(三)——多数据源路由
    sharding分表应用笔记(三)——多数据源路由目录sharding分表应用笔记(三)——多数据源路由1背景2配置2.1命名空间配置2.2spring-jdbc路由配置3指定路由3.1自定义注解3.2功能实现3.3用例1背景应用背景:物理数据源只有一个;对于部分数据量大的表实行按月分表处理,其他的表仍然......
  • win11笔记本换内存后,报错,及解决:0x00007FF8011F6693指令引用了0x0000000000000000内存
    笔记本原装内存为一对镁光8GDDR54800MHz换单条镁光32GDDR55600MHz内存后,重启电脑出现如下报错:0x00007FF8011F6693指令引用了0x0000000000000000内存。该内存不能为read。要终止程序,请单击”确定” 联系内存的卖家客服提供的解决步骤虽然我没看到滚屏,但是重启后问题一样......
  • FPGA入门笔记003——计数器IP核调用与验证
    FPGA设计方式主要有三种:1、原理图(不推荐);2、VerilogHDL设计方式;3、IP核输入方式计数器IP核调用与验证步骤如下:1、添加IP核文件打开QuartusII,新建一个项目,名称为counter_ip。选择Tools->MegaWizardPlug-InManager。选择第一个选项。在搜索栏中输入COUNTER,单击LPM_COU......
  • openGauss学习笔记-127 openGauss 数据库管理-设置账本数据库-修复账本数据库
    openGauss学习笔记-127openGauss数据库管理-设置账本数据库-修复账本数据库127.1前提条件系统中需要有审计管理员或者具有审计管理员权限的角色。数据库正常运行,并且对防篡改数据库执行了一系列增、删、改等操作,保证在查询时段内有账本操作记录结果产生。127.2背景信息......
  • java反序列化----CC7利用链学习笔记(Hashtable)
    目录java反序列化----CC7利用链学习笔记(Hashtable)环境搭建利用链java反序列化----CC7利用链学习笔记(Hashtable)环境搭建jdk8u71<dependency><groupId>commons-collections</groupId><artifactId>commons-collections</artifactId>......
  • 学习笔记十
    块设备I/O和缓冲区管理块设备I/O缓冲区由于与内存访问相比,磁盘I/O速度较慢,所以不希望在每次执行读写文件操作时都执行磁盘I/O。因此,大多数文件系统使用I/O缓冲来减少进出存储设备的物理I/O数量。合理设计的I/O缓冲方案可显著提高文件I/O效率并增加系统吞吐量。I/O缓冲区原理:文......
  • 11月份读书笔记--《ERP原理与应用教程》
    起因是因为要进行ERP系统的制作,但是缺少一些知识,于是在网上进行查找资料,毕竟先知道怎么做比瞎做要强太多了,在网上进行一些资料,实例的查找后,对于一些东西认识模糊不清,不认同,认为是错误的,大概是我自己的原因,太低级了已到达他们认为最基础的东西都不理解,于是花了一下午,大概两个多......
  • 第十周学习笔记
    块设备I/O和缓冲区管理......
  • 学习笔记10
    学习笔记10十二章知识点归纳本章主要讨论了块设备I/O和缓冲区管理,包括块设备I/O的原理、I/O缓冲的优点、Unix的缓冲区管理算法以及信号量设计的新缓冲区管理算法。以下是一些重要的知识点:1.块设备I/O缓冲区基本原理:文件系统使用一系列I/O缓冲区作为块设备的缓存内......