Tries & Suffix Structures

Trie ও suffix structure

Read: ~35 min Advanced 6 practice problems Live code runner

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.

Trie মানে hash map বা balanced BST-এর বদলে — যখন আমাদের prefix matching দরকার, autocomplete দরকার, বা অনেক string-এর সাধারণ prefix নিয়ে কাজ করতে হয়।
C D A O T R G · C D CA DO CAT CAR DOG Figure 20.1 — Trie of {CAT, CAR, DOG}. Green nodes mark word ends. CARD would extend the CAR path with one more "D" leaf.

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.

trie.cpp
#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.

autocomplete.cpp
#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).
Bit-trie একটি অসাধারণ trick — XOR maximum pair O(n·32)-এ পাওয়া যায়। Suffix trie ধারণাগতভাবে সুন্দর, কিন্তু O(n²) memory খায় — তাই বাস্তবে suffix array ব্যবহৃত হয় (Module 38)।

5. Practice Problems

  1. Count how many stored words start with a given prefix.
    দেওয়া prefix দিয়ে শুরু হওয়া word-এর সংখ্যা।
    ✨ Show Answer (উত্তর দেখুন)

    Approach: store an integer cnt at every node — number of stored words that pass through this node. On insert, increment cnt at every visited node. On query, walk to the prefix node and return cnt.

  2. 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.

  3. 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.

  4. 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;
    }
  5. 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.

  6. 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.

Trie মানে prefix-based যেকোনো query O(L)। Autocomplete, dictionary, IP routing, এমনকি XOR pair — সব trie-friendly সমস্যা।

Next Module → Hash Tables — Phase 5 শুরু।