CHUYÊN ĐỀ COMPETITIVE PROGRAMMING — TUẦN 14

Capstone — Bit, Trie & Mock Interview

Tuần cuối cùng — tổng hợp toàn khoá. Học bit manipulation, Trie cho string, quy trình mock interview chuyên nghiệp, và lộ trình tự học sau khoá.

7 worked examples · 2 knowledge checks · Mock interview framework

Phần 1. Bit Manipulation

Kỹ thuật bitwise — gọn, nhanh, và đôi khi là lời giải duy nhất
1.1

1.1 Bit Operators — Bộ công cụ

OpTênVí dụKết quả
&AND0b1100 & 0b10100b1000
|OR0b1100 | 0b10100b1110
^XOR0b1100 ^ 0b10100b0110
~NOT~0b0011...11111100
<<Left shift1 << 38 (= 2³)
>>Right shift16 >> 24 (= 16/4)

Tính chất XOR — quan trọng nhất

Ba tính chất này tạo nên trick "Single Number" và nhiều bài XOR khác.

1.2

1.2 Common Bit Tricks — Phải thuộc

Mục đíchCú phápGiải thích
Check bit thứ i(x >> i) & 1Dịch i bước rồi AND với 1
Set bit thứ ix | (1 << i)OR với mask
Clear bit thứ ix & ~(1 << i)AND với mask đảo
Toggle bit thứ ix ^ (1 << i)XOR với mask
Check là power of 2x > 0 and x & (x-1) == 0Power of 2 chỉ có 1 bit on
Lowest set bit (LSB)x & -xTrick "lowbit" của Fenwick Tree
Bỏ LSBx & (x-1)Dùng đếm số bit on (Brian Kernighan)
Đếm bits trong Pythonbin(x).count('1')Idiom Python ngắn nhất
Trick x & (x-1):
Bỏ bit thấp nhất đang on. Lặp đến khi x = 0 → số lần lặp = số bit on. Đây là Brian Kernighan's algorithm — chạy đúng số bit on, nhanh hơn duyệt 32 bit.
1.3

1.3 Worked Example 1 — Single Number (#136)

Đề: Mảng nums. Mọi phần tử xuất hiện 2 lần, trừ 1 phần tử xuất hiện 1 lần. Tìm nó. Yêu cầu O(n) time, O(1) space.

Approach hashing — không thoả O(1) space

def singleNumber(nums):
    seen = set()
    for x in nums:
        if x in seen: seen.remove(x)
        else: seen.add(x)
    return seen.pop()
# O(n) time, O(n) space — vi phạm

Approach XOR — đẹp như thơ

def singleNumber(nums):
    result = 0
    for x in nums:
        result ^= x
    return result
# Hoặc: from functools import reduce; reduce(xor, nums)

Time O(n), Space O(1).

Vì sao đúng?
Mỗi phần tử xuất hiện 2 lần XOR cho 0 (x ^ x = 0). Phần tử lẻ XOR với 0 = chính nó. Thứ tự XOR không quan trọng (giao hoán). Đây là minh hoạ kinh điển cho sức mạnh tính chất đại số.
1.4

1.4 Worked Example 2 — Number of 1 Bits (#191)

Đề: Cho số nguyên n. Đếm số bit 1 trong biểu diễn nhị phân.

Approach 1 — Duyệt 32 bit

def hammingWeight(n):
    count = 0
    while n:
        count += n & 1
        n >>= 1
    return count
# O(32) = O(1)

Approach 2 — Brian Kernighan (chỉ duyệt số bit on)

def hammingWeight(n):
    count = 0
    while n:
        n &= n - 1                      # bỏ LSB
        count += 1
    return count
# Chạy đúng k = số bit on. Khi k << 32, nhanh hơn.

Approach 3 — Python idiom

def hammingWeight(n):
    return bin(n).count('1')
# Hoặc: n.bit_count()  (Python 3.10+)

Trong phỏng vấn, viết approach 2 (Brian Kernighan) sẽ thể hiện chiều sâu — nhiều người chỉ biết approach 1.

1.5

1.5 Worked Example 3 — Counting Bits (#338)

Đề: Cho n. Trả mảng ans[i] = số bit 1 trong i, với i = 0..n.

Approach ngây thơ — gọi #191 cho mỗi i

O(n × 32) — đủ pass nhưng không tận dụng được tính chất.

Approach DP — O(n)

Quan sát: ans[i] = ans[i >> 1] + (i & 1).

def countBits(n):
    ans = [0] * (n + 1)
    for i in range(1, n + 1):
        ans[i] = ans[i >> 1] + (i & 1)
    return ans

Time O(n), Space O(n).

Đây là bit manipulation kết hợp DP:
ans[i >> 1] đã được tính trước (vì i >> 1 < i). DP cho phép tận dụng kết quả con. Pattern xuất hiện ở nhiều bài bit DP nâng cao.
1.6

1.6 Bitmask DP — Giới thiệu

Khi tập items có size n nhỏ (≤ 20), có thể dùng bitmask để biểu diễn subset:

Ví dụ: Travelling Salesman Problem nhỏ

# dp[mask][i] = chi phí thấp nhất thăm tập 'mask' và kết thúc tại i
# Recurrence: dp[mask][i] = min(dp[mask ^ (1<<i)][j] + dist[j][i])
#                            cho mọi j thuộc mask, j != i

Time O(2ⁿ × n²). Với n = 15, ~7 triệu ops — chấp nhận được.

Tín hiệu nhận biết Bitmask DP

Đây là kỹ thuật nâng cao — không bắt buộc thuộc nhưng nên biết để khi gặp constraint nhỏ bất thường.

1.Q

Knowledge Check — Bit Manipulation

Q1: Trick x & (x-1) làm gì?
x = 1100, x-1 = 1011, x & (x-1) = 1000 — bit thấp nhất đã bị clear. Đây là Brian Kernighan's algorithm: lặp cho đến khi x = 0 → số lần lặp = số bit on (chỉ chạy đúng số bit on, không phải 32).
Q2: Trong Single Number (#136), XOR giải được vì tính chất nào?
Cần CẢ 3 tính chất: (1) x^x=0 để cặp triệt tiêu, (2) giao hoán + kết hợp để thứ tự không quan trọng. Phần tử lẻ XOR với 0 = chính nó.

Phần 2. Trie (Prefix Tree)

Cấu trúc cho string — search, prefix, autocomplete
2.1

2.1 Trie là gì?

Định nghĩa:
Trie (prefix tree) là cây nơi mỗi node đại diện cho một ký tự, mỗi đường từ root đến leaf đại diện cho một từ. Các từ chia sẻ prefix dùng chung phần đầu của đường.
a t n p o d p Words: "and", "app", "to"

Operations và complexity

So với hashing: hash O(L) cho lookup chính xác — không hỗ trợ prefix search hiệu quả. Trie có thêm prefix.

2.2

2.2 Trie Implementation

class TrieNode:
    def __init__(self):
        self.children = {}            # ký tự → TrieNode
        self.is_end = False           # đánh dấu kết thúc 1 từ

class Trie:
    def __init__(self):
        self.root = TrieNode()

    def insert(self, word):
        node = self.root
        for ch in word:
            if ch not in node.children:
                node.children[ch] = TrieNode()
            node = node.children[ch]
        node.is_end = True

    def search(self, word):
        node = self._traverse(word)
        return node is not None and node.is_end

    def startsWith(self, prefix):
        return self._traverse(prefix) is not None

    def _traverse(self, s):
        node = self.root
        for ch in s:
            if ch not in node.children:
                return None
            node = node.children[ch]
        return node

Đây là WE4: Implement Trie (#208). Time mỗi op O(L), Space O(tổng độ dài tất cả words).

Mẹo "is_end":
Thiếu cờ này, không phân biệt được "app" và "apple" — cả hai đều có đường trong trie nhưng chỉ "app" là từ thực sự nếu chỉ insert "app". is_end đánh dấu kết thúc từ.
2.3

2.3 Worked Example 5 — Word Search II (Hard)

Đề (LeetCode #212): Lưới ký tự + danh sách words. Trả về mọi từ trong words tìm được trên lưới (4 hướng, không lặp ô).

Brute force

Với mỗi word, gọi Word Search (#79, Tuần 11). Time O(M × N × số_word × 4^L) — chậm với nhiều word.

Trie + Backtracking — O(M × N × 4^L)

Build Trie từ tất cả words. DFS một lần qua lưới, đi theo cây Trie:

def findWords(board, words):
    # Build Trie
    root = {}
    for w in words:
        node = root
        for ch in w:
            node = node.setdefault(ch, {})
        node['$'] = w                           # lưu từ khi end

    res = []
    m, n = len(board), len(board[0])
    def dfs(r, c, node):
        ch = board[r][c]
        if ch not in node: return
        nxt = node[ch]
        if '$' in nxt:
            res.append(nxt.pop('$'))           # tìm thấy từ, xoá để không trùng
        board[r][c] = '#'
        for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
            nr, nc = r+dr, c+dc
            if 0 <= nr < m and 0 <= nc < n and board[nr][nc] != '#':
                dfs(nr, nc, nxt)
        board[r][c] = ch
    for r in range(m):
        for c in range(n):
            dfs(r, c, root)
    return res
2.4

2.4 Word Search II — Vì sao Trie nhanh hơn?

Brute force vs Trie approach

AspectBrute (call WS cho mỗi word)Trie + DFS
TimeO(M × N × W × 4^L)O(M × N × 4^L)
Khi W = 100, L = 1010⁴ × 10⁶ = 10¹⁰10⁴ × 10⁶ = 10¹⁰... nhưng 4^L thực tế cắt sớm
PruningMỗi word tìm độc lậpChia sẻ prefix → đóng nhánh sớm khi không match prefix nào

Pruning quan trọng — xoá khi tìm thấy

nxt.pop('$'): khi tìm thấy 1 từ, xoá khỏi Trie. Lần sau gặp prefix này nhưng không có từ → có thể prune.

Pruning nâng cao — xoá lá

Khi DFS quay lại và thấy node Trie không còn con → xoá node khỏi cha. Lần thăm sau biết "không còn từ nào ở nhánh này" → dừng sớm.

Đây là combo nâng cao:
Trie (cấu trúc) + Backtracking (Tuần 11) + Grid DFS (Tuần 10). Bài "boss" của Tuần 14 — minh chứng kỹ năng tổng hợp các pattern trước đó.
2.Q

Knowledge Check — Trie

Q1: Trie ưu thế hơn HashMap khi nào?
HashMap O(L) cho lookup chính xác; Trie cũng O(L) nhưng có ưu thế cho prefix queries — không thể làm hiệu quả với hash. Khi bài có "startsWith", "autocomplete", "common prefix" → Trie.
Q2: Trong Word Search II với Trie, kỹ thuật pop('$') sau khi tìm thấy giải quyết vấn đề gì?
Một từ có thể được "vẽ" nhiều cách qua các ô khác nhau. Sau khi tìm thấy lần đầu, xoá '$' đảm bảo lần sau không add lại. Đây cũng là một dạng pruning giúp DFS ngắn hơn.

Phần 3. Quy trình Mock Interview

UMPIRE framework — chuẩn vàng cho phỏng vấn coding
3.1

3.1 UMPIRE — Khung sườn 6 bước

UMPIRE là acronym của 6 bước nên thực hiện trong mỗi phỏng vấn coding. Tổng thời gian khuyến nghị 45 phút.

BướcTênThời gianMục đích
UUnderstand3-5 phútHiểu đề, làm rõ ràng buộc, edge cases
MMatch2-3 phútMatch với pattern đã biết
PPlan5-7 phútPseudocode + complexity dự kiến
IImplement15-20 phútCode thật
RReview3-5 phútĐọc lại, fix lỗi nhỏ
EEvaluate3-5 phútTrace test case, complexity, optimisation
Bí quyết phỏng vấn FAANG:
Đa số ứng viên thất bại không phải vì code sai — mà vì nhảy thẳng vào code mà không U-M-P trước. Interviewer chấm 50% ở "thinking process". Hãy nói to: "Em đang ở bước Understand", "Em match bài này với Sliding Window pattern".
3.2

3.2 Bước U-M-P — Thực hành

U — Understand: 5 câu hỏi cần hỏi

  1. Range của input? (n có thể lên 10⁵, 10⁹...?)
  2. Có giá trị âm? Có duplicate? Có rỗng?
  3. Output format chính xác là gì?
  4. Có nhiều test case hay 1 lần?
  5. Edge case: input rỗng, 1 phần tử, đã sort?

M — Match: 4 nhóm pattern phổ biến

P — Plan: Pseudocode 5-7 dòng

# Two Sum example pseudocode
# 1. Build hash map: value → index
# 2. For each (i, num):
#    - If target - num in map: return [map[target-num], i]
#    - Else: map[num] = i
# Time O(n), Space O(n)

Viết pseudocode ngắn trên whiteboard/note rồi mới code thật.

3.3

3.3 Communication Tips — Yếu tố thắng phỏng vấn

Think out loud — Nói khi đang nghĩ

Bốn cụm từ "vàng" trong phỏng vấn

  1. "Để em làm rõ đề..." — tránh hiểu sai.
  2. "Em nghĩ approach naive là... rồi sẽ tối ưu sau." — show thinking process.
  3. "Trade-off ở đây là..." — show senior thinking.
  4. "Em test trên ví dụ này..." — show debug skill.

Khi bí — đừng im lặng

Nói: "Em hơi bí, em sẽ thử cách khác — start với brute force để có baseline rồi cải tiến." Interviewer thích người không hoảng loạn — không thích người im lặng nhìn màn hình 5 phút.

Khi sai — đừng giấu

Nếu phát hiện bug giữa code, nói thẳng: "Em phát hiện lỗi ở dòng X — em sẽ sửa." Tự tìm ra bug được điểm cao hơn nhiều so với bị interviewer chỉ ra.

3.4

3.4 10 Common Interview Mistakes — Tránh ngay

  1. Nhảy thẳng vào code mà không Understand.
  2. Im lặng nghĩ > 30s — interviewer không biết đầu em đang ở đâu.
  3. Không hỏi clarifying questions — assume sai input format.
  4. Bỏ qua complexity analysis — không nói O(n) sau khi code xong.
  5. Không test code sau khi viết — bug rõ ràng vẫn submit.
  6. Code lộn xộn, không có comment — interviewer phải đoán logic.
  7. Không xử lý edge cases — empty, single element, all same.
  8. Cố gắng "sửa cho tốt" khi đang chạy đua thời gian — submit working version trước.
  9. Phản ứng tiêu cực khi bị hint — hint là dấu hiệu interviewer muốn em thành công.
  10. Không hỏi về role/team ở cuối — bỏ lỡ cơ hội thể hiện sự quan tâm.
Nguyên tắc vàng:
"Code làm việc chưa tối ưu" > "code tối ưu bị bug" > "code đẹp không chạy". Submit working solution trước, optimise sau khi còn thời gian.

Phần 4. Capstone — Tổng hợp 14 tuần

Pattern recognition cheat sheet và mock interview problem
4.1

4.1 Pattern Recognition Cheat Sheet — 14 tuần

TuầnPatternTín hiệu
1Big-O + PythonMọi bài
2Two Pointers + Prefix Sum"Subarray", sorted, palindrome, range query
3Hashing"Có tồn tại", "đếm tần suất", "anagram"
4Sliding Window"Substring/subarray với điều kiện"
5Linked List"Đảo", "cycle", "merge", "Nth from end"
6Stack + Monotonic + Deque"Next greater", "matching", "sliding window max"
7Binary SearchSorted array, "min X sao cho có thể Y", n ≤ 10⁹
8Recursion + Tree + D&C"Cây", "đệ quy", "merge/quick sort"
9Heap + BST"Top K", "merge K", "median stream"
10Graph (BFS/DFS/Topo/Union-Find)"Đảo", "shortest path", "course schedule"
11Backtracking"Liệt kê tất cả", "permutations", "constraint satisfaction"
12-13Dynamic Programming"Min/max", "đếm số cách", overlapping subproblems
14Bit + Trie"XOR", "prefix search", "n ≤ 20"
4.2

4.2 Worked Example 7 — LRU Cache (#146) — Mock Interview

Đề: Thiết kế Least Recently Used Cache với capacity. Hỗ trợ:

U — Understand

M — Match

Cần O(1) lookup → HashMap (Tuần 3). Cần O(1) thao tác hai đầu (most recent + least recent) → Doubly Linked List (Tuần 5). Combo của 2 pattern!

P — Plan

# HashMap: key → node trong DLL
# DLL: head ←→ ... ←→ tail
# get: tra hash → move node lên front
# put: nếu key có → update + move; nếu không → add front, evict tail nếu vượt capacity
4.3

4.3 LRU Cache — Implementation

class Node:
    def __init__(self, key=0, val=0):
        self.key = key; self.val = val
        self.prev = self.next = None

class LRUCache:
    def __init__(self, capacity):
        self.cap = capacity
        self.cache = {}                          # key → node
        self.head = Node(); self.tail = Node()    # dummy
        self.head.next = self.tail; self.tail.prev = self.head

    def _remove(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev

    def _add_front(self, node):
        node.next = self.head.next
        node.prev = self.head
        self.head.next.prev = node
        self.head.next = node

    def get(self, key):
        if key not in self.cache: return -1
        node = self.cache[key]
        self._remove(node); self._add_front(node)
        return node.val

    def put(self, key, value):
        if key in self.cache:
            self._remove(self.cache[key])
        node = Node(key, value)
        self._add_front(node)
        self.cache[key] = node
        if len(self.cache) > self.cap:
            lru = self.tail.prev
            self._remove(lru)
            del self.cache[lru.key]
4.4

4.4 LRU Cache — Tinh thần "Combine Patterns"

R — Review & E — Evaluate

Test case mẫu: capacity=2, put(1,1), put(2,2), get(1)=1, put(3,3) (evict 2), get(2)=-1.

Sau put(1,1): {1:N1}, DLL: head ↔ N1 ↔ tail
Sau put(2,2): {1:N1, 2:N2}, DLL: head ↔ N2 ↔ N1 ↔ tail
Sau get(1) = 1: move N1 lên front → DLL: head ↔ N1 ↔ N2 ↔ tail
Sau put(3,3): cap đầy → evict N2 (cuối). {1:N1, 3:N3}, DLL: head ↔ N3 ↔ N1 ↔ tail
Sau get(2): không có → -1 ✓

Complexity

Get/put đều O(1). Space O(capacity).

Bài học từ LRU Cache

Đây là tinh thần phỏng vấn senior:
Bài "khó" thường là combo của các bài "dễ" đã biết. Khi gặp design problem, tự hỏi: "Cần O(?) cho operation A?", "Cần O(?) cho operation B?" — match với cấu trúc tương ứng → ráp lại.
4.5

4.5 Sample Mock Interview Script

Đề: "Cho mảng nums và k. Tìm subarray dài nhất có nhiều nhất k loại số khác nhau."

Phỏng vấn 45 phút mẫu

[Phút 0-3] Understand:
Em: "Để em làm rõ — subarray là liên tiếp, đúng không?"
Anh: "Đúng."
Em: "n có thể lên bao nhiêu? Có số âm?"
Anh: "n ≤ 10⁵, mọi số nguyên."
Em: "Edge case: k = 0 → trả 0; mảng rỗng → trả 0."

[Phút 3-5] Match:
Em: "Đề có 'subarray liên tiếp' + 'điều kiện trên window' → em nghĩ đây là Sliding Window
     biến thể, tương tự #340 Longest Substring K Distinct Characters."

[Phút 5-10] Plan:
Em: "Em dùng Variable Window:
     - Counter theo dõi loại số trong window.
     - Right mở rộng window.
     - Khi len(counter) > k: shrink left cho đến khi ≤ k.
     - Cập nhật max length.
     Time O(n), Space O(k)."

[Phút 10-30] Implement: viết code, nói to mỗi bước.

[Phút 30-40] Review & Evaluate:
Em trace với [1,2,1,2,3], k=2 → expect 4 ([1,2,1,2]).
Em check edge: k=0, k >= n.

[Phút 40-45] Discussion:
Em: "Có thể tối ưu nếu k cố định và luôn ≤ 26 (chữ cái) — dùng array thay dict
     để giảm hằng số. Cũng có thể chuyển sang stream nếu input đến từng phần."
Anh: hỏi follow-up.

Phần 5. Roadmap sau khoá học

14 tuần là khởi đầu — đây là cách tiếp tục phát triển
5.1

5.1 Lộ trình tự học sau khoá

1-3 tháng tiếp theo — Củng cố

3-6 tháng — Mở rộng

6-12 tháng — Áp dụng vào sự nghiệp

5.2

5.2 Tài nguyên đáng dùng

Practice platforms

Books

Online courses

Cộng đồng tiếng Việt

6.0

Tổng kết toàn khoá — 14 tuần đã đi qua

Số liệu

Bộ kỹ năng đạt được

Mức độ kỳ vọng đạt được

Đây là điểm khởi đầu — chứ không phải đích đến. Hành trình tiếp tục.

CHUYÊN ĐỀ COMPETITIVE PROGRAMMING

Cảm ơn các em.

Code is poetry — học pattern, không học bài.

"The expert in anything was once a beginner."

Hết khoá — Chúc các em phỏng vấn thành công.

Mục lục

Nhấn T hoặc Esc để đóng