論如何優雅的處理迴文串 - 迴文自動機詳解

 

寫在前面

最近無心中看到了這個數據結構,順便也就學習了一下。php

並且發現網上關於這個算法的描述有不少地方是錯的,在這裏作了一些更正。node

處理字符串的算法不少:ios

    KMP,E-KMP,AC自動機,後綴三兄弟:後綴樹、後綴數組、後綴自動機,Trie樹、Trie圖,符串hash...算法

但以上數據結構在處理迴文串上仍是稍有欠缺,用這些來處理迴文顯得過小題大作。數組

因而有了Manacher算法,代碼短、容易理解、時間O(n)、無需考慮奇偶迴文狀況,很完美的算法!數據結構

固然這篇博客的重點不在Manacher算法,有關Manacher算法請點擊這裏!函數

Manacher算法能夠在O(n)時間內處理出S串每一個位置的最長迴文串,但若是要統計S串中有多少迴文串,學習

或者S串的全部子串的迴文串的個數,這時就要用到一種和Manacher同樣優雅的數據結構:迴文自動機。ui

What  Is  Palindromic auto-machine?

迴文自動機,又叫回文樹,是由俄羅斯人 MikhailRubinchik於2014年夏發明的,參看連接this

這是一種比較新的數據結構,在原文中已有詳細介紹與代碼實現。

迴文樹其實不是嚴格的樹形結構,由於它有是兩棵樹,分別是偶數長度的迴文樹和奇數長度的迴文樹,樹中每一個節點表明一個迴文串。

爲了方便,第一棵樹的根是一個長度爲0的串,第二棵就是爲-1的串,不要感到奇怪,就是-1。

能夠證實,最多隻有n個結點(n是串的長度)。這個能夠用Manacher算法來證實。

若是某結點表明的是串ccabacc,那麼它的父親表明的串就是去掉先後兩個字符cabac。

每一個點還有一個fail指針,表示這個串的後綴中最長的迴文串,好比babab的fail指向bab,bab的指向b。

方法的思想和KMP,AC自動機很相似,若是你理解了KMP與AC自動機,那麼這個算法基本能夠一看就懂。

數聽說明

  • len[i]:節點i的迴文串的長度
  • next[i][c]:節點i的迴文串在兩邊添加字符c之後變成的迴文串的編號(和字典樹的next指針相似)
  • fail[i]:相似於AC自動機的fail指針,指向失配後須要跳轉到的節點
  • cnt[i]:節點i表示的迴文串在S中出現的次數(建樹時求出的不是徹底的,count()加上子節點之後纔是正確的)
  • num[i]:以節點i迴文串的末尾字符結尾的但不包含本條路徑上的迴文串的數目。(也就是fail指針路徑的深度)
  • last:指向最新添加的迴文結點
  • S[i]表示第i次添加的字符
  • p表示添加的節點個數

How To Build Palindromic auto-machine?

假設如今咱們有串S='abbaabba'。

首先咱們添加第一個字符'a',S[++ n] = 'a',而後判斷此時S[n-len[last]-1]是否等於S[n]

即上一個串-1的位置和新添加的位置是否相同,相同則說明構成迴文,不然,last=fail[last]。

此時last=0,咱們發現S[1-0-1]!=S[1],因此last=fail[last]=1,

而後咱們發現S[1-(-1)-1]==S[1](即本身等於本身,因此咱們讓len[1]等於-1可讓這一步更加方便)。

令cur等於此時的last(即cur=last=1),判斷此時next[cur]['a']是否已經有後繼,

若是next[cur]['a']沒有後繼,咱們就進行以下的步驟:

新建節點(節點數p++,且以後p=3),並讓now等於新節點的編號(now=2),

則len[now]=len[cur]+2(每個迴文串的長度老是在其最長子迴文串的基礎上在兩邊加上兩個相同的字符構成的,因此是+2,

同時體現出咱們讓len[1]=-1的優點,一個字符自成一個奇迴文串時迴文串的長度爲(-1)+2=1)。

而後咱們讓fail[now]=next[get_fail ( fail[cur] )]['a'],即獲得fail[now](此時爲fail[2] = 0),

其中的get_fail函數就是讓找到第一個使得S[n-len[last]-1]==S[n]的last。而後next[cur]['a'] = now。

當上面步驟完成後咱們讓last = next[cur][c](無論next[cur]['a']是否有後繼),而後cnt[last] ++。

此時迴文樹爲下圖狀態:


如今咱們添加第二個字符字符'b'到迴文樹中:

繼續添加第三個字符'b'到迴文樹中:


繼續添加第四個字符'a'到迴文樹中:


 繼續添加第五個字符'a'到迴文樹中:


 繼續添加第六個字符'b'到迴文樹中:



 繼續添加第七個字符'b'到迴文樹中:


 繼續添加第八個字符'a'到迴文樹中:


 到此,串S已經徹底插入到迴文樹中了,如今全部的數據以下:

 

 

 

 

而後咱們將節點x在fail指針樹中將本身的cnt累加給父親,從葉子開始倒着加,最後就能獲得串S中出現的每個本質不一樣迴文串的個數。

構造迴文樹須要的空間複雜度爲O(N*字符集大小),時間複雜度爲O(N*log(字符集大小)),這個時間複雜度比較神奇。若是空間需求太大,能夠改爲鄰接表的形式存儲,不過相應的要犧牲一些時間。

The Use Of Palindromic auto-machine

  1. 求串S前綴0~i內本質不一樣迴文串的個數(兩個串長度不一樣或者長度相同且至少有一個字符不一樣即是本質不一樣)
  2. 求串S內每個本質不一樣迴文串出現的次數
  3. 求串S內迴文串的個數(其實就是1和2結合起來)
  4. 求如下標i結尾的迴文串的個數

some problem

  1. 2014-2015 ACM-ICPC, Asia Xian Regional Contest G The Problem to Slow Down You
  2. ural1960. Palindromes and Super Abilities
  3. WHU1583 Palindrome
  4. Подпалиндромы

Template

/*
* this code is made by crazyacking
* Verdict: Accepted
* Submission Date: 2015-08-19-21.48
* Time: 0MS
* Memory: 137KB
*/
#include <queue>
#include <cstdio>
#include <set>
#include <string>
#include <stack>
#include <cmath>
#include <climits>
#include <map>
#include <cstdlib>
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
#define  LL long long
#define  ULL unsigned long long
using namespace std;
const int MAXN = 100005 ;
const int N = 26 ;
char s[MAXN];
struct Palindromic_Tree
{
    int next[MAXN][N] ;//next指針,next指針和字典樹相似,指向的串爲當前串兩端加上同一個字符構成
    int fail[MAXN] ;//fail指針,失配後跳轉到fail指針指向的節點
    int cnt[MAXN] ;
    int num[MAXN] ; // 當前節點經過fail指針到達0節點或1節點的步數(fail指針的深度)
    int len[MAXN] ;//len[i]表示節點i表示的迴文串的長度
    int S[MAXN] ;//存放添加的字符
    int last ;//指向上一個字符所在的節點,方便下一次add
    int n ;//字符數組指針
    int p ;//節點指針
    int newnode(int l)     //新建節點
    {
          for(int i = 0 ; i < N ; ++ i) next[p][i] = 0 ;
          cnt[p] = 0 ;
          num[p] = 0 ;
          len[p] = l ;
          return p ++ ;
    }
    void init()   //初始化
    {
          p = 0 ;
          newnode(0) ;
          newnode(-1) ;
          last = 0 ;
          n = 0 ;
          S[n] = -1 ;//開頭放一個字符集中沒有的字符,減小特判
          fail[0] = 1 ;
    }
    int get_fail(int x)     //和KMP同樣,失配後找一個儘可能最長的
    {
          while(S[n - len[x] - 1] != S[n]) x = fail[x] ;
          return x ;
    }
    void add(int c,int pos)
    {
          printf("%d:",p);
          c -= 'a';
          S[++ n] = c ;
          int cur = get_fail(last) ;   //經過上一個迴文串找這個迴文串的匹配位置
          printf("%d ",cur);
          if(!next[cur][c])     //若是這個迴文串沒有出現過,說明出現了一個新的本質不一樣的迴文串
          {
                int now = newnode(len[cur] + 2) ;   //新建節點
                fail[now] = next[get_fail(fail[cur])][c] ;   //和AC自動機同樣創建fail指針,以便失配後跳轉
                next[cur][c] = now ;
                num[now] = num[fail[now]] + 1 ;
                for(int i=pos-len[now]+1; i<=pos; ++i) printf("%c",s[i]);
          } last = next[cur][c] ;
          cnt[last] ++ ;
          putchar(10);
    }
    void count()
    {
          for(int i = p - 1 ; i >= 0 ; -- i) cnt[fail[i]] += cnt[i] ;
          //父親累加兒子的cnt,由於若是fail[v]=u,則u必定是v的子迴文串!
    }
} run;
int main()
{
    scanf("%s",&s);
    int n=strlen(s);
    run.init();
    for(int i=0; i<n; i++) run.add(s[i],i);
    run.count();
    return 0;
}
相關文章
相關標籤/搜索