1. 程式人生 > >AC自動機入門詳解

AC自動機入門詳解

一.AC自動機的引入.

我們都知道KMP可以用來一個子串與母串之間的匹配,只需要通過一個next指標就可以實現O(n+m)匹配,已經達到了演算法下界,是一個很優秀的演算法了.

但是我們如何考慮多個子串與母串之間的匹配呢?

如果多個子串與母串之間的匹配用KMP來實現,效率可能就不那麼高了,這可怎麼辦呢?這就是AC自動機的由來.

AC自動機是什麼?其實就是在Trie樹上跑KMP.


二.Trie樹與KMP.

我們考慮單串時的KMP是怎麼匹配的?利用的是next陣列.

再考慮一下多串的一般套路?放進Trie樹.

那麼多串匹配怎麼辦?求Trie樹上的next陣列(在AC自動機裡就叫fail指標了)!

考慮Trie樹上在匹配一個串時,失配了怎麼辦?往Fail指標跳!就像下面這張圖:

我們認為這張圖中所有相同顏色的鏈與線段都是相同的,那麼當Trie樹上紅色的鏈失配時我們想要繼續匹配,就必須要讓紅色線段的一個字尾(例如藍色線段)與Trie樹上的另外一條以根為一段的鏈相同(例如藍色的鏈).fail指標也就是連線紅色鏈的結尾與最長的滿足條件的藍色鏈的結尾(當然藍色得比紅色短)的一個指標(綠色箭頭).

是不是突然感覺AC自動機很容易啊…然而理解fail指標不代表會構造fail指標(fail指標學過KMP與Trie樹就能懂了吧)…


三.構造fail指標.

構造fail指標其實很簡單,因為我們發現fail指標一定是從深度大的節點指向深度小的節點,又可以發現一個節點的fail指標可以通過它父親的fail指標構造,所以我們選擇使用BFS構造fail指標.

具體就是BFS每一層,一個節點的fail指標就用它父親的fail指標嘗試是否能匹配,不能匹配就繼續跳fail指標,知道能匹配或跳到了根(與KMP的next很像的跳法).

程式碼如下:

void Get_fail(){      //注意程式碼中Trie樹的根編號為0
  for (int i=0;i<C;++i)      //初始不能直接push一個根,不然會導致第二層節點的fail指標指向自己 
    if (tr[0].s[i]) q.push(tr[0].s[i]);
  while (!q.empty()){
    int x=q.front(),t;q.pop();
	for
(int i=0;i<C;++i) if (tr[x].s[i]){ t=tr[x].fail; while (t&&!tr[t].s[i]) t=tr[t].fail; tr[tr[x].s[i]].fail=tr[t].s[i]; q.push(tr[x].s[i]); } } }

值得注意的是,當我們去掉Trie樹上fail指標外的邊時,我們會發現Trie樹上的節點與所有fail指標構成了一棵樹!這棵樹就被稱為fail樹.

既然我們有了一棵樹,我們就可以對這棵樹進行一些操作了.於是一些毒瘤題就這麼應運而生(比如說BZOJ2434阿狸的打字機).


四.Trie圖.

Trie圖是AC自動機的確定化形式,也就是說Trie圖是一個DFA(自動機分DFA或NFA).

Trie圖的主要思想就是強行把一棵樹變成圖將x為空的兒子指標s[i]指向x的fail指標的s[i],相當於直接把fail樹上的邊加到了Trie樹上.這樣做會使程式碼變得簡潔一些,而且它能很大程度上幫你少跳一些指標(查詢的時候就不用寫while迴圈了,Trie圖只需要跳一次指標就夠了).

程式碼如下:

void Get_fail(){      //注意程式碼中Trie樹的根編號為0
  for (int i=0;i<C;++i)      //初始不能直接push一個根,不然會導致第二層節點的fail指標指向自己 
    if (tr[0].s[i]) q.push(tr[0].s[i]);
  while (!q.empty()){
  	int x=q.front();q.pop();
  	for (int i=0;i<26;++i)
  	  if (!tr[x].s[i]) tr[x].s[i]=tr[tr[x].fail].s[i];
  	  else tr[tr[x].s[i]].fail=tr[tr[x].fail].s[i],q.push(tr[x].s[i]);
  }
}



五.查詢.

接下來的查詢以查詢每一個串是否出現為例.

查詢最樸素的想法就是直接暴力在每一個點跳fail指標匹配,然而這樣做是沒有效率保證的.

考慮對於fail樹,發現只要一個節點到根這一段可以匹配時,它的兒子同樣也會被匹配.所以我們只需要記錄Trie樹上每一個點是否被匹配當做一個標記,處理完之後在fail樹把父親的標記下傳就可以了.

程式碼如下:

LL Query(char *c,int len){
  int x=0;
  for (int i=1;i<=len;++i){
    while (x&&!tr[x].s[c[i]-'a']) x=tr[x].fail;
	x=tr[x].s[c[i]-'a'];
	b[x]=1;
  }
  LL ans=0;
  for (int i=co;i>=1;--i)
    if (b[ord[i]]){      //ord陣列是fail樹的bfs序
      b[ord[i]]=0;
      b[tr[ord[i]].fail]=1;
      ans+=LL(tr[ord[i]].cnt);
	}
  return ans;
}



六.時間複雜度分析.

這裡主要分析fail指標構造的時間複雜度.

我們考慮BFS時fail指標跳動的總次數,與KMP類似的,我們發現每跳一次fail指標都會使得深度減1,而深度最多增加串長次.所以對於Trie樹中插入的每一個串構造fail指標時間複雜度都是與串長同級的,設總共往Trie中插入了n個字元,構造fail指標的總時間複雜度就是   O ( n ) \ O(n) 的.

同樣的,在查詢串的時候外層列舉i的時間複雜度為   O ( m ) \ O(m) ,也就是說深度最多增長了m次,所以fail指標的跳動次數最多也是m次,總時間複雜度就是   O ( m ) \ O(m) .但是由於後面我們還需要遍歷一遍Trie樹,所以一次查詢總時間複雜度為   O ( n + m ) \ O(n+m) .


七.例題與程式碼.

題目1:hdu2222.
程式碼如下:

#include<bits/stdc++.h>
  using namespace std;

#define Abigail inline void
typedef long long LL;
#define m(a) memset(a,0,sizeof(a))

const int C=26,M=1000000;

struct Trie{
  int s[C],cnt,fail;
  Trie(){m(s);cnt=fail=0;}
}tr[M+9];
int cn;

void Build(){tr[cn=0]=Trie();}

void Insert(char *c,int len){
  int x=0;
  for (int i=1;i<=len;++i)
    if (tr[x].s[c[i]-'a']) x=tr[x].s[c[i]-'a'];
    else {
      tr[x].s[c[i]-'a']=++cn;
      tr[x=cn]=Trie();
    }
  ++tr[x].cnt;
}

queue<int>q;
int ord[M+9],co;      //ord記錄的是fail樹的bfs序 

void Get_fail(){      //注意程式碼中Trie樹的根編號為0
  ord[co=1]=0;
  for (int i=0;i<C;++i)      //初始不能直接push一個根,不然會導致第二層節點的fail指標指向自己 
    if (tr[0].s[i]) q.push(tr[0].s[i]);
  while (!q.empty()){
  	int x=q.front(),t;q.pop();
  	ord[++co]=x;
  	for (int i=0;i<C;++i)
	  if (tr[x].s[i]){
  	    t=tr[x].fail;
  	    while (t&&!tr[t].s[i]) t=tr[t].fail;
  	    tr[tr[x].s[i]].fail=tr[t].s[i];
  	    q.push(tr[x].s[i]);
  	  }
  }
}

int b[M+9];

LL Query(char *c,int len){
  int x=0;
  for (int i=1;i<=len;++i){
    while (x&&!tr[x].s[c[i]-'a']) x=tr[x].fail;
	x=tr[x].s[c[i]-'a'];
	b[x]=1;
  }
  LL ans=0;
  for (int i=co;i>=1;--i)
    if (b[ord[i]]){
      b[ord[i]]=0;
      b[tr[ord[i]].fail]=1;
      ans+=LL(tr[ord[i]].cnt);
	}
  return ans;
}

int n,m;
char c[M+9];

Abigail into(){
  Build();
  scanf("%d",&n);
  for (int i=1;i<=n;++i){
    scanf("%s",c+1);
    m=strlen(c+1);
    Insert(c,m);
  }
  scanf("%s",c+1);
  m=strlen(c+1);
}

Abigail work(){
  Get_fail();
}

Abigail outo(){
  printf("%lld\n",Query(c,m));
}

int main(){
  int T=1;
  scanf("%d",&T);
  while (T--){
    into();
    work();
    outo();
  }
  return 0;
}

應該還有一種Trie圖寫法,程式碼不願意寫了.

題目2:luogu3808.
程式碼如下:

#include<bits/stdc++.h>
  using namespace std;

#define Abigail inline void
typedef long long LL;
#define m(a) memset(a,0,sizeof(a))

const int C=26,M=1000000;

struct Trie{
  int s[C],cnt,fail;
  Trie(){m(s);cnt=fail=0;}
}tr[M+9];
int cn;

void Build(){tr[cn=0]=Trie();}

void Insert(char *c,int len){
  int x=0;
  for (int i=1;i<=len;++i)
    if (tr[x].s[c[i]-'a']) x=tr[x].s[c[i]-'a'];
    else {
      tr[x].s[c[i]-'a']=++cn;
      tr[x=cn]=Trie();
    }
  ++tr[x].cnt;
}

queue<int>q;
int ord[M+9],co;      //ord記錄的是fail樹的bfs序 

void Get_fail(){      //注意程式碼中Trie樹的根編號為0
  ord[co=1]=0;
  for (int i=0;i<C;++i)      //初始不能直接push一個根,不然會導致第二層節點的fail指標指向自己 
    if (tr[0].s[i]) q.push(tr[0].s[i]);
  while (!q.empty()){
  	int x=q.front(),t;q.pop();
  	ord[++co]=x;
  	for (int i=0;i<C;++i)
	  if (tr[x].s[i]){
  	    t=tr[x].fail;
  	    while (t&&!tr[t].s[i]) t=tr[t].fail;
  	    tr[tr[x].s[i]].fail=tr[t].s[i];
  	    q.push(tr[x].s[i]);
  	  }
  }
}

int b[M+9];

LL Query(char *c,int len){
  int x=0;
  for (int i=1;i<=len;++i){
    while (x&&!tr[x].s[c[i]-'a']) x=tr[x].fail;
	x=tr[x].s[c[i]-'a'];
	b[x]=1;
  }
  LL ans=0;
  for (int i=co;i>=1;--i)
    if (b[ord[i]]){
      b[ord[i]]=0;
      b[tr[ord[i]].fail]=1;
      ans+=LL(tr[ord[i]].cnt);
	}
  return ans;
}

int n,m;
char c[M+9];

Abigail into(){
  Build();
  scanf("%d",&n);
  for (int i=1;i<=n;++i){
    scanf("%s",c+1);
    m=strlen(c+1);
    Insert(c,m);
  }
  scanf("%s",c+1);
  m
            
           

相關推薦

AC自動機入門

一.AC自動機的引入. 我們都知道KMP可以用來一個子串與母串之間的匹配,只需要通過一個next指標就可以實現O(n+m)匹配,已經達到了演算法下界,是一個很優秀的演算法了. 但是我們如何考慮多個子串與母串之間的匹配呢? 如果多個子串與母串之間的匹配用KMP來實現,效率可能就

AC 自動機演算法(轉)

首先簡要介紹一下AC自動機:Aho-Corasick automation,該演算法在1975年產生於貝爾實驗室,是著名的多模匹配演算法之一。一個常見的例子就是給出n個單詞,再給出一段包含m個字元的文章,讓你找出有多少個單詞在文章裡出現過。要搞懂AC自動機,先得有模式樹(字典樹)Trie和KMP模式匹配演算法

AC自動機步驟

AC自動機 演算法意義: 求多個字串是否在主串中出現過。可依據情況分別求出出現次數,出現位置等。學習基礎: 要搞懂AC自動機,先得有模式樹(字典樹)Trie和KMP模式匹配演算法的基礎知識。AC自動機的構造: 1.構造一棵Trie,作為AC自動機的搜尋資料結構。2.構造

[置頂]AC自動機-演算法

What's Aho-Corasick automaton?   一種多模式串匹配演算法,該演算法在1975年產生於貝爾實驗室,是著名的多模式匹配演算法之一。   簡單的說,KMP用來在一篇文章中匹配一個模式串;但如果有多個模式串,需要在一篇文章中把出現過的模式串都匹配出來,

Asp.Net MVC3 簡單入門過濾器Filter

添加 重復 權限 組件 再次 ace text ext 開發 前言 在開發大項目的時候總會有相關的AOP面向切面編程的組件,而MVC(特指:Asp.Net MVC,以下皆同)項目中不想讓MVC開發人員去關心和寫類似身份驗證,日誌,異常,行為截取等這部分重復的代碼,那我們可以

線段樹 入門

ear 接下來 數組 編譯器 一位 離散化 都是 並且 建立 概念(copy度娘): 線段樹是一種二叉搜索樹,與區間樹相似,它將一個區間劃分成一些單元區間,每個單元區間對應線段樹中的一個葉結點。 使用線段樹可以快速的查找某一個節點在若幹條線段中出現的次數,時間復雜度為O

UVALive-4670 AC自動機入門題 求出現次數最多的子串

efi con sig http ati code fine mod long /** 鏈接:http://vjudge.net/problem/UVALive-4670 詳見lrj訓練指南P216 */ #include<bits/stdc++.h> usi

hdu3065 病毒侵襲持續中 AC自動機入門題 N(N <= 1000)個長度不大於50的模式串(保證所有的模式串都不相同), 一個長度不大於2000000的待匹配串,求模式串在待匹配串中的出現次數。

sizeof archive 模式 emp tomat .... truct print sca /** 題目:hdu3065 病毒侵襲持續中 鏈接:http://acm.hdu.edu.cn/showproblem.php?pid=3065 題意:N(N <= 1

PHP基礎入門(一)【世界上最好用的編程語言】

轉換成 c語言 127.0.0.1 mac const 讀取 成對 後臺 isset 簡介 ---------  PHP(超文本預處器)是一種通用開源腳本語言。語法吸收了C語言、Java和Perl的特點,利於學習,使用廣泛,主要適用於Web開發領域。PHP 獨

Selenium Grid分布式測試入門

lena 客戶端 odi before ons cycle lean efault 命令 本文對Selenium Grid進行了完整的介紹,從環境準備到使用Selenium Grid進行一次完整的多節點分布式測試。 運行環境為Windows 10,Selenium版本為

無向圖的割頂和橋,無向圖的雙連通分量入門及模板 -----「轉載」

dbr break nts word 否則 mark push gravity 無向連通圖 https://blog.csdn.net/stillxjy/article/details/70176689 割頂和橋:對於無向圖G,如果刪除某個節點u後,連通分量數目

linux三劍客之sed入門

linux 三劍客 sed sed介紹sed流編輯器(stream editor),在三劍客中排行老二,是一款簡單的文本編輯語言。sed並不直接處理源文件,而是逐行讀取源文件的內容到內存(稱模式空間)中,然後在模式空間中使用sed命令處理,再打印模式空間處理後的內容到標準輸出。sed的能夠實現的功

生成函數(母函數)入門

參考 nsh 意義 數值 tar 得到 再次 fin 表達式 本文章從以上兩位大佬的博客參考而來!再次感謝! 母函數,又稱生成函數,是ACM競賽中經常使用的一種解題算法,常用來解決組合方面的題目。 在數學中,某個序列的母函數(Generating funct

樹鏈剖分入門

管理 組成 你們 其它 現在 範圍 pro 所有 關系 樹鏈剖分入門詳解 以前沒有接觸過樹鏈剖分的同學們看到這個東西是不是覺得很高大上呢,下面我將帶你們進入樹的世界(講得不好別打我) 首先我們來看一道題 NOI2015D2T2軟件包管理器 題目描述如下 Linux用戶和O

區塊鏈以及區塊鏈技術入門(2)

很多人迷惑於區塊鏈和以太坊,不知如何學習,本文簡單說了一下學習的一些方法和資源。 一、    以太坊和區塊鏈的關係      從區塊鏈歷史上來說,先誕生了比特幣,當時並沒有區塊鏈這個技術和名詞,然後業界從比特幣中提取了技術架構和體系,稱

區塊鏈以及區塊鏈技術入門(1)

區塊鏈是目前一個比較熱門的新概念,蘊含了技術與金融兩層概念。從技術角度來看,這是一個犧牲一致性效率且保證最終一致性的的分散式的資料庫,當然這是比較片面的。從經濟學的角度來看,這種容錯能力很強的點對點網路,恰恰滿足了共享經濟的一個必須要求——低成本的可信環境。 1. 技術人員看待區塊鏈的正確姿勢

主席樹入門+題目推薦

主席樹學名可持久化線段樹,就是這個可持久化,衍生了多少資料結構 為什麼會有主席樹這個資料結構呢?它被髮明是用來解決什麼問題的呢? 給定n個數,m個操作,操作型別有在某個歷史版本下單點修改,輸出某個歷史版本下某個位置的值的值,n和m小於等於1e6 乍一看是不是一點頭緒也沒有。我們先來想想暴力怎麼

經典ASP NET MVC3 0入門

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

lambda表示式入門

轉自 2018-03-02 Sevenvidia 碼農翻身 1、什麼是Lambda? 我們知道,對於一個Java變數,我們可以賦給其一個“值”。   如果你想把“一塊程式碼”賦給一個Java變數,應該怎麼做呢?  比如,我想把右邊那塊程式碼,賦給一個叫做aBlo

知識:整合營銷新手入門

現在很多企業會提到整合營銷的概念。從字面理解,可能是把各種企業資源柔和在一起,發揮各自的最大化效益。整合營銷的目的是把現有的各種營銷方式進行合理化“加工”以後,再服務於企業的一種營銷策略。然而,在企業實際操作過程中,大部分經營者曲解了整合營銷的出發點,知道整合營銷很好,都在積極嘗試,但在嘗試