回文自动机,border原理以及可持久化(待更新)

关于一般的回文自动机

回文自动机,由于其fail指针组成一颗失配树,又名回文树。关于这个算法是谁发现的,并不确切,我最早能追溯到是codeforces上一篇文章。不过至少在七年前,回文自动机应该不流行。例如像hdu3948,统计一个字符串中本质不同的回文串个数,这种模板题,相当部分人的题解是后缀数组,或者Manacher加上哈希。后来这种题目在17年某个网络赛上刻意卡掉了哈希算法,基本就只有自动机的做法了。现在而言,回文自动机已经算是OIer/ICPCer们的基本功了。

普通自动机,实现的细节不再叙述,网上资料也满天飞。这里给出一个模板:

#include <cstdio>
#include <cstring>

using namespace std;

namespace PAM {
    const int N = 500005, M = 26;
    int T[N][M], fail[N], len[N], sz, last, cnt[N];
    char s[N];

    void init() {
        len[0] = 0, len[1] = -1;
        sz = 1, last = 0;
        fail[0] = 1;
        memset(T[0], 0, sizeof(T[0]));
        memset(T[1], 0, sizeof(T[1]));
    }

    int get(int x, int pos) {
        while (pos == len[x] || s[pos - len[x] - 1] != s[pos])
            x = fail[x];
        return x;
    }

     void add(int c, int pos) {
        int t = get(last, pos);
        if (!T[t][c]) {
            len[++sz] = len[t] + 2;
            fail[sz] = T[get(fail[t], pos)][c];
            T[t][c] = sz;
            memset(T[sz], 0, sizeof(T[sz]));
        }
        last = T[t][c];
        cnt[last]++;
    }
}

//for (int i = PAM::sz; i >= 2; i--)
//        PAM::cnt[PAM::fail[i]] += PAM::cnt[i];
using namespace PAM;

其中每个节点都代表一个独一无二的回文子串。fail指向了该串最大回文后缀的标号。T则指向了该串向两边扩展同一个字母得到的新串,len则代表了这个回文串的长度,cnt则代表了本回文串出现的次数,一般的题目都会用到。

注意,每一次从左到右add字符时,都会从上一个字符所在的回文后缀last开始,查看是否可以走匹配,如果不行,则需要不断跳fail,一直到空或者找到匹配,匹配的话,我们看此时当前这个节点是否存在,不存在就新建。总效率是O(n*M)的,原因在于观察last字符串的对称轴的移动,是不会往左的。向右边走的次数当然不超过n,而每一次停止,都意味着左右两个字符相匹配。因为我们只添加n个字符,所以停止次数也不会超过n

当然这是均摊意义上的。其中少量add的时间可能比较大,因为走fail可能会产生一条链,例如aaa...b,扩展b时便会如此。

Border理论

一些定理

部分参考自:https://zhuanlan.zhihu.com/p/93152631

定义

  1. ts的border,定义为t既是s的前驱串,又是s的后缀串。

  2. ts的周期,定义为s可以看做nt连接再截取前缀的结果(|t|\le|s|)。

定理1:ts的border,当且仅当s去掉后面一个t得到的字符串,是s的周期。

证明简单,可画图,从略。

定理2:t是回文串s的后缀,ts的border当且仅当t是回文串。

证明简单,可画图,从略。

定理3:t是字符串s的border(|s|\le 2|t|),s是回文串当且仅当t是回文串。

证明:s是回文,t当然是。t是回文时,有s[i]=s[|t|+1-i]=s[|t|+1-i+|s|-|t|]=s[|s|+1-i]\ (i\le\lfloor\frac{s}{2}\rfloor),因此s也是回文的。

证毕

定理4:t 是字符串s的 border,则s去掉后面一个t得到的字符串,是s的周期,而如果是最小周期,当且仅当ts的最长回文真后缀。

这是定理1衍生的,显然。

定理5:x是一个回文串,yx的最长回文真后缀,zy的最长回文真后缀。令u,v分别为满足x=uy,y=vz的字符串,则有下面三条性质:

  1. |u|\ge |v|
  2. 如果|u|>|v|,那么|u|\ge|z|
  3. 如果|u|=|v|,那么u=v

这个定理是最重要的一条。会在回文自动机中得到很好应用。

证明摘自:https://xlor.cn/2019/11/mpf/

图片.png

由引理 4的推论, |u|=|x|-|y|x的最小周期,|v|=|y|-|z|y的最小周期。考虑反证法,假设|u|<|v| ,因为 yx 的后缀,所以 u既是x 的周期,也是 y的周期,而vy的最小周期,矛盾。所以|u|\ge|v|

因为 yx 的 border,所以vx 的前缀,设字符串w,满足 x=v w(如下图所示),其中 zw 的 border。考虑反证法,假设|u|\le|v|,那么|z u| \leq 2|z|,所以由引理 3 , w是回文串,由引理 1 , wx的 border,又因为|u|>|y| ,所以|w|>|y|,矛盾。所以|u|>|v|

u,v都是 x 的前缀,|u|=|v|,所以u=v

推论:

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

定理6:非空回文字符串S如果存在一个回文后缀T,那么S的形式必然可以表示为YXYXY...XY的形式。这里X,Y为回文串,可以为空串。并且|X|+|Y|=|S|-|T|,|Y|=|S|\mod |S|-|T|

证明:

其实由定理4,这个定理的结论基本上接近显然了。这里给出不基于定理4的另一个证明,从另一个角度加以理解。

  1. 倘若a=|T|2*a-|S|>0,那么令b=2*a-|S|,有2*b-a>0,我们如下图构造出了回文字符串AB,使得B是回文串A的回文后缀。如果我们找到了字符串A的形式,那么将A的右侧的|S|-a个字符对称过来再拼接到左侧,就可以找到S的形式。可以递归下去。
图片.png
  1. 倘若a=|T|2*a-|S|\le0,那么这个情况就很简单,是YXY的形式,符合引理2所言,特殊地,当|X|=0,也就是a=\frac{|S|}{2}时,S可表示为两个回文串拼接而成。
    图片.png

我们注意到每一次递归,|S_i|-|T_i|=|S_{i-1}|-|T_{i-1}|=...=|S|-|A|,因此,每次递归回溯,向左侧填充的字符串是完全一样的,都是YX的形式,于是综上所述S=YXYXY...XY。注意,一个回文串重复多次,只是一种特例的情况。

证毕

Border理论的应用

HDU6599 I Love Palindrome String

统计这样的回文串个数:满足自身回文同时满足前\lceil\frac{|s|}{2} \rceil个字符组成的也是回文串。

有很多做法:马拉车配合哈希,回文自动机的fail树上跑dfs检查等等。知道Border原理之后就有一个很简单的方法:判断每个节点(len[i] >> 1) \% (len[i] - len[fail[i]])的值是否为0,是则是符合题意的字符串。

原因很简单,一个回文串有\lfloor\frac{|s|}{2} \rfloor长的回文后缀,意味着这个后缀在第一阶等差数列上,倘若不在,就找到第二阶梯第一个字符串s_2,上个字符串为s_1和上上个为s_0,那么按照定理5,有|s_0|-|s_1|\ge|s_2|,又|s_0|-|s_1|+|s_2|\le|s|-(|s_1|-|s_2|)< |s|,因此s_2<\lceil \frac{|s|}{2} \rceil,这是不可能的。

既然第一阶等差数列上,那么自然有|s|-\lceil\frac{|s|}{2} \rceil|s|-|fail[s]|的倍数。因此满足条件的回文串至少一定满足这个倍数式子。满足式子的回文串,当然也是满足条件的:因为由定理4或者定理6,满足倍数的后缀一定是回文串。

类似的问题还有洛谷4287 双倍回文。当然,由于那道题求的不是个数,而是最大长度,因此用Manacher算法也可以做。

Codeforces932 G

这题应该相当经典了。可以说是万恶之源(雾。

首先我们发现,如果构造字符串t=s[0] s[n-1] s[1] s[n-2] s[2] s[n-3] \ldots s[n / 2-1] s[n / 2],那么问题会变成将字符串t最少划分为多少个长度为偶数的字符串。

普通的dp方法很显然,就是沿着回文自动机失配边走。但是每个节点走完fail,次数的和是可以达到n^2级别的,(比如:aaa...aa)。这里需要优化。

以下说明同样参考自博客:https://xlor.cn/2019/11/mpf/

回文树上的需要多维护两个信息dif[i]slink[i] 。 前者表示节点和最长回文后缀的长度之差,即len[u]-len[fail[u]] 。 后者表示节点一直沿着 fail 向上跳到第一个节点v ,使得dif[v]\not =dif[i],也就是i

所在等差数列中长度最小的那个节点。

根据上面证明的结论,如果使用slink 指针向上跳的话,每向后填加一个字符,只需要向上跳O(\log|s|)次。因此,可以考虑将一个等差数列表示的所有回文串的dp 值之和g,记录到最长的那一个回文串对应节点上。

图片.png

g[v]=\sum_{slink[x]=v}dp[i-len[x]]

假设当前枚举到第 i个字符,回文树上对应节点为 xg[x]为橙色三个位置的dp值之和(最短的回文串slink[x] 算在下一个等差数列中)。 fail[x]上一次出现位置是 i-dif[x](在 i-dif[x]处结束), g[fail[x]]包含的 dp值是蓝色位置。因此,g[x]实际上等于g[fail[x]] 和多出来一个位置的dp值之和,多出来的位置是i-slink[x]-diff[x]。最后再用g[x]去更新dp[i],这部分等差数列的贡献就计算完毕了,不断跳slink[x],重复这个过程即可。

参考代码:

#include <cstdio>
#include <cstring>

#define mo 1000000007

using namespace std;

namespace PAM {
    const int N = 1000005, M = 26;
    int T[N][M], fail[N], len[N], sz, last, slink[N], dif[N];
    char s[N];

    void init() {
        len[0] = 0, len[1] = -1;
        sz = 1, last = 0;
        fail[0] = 1;
        memset(T[0], 0, sizeof(T[0]));
        memset(T[1], 0, sizeof(T[1]));
    }

    int get(int j, int pos) {
        while (pos == len[j] || s[pos - len[j] - 1] != s[pos])
            j = fail[j];
        return j;
    }

    void add(int c, int pos) {
        int t = get(last, pos);
        if (!T[t][c]) {
            len[++sz] = len[t] + 2;
            fail[sz] = T[get(fail[t], pos)][c];
            T[t][c] = sz;
            memset(T[sz], 0, sizeof(T[sz]));
            dif[sz] = len[sz] - len[fail[sz]];
            slink[sz] = dif[sz] == dif[fail[sz]] ? slink[fail[sz]] : fail[sz];
        }
        last = T[t][c];
    }
}

using namespace PAM;

char t[N];
int n, dp[N], g[N];

int main() {
    scanf("%s", t);
    n = strlen(t);
    if (n & 1) {
        putchar('0');
        return 0;
    }
    for (int i = 0, j = 0; i < n >> 1; i++)
        s[j++] = t[i], s[j++] = t[n - i - 1];
    init();
    for (int i = 0; i < n; i++) {
        add(s[i] - 'a', i);
        for (int j = last, np; j > 0; j = slink[j]) {
            np = i - len[slink[j]] - dif[j];  //np最小为-1
            g[j] = np >= 0 ? dp[np] : 1;
            if (dif[j] == dif[fail[j]])
                g[j] = (g[j] + g[fail[j]]) % mo;
            if (i & 1)  //注意回文串必须是偶数,所以对应i%2==1
                dp[i] = (dp[i] + g[j]) % mo;
        }
    }
    printf("%d", dp[n - 1]);
    return 0;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容