0x16.基本數據結構 — Trie樹(字典樹)+ A C 自 動 機

聲明:
本系列博客是《算法競賽進階指南》+《算法競賽入門經典》+《挑戰程序設計競賽》的學習筆記,主要是因爲我三本都買了 按照《算法競賽進階指南》的目錄順序學習,包含書中的少部分重要知識點、例題解題報告及我個人的學習心得和對該算法的補充拓展,僅用於學習交流和複習,無任何商業用途。博客中部分內容來源於書本和網絡(我儘量減少書中引用),由我個人整理總結(習題和代碼可全都是我自己敲噠)部分內容由我個人編寫而成,如果想要有更好的學習體驗或者希望學習到更全面的知識,請於京東搜索購買正版圖書:《算法競賽進階指南》——作者李煜東,強烈安利,好書不火系列,謝謝配合。


下方鏈接爲學習筆記目錄鏈接(中轉站)

學習筆記目錄鏈接


ACM-ICPC在線模板


TrieTrie樹來處理整數異或問題是真的舒服!

 {\ } {\ } {\ } {\ } KMPKMP可以處理一個字符串在一個字符串上的匹配。
又∵  {\ } {\ }trietrie可以處理一個字符串在多個字符串上的匹配。
又∵  {\ }ACAC自動機可以處理多個字符串在一個字符串上的匹配。
 {\ } {\ } {\ } {\ } {\ }ACAC自動機=KMPKMP+TrieTrie

hhhhhhhh......hhhhhhhh......

來這裏學KMP 嚶嚶嚶

一、TrieTrie

TrieTrie樹(又稱字典樹,前綴樹)是一種樹形結構,是一種哈希樹的變種。典型應用是用於統計,排序和保存大量的字符串(但不僅限於字符串),所以經常被搜索引擎系統用於文本詞頻統計,用於實現字符串快速檢索的多叉樹結構。TrieTrie的每個節點都擁有若干個字符指針,若在插入或者檢索字符串時掃描到一個字符 cc ,就沿着當前節點 cc 字符的指針,走向該指針指向的節點。

Trie樹線性(霧)處理多組字符串匹配問題,而KMP線性處理一對字符串的匹配問題。(據說學會這兩個就可以去學習ACAC自動機啦!)

TrieTrie是典型的用空間來換時間的做法

在這裏插入圖片描述
其中黑色的球就是字符串尾

圖源百度百科

TrieTrie的基本操作

這裏不想用指針(指針容易出錯,寫起來還超級麻煩)所以用數組模擬

0.初始化

注意 TrieTrie 數組存的是結點 ppchch 指針指向的結點編號,這裏的ch指針爲了方便被轉化成了一個整數,這點千萬不能忘

僅包含根節點,根節點中所有字符指針均爲空。

// 假設字符串由小寫字母構成
int trie[SIZE][26], tot = 1;

1.插入

注意 trietrie 數組存的是結點 ppchch 指針指向的結點編號,這裏的ch指針爲了方便被轉化成了一個整數,這點千萬不能忘

當需要插入字符串SS時,我們先令一個指針PP指向根節點,然後依次掃描SS中的每一個字符cc

PP的指向c的指針指向一個已經存在的節點QQ,則令P=QP = Q
PP的指向c的指針爲空,則新建一個節點QQ,令c的指針指向QQ,之後令P=QP = Q
SS中的所有字符均掃描完畢時,在當前節點PP上標記其爲一個字符串的末尾。

// Trie的插入
void inserts(char* str){
    int len = strlen(str),p = 1;//p是根結點
    for(int k = 0;k < len;++k){
        int ch = str[k]-'a';//相當於ch字符指針,這裏轉化成了一個整數
        if(trie[p][ch] == 0)//若爲空(當前結點沒有這個字符指針),新建一個ch字符結點Q
            trie[p][ch] = ++tot;//存的是結點編號
        p = trie[p][ch];//p指向Q(不管有沒有這個結點,都要使得p指向Q)
    }
    ed[p] = true;//標記葉子結點等於true
}

2.檢索

注意 trietrie 數組存的是結點 ppchch 指針指向的結點編號,這裏的ch指針爲了方便被轉化成了一個整數,這點千萬不能忘

當需要檢索一個字符串SSTrieTrie中是否存在時,我們另一個指針P指向根節點,然後依次掃描SS中的每一個字符cc

如果PP的指向cc的指針爲空,說明SSTrieTrie中不存在,結束檢索;
如果PP的指向cc的指針指向一個存在的節點 QQ,則令P=QP = Q
SS中的所有字符均掃描完畢是,若當前節點PP 被標記爲一個字符串的末尾,說明SSTrieTrie 中存在,否則,SS不存在。

空間複雜度爲Θ(N×C)\Theta(N \times C),其中NN爲節點的個數,CC爲字符集的大小。

// Trie的檢索
bool searchs(char* str){
    int len = strlen(str),p = 1;
    for(int k = 0;k < len;++k){
        p = trie[p][str[k]-'a'];//注意trie數組存的是結點p的ch指針,這裏的ch指針爲了方便被轉化爲了一個整數
        if(p == 0)//一旦沒了(非洲警告!),就說明不存在
            return false;
    }
    return ed[p];//按ed[p]的值來決定,因爲必須到達葉子結點,才爲true
}

二、TrieTrie樹例題:

0.luogu P2580 於是他錯誤的點名開始了(模板題)

題目鏈接:https://www.luogu.com.cn/problem/P2580
在這裏插入圖片描述
老規矩先給出一道模板題練練手(水水題 )。

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<vector>
#include<bitset>
#include<limits.h>
#define ls (p<<1)
#define rs (p<<1|1)
#define mid (l+r>>1)
#define over(i,s,t) for(register int i=s;i<=t;++i)
#define lver(i,t,s) for(register int i=t;i>=s;--i)
//#define int __int128
using namespace std;
#undef mid
typedef long long ll;
typedef unsigned long long ull;
//typedef pair<int,int> PII;

const int N=1e6+7;
const ll mod=1e9+7;
const ll INF=1e15+7;
const double EPS=1e-10;
const int p=131;//13331


int n,m;
int trie[N][26],tot = 1;
int ed[N];
void inserts(char* str){
    int len = strlen(str),p = 1;
    for(int k = 0;k<len;++k){
        int ch = str[k]-'a';
        if(trie[p][ch] == 0)
            trie[p][ch] = ++tot;
        p = trie[p][ch];
    }
    ed[p]=true;
}

int val[N];

int searchs(char* str){
    int len = strlen(str),p=1;
    for(int k = 0;k<len;++k){
        p = trie[p][str[k]-'a'];
        if(p == 0)return 0;
        }
    if(!val[p]&&ed[p]){
        val[p]=true;
        return 1;
    }
    else if(!ed[p])return 0;
    else return 2;
}
char str[N];
int main()
{
    scanf("%d",&n);
    over(i,1,n){

        scanf("%s",str);//糟糕的 scanf 輸入體驗
        inserts(str);
    }
    scanf("%d",&m);
    over(i,1,m){

        scanf("%s",str);
        int ans = searchs(str);
        if(ans == 0)puts("WRONG");
        else if(ans == 1)puts("OK");
        else puts("REPEAT");
    }
    return 0;
}

1.AcWing 142. 前綴統計(Trie樹模板進階題)

題目鏈接:https://www.acwing.com/problem/content/description/145/
在這裏插入圖片描述
首先對於任意一道題,任意一組數據,都要考慮重複數據的情況!

本題要求統計前綴個數,一想到字符串的前綴,我們就應該想到字典樹(前綴樹。
我們把這n個字符串插入到一顆trie樹裏,trie樹的每一個結點都存儲一個整數在ed數組裏,用來記錄該結點是多少個字符串的末尾結點。(因爲要考慮相同數據的情況,這裏不能跟普通的trie樹一樣只是結尾標記)
然後對於每一組詢問,在trie樹裏檢索輸入的字符串T,並在檢索的過程中累加路徑上每個結點的ed值,既是最後的答案。

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<vector>
#include<bitset>
#include<limits.h>
#define ls (p<<1)
#define rs (p<<1|1)
#define mid (l+r>>1)
#define over(i,s,t) for(register int i=s;i<=t;++i)
#define lver(i,t,s) for(register int i=t;i>=s;--i)
//#define int __int128
using namespace std;
#undef mid
typedef long long ll;
typedef unsigned long long ull;
//typedef pair<int,int> PII;

const int N=1e6+7;
const ll mod=1e9+7;
const ll INF=1e15+7;
const double EPS=1e-10;
const int p=131;//13331

int n,m;
int trie[N][26];
int tot=1;
int ed[N];
char s[N];

void inserts(char* str){
    int len = strlen(str),p = 1;
    for(int k = 0;k < len;++k){
        int ch = str[k]-'a';
        if(trie[p][ch] == 0)
            trie[p][ch] = ++tot;
        p = trie[p][ch];
    }
    ed[p]++;
}

int searchs(char* str){
    int len = strlen(str),p = 1,ans = 0;
    for(int k = 0 ; k < len ; ++k){
        p = trie[p][str[k]-'a'];
        if(p == 0)
            return ans;
        ans += ed[p];//因爲本題求的是前綴,所以要累加路徑上的每個結點的ed值(可能爲0嘛)
    }
    return ans;
}

int main()
{
    scanf("%d%d",&n,&m);
    over(i,1,n){
        scanf("%s\n",s);//這個"\n"寫不寫沒有任何區別......
        inserts(s);
    }
    over(i,1,m){
        scanf("%s\n",s);
        printf("%d\n",searchs(s));
    }
    return 0;
}

2.AcWing 143. 最大異或對

在這裏插入圖片描述
首先我們要意識到一點:任何一個整數intint都可以看成一個3232位的二進制0101串(數值小就在前面補0,其中int是232-2^{32} ~ 2312^{31},第一位是符號位,所以只需要循環0 ~ 30位即可)。

我們考慮每一個二元組i,j(i,j),且i>ji>j,那麼本題的目標就是想要找到Ai XOR AjA_i\ XOR\ A_j最大值,也就是說,對於每一個i(1<=i<=n)i(1<=i<=n),我們希望找到一個j(1<=j<i)j(1<=j<i),使得Ai XOR AjA_i\ XOR\ A_j的值最大,並輸出這個最大值。

既然可以將這個數轉換爲字符串,那麼就可以用trietrie樹來處理。那麼我們對於每一個數,從高位往低位循環,由於要求異或和,很明顯,異或和爲1那麼值越大,所以我們對於每一位,都儘量選擇與當前的數相反的方向走(因爲是從高位往低位,所以每次貪心1最後的值最大。)我們每一次都走與當前AiA_i這一位相反的位置走,也就是讓XORXOR值最大,如果說沒有路可以走的話,那麼就走相同的路,這個時候XORXOR值不會有任何變化。最後maxmax找到最大的這個值即可。
代碼里加了好多註釋,您應該能看懂。

#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<vector>
#include<bitset>
#include<limits.h>
#define ls (p<<1)
#define rs (p<<1|1)
#define mid (l+r>>1)
#define over(i,s,t) for(register int i=s;i<=t;++i)
#define lver(i,t,s) for(register int i=t;i>=s;--i)
//#define int __int128
using namespace std;
#undef mid
typedef long long ll;
typedef unsigned long long ull;
//typedef pair<int,int> PII;

const int N=1e5+7;
const ll mod=1e9+7;
const ll INF=1e15+7;
const double EPS=1e-10;
const int p=131;//13331

int n,m;
int trie[N*31][2];
int tot=1;
int ed[N];
char s[N];

void inserts(int x){
    int p = 1;
    lver(k,30,0){//int是2^31,有一位是符號位
        int ch = x>>k&1;//去除第k位的值
        if(trie[p][ch] == 0)
            trie[p][ch] = ++tot;
        p = trie[p][ch];
    }
}

int searchs(int x){
    int p = 1,ans = 0;
    lver(k,30,0){
        int ch = x>>k&1;
        if(trie[p][ch^1]){//儘量取反,有就往反方向走
            p = trie[p][ch^1];
            ans |= (1<<k);//都已經往這邊走了,肯定是走與這一位相反的路,所以異或後肯定是1,所以把ans的這一位置爲1即可
        }//ans |= (1<<k);//ans的第k位,0變成1,1還是1.
        else p = trie[p][ch];//無奈地走相同的路,ans不變
    }
    return ans;
}

int a;
int ans;

int main()
{
    scanf("%d",&n);
    over(i,1,n){
        scanf("%d",&a);
        inserts(a);
        ans = max(ans,searchs(a));
    }
    printf("%d\n",ans);
    return 0;
}

3.AcWing 144. 最長異或值路徑(luogu P4551)

題目鏈接:https://www.luogu.com.cn/problem/P4551

在這裏插入圖片描述

三、ACAC自動機!

KMPKMP可以處理一個字符串在一個字符串上的匹配。
trietrie可以處理一個字符串在多個字符串上的匹配。
ACAC自動機可以處理多個字符串在一個字符串上的匹配。
ACAC自動機=KMPKMP+TrieTrie
hhhhhhhh......hhhhhhhh......

掛一個博客就好

%%%yybyyb大佬 tqltql

https://www.cnblogs.com/cjyyb/p/7196308.html

其實到也沒有那麼難

注:如果您通過本文,有(qi)用(guai)的知識增加了,請您點個贊再離開,如果不嫌棄的話,點個關注再走吧,日更博主每天在線答疑 ! 當然,也非常歡迎您能在討論區指出此文的不足處,作者會及時對文章加以修正 !如果有任何問題,歡迎評論,非常樂意爲您解答!( •̀ ω •́ )✧
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章