Trie(字典樹)


Trie(字典樹)

[字典樹](Trie Tree) 是一種樹形結構,是一種哈希樹的變種。典型應用是用於統計,排序和保存大量的字符串(但不僅限於字符串)。
它的優點是:利用字符串的公共前綴來減少查詢時間,最大限度地減少無謂的字符串比較,查詢效率比哈希樹高。
                                                   ——百度 · 百科

image

image

理解son[] []:存儲子節點的位置,最多26條分支

  • Tire樹本質上一個多叉樹,最多可以分多少叉呢?因為此題存的都是小寫字母,所以是26叉.
  • 這里就解釋了son這個二維數組的第二維的含義,就是他最多有26個孩子,那么他是誰呢,他當然是結點了,那結點之間怎么區分,或者這些孩子的爸爸叫啥,爸爸們用下標來區別,所以第一維就是爸爸們的id,son[0] [1]含義就是0號爸爸有個兒子b ,那son[0] [1] = 2,就是0號爸爸有個兒子b,兒子的id是2; 這些id就是由idx 來統一管理分配

son[0] [u] 這個的含義就是根節點它的孩子u結點的下標,跟單鏈表里的next指針其實是一樣的。
不過有區別的一點是我們的單鏈表它的孩子只有一個,是一條鏈子。
但是我們的Trie是后面可能有26個孩子(因為英文字母只有26個),即有26種路徑可以選的單鏈表。

理解idx:

idx:同鏈表,表示當前要插入的節點是哪一個,每創建一個節點idx+1(idx 當前用到了哪個下標 初值為0 即為空節點 也是根節點)

acwing:Trie統計字符串

維護一個字符串集合,支持兩種操作:

  1. I x 向集合中插入一個字符串 xx;
  2. Q x 詢問一個字符串在集合中出現了多少次。

共有 NN 個操作,輸入的字符串總長度不超過 105105,字符串僅包含小寫英文字母。

輸入格式

第一行包含整數 NN,表示操作數。

接下來 NN 行,每行包含一個操作指令,指令為 I xQ x 中的一種。

輸出格式

對於每個詢問指令 Q x,都要輸出一個整數作為結果,表示 xx 在集合中出現的次數。

每個結果占一行。

數據范圍

1≤N≤2∗1041≤N≤2∗104

輸入樣例:

5
I abc
Q abc
Q ab
I ab
Q ab

輸出樣例:

1
0
1

#include<iostream>

using namespace std;

const int N = 1e5 + 10;
// son[N][26] 存儲子節點的位置,最多26條分支
// cnt[] 存儲以某節點為結尾的字符串的個數(同時起到標記作用)
// idx:同鏈表,表示當前要插入的節點是哪一個,每創建一個節點idx+1(idx 當前用到了哪個下標 初值為0 即為空節點 也是根節點)
int son[N][26], cnt[N], idx;
char str[N];

void insert(char *str)
{
    int p = 0; // p是從根節點開始遍歷的
    for(int i = 0; str[i]; i++) // 字符串以'/0'結尾
    {
        int u = str[i] - 'a'; // 將字符映射為數字(字典樹)0~25
        if(son[p][u] == 0) son[p][u] = ++idx; // 如果該元素還沒有創建則創建節點
        p = son[p][u]; // 指針p移動
    }
    
    // 插入完之后p指向了該字符串的最后一個元素(節點)
    cnt[p] ++; // 結束時的標記,也是記錄以此節點結束的字符串個數
}

int query(char *str)
{
    int p = 0; // 查詢也是從根節點開始的
    for(int i = 0; str[i]; i++)
    {
        int u = str[i] - 'a';
        if(son[p][u] == 0) return 0; // 樹中未找到該元素,則表明無此串
        p = son[p][u]; // 找到則p移動
    }
    // 最后都存在(找到)返回該串出現的次數
    return cnt[p];
    
}

int main()
{       
    int n;
    cin>>n;
    while(n --)
    {
        string opt;
        cin>> opt >> str;
        if(opt == "I") insert(str);
        else cout<< query(str) << endl;
        
    }
    return 0;
}

acwing:最大異域對

在給定的 NN 個整數 A1,A2……ANA1,A2……AN 中選出兩個進行 xorxor(異或)運算,得到的結果最大是多少?

輸入格式

第一行輸入一個整數 NN。

第二行輸入 NN 個整數 A1A1~ANAN。

輸出格式

輸出一個整數表示答案。

數據范圍

1≤N≤1051≤N≤105,
0≤Ai<2310≤Ai<231

輸入樣例:

3
1 2 3

** 輸出樣例:**

3

字典樹不光可以用來存儲字符,還可能存儲整數!

1、 son數組定義是二維數組,son[ n ] [ m ]我在初學Trie樹的時候很難理解,可以先理解它的第二維度,只有兩種狀態0/1,是因為這一位表示的是某個數字的的某一位是0 / 1,而第一維的大小是我們用的十位二進制表示下一共有多少位數,在本題中,數字一共有N個數字,N是小於10^5的,一個數在int下是32位,則我們需要至少3200000的一維坐標,而p = son[n] [0] / p = son[n] [1]其實存的值就是他的兩個子節點的一維坐標的值。
那么x >> i & 1 其實就是我想知道x的二進制表示中的第i位(二進制位從第0位開始表示第0位 - 第 31 位),本題的題目范圍到2^31那么就是i從30 - 0。

2、構建邏輯其實相對簡單,就是將數的二進制表示插入到son數組中,如果沒有那么就將他的值附上++idx即可
3、查詢的時候為了異或大小最大,那么就應該從根節點開始遍歷最高位開始盡量讓異或結果為1,因此在遍歷查詢數字的時候,盡量找與它自己不相同的數。取到第i位的時候第i位的值為u,查看son[u] [!s](不同方向上)是否存在,如果存在那么將res = (res << 1) + !u(這一位異或存在最大值為1),如果沒有那么就讓p = son[p] [u]。

暴力:超時

【參考代碼】

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1e5 + 10;
const int M = N * 2;
int son[M][2], a[N];
int idx;

int main()
{
    int n, res = 0;
    cin >> n;
    for (int i = 0; i < n; i ++ ) cin >> a[i];
    
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < i; j ++ )
            res = max(res, a[i] ^ a[j]);
    cout << res;
    
    return 0;
}

字典樹:O(n*logn)

【參考代碼】

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010, M = 3100010;

int n;
int a[N], son[M][2], idx;


void insert(int x) // 插入操作同字符串
{
    int p = 0;
    for(int i = 30; i >= 0; i--)
    {
        int u = x >> i & 1;
        if(son[p][u] == 0) son[p][u] = ++ idx;
        p = son[p][u];
    }
}

int query(int x)
{
    int p = 0, res = 0;
    for(int i = 30; i >= 0; i--)
    {
        int u = x >> i & 1;
        
        if(son[p][!u])  //如果另外一個方向存在的話就走到另外一個方向上去
        {
            p = son[p][!u];
            res = (res << 1) + !u; // 左移一位然后加上當前位的數
        }
        else //如果另外一個方向不存在的就只能接着往下走(找不到只好將就了)
        {
            p = son[p][u];
            res = (res << 1) + u; // 左移等價於乘2 : res = res * 2 + u
        }
    }
    
    // 返回整個路徑記錄的答案
    return res;
}

int main()
{
    cin>>n;
    for (int i = 0; i < n; i ++ ) cin>>a[i];
    
    int ans = 0;
    for(int i = 0; i < n; i++)
    {
        // 為了避免邊界條件的判斷(樹為空時),因此我們先插入再查詢與當前a[i]最匹配的數
        insert(a[i]);
        int t = query(a[i]);
        // 邊插入邊查詢
        ans = max(ans, a[i] ^ t);
    }
    
    cout << ans;
    
    return 0;
}

關於res = (res << 1) + !u理解:res是用來記錄最好配對數的路徑,例如,假設當前res的路徑res = 1101,如果下一位的數是1,更新res,則將1101左移一位(11010),然后加上1(11011);如果下一位的數是0,更新res,則將1101左移一位(11010),然后加上0(11010)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM