首页 > 其他分享 >PAM学习笔记

PAM学习笔记

时间:2023-07-10 18:47:15浏览次数:57  
标签:int texttt len 学习 笔记 fail diff PAM 回文

PAM

回文树(又称回文自动机 \(\texttt{PAM}\)),是一种可以高效解决大部分回文串问题的算法,在大部分情况下可以替代马拉车(当然模板题好像都替代不了),是一种不错的算法。

结构

trie

类似于其他的自动机(\(\texttt {AC}\) 自动机、\(\texttt{SAM}\)),\(\texttt{PAM}\) 也有一颗 \(\texttt{trie}\) 树以及相应的 \(\texttt{fail}\) 指针。但是不太相同的是,在 \(\texttt{PAM}\) 中,有两棵树(520 这天学这个正适合)。

555

为什么呢?因为回文串分为奇回文串和偶回文串,奇回文串当然可以直接插入,但是偶回文串就出现了问题。我们当然可以用类似马拉车的方法,添加不存在的字符来使它变成奇回文串,但是这样太麻烦了。于是我们可以建立两棵树,一颗奇树,一颗偶树,奇树的儿子都代表奇回文串,偶树的儿子都代表偶回文串。

下图是字符串为 \(\texttt{abaaabacca}\) 的回文树。

手绘不行请见谅

在这个字符串中,出现了以下这些回文串:

\[\texttt{a},\texttt{b},\texttt{c},\texttt{aba},\texttt{aaa},\texttt{cc},\texttt{acca},\texttt{baaab},\texttt{abaaaba} \]

我们可以注意到,在这个 trie 树上,一条转移边代表的是在字符串两边加上同一个字符,这也符合回文串的定义。

fail 指针

在 PAM 中,也拥有 fail 指针,且意义与 AC 自动机的相同,定义 \(fail[x]\) 为 \(x\) 代表的回文子串的最长回文后缀。

在 trie 树上,我们可以画出所有的 fail 指针。

红边为 fail(别吐槽画工了)

我们可以注意到,\(0\) 号点的 \(fail\) 指向了 \(-1\) 号点,这是因为奇根不可能失配(单个字符也是回文串)。

构建

在构建时,我们可以发现:如果我们要插入第 \(i\) 个位置的字符,它会和原字符串的末尾字符形成新的回文串。我们设这个最大回文串的长度为 \(len\)。

因为这个串是回文串,因此 \(s[i - len - 1\sim i]\) 为回文串,两边同时扣掉一个字符,我们就可以发现 \(s[i - len \sim i - 1]\) 也为回文串。因此我们只需要找一个最长的后缀回文串,满足 \(s[i - len - 1] = s[i]\),我们就可以把这个字符插入到 \(s[i-len-1]\) 这个位置所代表的点后。

用图片描述就是:

如何找到这个最长的后缀呢?我们就可以利用 fail 指针。因为我们要找的都是后缀回文串,所以我们可以一直跳 fail,直到 \(s[i-len-1]\) 和 \(s[i]\) 相等为止。

而新建的这个点的 fail 指针也就可以指向匹配的点(这里也说不清楚),长度就是它父节点加二。

我们在构建时也可以求出一个十分常用的数组 trans,表示长度小于等于当前点的一半的最长回文后缀。

建树代码:

inline int getfail(int x, int i)
{
    while(i - len[x] < 1 || s[i - len[x] - 1] != s[i]) x = fail[x];
    return x;
}

inline void build(int n)
{
    len[1] = -1, fail[0] = 1;
    idx = 1;
    for(int i = 1; i <= n; i ++ )
    {
        pos = getfail(cur, i);
        int u = s[i] - 'a';
        if(!tr[pos][u])
        {
            fail[++ idx] = tr[getfail(fail[pos], i)][u];
            tr[pos][u] = idx;
            len[idx] = len[pos] + 2;
            if(len[idx] <= 2) trans[idx] = fail[idx];
            else
            {
                int tmp = trans[cur];
                while(s[i - len[tmp] - 1] != s[i] || ((len[tmp] + 2) << 1) > len[idx])
                    tmp = fail[tmp];
                trans[idx] = tr[tmp][u];
            }
        }
        cur = tr[pos][u];
    }
}

例题

【模板】回文自动机(PAM)

作为模板题,还是比较简单的,统计答案只需要在 fail 指针的基础上加一即可。

for(int i = 1; i <= n; i ++ )
{
    s[i] = (s[i] - 97 + last) % 26 + 97;
    int pos = getfail(cur, i);
    if(!t[pos][s[i] - 'a'])
    {
        fail[++ idx] = t[getfail(fail[pos], i)][s[i] - 'a'];
        t[pos][s[i] - 'a'] = idx;
        len[idx] = len[pos] + 2;
        num[idx] = num[fail[idx]] + 1;
    }
    cur = t[pos][s[i] - 'a'];
    last = num[cur];
    printf("%d ", last);
}

P3649 [APIO2014] 回文串

本题需要我们统计回文串的出现次数,而当一个回文串出现时,它所对应的 fail 指针所对应的回文串也会出现一次,而它 fail 的 fail 也会出现。而我们如果每次都选择跳 fail,就会导致时间复杂度爆炸。

我们可以按照类似于AC自动机加强版的思路,因为我们插入字符串时是按照拓扑序,所以我们可以先在找到的点上打个标记,最后按照拓扑序向上统计答案即可。

inline void topsort()
{
    for(int i = idx; i >= 0; i -- ) cnt[fail[i]] += cnt[i];
}

for(int i = 0; i <= idx; i ++ ) ans = max(ans, (ll)len[i] * cnt[i]);

P4287 [SHOI2011]双倍回文

本题就用到了我们上面提到的 trans 数组。当一个点的 trans 指向的点的长度恰好为当前点长度的一半时,这个点即为一个双重回文串。

P4762 [CERC2014]Virus synthesis

因为进行一次 2 操作一定优于进行一次 1 操作,因此我们要尽可能的进行 2 操作。

我们设 \(f[i]\) 表示形成在 PAM 中编号为 \(i\) 的点所对应的回文串所需的最小操作次数。我们可以有如下转移:

  1. 在 PAM 上,我们设 \(x\) 扩展出 \(i\),则当我们将 \(x\) 的一半加上一个字母,再翻折一下,就可以得到 \(i\) 对应的字符串。因此 \(f[i] = \min (f[x] + 1, f[i])\)。

  2. 设 \(trans_i = p\),我们就可以得到下面的转移方程:\(f[i] = \min(f[i], f[p] + \frac{len_i}{2} - len_p + 1)\)。

最后统计答案,\(ans = \min \{ f[i] + n - len_i \}\)。

我们可以在 bfs 过程中进行状态的转移。

queue<int> q;
inline void bfs()
{
    while(q.size()) q.pop();
    q.push(0);
    while(q.size())
    {
        int u = q.front();
        q.pop();
        ans = min(ans, f[u] + n - len[u]);
        for(int i = 0; i < 4; i ++ )
        {
            int v = tr[u][i];
            if(!v) continue;
            f[v] = min(f[u] + 1, f[trans[v]] + len[v] / 2 - len[trans[v]] + 1);
            q.push(v);
        }
    }
}

Palindrome Partition

本题将字符串转换一下即可变为一个回文划分问题。

回文划分问题:将字符串 \(s\) 分成 \(k\) 段,使得 \(s_1, s_2 \cdots s_k\) 均为回文串。

在本题中,设 \(f_i\) 表示长度为 \(i\) 的前缀的划分方案。于是我们不难得出下面的转移方程:

设谓词 \(\mathrm{G(x)}\) 为 \(x\) 满足回文性质,则

\[f_i=\sum \limits _{\mathrm{G} (s[j+1\sim i])}f_j \]

这个转移的复杂度是 \(O(n^2)\) 的,我们考虑优化。

这里我们再引入两个信息:\(diff[u]\) 和 \(slink[u]\)。\(diff[u]\) 表示该节点和它的 \(fail\) 指针节点的长度差,即为 \(len[u] - len[fail[u]]\)。\(slink[u]\) 表示 \(u\) 节点一直沿着它的 \(fail\) 指针向上跳,一直跳到第一个满足 \(diff[x] \not\ne diff[u]\) 的 \(x\) 号点。可以证明,一个节点从 \(slink\) 开始跳,跳到根节点的次数不会超过 \(\log |s|\) 次。

我们就可以将每一段等差数列中的 \(f\) 值之和存到该等差数列的末端。记为 \(g\)。为了求出这个 \(g\),我们可以看下图:

来源:OIwiki

当前跳到了 \(x\) 号节点,我们将要求当前节点的 \(g[x]\),我们可以看到,当前节点的 \(g[x]\) 只比 \(g[fail[x]]\) 多了一个值(蓝色为 \(g[x]\),橙色为 \(g[fail[x]]\)),多出的位置为 \(i-len[slink[x]]-diff[x]\)(我也不会),我们就可以在跳 \(slink\) 的过程中不断更新 \(f\)。

代码:

void insert(int i)
{
    pos = getfail(cur, i);
    int u = s[i] - 'a';
    if(!tr[pos][u])
    {
        int x = New(len[pos] + 2);
        fail[x] = tr[getfail(fail[pos], i)][u];
        tr[pos][u] = x;
        diff[x] = len[x] - len[fail[x]];
        if(diff[x] == diff[fail[x]])
            slink[x] = slink[fail[x]];
        else slink[x] = fail[x];
    }
    cur = tr[pos][u];
}

void build(int n)
{
    init();
    f[0] = 1;
    for(int i = 1; i <= n; i ++ )
    {
        insert(i);
        for(int x = cur; x > 1; x = slink[x])
        {
            g[x] = f[i - len[slink[x]] - diff[x]];
            if(diff[x] == diff[fail[x]]) g[x] = add(g[x], g[fail[x]]);
            if((i & 1) == 0) f[i] = add(f[i], g[x]);
        }
    }
}

标签:int,texttt,len,学习,笔记,fail,diff,PAM,回文
From: https://www.cnblogs.com/crimsonawa/p/17541997.html

相关文章

  • ChatGPT 英语学习 prompt
    YouareanativeEnglishspeaker,andyouareteachingahighschoolstudentaboutEnglish.YouwillbegivenEnglishwordorphrases,pleaseexplaintheirmeaninginaeasy-to-understandway,including:thecharacteristicorpropertyofacertainword,......
  • 斜率优化dp学习笔记
    前置知识单调队列优化dp,计算几何基础知识,小学数学。斜率优化在dp中,我们常常能遇到一类转移方程,其中含有\(i\)相关的项与\(j\)相关的项的乘积,形如这种形式:\[f_i=\max_{j<i}\{f_j+a_ib_j\}\]我们常见的单调队列优化只能优化只含\(j\)项的方程,对于这种方程无能为力。......
  • cdq分治学习笔记
    做着做着cdq分治的题发现自己太菜了写不出来题XD,所以来写写学习笔记。CDQ分治CDQ分治一般有两个用途:解决偏序问题、将动态问题转化为静态问题。偏序问题我们先来介绍第一个问题:偏序问题。普通的二维偏序就类似于逆序对,每个元素有两个属性\(a_i,b_i\),我们需要统计\(a_......
  • java类和对象学习总结
    当一个引用赋值为null的时候,就代表这个引用不指向任何的对象引用不能指向引用,只能说引用指向了另一个引用的对象.一个引用不能指向多个对象this引用的学习:代表的是当前对象的引用,每一个成员方法的第一个参数默认是thisthis.year this.month   加上this代表给当前的对象......
  • 3学习记录
    pycharm1.”enter+shift“----一键格式化代码2.”#“”ctrl+/“----单行注释3.“Ctrl+d”------直接复制一行代码4.“Ctrl+y”-----删除一行代码5.tab----代码向后缩进6.Shift+Tab----代码向前取消缩进7.CTRL+SHIFT+/----块注释......
  • web安全学习日志---xss漏洞(跨站脚本攻击)
    1.反射性xss(reflacted) 仅执行一次,非持久型。主要存在于攻击者将恶意脚本附加到url的参数中,发送给受害者,服务端未经严格过滤处理而输出在用户浏览器中,导致浏览器执行代码数据。利用场景:直接插入JS代码,修改url参数  攻<script>alert('hack')</script>防$name=str_replac......
  • 整体二分学习笔记
    整体二分引入对于一堆询问,如果每个单独的询问都可以二分解决的话,时间复杂度为\(O(NM\logN)\),但实际上每次二分都会有一些残留信息被我们扔掉,如果我们将所有询问一起二分,就可以最大时间的减小复杂度。讲解经典例题:区间第k大给定一个序列a和一个整数S,有2种操作:1.将a......
  • 《ReAct: SYNERGIZING REASONING AND ACTING IN LANGUAGE MODELS》论文学习
    一、论文主要思想本文首先认为,到目前为止,LLM在语言理解方面令人印象深刻,它们已被用来生成CoT(思想链)来解决一些问题,它们也被用于执行和计划生成。尽管这两者是分开研究的,但本文旨在以交错的方式将推理和行动结合起来,以提高LLM的表现。这个想法背后的原因是,如果你考虑一下作为......
  • redis学习十七:redis事务
    概念:可以一次执行多个命令,本质是一组命令的集合。一个事务中的所有命令都会序列化,按顺序地串行化执行而不会被其他命令插入,不许加塞。1.单独的隔离操作redis的事务仅仅是保证事务里的操作会被连续独占的执行,redis命令执行是单线程架构,在执行完事务内所有指令前是不可能再去......
  • 【项目实战】十分钟学习完 Spring Boot 拦截器
    ......