Бор деген не?
Бор, немесе trie, — string-терді сақтайтын ағаш. Онда әр вершина қандай да бір prefix-ке сәйкес келеді.
edge — бір символ
root-тан жол — string немесе string prefix-і
Бор мына жағдайларда ыңғайлы:
- string қосу
- string бар ма тексеру
- prefix бар ма тексеру
- берілген prefix-пен басталатын string санын табу
- сөздікпен жұмыс істеу
Мысал
Борға мына string-тер қосылған болсын:
ab
abc
ac
Онда ортақ prefix a бір рет қана сақталады.
└── a
├── b
│ └── c
└── c
ab string-і b вершинасында аяқталады, abc string-і c вершинасында аяқталады, ac string-і басқа c вершинасында аяқталады.
Негізгі идея
String қосқанда символдар бойынша солдан оңға жүреміз. Егер керек transition жоқ болса, жаңа вершина жасаймыз.
current symbol c
transition жоқ болса → жаңа vertex жасаймыз
келесі vertex-ке өтеміз
соңында word end белгілейміз
Массив арқылы реализация
Кіші ағылшын әріптері a...z үшін массив қолдану ыңғайлы:
int tr[N][26];
Мұнда:
егер tr[v][c] = 0 болса, transition жоқ
cnt[v] — v вершинасында қанша word аяқталады
pass[v] — v арқылы қанша word өтеді
idx — жасалған vertex саны
Базалық массивтер
const int N = 1000005;
int tr[N][26];
int cnt[N];
int pass[N];
int idx;
Егер барлық қосылатын string ұзындықтарының суммасы S болса, бордағы vertex саны ең көбі S + 1.
Символды санға қалай айналдырамыз?
Кіші ағылшын әріптері үшін:
int c = s[i] - 'a';
Сонда:
'b' → 1
'c' → 2
...
'z' → 25
String қосу
insert функциясы string-ті борға қосады.
void insert(string s) {
int v = 0;
pass[v]++;
for (char ch : s) {
int c = ch - 'a';
if (tr[v][c] == 0) {
tr[v][c] = ++idx;
}
v = tr[v][c];
pass[v]++;
}
cnt[v]++;
}
Соңында:
pass[v]++ → осы vertex арқылы word өтеді
Толық string іздеу
String бар ма тексеру үшін, оның символдары бойынша борда жүреміз.
Егер бір жерде transition жоқ болса, string жоқ. Егер соңына жетсек, cnt[v] тексереміз.
bool search(string s) {
int v = 0;
for (char ch : s) {
int c = ch - 'a';
if (tr[v][c] == 0) {
return false;
}
v = tr[v][c];
}
return cnt[v] > 0;
}
Prefix тексеру
Берілген prefix-пен басталатын кем дегенде бір word бар ма, соны тексеру үшін prefix символдары бойынша жүреміз.
bool startsWith(string s) {
int v = 0;
for (char ch : s) {
int c = ch - 'a';
if (tr[v][c] == 0) {
return false;
}
v = tr[v][c];
}
return true;
}
Мұнда cnt[v] тексерудің қажеті жоқ, себебі бізге word end емес, path бар ма сол маңызды.
Берілген prefix-пен қанша word басталады?
Бұл үшін pass массиві керек. Ол vertex арқылы қанша word өткенін көрсетеді.
int countPrefix(string s) {
int v = 0;
for (char ch : s) {
int c = ch - 'a';
if (tr[v][c] == 0) {
return 0;
}
v = tr[v][c];
}
return pass[v];
}
Нақты word қанша рет қосылды?
Бұл үшін cnt[v] қолданамыз. Ол v вершинасында қанша word аяқталатынын сақтайды.
int countWord(string s) {
int v = 0;
for (char ch : s) {
int c = ch - 'a';
if (tr[v][c] == 0) {
return 0;
}
v = tr[v][c];
}
return cnt[v];
}
Массив арқылы бордың толық коды
#include <bits/stdc++.h>
using namespace std;
const int N = 1000005;
int tr[N][26];
int cnt[N];
int pass[N];
int idx;
void insert(string s) {
int v = 0;
pass[v]++;
for (char ch : s) {
int c = ch - 'a';
if (tr[v][c] == 0) {
tr[v][c] = ++idx;
}
v = tr[v][c];
pass[v]++;
}
cnt[v]++;
}
bool search(string s) {
int v = 0;
for (char ch : s) {
int c = ch - 'a';
if (tr[v][c] == 0) {
return false;
}
v = tr[v][c];
}
return cnt[v] > 0;
}
bool startsWith(string s) {
int v = 0;
for (char ch : s) {
int c = ch - 'a';
if (tr[v][c] == 0) {
return false;
}
v = tr[v][c];
}
return true;
}
int countWord(string s) {
int v = 0;
for (char ch : s) {
int c = ch - 'a';
if (tr[v][c] == 0) {
return 0;
}
v = tr[v][c];
}
return cnt[v];
}
int countPrefix(string s) {
int v = 0;
for (char ch : s) {
int c = ch - 'a';
if (tr[v][c] == 0) {
return 0;
}
v = tr[v][c];
}
return pass[v];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
insert("ab");
insert("abc");
insert("ac");
insert("ab");
cout << search("ab") << '\n';
cout << search("a") << '\n';
cout << startsWith("a") << '\n';
cout << countWord("ab") << '\n';
cout << countPrefix("a") << '\n';
return 0;
}
Мысал output
1
0
1
2
4
Неге:
- search("ab") = 1, себебі word бар
- search("a") = 0, себебі prefix бар, бірақ a word ретінде жоқ
- startsWith("a") = 1, себебі a-дан басталатын word бар
- countWord("ab") = 2, себебі ab екі рет қосылды
- countPrefix("a") = 4, себебі 4 word-тың бәрі a-дан басталады
Word өшіру
Егер word бұрын қосылған болса, оны өшіруге болады. Path бойынша жүріп pass азайтамыз, соңында cnt азайтамыз.
bool eraseWord(string s) {
if (countWord(s) == 0) {
return false;
}
int v = 0;
pass[v]--;
for (char ch : s) {
int c = ch - 'a';
v = tr[v][c];
pass[v]--;
}
cnt[v]--;
return true;
}
insert / find / prefix командалары
Көп задачада query түрлері былай беріледі:
find s — толық string бар ма тексеру
pref s — prefix s-пен қанша string басталады
Query-лермен толық программа
#include <bits/stdc++.h>
using namespace std;
const int N = 1000005;
int tr[N][26];
int cnt[N];
int pass[N];
int idx;
void insert(string s) {
int v = 0;
pass[v]++;
for (char ch : s) {
int c = ch - 'a';
if (tr[v][c] == 0) {
tr[v][c] = ++idx;
}
v = tr[v][c];
pass[v]++;
}
cnt[v]++;
}
int countWord(string s) {
int v = 0;
for (char ch : s) {
int c = ch - 'a';
if (tr[v][c] == 0) {
return 0;
}
v = tr[v][c];
}
return cnt[v];
}
int countPrefix(string s) {
int v = 0;
for (char ch : s) {
int c = ch - 'a';
if (tr[v][c] == 0) {
return 0;
}
v = tr[v][c];
}
return pass[v];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int q;
cin >> q;
while (q--) {
string type, s;
cin >> type >> s;
if (type == "add") {
insert(s);
} else if (type == "find") {
if (countWord(s) > 0) {
cout << "YES\n";
} else {
cout << "NO\n";
}
} else if (type == "pref") {
cout << countPrefix(s) << '\n';
}
}
return 0;
}
Input / Output
Input
7
add ab
add abc
add ac
find ab
find a
pref a
pref ab
Output
YES
NO
3
2
Цифрлар үшін бор
Егер string тек 0...9 цифрларынан тұрса, alphabet size 10 болады.
int tr[N][10];
int c = ch - '0';
Бұл телефон номерлері туралы задачаларда жиі кездеседі.
Үлкен әріптер үшін бор
Егер тек үлкен әріптер A...Z болса:
int tr[N][26];
int c = ch - 'A';
Егер alphabet үлкен болса
Егер символ көп болса, tr[N][alphabet] өте көп memory алуы мүмкін. Онда map немесе unordered_map қолдануға болады.
vector<unordered_map<char, int>> tr;
Телефон номерлері conflict тексеру
Типтік задача: телефон номерлерінің тізімі бар. Бір номер екінші номердің prefix-і болып тұр ма, соны тексеру керек.
Мысалы:
911
9112
Мұнда 911 — 9112-нің prefix-і, демек conflict бар.
Conflict тексеру идеясы
String қосқанда:
- жолда келе жатып word аяқталатын vertex кездессек, ескі word жаңа word-тың prefix-і
- қосу біткеннен кейін current vertex-тің child-тары болса, жаңа word ескі word-тың prefix-і
Conflict тексеру коды
int tr[N][10];
int cnt[N];
int idx;
bool hasChild(int v) {
for (int c = 0; c < 10; c++) {
if (tr[v][c] != 0) {
return true;
}
}
return false;
}
bool insertPhone(string s) {
int v = 0;
for (char ch : s) {
if (cnt[v] > 0) {
return false;
}
int c = ch - '0';
if (tr[v][c] == 0) {
tr[v][c] = ++idx;
}
v = tr[v][c];
}
if (cnt[v] > 0) {
return false;
}
if (hasChild(v)) {
return false;
}
cnt[v]++;
return true;
}
Асимптотика
| Операция | Уақыт |
|---|---|
| Ұзындығы L string қосу | O(L) |
| Ұзындығы L string іздеу | O(L) |
| Ұзындығы L prefix тексеру | O(L) |
| Prefix-пен word санын табу | O(L) |
Memory:
tr[N][26] үшін memory шамамен:
N * 26 * sizeof(int)
Бор қашан қолданылады?
string тез іздеу керек
prefix тез іздеу керек
prefix-пен басталатын string санын табу керек
prefix conflict тексеру керек
string dictionary керек
Типтік қателер
- root — 0 вершина екенін ұмыту
- 0-ді жаңа vertex ретінде қолдану, бірақ 0 root болып тұр
- Жаңа vertex жасағанда idx арттырмау
- Word end белгілеу үшін cnt[v]++ ұмыту
- cnt пен pass шатастыру
- N массив size-ын тым кіші алу
- Цифрлар үшін tr[N][26] және ch - 'a' қолдану
- Бірнеше test арасында массивтерді тазаламау
Test арасында борды қалай тазалаймыз?
Егер test көп болса, тек қолданылған vertex-терді тазалауға болады.
void clearTrie() {
for (int i = 0; i <= idx; i++) {
for (int c = 0; c < 26; c++) {
tr[i][c] = 0;
}
cnt[i] = 0;
pass[i] = 0;
}
idx = 0;
}
Қысқа шаблон
const int N = 1000005;
int tr[N][26];
int cnt[N];
int pass[N];
int idx;
void insert(string s) {
int v = 0;
pass[v]++;
for (char ch : s) {
int c = ch - 'a';
if (tr[v][c] == 0) {
tr[v][c] = ++idx;
}
v = tr[v][c];
pass[v]++;
}
cnt[v]++;
}
int countWord(string s) {
int v = 0;
for (char ch : s) {
int c = ch - 'a';
if (tr[v][c] == 0) {
return 0;
}
v = tr[v][c];
}
return cnt[v];
}
int countPrefix(string s) {
int v = 0;
for (char ch : s) {
int c = ch - 'a';
if (tr[v][c] == 0) {
return 0;
}
v = tr[v][c];
}
return pass[v];
}
Ең бастысы
root = 0
tr[v][c] = v вершинасынан c символы бойынша transition
cnt[v] = v ішінде қанша word аяқталады
pass[v] = v арқылы қанша word өтеді
insert/search/prefix O(string length) жұмыс істейді