Tải bản đầy đủ (.doc) (27 trang)

Cây tiền tố (Trie) và ứng dụng của cây tiền tố trong các bài toán

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (228.37 KB, 27 trang )

Cây tiền tố
I. MỞĐẦU
Cây tiền tố (trie)được sử dụng để quản lý và phục vụ tìm kiếm nhanh các đối tượng có phần
đầu giống nhau.Ví dụ, khi tìm kiếm trên google.com, nếu ta gõ vào cửa sổ tìm kiếm ký tự “O”
hệ thống sẽ đưa ra ra một số đối tượng bắt đầu bằng chữ cái O: “Ola”, “Ongame”, “One
piece”, “Oggy”, … Nếu ta gõ tiếp “Ol” thì thông tin đưa ra sẽ là: “Olympus”, “Ola”, “Ola
me”, “Ole”, … Khi gõ “Oly” sẽ có nội dung gợi ý lựa chọn: “Olympus”, “Olympic”,
“Olympia”, “Olympus has fallen”, … Nếu nội dung gõ trong thanh tìm kiếm là “Olympiad in”
thì ở cửa sổ dự báo sẽ có nội dung: “Olympiad india”, “Olympiad in informatics”, “Olympiad
inequalities”, “Olympiad in chippenham”, …
Cây tiền tố gồm một gốc không chứa thông tin, trên mỗi cạnh lưu một ký tự, mỗi nút và
đường đi từ gốc đến nút đó thể hiện một xâu, gồm các ký tự là các ký tự thuộc cạnh trên
đường đi đó.
Việc tạo và xử lý cây tiền tố là nội dung chủ yếu của các hệ quản trị cơ sở dữ liệu văn
bản.Dưới đây ta sẽ xét một số phép xử lý đặc thù trên cây tiền tố thông qua một số bài toán cụ
thể.

II. ỨNG DỤNG CỦA CÂY TIỀN TỐ TRONG CÁC BÀI TOÁN
1. Bài toán 1:Tìm kiếm nhiều mẫu
1.1.

Đề bài (Nguồn: Truyền thống)

Cho xâu t và tập các xâu mẫu s1, s2, …, sn. Hãy đếm số lần xuất hiện của các xâu mẫu trong
xâu t.
Dữ liệu: Dòng đầu tiên chứa xâu t có độ dài không vượt quá 5×105. Dòng thứ hai ghi số
nguyên n (1 ≤ n ≤ 100). Dòng thứ i trong n dòng tiếp theo chứa xâu si có độ dài không vượt
quá 15. Các xâu si chỉ chứa các chữ cái Latin thường ‘a’..‘z’ và hoa ‘A’..‘Z’, các chữ số
‘0’..’9’ và các ký hiệu “!?.,:;-_’#$%&/=*+(){}[]”. Xâu t cũng chứa các ký tự đó và
thêm ký tự dấu cách. Chú ý rằng có thể có một số xâu mẫu giống nhau và mỗi vị trí nó xuất
hiện trong xâu t ta chỉ đếm 1 lần.


Kết quả: Ghi ra số lần xuất hiện của các xâu mẫu s1, s2, …, sn trong xâu t.
Ví dụ:
find_patterns.inp
shers
5
he
she
his
he
hers

find_patterns.out
3

Các xâu mẫu xuất hiện 3 lần trong xâu t là: shers, shers, shers.
1


1.2.

Thuật toán

Chúng ta đã biết một số thuật toán tìm kiếm một mẫu trong một xâu như thuật toán RabinKarp (RK), thuật toán Knuth-Moris-Pratt (KMP). Trong bài này, chúng ta sẽ xét thuật toán
tìm kiếm nhiều mẫu trong một xâu.
Rõ ràng một phương pháp đơn giản để giải bài toán trênlà ta áp dụng thuật toán Rabin-Karp
hoặc thuật toán Knuth-Moris-Pratt với lần lượt từng xâu mẫu.Thuật toán theo cách này có độ
phức tạp là O(n×m), ở đón là số xâu mẫu vàm là độ dài của xâu t.
Một thuật toán hiệu quả để giải quyết bài toán tìm kiếm nhiều mẫu được phát minh bởi Alfred
V. Aho và Margaret J. Corasick và gọi là thuật toán tìm kiếmnhiều mẫu Aho-Corasick, được
mô tả như sau.

Đầu tiên chúng ta tạo một trie biểu diễn tập các xâu mẫu:
• Mỗi cạnh của trie có nhãn là một ký tự.
• Hai cạnh đi ra từ một nút có nhãn khác nhau.
• Nhãn của một nút v là ghép các nhãn của các cạnh trên đường đi từ nút gốc tới nút v
và kí hiệu là L(v). Khi đó với mỗi xâu mẫu si, tồn tại một nút v với L(v) = si.
• Nhãn L(v) của một nút lá v bất kỳ là bằng một xâu mẫu si nào đó.
Ví dụ cây trie với các xâu mẫu là “he”, “she”, “his”, “hers” như sau:
h

0

e

1

“he”
r
2

i
s

s

6
3

h

e


4

8

s

9 “hers”

7 “his”

5 “she”

Đoạn chương trình dưới đây cài đặt việc tạo trie. Giả thiết cho thấy tổng độ dài của các xâu
mẫu không lớn hơn 15×100=1500 nên ta có các khai báo biến và cài đặt như sau.
int n, to[1501][128], link[1501], word[1501], sz = 0;
bool leaf[1501];
void init_trie() {
memset(to, 0, sizeof(to));
memset(leaf, false, sizeof(leaf));
memset(link, 0, sizeof(link));
memset(word, 0, sizeof(word));
}
void add_str(string s, int id) {
int u = 0;
for (int i = 0; i < s.size(); i++) {
int j = s[i];
if (to[u][s[i]] == 0) to[u][j] = ++sz;
u = to[u][j];
}

leaf[u] = true;
// nut u nay la diem cuoi cua mot mau
word[u] = id;
// ghi nhan chi so cua mau
}

2


Bây giờ giả sử ta cần tìm kiếm các mẫu trong xâu “shers”. Ta bắt đầu từ nút 0 và đi theo cạnh
‘s’ tới nút 3, tiếp theo từ nút 3 đi theo cạnh ‘h’ tới nút 4, rồi từ nút 4 đi theo cạnh ‘e’ tới nút 5.
Vì nút 5 là nút lá nên ta ghi nhận có một vị trí xuất hiện mẫu (chú ý đây là vị trí cuối của mẫu
xuất hiện). Nhưng từ nút 5 lại không có cạnh ‘r’ đi ra, vì vậy ta cần phải quay lui lại phía
trước xem có mẫu nào khác xuất hiện không. Việc quay lui lại là không phải từ đầu mà chỉ
quay lui lại nút có nhãn là phần hậu tố dài nhất của nút 5, tức là nút 2. Nhưng nút 2 lại là nút
lá nên ta lại ghi nhận thêm một vị trí nữa mẫu xuất hiện. Sau đó từ nút 2 ta lại đi theo cạnh ‘r’
tới nút 8, rồi từ nút 8 đi theo cạnh ‘s’ tới nút 9. Nút 9 là nút lá nên ta lại ghi nhận thêm một vị
trí nữa mẫu xuất hiện.Đến đây ta dừng việc tìm kiếm vì đã xét đến cuối xâu.
Như vậy với mỗi nút u ta cần biết nút v sao cho nhãn của nút v là phần hậu tố dài nhất của
nhãn nút u và ta đặt link[u] = v. Các đường nét đứt trong trie dưới đây là link của mỗi nút.
h

0

e

1

“he”
r

2

i
s

s

6
3

h

8

e

4

s

9 “hers”

7 “his”
5 “she”

Thủ tục push_link dưới đây sẽ xác định link cho mỗi nút của trie:
void push_link() {
queue<int> q;
q.push(0);
while (!q.empty()) {

int u = q.front(); q.pop();
int v = link[u];
leaf[u] |= leaf[v];
for (int i = 0; i < 128; i++)
if (to[u][i] != 0) {
link[to[u][i]] = ((u != 0) ? to[v][i] : 0);
q.push(to[u][i]);
}
else
to[u][i] = to[v][i];
}
}

Hàm search_str sau thể hiện việc tìm kiếm các mẫu và trả lại số mẫu xuất hiện trong xâu.
int search_str(string s) {
int cnt = 0, u = 0;
for (int i = 0; i < s.size(); i++) {
u = to[u][s[i]];
int v = u;
while (leaf[v]) {
if (word[v] != 0) cnt++;
v = link[v];
}
}
return cnt;
}

3



Thuật toán Aho-Corasick có độ phức tạp thời gian là O(z+n+m), ở đó z là tổng độ dài các xâu
mẫu,n là số mẫu vàm là độ dài xâu.
1.3.

Chương trình

#include <bits/stdc++.h>
using namespace std;
int n, to[1501][128], link[1501], word[1501], sz = 0;
bool leaf[1501];
string t;
void init_trie() {
memset(to, 0, sizeof(to));
memset(leaf, false, sizeof(leaf));
memset(link, 0, sizeof(link));
memset(word, 0, sizeof(word));
}
void add_str(string s, int id) {
int u = 0;
for (int i = 0; i < s.size(); i++) {
int j = s[i];
if (to[u][s[i]] == 0) to[u][j] = ++sz;
u = to[u][j];
}
leaf[u] = true;
word[u] = id;
}
void push_link() {
queue<int> q;
q.push(0);

while (!q.empty()) {
int u = q.front(); q.pop();
int v = link[u];
leaf[u] |= leaf[v];
for (int i = 0; i < 128; i++)
if (to[u][i] != 0) {
link[to[u][i]] = ((u != 0) ? to[v][i] : 0);
q.push(to[u][i]);
}
else
to[u][i] = to[v][i];
}
}
int search_str(string s) {
int cnt = 0, u = 0;
for (int i = 0; i < s.size(); i++) {
u = to[u][s[i]];
int v = u;
while (leaf[v]) {
if (word[v] != 0) cnt++;
v = link[v];
}
}
return cnt;
}
int main () {
freopen("find_patterns.inp", "r", stdin);
freopen("find_patterns.out", "w", stdout);

4



init_trie();
getline(cin, t);
scanf("%d\n", &n);
for (int i = 1; i <= n; i++) {
char s[15];
scanf("%s\n", &s);
add_str(s, i);
}
push_link();
printf("%d\n", search_str(t));
return 0;

}

1.4.

Test

Đường dẫn tải test cho bài toán:
/>1.5.

Cảm nhận

Bài toán tìm kiếm nhiều mẫu là bài toán hết sức kinh điển. Rõ ràng là có nhiều thuật toán
cùng giải quyết bài toán với độ phức tạp khác nhau. Thuật toán Aho-Corasick đã sử dụng cấu
trúc cây tiền tố (trie) để giải quyết với độ phức tạp tuyến tính.
Thuật toán tìm kiếm nhiều mẫu được áp dụng rất nhiều trong thực tế. Chẳng hạn ta có một
danh sách các mẫu virus máy tính và ta cần xem mỗi tệp nào bị nhiễm các virus nào. Hay là

trong bài toán sinh học, ta cần xem các gen nào có trong DNA, …

2. Bài toán 2: Tách từ
2.1.

Đề bài (Nguồn: Croatia 2006 / Final Exam #1)

Một xâu cần được chia thành các đoạn con mà mỗi đoạn con thuộc một tập các từ cho trước.
Bạn hãy viết chương trình tính số cách khác nhau để chia một xâu cho trước. Vì số cách chia
có thể rất lớn nên bạn chỉ cần đưa số dư của nó khi chia cho 1337377.
Dữ liệu: Dòng đầu tiên chứa một xâu với độ dài tối đa 300.000 ký tự. Dòng thứ hai chứa một
số nguyên N (1 ≤ N ≤ 4000). Mỗi dòng trong N dòng tiếp theo chứa một từ trong tập. Mỗi từ
gồm nhiều nhất 100 ký tự. Không có hai từ nào giống nhau và tất cả các ký tự là các chữ cái
tiếng Anh in thường.
Kết quả: Chứa một số nguyên là số cách chia xâu thành các từ theo modun 1337377.
Ví dụ:
tach_tu.inp
abcd
4
a
b
cd
ab
afrikapaprika

tach_tu.out
2

1


5


4
afr
ika
pap
r
ababababababababababababababababababababab
3
a
b
ab

2.2.

759775

Thuật toán

Gọi t là xâu cần đếm số các tách thành các từ và các từ là s1, s2, …, sN. Giả thiết chỉ số các ký
tự của xâu được đánh chỉ số bắt đầu từ 0. Gọi cnt[i] là số cách tách xâu t[0..i] thành các từ. Ta
sẽ tínhcnt[i] như sau. Xét các từ s1, s2, …, sN, nếu từ thứ j là hậu tố của xâu t[0..i] thì ta công
thêm vào cnt[i] một lượng là cnt[i-s[j].size()], tức là:
N

cnt[i ] = ∑ cnt [ i − s[j ].size() ]
j =1

nếu xâu sj là một hậu tố của xâu t[0..i].

Bây giờ vấn đề là kiểm tra xem từ nào là hậu tố của xâu t[0..i]. Để làm điều này, ta xây dựng
một trie cho các từ và tìm kiếm xâu t trên trie. Khi xét đến kí tự thứ i của xâu t, ta xem nút
tương ứng với ký tự này trên trie có là nút lá hay không. Nếu nó là nút lá, tức là có một từ
xuất hiện ở phần hậu tố của xâu t[0..i] và khi đó ta tính cnt[i] theo công thức trên.
Câu trả lời của bài toán là cnt[t.size()-1].
Độ phức tạp của thuật toán là O(N×100+N+ t.size()).
2.3.

Chương trình

#include <bits/stdc++.h>
using namespace std;
const int MOD = 1337377;
string t;
int n, m, len[4001], to[400001][26], link[400001], word[400001], sz = 0,
cnt[300001];
bool leaf[400001];
void init_trie() {
memset(to, 0, sizeof(to));
memset(leaf, false, sizeof(leaf));
memset(link, 0, sizeof(link));
memset(word, 0, sizeof(word));
}
void add_str(string s, int id) {
int u = 0;
for (int i = 0; i < s.size(); i++) {
int j = s[i]-'a';
if (to[u][j] == 0) to[u][j] = ++sz;
u = to[u][j];
}

leaf[u] = true;
word[u] = id;

6


}
void push_link() {
queue<int> q;
q.push(0);
while (!q.empty()) {
int u = q.front(); q.pop();
int v = link[u];
leaf[u] |= leaf[v];
for (int i = 0; i < 26; i++)
if (to[u][i] != 0) {
link[to[u][i]] = ((u != 0) ? to[v][i] : 0);
q.push(to[u][i]);
}
else
to[u][i] = to[v][i];
}
}
int search_str(string s) {
int u = 0;
for (int i = 0; i < s.size(); i++) {
u = to[u][s[i]-'a'];
int v = u;
cnt[i] = 0;
while (leaf[v]) {

if (word[v] != 0)
if (i-len[word[v]] == -1)
cnt[i] = (cnt[i] + 1) % MOD;
else
cnt[i] = (cnt[i] + cnt[i-len[word[v]]]) % MOD;
v = link[v];
}
}
}
int main () {
freopen("tach_tu.inp", "r", stdin);
freopen("tach_tu.out", "w", stdout);
char s[300001];
scanf("%s\n%d\n", &s, &n);
t = s;
init_trie();
for (int i = 1; i <= n; i++) {
scanf("%s\n", &s);
add_str(s, i);
len[i] = strlen(s);
}
push_link();
search_str(t);
cout << cnt[t.size()-1] << "\n";
return 0;
}

2.4.

Test


Đường dẫn tải test cho bài toán:
/>2.5.

Cảm nhận

7


Cấu trúc dữ liệu cây tiền tố được dùng trong các bài xử lý xâu, đặc biệt là việc tìm nhanh xem
một tập các xâu có xuất hiện trong một xâu cho trước và ở các vị trí nào. Bài toán trên là ứng
dụng của cấu trúc cây tiền tố để đếm số cách tạo một xâu từ các xâu mẫu.

3. Bài toán 3: Mật khẩu
3.1.

Đề bài (Nguồn: Sưu tầm)

Người quản trị đang cố gắng xây dựng hệ thống bảo vệ dữ liệu cho công ty. Họ biên soạn một
cuốn từ điển gồm M (1 ≤ M ≤ 300) từ khác nhau, mỗi từ chỉ chứa các chữ cái Latin in thường
và có độ dài không vượt quá 300. Mật khẩu được tạo ra như bằng cách móc nối các từ trong
từ điển, một từ có thể xuất hiện nhiều lần. Để giữ bí mật, người dùng có thể thêm các chữ cái
in thường vào trước, sau hoặc giữa các từ trong từ điển, tạo thành mật khẩu P là từ có chiều
dài N (5 ≤ N ≤ 300.000).
Bạn hãy viết chương trình đọc vào từ P có chiều dài N, xác định số lượng tối thiểu các chữ cái
cần loại bỏ khỏi Pđể có được một từ mới là từ ghép của một số từ trong từ điển.
Ví dụ với từ “afbachtdspya”, cần loại bỏ các chữ cái ở vị trí in đậm để nhận được từ “abacha”
là ghép của các từ trong từ điển: “a”, “bach”, “a”.
Dữ liệu: Dòng đầu tiên ghi hai số nguyên N và M. Dòng thứ hai ghi từ P. Mỗi dòng trong số
M dòng tiếp theo sẽ ghi một từ trong từ điển.

Kết quả: Ghi ra số lượng chữ cái tối thiểu cần xóa.
Ví dụ:
mat_khau.inp
12 5
afbachtdspya
aba
a
bach
dsy
zero
33 5
throughthestormwereachtheshoreyou
rough
the
storm
reach
shore

3.2.

mat_khau.out
6

7

Thuật toán

Bài toán này về cơ bản là giống bài toán “tách từ” ở trên. Ở đây ta gọi cnt[i] là số lượng tối
thiểu các chữ cái cần loại bỏ khỏi xâuP[0..i] để có được một từ mới là từ ghép của một số từ
trong từ điển. Vì vậy nếu xâu sj là một hậu tố của xâu P[0..i] thì ta có:

cnt[i] = min(cnt[i - s[j].size()).
Câu trả lời của bài toán là cnt[N-1].

8


3.3.

Chương trình

#include <bits/stdc++.h>
using namespace std;
int n, m, len[301],
cnt[300001];
bool leaf[90001];
string p;

to[90001][26],

link[90001],

word[90001],

void init_trie() {
memset(to, 0, sizeof(to));
memset(leaf, false, sizeof(leaf));
memset(link, 0, sizeof(link));
memset(word, 0, sizeof(word));
}
void add_str(string s, int id) {

int u = 0;
for (int i = 0; i < s.size(); i++) {
int j = s[i]-'a';
if (to[u][j] == 0) to[u][j] = ++sz;
u = to[u][j];
}
leaf[u] = true;
word[u] = id;
}
void push_link() {
queue<int> q;
q.push(0);
while (!q.empty()) {
int u = q.front(); q.pop();
int v = link[u];
leaf[u] |= leaf[v];
for (int i = 0; i < 26; i++)
if (to[u][i] != 0) {
link[to[u][i]] = ((u != 0) ? to[v][i] : 0);
q.push(to[u][i]);
}
else
to[u][i] = to[v][i];
}
}
int search_str(string s) {
int u = 0;
for (int i = 0; i < s.size(); i++) {
u = to[u][s[i]-'a'];
int v = u;

cnt[i] = ((i == 0) ? 1 : cnt[i-1]+1);
while (leaf[v]) {
if (word[v] != 0)
cnt[i] = min(cnt[i], cnt[i-len[word[v]]]);
v = link[v];
}
}
}
int main () {
freopen("mat_khau.inp", "r", stdin);
freopen("mat_khau.out", "w", stdout);
scanf("%d%d\n", &n, &m);

9

sz

=

0,


}

3.4.

char s[300001];
scanf("%s\n", &s);
p = s;
init_trie();

for (int i = 1; i <= m; i++) {
scanf("%s\n", &s);
add_str(s, i);
len[i] = string(s).size();
}
push_link();
search_str(p);
printf("%d\n", cnt[n-1]);
return 0;

Test

Đường dẫn tải test cho bài toán:
/>3.5.

Cảm nhận

Bài toán trên là ứng dụng của thuật toán quy hoạch động với cấu trúc dữ liệu cây tiền tố.

4. Bài toán 4: Biểu tượng cảm xúc
4.1.

Đề bài (Nguồn: The 2007 ACM South American Programming Contest)

Biểu tượng cảm xúc được sử dụng trong chat và e-mail để thể hiện những cảm xúc mà các từ
rất khó diễn tả. Có rất nhiều biểu tượng cảm xúc đẹp, nhưng cũng có một số biểu tượng cảm
xúc xấu khiến một số người cảm thấy thực sự khó chịu.
George là một trong số những người như vậy. Anh ta không thích các biểu tượng cảm xúc
xấu, vì vậy anh ta đang chuẩn bị một kế hoạch để loại bỏ tất cả các biểu tượng cảm xúc xấu
trong tất cả các e-mail của mình. Bạn hãy giúp anh ta làm việc này.

Chương trình của bạn được cho trước danh sách các biểu tượng cảm xúc xấu. Mỗi biểu tượng
cảm xúc là một xâu ký tự không chứa ký tự dấu cách. Bạn cũng được cho trước các dòng của
văn bản. Bạn cần thay đổi tối thiểu một số ký tự của văn bản thành dấu cách sao cho văn bản
không còn biểu tượng cảm xúc xấu nào. Một biểu tượng cảm xúc được xem xuất hiện trong
văn bản nếu nó xuất hiện trên một dòng và được tạo thành bằng ký tự liên tiếp.
Dữ liệu: Dòng đầu tiên chứa số hai số nguyên N và M (1 ≤ N, M ≤ 100) tương ứng là số biểu
tượng cảm xúc xấu và số dòng trong văn bản. Mỗi dòng trong số N dòng tiếp theo chứa một
xâu không rỗng (tối đa 15 ký tự) mô tả một biểu tượng cảm xúc xấu. Mỗi dòng trong số M
dòng tiếp theo chứa một dòng của văn bản (tối đa 80 ký tự). Các ký tự hợp lệ với biểu tượng
cảm xúc là các chữ cái tiếng Anh in thường và hoa, các chữ số và các ký hiệu “!?.,:;_’#$%&/=*+(){}[]”. Mỗi dòng của văn bản cũng chứa các ký tự như trong biểu tượng
cảm xúc và thêm ký tự dấu cách.
Kết quả: Ghi ra một số nguyên là số ký tự tối thiểu trong văn bản thay đổi thành dấu cách sao
cho văn bản không còn biểu tượng cảm xúc xấu nào.

10


Ví dụ:
emoticons.inp
4 6
:-)
:-(
(-:
)-:
Hello uncle John! :-) :-D
I am sad or happy? (-:-(?
I feel so happy, my head spins
(-:-)(-:-)(-:-)(-:-) :-) (-: :-)
but then sadness comes :-(
Loves you, Joanna :-)))))

3 1
:)
):
))
:):)):)):)):(:((:(((:):)

4.2.

emoticons.out
11

8

Thuật toán

Duyệt các mẫu xuất hiện trong văn bản theo vị trí cuối của mẫu xuất hiện theo thứ tự tăng. Ta
sẽ thay ký tự cuối cùng của mẫu bằng dấu cách, việc này sẽ xóa được nhiều biểu tượng xấu
nhất có thể.
Vì vậy theo thuật toán Aho-Corasick, khi tìm thấy vị trí cuối một mẫu xuất hiện ta sẽ tăng
biến đếm số ký tự cần xóa lên 1 và tìm kiếm tiếp tục từ vị trí tiếp theo, tức là trạng thái mới u
= 0.
4.3.

Chương trình

#include <bits/stdc++.h>
using namespace std;
int n, m, to[1501][128], link[1501], word[1501], sz = 0, ans = 0;
bool leaf[1501];
string t;

void init_trie() {
memset(to, 0, sizeof(to));
memset(leaf, false, sizeof(leaf));
memset(link, 0, sizeof(link));
memset(word, 0, sizeof(word));
}
void add_str(string s, int id) {
int u = 0;
for (int i = 0; i < s.size(); i++) {
int j = s[i];
if (to[u][s[i]] == 0) to[u][j] = ++sz;
u = to[u][j];
}
leaf[u] = true;
word[u] = id;
}
void push_link() {
queue<int> q;
q.push(0);

11


while (!q.empty()) {
int u = q.front(); q.pop();
int v = link[u];
leaf[u] |= leaf[v];
for (int i = 0; i < 128; i++)
if (to[u][i] != 0) {
link[to[u][i]] = ((u != 0) ? to[v][i] : 0);

q.push(to[u][i]);
}
else
to[u][i] = to[v][i];
}
}
int search_str(string s) {
int cnt = 0, u = 0;
for (int i = 0; i < s.size(); i++) {
u = to[u][s[i]];
int v = u;
if (leaf[v]) {
cnt++;
u = 0;
}
}
return cnt;
}
int main () {
freopen("emoticons.inp", "r", stdin);
freopen("emoticons.out", "w", stdout);
scanf("%d%d\n", &n, &m);
init_trie();
for (int i = 1; i <= n; i++) {
char s[15];
scanf("%s\n", &s);
add_str(s, i);
}
push_link();
for (int i = 1; i <= m; i++) {

getline(cin, t);
ans += search_str(t);
}
printf("%d\n", ans);
return 0;
}

4.4.

Test

Đường dẫn tải test cho bài toán:
/>4.5.

Cảm nhận

Ứng dụng của cây tiền tố trong các bài toán xử lý xâu.

5. Bài toán 5: Kiểm duyệt
5.1.

Đề bài (Nguồn: USACO 2015)

12


Bác nông dân John đã đặt mua tạp chí Good Hooveskeeping cho các con bò của mình, vì vậy
chúng có rất nhiều tài liệu để đọc trong khi chờ đợi để vắt sữa. Thật không may trên các tạp
chí có một số từ mà bác John không muốn các con bò của mình nhìn thấy.
Bác John đãđưa tất cả các văn bản từ các tạp chí để tạo ra xâu S. Bác có một danh sách các từ

bị kiểm duyệt t1, t2, ..., tN, tức là bác muốn xóa chúng khỏi S. Để làm như vậy, bác John tìm sự
xuất hiện đầu tiên của một từ bị kiểm duyệt trong S (có chỉ số bắt đầu sớm nhất) và loại bỏ nó
ra khỏi S. Sau đó bác lặp lại quá trình này một lần nữa, xóa sự xuất hiện đầu tiên của một từ bị
kiểm duyệt khỏi S, lặp lại cho đến khi không xuất hiện một từ bị kiểm duyệt nào trong S. Lưu
ý rằng việc xóa một từ bị kiểm duyệt có thể tạo ra một sự xuất hiện mới của một từ bị kiểm
duyệt mà không tồn tại trước đó.
Bác John lưu ý không có từ bị kiểm duyệt nào xuất hiện như là một xâu con của một từ bị
kiểm duyệt khác. Vì vậy từ bị kiểm duyệt với chỉ số sớm nhất trong Sđược xác định duy nhất.
Hãy giúp bác John xác định nội dung cuối cùng của S sau khi hoàn tất kiểm duyệt.
Dữ liệu: Dòng đầu tiên chứa xâu S bao gồm các chữ cái tiếng Anh thường (‘a’..’z’) và có độ
dài tối đa là 105. Dòng thứ hai chứa số nguyên dương N là số từ bị kiểm duyệt. Dòng thứ i
trong N dòng tiếp theo chứa từ bị kiểm duyệt ti. Các từ bị kiểm duyệt chỉ chứa các chữ cái
tiếng Anh thường (‘a’..’z’) và tổng độ dài của tất cả các từ bị kiểm duyệt không vượt quá 10 5.
Kết quả: Ghi ra xâu S cuối cùng sau khi hoàn tất kiểm duyệt. Dữ liệu vào đảm bảo xâu S sẽ
khác rỗng sau khi hoàn tất kiểm duyệt.
Ví dụ:
kiem_duyet.inp
begintheescapexecutionatthebreakofdawn
2
escape
execution

5.2.

kiem_duyet.out
beginthatthebreakofdawn

Thuật toán

Ta sẽ xây dựng xâu kết quảR với lần lượt từng ký tự một từ xâu S. Mỗi khi xuất hiện 1 từ bị

kiểm duyệt thì ta sẽ xóa nó khỏi R. Từ kiểm duyệt này xuất hiện ở cuối xâu R nên việc xóa nó
sẽ nhanh(ta dùng hàm thành phần resize của string).
Như vậy từ bị kiểm duyệt sẽ xuất hiện ở vị trí cuối nên ta dùng thuật toán Aho-Corasick với
cấu trúc trie. Khi xóa từ bị kiểm duyệt, ta cần trở lại trạng thái trước đó. Vì vậy cần lưu trạng
thái khi xử lý từng ký tự một để quay lui khi xóa từ bị kiểm duyệt.
5.3.

Chương trình

#include <bits/stdc++.h>
using namespace std;
int
n,
to[100001][26],
status[100001], sz = 0;
bool leaf[100001];
string s, r = "";
char t[100001];

link[100001],

void init_trie() {
memset(to, 0, sizeof(to));

13

word[100001],

len[100001],



}

memset(leaf, false, sizeof(leaf));
memset(link, 0, sizeof(link));
memset(word, 0, sizeof(word));

void add_str(string s, int id) {
int u = 0;
for (int i = 0; i < s.size(); i++) {
int j = s[i]-'a';
if (to[u][j] == 0) to[u][j] = ++sz;
u = to[u][j];
}
leaf[u] = true;
word[u] = id;
len[id] = s.size();
}
void push_link() {
queue<int> q;
q.push(0);
while (!q.empty()) {
int u = q.front(); q.pop();
int v = link[u];
leaf[u] |= leaf[v];
for (int i = 0; i < 26; i++)
if (to[u][i] != 0) {
link[to[u][i]] = ((u != 0) ? to[v][i] : 0);
q.push(to[u][i]);
}

else
to[u][i] = to[v][i];
}
}
void search_str(string s) {
int u = 0;
for (int i = 0; i < s.size(); i++) {
u = to[u][s[i]-'a'];
r += s[i];
status[r.size()] = u; // luu trang thai khi xu ly den ki tu nay
int v = u;
// tren xau r
while (leaf[v]) {
if (word[v] != 0) {
r.resize(r.size()-len[word[v]]);
u = status[r.size()]; // quay lai trang thai khi xu ly ki
}
// tu nay tren xau r
v = link[v];
}
}
}
int main () {
freopen("kiem_duyet.inp", "r", stdin);
freopen("kiem_duyet.out", "w", stdout);
init_trie();
scanf("%s\n%d\n", &t, &n);
s = t;
for (int i = 1; i <= n; i++) {
scanf("%s\n", &t);

add_str(t, i);
}
push_link();
search_str(s);
printf("%s\n", r.c_str());

14


}

return 0;

5.4.

Test

Đường dẫn tải test cho bài toán:
/>5.5.

Cảm nhận

Bài toán trên cho ta thấy sự ứng dụng rất đa dạng của cấu trúc dữ liệu cây tiền tố trong xử lý
xâu.

6. Bài toán 6: Xor lớn nhất
6.1.

Đề bài (Nguồn: Sưu tầm)


Cho dãy gồm n số nguyên không âm a1, a2, …, an. Hãy tìm giá trị xor lớn nhất của 2 phần tử
của dãy.
Ở đây xor là phép tính cộng bit không nhớ (phép xor trong Pascal hay ^ trong C/C++).
Dữ liệu: Dòng đầu tiên chứa số nguyên n (2 ≤ n ≤ 105). Dòng thứ hai chứa n số nguyên a1, a2,
…, an (0 ≤ ai ≤ 109) ngăn cách nhau bởi một dấu cách.
Kết quả: Ghi ra giá trị xor lớn nhất của 2 phần tử của dãy.
Ví dụ:
max_xor.inp
4
2 7 3 6

max_xor.out
5

Ràng buộc:
• Subtask 1 (30%): 2 ≤ n ≤ 5000.
• Subtask 2 (70%): 2 ≤ n ≤ 105.
6.2.

Thuật toán

Subtaks 1:Duyệt mọi cặp (ai, aj) với 1 ≤ i ≤ j ≤ n, tính aixoraj và lưu kết quả lớn nhất.
#include <bits/stdc++.h>
using namespace std;
int n, a[100001], ans = 0;
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 1; i <= n; i++)
for (int j = i+1; j <= n; j++)

ans = max(ans, a[i] xor a[j]);
printf("%d\n", ans);
return 0;
}

15


Độ phức tạp của thuật toán: O(n2).
Subtask 2:
Do 0 ≤ ai ≤ 109 nên ta coi mỗi số ai như là một dãy 31 bít. Xét lần lượt từng số ai với i = 1, 2,
…, n. Với mỗi số ai ta tìm trong các số a1, a2, …, ai-1 số nào xor với ai cho kết quả lớn nhất.
Để làm điều này ta cần tạo trie cho dãy các bít của a1, a2, …, ai-1. Xét các bít của ai từ trọng số
cao đến thấp và tìm đường đi trên trie theo dãy bít này sao cho xor cho giá trị lớn nhất. Các
chi tiết cụ thể xem chú thích trong chương trình sau.
6.3.

Chương trình

#include <bits/stdc++.h>
using namespace std;
int n, to[3100001][2], sz = 0, ans = 0;
void init_trie() {
memset(to, 0, sizeof(to));
}
void add_bit(int a) {
for (int i = 30, u = 0; i >= 0; i--) {
int j = (a >> i) & 1;
if (to[u][j] == 0) to[u][j] = ++sz;
u = to[u][j];

}
}
int max_xor(int a) {
int num = 0;
for (int i = 30, u = 0; i >= 0; i--) {
int b = (a >> i) & 1; // bit thu i cua a
if (to[u][1-b] != 0) { // neu 2 bit khac nhau thi xor bang 1
num += 1 << i;
// ta di theo nhanh co bit khac b truoc,
u = to[u][1-b];
// se cho so lon hon
}
else
u = to[u][b];
}
return num;
}
int main() {
freopen("max_xor.inp", "r", stdin);
freopen("max_xor.out", "w", stdout);
scanf("%d", &n);
init_trie();
int a;
scanf("%d", &a);
add_bit(a);
for (int i = 2; i <= n; i++) {
scanf("%d", &a);
ans = max(ans, max_xor(a));
add_bit(a);
}

printf("%d\n", ans);
return 0;
}

Độ phức tạp thuật toán là: O(n×31).

16


6.4.

Test

Đường dẫn tải test cho bài toán:
/>6.5.

Cảm nhận

Bài toán này cho ta thấy ngoài việc dùng cấu trúc dữ liệu cây tiền tố để xử lý xâu, ta còn ứng
dụng vào việc xử lý các dãy bít.

7. Bài toán 7: Tổng xor
7.1.

Đề bài (Nguồn: Regionals 2009 Asia - Amritapuri)

Cho dãy A gồm n số nguyên không âm a1, a2, …, an. Hãy chọn ra một dãy con gồm các phần
tử liên tiếp của dãy A sao cho xor tất cả các phần tử của dãy con là lớn nhất, tức là tìm dãy
con ai, ai+1, …, aj (1 ≤ i ≤ j ≤ n) sao cho (aixorai+1xor … xoraj) lớn nhất.
Ở đây xor là phép tính cộng bit không nhớ (phép xor trong Pascal hay ^ trong C/C++).

Dữ liệu: Dòng đầu tiên chứa số nguyên n (2 ≤ n ≤ 105). Dòng thứ hai chứa n số nguyên a1, a2,
…, an (0 ≤ ai ≤ 109) ngăn cách nhau bởi một dấu cách.
Kết quả: Ghi ra giá trị xor lớn nhất của dãy con tìm được.
Ví dụ:
sum_xor.inp
5
3 7 7 7 0
5
3 8 2 6 4

sum_xor.out
7
15

Ràng buộc:
• Subtask 1 (50%): 2 ≤ n ≤ 103.
• Subtask 2 (50%): 2 ≤ n ≤ 105.
7.2.

Thuật toán

Gọi f(l, r) là xor của mảng con từ l đến r, tức là f(l, r) = alxoral+1xor … xorar. Rõ ràngf(l, r) có
tính chất sau: f(l, r) = f(1, r) xorf(1, l-1).
Bây giờ ta tính xor lớn nhất của mảng con kết thúc tại vị trí i. Tức là cần tính giá trị lớn nhất
của f(l, i) = f(1, i) xorf(1, l-1) ở đó l ≤ i. Giả sử rằng chúng ta đã chèn f(1, l-1) vào trie với mọi
l ≤ i. Rõ ràng đây chính là bài toán “xor lớn nhất” ở mục 6.
7.3.

Chương trình


#include <bits/stdc++.h>
using namespace std;
int n, to[3100001][2], sz = 0;

17


void init_trie() {
memset(to, 0, sizeof(to));
}
void add_bit(int a) {
for (int i = 30, u = 0; i >= 0; i--) {
int j = (a >> i) & 1;
if (to[u][j] == 0) to[u][j] = ++sz;
u = to[u][j];
}
}
int max_xor(int a) {
int num = 0;
for (int i = 30, u = 0; i >= 0; i--) {
int b = (a >> i) & 1;
if (to[u][1-b] != 0) {
num += 1 << i;
u = to[u][1-b];
}
else
u = to[u][b];
}
return num;
}

int main() {
freopen("sum_xor.inp", "r", stdin);
freopen("sum_xor.out", "w", stdout);
scanf("%d", &n);
int pre = 0, ans = 0;
init_trie();
add_bit(0);
for (int i = 1; i <= n; i++) {
int a;
scanf("%d", &a);
pre ^= a;
ans = max(ans, max_xor(pre));
add_bit(pre);
}
printf("%d\n", ans);
return 0;
}

7.4.

Test

Đường dẫn tải test cho bài toán:
/>7.5.




Cảm nhận
Bài toán 2 điểm động quy về bài toán 1 điểm động.

Là sự mở rộng của bài toán 6 qua tính chất của phép xor.
Ứng dụng của cấu trúc cây tiền tố và tính chất của phép xor.

8. Bài toán 8: Xor mảng con
8.1.

Đề bài(Nguồn: />18


Cho mảng gồm n số nguyên không âm a1, a2, …, an. Hãy tính số mảng con gồm các phần tử
liên tiếp sao cho khi xor tất cả các phần tử của mảng con được giá trị nhỏ hơn k, tức là tính số
mảng con ai, ai+1, …, aj (1 ≤ i ≤ j ≤ n) sao cho aixorai+1xor … xorajỞ đây xor là phép tính cộng bit không nhớ (phép xor trong Pascal hay ^ trong C/C++).
Dữ liệu: Dòng đầu tiên chứa 2 số nguyên n và k (1 ≤ n ≤ 105, 0 ≤ k ≤ 109). Dòng thứ hai chứa
n số nguyên a1, a2, …, an (0 ≤ ai ≤ 109) ngăn cách nhau bởi một dấu cách.
Kết quả: Ghi ra số mảng con có giá trị xor nhỏ hơn k.
Ví dụ:
sub_xor.inp
5 2
4 1 3 2 7

sub_xor.out
3

Ràng buộc:
• Subtask 1 (50%): 1 ≤ n ≤ 103.
• Subtask 2 (50%): 1 ≤ n ≤ 105.
8.2.

Thuật toán


Về cách tổ chức dữ liệu và cài đặt giống bài toán “Tổng xor”. Điểm khác biệt duy nhất là thay
vì tìm mảng con có xor lớn nhất, ta tìm mảng con có xor nhỏ hơn k. Chi tiết của việc này xem
chú thích trong chương trình dưới đây.
8.3.

Chương trình

#include <bits/stdc++.h>
using namespace std;
int n, k, to[3100001][2], cnt[3100001][2], sz = 0;
long long ans = 0;
void init_trie() {
memset(to, 0, sizeof(to));
memset(cnt, 0, sizeof(cnt));
ans = 0;
}
void add_bit(int a) {
for (int i = 30, u = 0; i >= 0; i--) {
int j = (a >> i) & 1;
cnt[u][j]++; // so nut la neu di theo duong di nay
if (to[u][j] == 0) to[u][j] = ++sz;
u = to[u][j];
}
}
int get(int a) {
int num = 0;
for (int i = 30, u = 0; i >= 0; i--) {
int b = (a >> i) & 1;
// bit thu i cua a

if (((k>>i)&1) == 1) {
// bit thu i cua k
num += cnt[u][b];
if (to[u][1-b] == 0) break;
u = to[u][1-b];

19


}
else {
if (to[u][b] == 0) break;
u = to[u][b];
}

}
return num;

}

int main() {
scanf("%d%d", &n, &k);
init_trie();
add_bit(0);
int pre = 0;
for (int i = 1; i <= n; i++) {
int a;
scanf("%d", &a);
pre ^= a;
ans += get(pre);

add_bit(pre);
}
printf("%lld\n", ans);
return 0;
}

8.4.

Test

Đường dẫn tải test cho bài toán:
/>8.5.




Cảm nhận
Là sự mở rộng của bài toán 7.
Cũng là bài toán 2 điểm động quy về bài toán 1 điểm động.
Ứng dụng của cấu trúc cây tiền tố và tính chất của phép xor.

9. Bài toán 9: Số lượng khoảng
9.1.

Đề bài (Nguồn: Kỹ thuật lập trình - Nguyễn Thanh Tùng - Trường ĐHBK Hà Nội)

Cho mảng số nguyên không âm a1, a2, ..., an. Với số x nguyên không âm cho trước hãy tính số
lượng cặp số (l, r) thỏa mãn các điều kiện:
• 1≤l≤r≤n
• x ≤ alxoral+1xor … xorar

Ở đây xor là phép tính cộng bit không nhớ (phép xor trong Pascal hay ^ trong C/C++).
Dữ liệu: Dòng đầu tiên chứa 2 số nguyên n và x (1 ≤ n ≤ 105, 0 ≤ x ≤ 109). Dòng thứ hai chứa
n số nguyên a1, a2, ..., an (0 ≤ ai ≤ 109, i = 1÷n).
Kết quả:Đưa ra một số nguyên là số lượng cặp tìm được.
Ví dụ:

20


seg_num.inp
5 0
1 2 3 4 5
3 3
1 2 3

seg_num.out
15
2

Ràng buộc:
• Subtask 1 (50%): 1 ≤ n ≤ 103.
• Subtask 2 (50%): 1 ≤ n ≤ 105.
9.2.

Thuật toán

Về cơ bản bài toán này giống bài toán “Xor mảng con”, thay vì tính số lượng mảng con có
xor nhỏ hơn x bởi lớn hơn hoặc bằng x. Vì vậy ta chỉ cần chỉnh sửa lại đôi chút cài đặt của bài
toán trên.
9.3.


Chương trình

#include <bits/stdc++.h>
using namespace std;
int n, x, to[3100001][2], cnt[3100001][2], sz = 0;
long long ans = 0;
void init_trie() {
memset(to, 0, sizeof(to));
memset(cnt, 0, sizeof(cnt));
}
void add_bit(int a) {
for (int i = 30, u = 0; i >= 0; i--) {
int j = (a >> i) & 1;
cnt[u][j]++;
if (to[u][j] == 0) to[u][j] = ++sz;
u = to[u][j];
}
}
int get(int a) {
int num = 0, i, u;
for (i = 30, u = 0; i >= 0; i--) {
int b = (a >> i) & 1;
// bit thu i cua a
if (((x>>i)&1) == 0) {
// bit thu i cua x
num += cnt[u][1-b];
if (to[u][b] == 0) break;
u = to[u][b];
}

else {
if (to[u][1-b] == 0) break;
u = to[u][1-b];
}
}
return (i < 0) ? num+1 : num;
}
int main() {
freopen("seg_num.inp", "r", stdin);
freopen("seg_num.out", "w", stdout);
scanf("%d%d", &n, &x);
init_trie();

21


}

add_bit(0);
int pre = 0;
for (int i = 1; i <= n; i++) {
int a;
scanf("%d", &a);
pre ^= a;
ans += get(pre);
add_bit(pre);
}
printf("%lld\n", ans);
return 0;


9.4.

Test

Đường dẫn tải test cho bài toán:
/>9.5.

Cảm nhận

Sau khi làm bài toán 8, thì bài toán này là sự ôn tập lại kỹ năng về cấu trúc cây tiền tố.

10.Bài toán 10: Xor dãy số
10.1. Đề bài (Nguồn: VNOI Online 2016)
Cho dãy A gồm N số nguyên không âm. Ta lần lượt thực hiện Q thao tác trên dãy này:
• XORx: Với mọi i: Ai = Aixorx.
• FINDk: Tìm số lớn thứ k trong dãy A.
Hãy thực hiện các truy vấn trên.
Dữ liệu: Dòng đầu tiên ghi 2 số N và Q. Dòng thứ hai ghi N số là giá trị ban đầu của dãy A.
Tiếp theo là Q dòng, mỗi dòng ghi 1 trong 2 loại truy vấn.
Kết quả: Với mỗi truy vấn loại FIND, in ra kết quả tìm được.
Ví dụ:
seq_xor.inp
4 9
1 2 3 4
FIND 1
FIND 2
FIND 3
FIND 4
XOR 6
FIND 1

FIND 2
FIND 3
FIND 4

seq_xor.out
4
3
2
1
7
5
4
2

Giải thích ví dụ: Trước truy vấn XOR 6, dãy số là 1, 2, 3, 4. Sau truy vấn XOR 6, dãy số là 7,
4, 5, 2.
22


Ràng buộc:
• Subtask 1 (25%): N, Q ≤ 5000, 0 ≤ Ai ≤ 109, 0 ≤ x ≤ 109.
• Subtask 2 (40%): 0 ≤ x ≤ 100. Các subtask 2, 3 và 4 tiếp theo đều có: N ≤ 105, Q ≤ 105,
0 ≤ Ai ≤ 109.
• Subtask 3 (10%): 0 ≤ x ≤ 109, x luôn có dạng 2k.
• Subtask 4 (25%): 0 ≤ x ≤ 109.
10.2. Thuật toán
Coi mỗi số nhưmột dãy 31 bít. Dùng trie để biểu diễn n sốa[i] (mảng to[u][i]). Với mỗi nút ta
tính xem nó chứa bao nhiêu nút lá(mảngcnt[u][i]).
Duyệt trie này từ nút gốc và theo thứ tự từđiển, ta sẽđược các sốa[i] theo thứ tự tăng.
Vì phép xor có tính chất giao hoán nên mỗi phép xorx, ta không cập nhật ngay vào a[i] mà ta

xor các giá trịx này với nhau: X ^= x. Vì vậy trie không thay đổi.
Với phép “FIND k” tìm số lớn thứk trong dãy, tức là tìm số thứn-k+1 trong dãy theo thứ tự
tăng. Ta sẽ duyệt trie để lấy ra số mà sau khi “xorX” nó có thứ tựn-k+1 trong dãy tăng. Sau đó
lấy số này xor với X, ta thu được số lớn thứk cần tìm.
10.3. Chương trình
#include <bits/stdc++.h>
using namespace std;
int n, x, to[3100001][2], cnt[3100001][2], sz = 0;
long long ans = 0;
void init_trie() {
memset(to, 0, sizeof(to));
memset(cnt, 0, sizeof(cnt));
}
void add_bit(int a) {
for (int i = 30, u = 0; i >= 0; i--) {
int j = (a >> i) & 1;
cnt[u][j]++;
if (to[u][j] == 0) to[u][j] = ++sz;
u = to[u][j];
}
}
int get(int a) {
int num = 0;
for (int i = 30, u = 0; i >= 0; i--) {
int b = (a >> i) & 1;
// bit thu i cua a
if (((x>>i)&1) == 0) {
// bit thu i cua x
num += cnt[u][1-b];
if (to[u][b] == 0) return num;

u = to[u][b];
}
else {
if (to[u][1-b] == 0) return num;
u = to[u][1-b];
}
}

23


}

return num+1;

int main() {
freopen("seq_xor.inp", "r", stdin);
freopen("seq_xor.out", "w", stdout);
scanf("%d%d", &n, &x);
init_trie();
add_bit(0);
int pre = 0;
for (int i = 1; i <= n; i++) {
int a;
scanf("%d", &a);
pre ^= a;
ans += get(pre);
add_bit(pre);
}
printf("%lld\n", ans);

return 0;
}

10.4. Test
Đường dẫn tải test cho bài toán:
/>10.5. Cảm nhận
Đây là bài toán về ứng dụng của cây tiền tố trong xử lý bít và thứ tự từ điển.

III.

KẾT LUẬN

Trong các bài toán trên, một thao tác cơ bản cần phải thực hiện là tìm kiếm một dữ liệu:kiểm
tra một trạng thái đãđược thăm trước đó hay chưa mà không thể dùng mảng đánh dấu vì mỗi
trạng thái lại là một xâu ký tự, một dãy số, ... hay trong quá trình quy hoạch động, bạn cần
kiểm tra xem một dãy số hay một xâu kí tự có nằm trong một tập cho trước không.... Để tìm
kiếm một cách có hiệu quả là một việc không hề đơn giản. Có rất nhiều cấu trúc dữ liệu giúp
bạn thực hiện thao tác tìm kiếm: cây tìm kiếm nhị phân, hàm băm, ... và một cấu trúc dữ liệu
rất hiệu quả để xử lý là cây tiền tố (trie).
Sau đây là các ưu điểm của cây tiền tố:
• Thời gian tìm kiếm nhỏ hơn. Thao tác tìm kiếm một khóa độ dài mđòi hỏi O(m) phép
so sánh kí tự.
• Trie sử dụng ít bộ nhớ hơn bởi các tiền tố chung chỉ cần được lưu trữ một lần.
• Trie cho phép tìm kiếm tiền tố trùng hợp dài nhất.
• Số lượng nút từ gốc tới lá đúng bằng độ dài của khóa.
• Trie cho phép liệt kê các khóa theo thứ tự từđiển.
• Do không phải tính hàm băm nên trie thường nhanh hơn bảng băm trong trường hợp
khóa bé chẳng hạn như số nguyên hay con trỏ.

24



IV. TÀI LIỆU THAM KHẢO
1. Các bài tập sưu tầm trên Internet.
2. Kỹ thuật lập trình – Nguyễn Thanh Tùng – Trường ĐHBK Hà Nội.

25


×