字典樹簡介
當我們通過字典查一個字或單詞的時候,我們會通過前綴或關鍵字的來快速定位一個字的位置,進行快速查找。
字典樹就是類似字典中索引表的一種數據結構,能夠幫助我們快速定位一個字符串的位置。
字典樹是一種存儲字符串的數據結構,除了根節點,每個節點可以存儲一個字符,從根節點到樹上某一結點的路徑代表一個字符串
在向字典樹中添加字符串的時候,如果一個字符串在某個節點結束,我們可以給這個節點打上一個標記。這樣在后續訪問時就可以知道到此節點為止為以一個完成的字符串。
const int N=1e5+10;
//第一維表示節點標號,第二維表示該節點到下一節點
int nxt[N][26];
//標記數組
bool isend[N];
//根節點和樹的元素個數
int root=0,cnt=0;
字典樹的插入
void insert(char s[],int len)
{int now=0;for(int i=1;i<=len;i++){int x=s[i]-'a';if(!nxt[now][x])nxt[now][x]=++cnt;//當原樹中不存在這一條路徑時,添加節點,創建路徑now=nxt[now][x];}isend[now]=true;
}
?字典樹的查找
bool search(char s[],int len)
{int now=0;for(int i=1;i<=len;i++){int x=s[i]-'a';if(!nxt[now][x])return false;now=nxt[now][x];}return isend[now];
}
?字典樹的刪除的情況比較少見
這時候我們要記錄每個數節點的子樹大小,這里的子樹大小是指在這個節點下有幾個真實存在的字符串。
對于要刪除的字符串,先用查找字符串的方式找到該字符串在字典樹中的最后一個節點,刪除標記,然后回溯整條路徑,如果路徑上節點的子樹大小為零,就可以刪除這個節點
//刪除操作需要有一個e數組
//e[x],表示的是經過x節點有幾個字符串
//在刪除操作時,需要將該字符串經過的路徑e[x]--
//當發現該節點e[x]為1時,就說明只有這一條字符串經過該路徑,將此節點刪除,就將該字符串成功刪除
int e[N]
void insert(char s[],int len)
{int now=0;e[now]++;for(int i=1;i<=len;i++){int x=s[i]-'a';if(!nxt[now][x])nxt[now][x]=++cnt;now=nxt[now][x];e[now]++;}isend[now]=true;
}
void remove(char s[],int len)
{int now=0;e[now]--;for(int i=1;i<=len;i++){int x=s[i]-'a';int tmp=nxt[now][x];if(e[tmp]==1)e[now][x]=0;//刪除操作now=tmp;e[tmp]--;}isend[now]=false;
}
寫一道例題藍橋3755小藍的神秘圖書館
?
?
ac代碼如下
#include<iostream>
using namespace std;
using ll = long long;
const int N = 5e5 + 5;
int nxt[N][26];
ll e[N];
int cnt = 0;
int main()
{ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);int n, m; cin >> n >> m;for (int i = 1; i <= n; i++){string str; cin >> str;int len = str.length();int now = 0;e[now]++;for (int j = 0; j < len; j++){int x = str[j] - 'a';if (!nxt[now][x]){nxt[now][x] = ++cnt;}now = nxt[now][x];++e[now];}}while (m--){string str; cin >> str;int len = str.length();int now = 0;bool flag = true;for (int j = 0; j < len && flag; j++){int x = str[j] - 'a';if (!nxt[now][x]){flag = false;}else now = nxt[now][x];}if (flag){cout <<e[now] << endl;}else cout << 0 << endl;}return 0;
}
?字典樹除了可以對字符串進行前綴判定,還可以對字符串進行排序
原理就是既然字典樹是一顆樹,那我們就可以以遍歷樹的方式來遍歷字典樹,字典樹的每一節點都有26條向下的邊(當然大部分邊是不存在的)我們優先往小的字典序遍歷,進行一次dfs就可以對所有的字符串進行排序了
上例題
?代碼如下
#include<iostream>
#include<vector>
#include<map>
#include<string>
#include<algorithm>
#include<queue>
#include<cstring>
#include<stack>
#include<array>
#include<cmath>
#include<set>
#include<unordered_set>
using namespace std;
using ll = long long;
const ll inf = 0x3f3f3f3f3f3f3f3fll;
const int N = 1e5 + 10;
int nxt[N][26];
int cnt = 0,n;
bool isend[N];
int str[30];
void dfs(int x, int deep)
{if (isend[x]){for (int i = 1; i <= deep; i++)cout << (char)(str[i]+'a');cout << endl;}for (int i = 0; i < 26; i++){if (nxt[x][i]){str[deep + 1] = i;dfs(nxt[x][i], deep + 1);}}
}
int main()
{ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);cin >> n;for (int i = 1; i <= n; i++){string s; cin >> s;int len = s.length();int now = 0;for (int j = 0; j < len; j++){int x = s[j] - 'a';if (!nxt[now][x])nxt[now][x] = ++cnt;now = nxt[now][x];}isend[now] = true;}dfs(0, 0);return 0;
}
最后可以用字典樹來求解最長公共前綴問題
?
以樹的方式來看這個問題,求任意兩個字符串的最長公共前綴,其實就是求解兩個節點的最近公共祖先,那就得到了最長公共前綴
求解LCA問題,依舊可以使用倍增的思想
代碼如下
#include<iostream>
#include<vector>
#include<map>
#include<string>
#include<algorithm>
#include<queue>
#include<cstring>
#include<stack>
#include<array>
#include<cmath>
#include<set>
#include<unordered_set>
using namespace std;
using ll = long long;
const ll inf = 0x3f3f3f3f3f3f3f3fll;
const int N = 1e5 + 10;
int nxt[N][26];
int cnt = 0,n,m;
int ed[N],fa[N][30];
int d[N];
int main()
{ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);cin >> n;for (int i = 1; i <= n; i++){string s; cin >> s;int len = s.length();int now = 0;for (int j = 0; j < len; j++){int x = s[j] - 'a';if (!nxt[now][x]){nxt[now][x] = ++cnt;fa[cnt][0] = now;d[cnt] = d[now] + 1;//記錄深度}now = nxt[now][x];}ed[i] = now;//記錄每一個字符串的結束節點}//倍增求LCAfor (int i = 1; i <= 20; i++){for (int j = 1; j <= cnt; j++){if (fa[j][i - 1]){fa[j][i] = fa[fa[j][i - 1]][i - 1];}}}cin >> m;while (m--){int x, y; cin >> x >> y;x = ed[x], y = ed[y];if (d[x] < d[y])swap(x, y);int z = d[x] - d[y];for (int i = 0; i <= 20&&z; i++, z >>= 1){if (z & 1)x = fa[x][i];}if (x == y){cout << d[x] << endl;continue;}for (int i = 20; i >= 0; i--){if (fa[x][i] != fa[y][i]){x = fa[x][i];y = fa[y][i];}}cout << d[fa[x][0]] << endl;}return 0;
}
?接下來是01trie
01trie是一種二叉樹,每一個葉子節點對應一個二進制數,通過從根出發到該節點的路徑表示,深度小的表示二進制高位
01trie通常用于解決最大異或對問題,即求解a[i]^a[j]的最大值的問題
01trie支持三種操作
1.插入元素x
2.查詢01trie中與x異或后最大的元素
3.查詢比x更大的元素個數,可用于解決逆序對問題
還可以結合二分等算法實現更多操作
這樣就可以創建一個01trie,與字典樹幾乎一樣
01trie的插入
void insert(int x)
{int now=1;e[now]++;for(int i=30;i>=0;i--){ int y=(x>>i)&1;if(!nxt[now][y])nxt[now][y]=++cnt;now=nxt[now][y];e[now]++;}
}
01trie的查詢
//查詢01trie中與x異或的最大值
int query(int x)
{int now=1;int res=0;for(int i=30;i>=0;i--){int y=(x>>i)&1;if(nxt[now][!y])now=nxt[now][!y],res+=(1<<i);else now=nxt[now][y];}return res;
}
//查詢01trie中比x大的數目
int query(int x)
{int res=0;int now=1;for(int i=30;i>=0;i--){int y=(x>>i)&1;if(y==0)res+=e[nxt[now][1]);if(!nxt[now][y])break;now=nxt[now][y];}return res;
}
?下面來寫一道例題藍橋17205卓兒的最大異或和
考慮異或的性質,將前綴異或添加到字典樹中,每次詢問求與當前前綴異或的最大值,就可以詢問到每一個區間,每次取max就能得到答案
ac代碼如下
#include<iostream>
using namespace std;
using ll=long long;
const int N=32*1e5+10;
int nxt[N][2];
int n,cnt=1;
ll pre[N];
void insert(int x)
{int now=1;for(int i=30;i>=0;i--){int y=(x>>i)&1;if(!nxt[now][y])nxt[now][y]=++cnt;now=nxt[now][y];}
}
ll query(ll x)
{int now=1;ll res=0;for(int i=30;i>=0;i--){int y=(x>>i)&1;if(nxt[now][!y])now=nxt[now][!y],res+=(1ll<<i);else now=nxt[now][y];}return res;
}
int main()
{ios::sync_with_stdio(0),cin.tie(0);cin>>n;int x;ll ans=-1;insert(0);for(int i=1;i<=n;i++){cin>>x;pre[i]=pre[i-1]^x;ans=max(ans,query(pre[i]));insert(pre[i]);}cout<<ans<<endl;
}
?接下來是關于01trie刪點求最大異或
藍橋19721最大異或和
考慮枚舉每一個節點,然后刪除與它相鄰的節點,求一次最大異或和,再將點添加回去
ac代碼如下
#include<iostream>
#include<vector>
using namespace std;
const int N=32*1e5+5;
int nxt[N][2];
int e[N];
int n,cnt=1;
void insert(int x)
{int now=1;e[now]++;for(int i=30;i>=0;i--){int y=(x>>i)&1;if(!nxt[now][y])nxt[now][y]=++cnt;now=nxt[now][y];e[now]++;}
}
void remove(int x)
{int now=1;e[now]--;for(int i=30;i>=0;i--){int y=(x>>i)&1;now=nxt[now][y];e[now]--;}
}
int query(int x)
{int res=0;int now=1;for(int i=30;i>=0;i--){int y=(x>>i)&1;if(nxt[now][!y]&&e[nxt[now][!y]])now=nxt[now][!y],res|=(1<<i);else now=nxt[now][y];}return res;
}
int main()
{ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);cin>>n;vector<int>a(n+1);vector<vector<int>> p(n+1,vector<int>());for(int i=1;i<=n;i++){cin>>a[i];insert(a[i]);}for(int i=1;i<=n;i++){int x;cin>>x;if(x!=-1){p[x+1].push_back(i);p[i].push_back(x+1);}}int ans=0;for(int i=1;i<=n;i++){for(auto y:p[i])remove(a[y]);ans=max(ans,query(a[i]));for(auto y:p[i])insert(a[y]);}cout<<ans<<endl;
}
最后來用01trie求逆序對
?
建樹,然后就求出來了
#include<iostream>
#include<vector>
using namespace std;
using ll=long long;
const int N=32*1e5+10;
int nxt[N][2],e[N],n,cnt=1;
void insert(int x)
{int now=1;e[now]++;for(int i=30;i>=0;i--){int y=(x>>i)&1;if(!nxt[now][y])nxt[now][y]=++cnt;now=nxt[now][y];e[now]++;}
}
int query(int x)
{int now=1;int res=0;for(int i=30;i>=0;i--){int y=(x>>i)&1;if(y==0)res+=e[nxt[now][1]];if(!nxt[now][y])break;now=nxt[now][y];}return res;
}
int main()
{ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);cin>>n;ll ans=0;for(int i=1;i<=n;i++){int x;cin>>x;insert(x);ans+=query(x);}cout<<ans<<endl;
}
?歐克了
?
?