Tries & Suffix Structures
Trie ও suffix structure
1. Trie — A Tree Indexed by Characters
A trie (pronounced "try") stores a set of strings such that searching for any string of length L takes O(L), independent of the number of strings. Every edge is labelled by one character; every path from root to a marked node spells one stored word.
2. A Lowercase-English Trie From Scratch
Each node owns 26 child pointers (one per a..z) and a boolean isEnd. Insert and search in O(L); memory overhead is the price.
#include <bits/stdc++.h>
using namespace std;
struct Trie {
struct Node {
Node* nxt[26] = {};
bool end = false;
} *root;
Trie() : root(new Node()) {}
void insert(const string& w) {
Node* cur = root;
for (char c : w) {
int i = c - 'a';
if (!cur->nxt[i]) cur->nxt[i] = new Node();
cur = cur->nxt[i];
}
cur->end = true;
}
bool search(const string& w) {
Node* cur = root;
for (char c : w) {
cur = cur->nxt[c - 'a'];
if (!cur) return false;
}
return cur->end;
}
bool startsWith(const string& p) {
Node* cur = root;
for (char c : p) {
cur = cur->nxt[c - 'a'];
if (!cur) return false;
}
return true;
}
};
int main() {
Trie t;
for (string w : {"cat", "car", "card", "dog"}) t.insert(w);
cout << t.search("car") << "\n"; // 1
cout << t.search("care") << "\n"; // 0
cout << t.startsWith("car") << "\n"; // 1
cout << t.startsWith("do") << "\n"; // 1
cout << t.startsWith("bat") << "\n"; // 0
}
3. Autocomplete in Three Lines (Plus DFS)
Walk to the prefix node; DFS from there collecting up to k completions in lex order. Stop early once k results are gathered.
#include <bits/stdc++.h>
using namespace std;
struct Node { Node* nxt[26] = {}; bool end = false; };
Node* root = new Node();
void insert(const string& w) {
Node* c = root;
for (char x : w) {
int i = x - 'a';
if (!c->nxt[i]) c->nxt[i] = new Node();
c = c->nxt[i];
}
c->end = true;
}
void dfs(Node* n, string& pre, vector<string>& out, int k) {
if ((int)out.size() >= k || !n) return;
if (n->end) out.push_back(pre);
for (int i = 0; i < 26; i++) {
if (n->nxt[i]) {
pre.push_back('a' + i);
dfs(n->nxt[i], pre, out, k);
pre.pop_back();
}
}
}
vector<string> complete(const string& p, int k) {
Node* c = root;
for (char x : p) { c = c->nxt[x - 'a']; if (!c) return {}; }
vector<string> out; string pre = p;
dfs(c, pre, out, k);
return out;
}
int main() {
for (string w : {"car","card","care","careful","cargo","cat"}) insert(w);
for (auto& w : complete("car", 3)) cout << w << "\n";
}
4. Variants: Compressed Trie, Bit-Trie, Suffix Trie
- Compressed (Radix) trie: collapse single-child chains into one edge labelled with a string. Saves a lot of memory for sparse alphabets.
- Bit-trie: each edge is one bit. Used to find the maximum XOR pair of a set in O(n · 32).
- Suffix trie: insert all n suffixes of a string. Then "is X a substring?" is "does X exist in the trie?". Memory is O(n²) — too much in practice. The fix is a suffix array (Module 38).
5. Practice Problems
-
Count how many stored words start with a given prefix.দেওয়া prefix দিয়ে শুরু হওয়া word-এর সংখ্যা।
✨ Show Answer (উত্তর দেখুন)
Approach: store an integer
cntat every node — number of stored words that pass through this node. On insert, incrementcntat every visited node. On query, walk to the prefix node and returncnt. -
Find the longest common prefix of an array of strings using a trie.String array-এর longest common prefix — trie দিয়ে।
✨ Show Answer (উত্তর দেখুন)
Approach: insert all words. Walk from root downward as long as exactly one child exists and the current node is not a word-end. The path so far is the LCP.
-
Word search II — given a board of letters and a dictionary, find all dictionary words that appear as a path of adjacent cells.Board ও dictionary — সব trie-based word খুঁজুন।
✨ Show Answer (উত্তর দেখুন)
Approach: build a trie of the dictionary. DFS from every cell, walking the trie in parallel; prune as soon as the current trie node has no matching child. Standard pattern: trie + DFS with pruning.
-
Maximum XOR of two numbers in an array, in O(n · 32).Bit-trie দিয়ে maximum XOR pair বের করুন।
✨ Show Answer (উত্তর দেখুন)
a4.cpp#include <bits/stdc++.h> using namespace std; struct N { N* c[2] = {}; }; N* root = new N(); void add(int x) { N* p = root; for (int b = 31; b >= 0; b--) { int bit = (x >> b) & 1; if (!p->c[bit]) p->c[bit] = new N(); p = p->c[bit]; } } int maxXor(int x) { N* p = root; int r = 0; for (int b = 31; b >= 0; b--) { int bit = (x >> b) & 1; int want = bit ^ 1; if (p->c[want]) { r |= (1 << b); p = p->c[want]; } else p = p->c[bit]; } return r; } int main() { vector<int> a = {3, 10, 5, 25, 2, 8}; int best = 0; for (int x : a) { add(x); best = max(best, maxXor(x)); } cout << best; } -
Replace every word in a sentence with the shortest matching root from a dictionary (LeetCode 648).প্রতিটি word-কে সবচেয়ে ছোট root দিয়ে replace করুন।
✨ Show Answer (উত্তর দেখুন)
Approach: insert all roots into a trie. For each sentence word, walk the trie character by character; the first end-marked node along the path is the shortest root. If you reach a NULL pointer or never see an end, keep the original word.
-
A trie of n total characters takes how much memory with the 26-pointer-per-node design? When is it impractical?26-pointer trie-এর memory কত? কখন impractical?
✨ Show Answer (উত্তর দেখুন)
Answer: 26 × 8 bytes = 208 bytes per node, plus the bool. With n total characters, worst case n nodes → ~200 MB for 1M characters. Impractical when alphabet is huge (Unicode!) or sparse — switch to
unordered_map<char, Node*>per node, or use a compressed/radix trie.
Summary — Module 20
A trie stores strings such that any prefix operation is O(L). Autocomplete is "walk to the prefix node, DFS, collect k". Bit-tries solve XOR-pair problems in O(n·32). Suffix-trie idea is beautiful but expensive — we'll fix that with suffix arrays (Module 38). Phase 4 complete.