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á.
| Op | Tên | Ví dụ | Kết quả |
|---|---|---|---|
& | AND | 0b1100 & 0b1010 | 0b1000 |
| | OR | 0b1100 | 0b1010 | 0b1110 |
^ | XOR | 0b1100 ^ 0b1010 | 0b0110 |
~ | NOT | ~0b0011 | ...11111100 |
<< | Left shift | 1 << 3 | 8 (= 2³) |
>> | Right shift | 16 >> 2 | 4 (= 16/4) |
x ^ x = 0 — XOR với chính nó = 0.x ^ 0 = x — XOR với 0 không đổi.a ^ b ^ a = b — XOR có tính giao hoán + kết hợp.Ba tính chất này tạo nên trick "Single Number" và nhiều bài XOR khác.
| Mục đích | Cú pháp | Giải thích |
|---|---|---|
| Check bit thứ i | (x >> i) & 1 | Dịch i bước rồi AND với 1 |
| Set bit thứ i | x | (1 << i) | OR với mask |
| Clear bit thứ i | x & ~(1 << i) | AND với mask đảo |
| Toggle bit thứ i | x ^ (1 << i) | XOR với mask |
| Check là power of 2 | x > 0 and x & (x-1) == 0 | Power of 2 chỉ có 1 bit on |
| Lowest set bit (LSB) | x & -x | Trick "lowbit" của Fenwick Tree |
| Bỏ LSB | x & (x-1) | Dùng đếm số bit on (Brian Kernighan) |
| Đếm bits trong Python | bin(x).count('1') | Idiom Python ngắn nhất |
x & (x-1):Đề: 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.
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
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).
Đề: Cho số nguyên n. Đếm số bit 1 trong biểu diễn nhị phân.
def hammingWeight(n):
count = 0
while n:
count += n & 1
n >>= 1
return count
# O(32) = O(1)
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.
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.
Đề: Cho n. Trả mảng ans[i] = số bit 1 trong i, với i = 0..n.
O(n × 32) — đủ pass nhưng không tận dụng được tính chất.
Quan sát: ans[i] = ans[i >> 1] + (i & 1).
i >> 1: i bỏ LSB.i & 1: LSB của i (0 hoặc 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).
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.
Khi tập items có size n nhỏ (≤ 20), có thể dùng bitmask để biểu diễn subset:
# 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.
n ≤ 15..20.Đâ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.
x & (x-1) làm gì?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ó.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.
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).
is_end đánh dấu kết thúc từ.
Đề (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 ô).
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.
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
| Aspect | Brute (call WS cho mỗi word) | Trie + DFS |
|---|---|---|
| Time | O(M × N × W × 4^L) | O(M × N × 4^L) |
| Khi W = 100, L = 10 | 10⁴ × 10⁶ = 10¹⁰ | 10⁴ × 10⁶ = 10¹⁰... nhưng 4^L thực tế cắt sớm |
| Pruning | Mỗi word tìm độc lập | Chia sẻ prefix → đóng nhánh sớm khi không match prefix nào |
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.
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.
pop('$') sau khi tìm thấy giải quyết vấn đề gì?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ước | Tên | Thời gian | Mục đích |
|---|---|---|---|
| U | Understand | 3-5 phút | Hiểu đề, làm rõ ràng buộc, edge cases |
| M | Match | 2-3 phút | Match với pattern đã biết |
| P | Plan | 5-7 phút | Pseudocode + complexity dự kiến |
| I | Implement | 15-20 phút | Code thật |
| R | Review | 3-5 phút | Đọc lại, fix lỗi nhỏ |
| E | Evaluate | 3-5 phút | Trace test case, complexity, optimisation |
# 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.
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.
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.
| Tuần | Pattern | Tín hiệu |
|---|---|---|
| 1 | Big-O + Python | Mọi bài |
| 2 | Two Pointers + Prefix Sum | "Subarray", sorted, palindrome, range query |
| 3 | Hashing | "Có tồn tại", "đếm tần suất", "anagram" |
| 4 | Sliding Window | "Substring/subarray với điều kiện" |
| 5 | Linked List | "Đảo", "cycle", "merge", "Nth from end" |
| 6 | Stack + Monotonic + Deque | "Next greater", "matching", "sliding window max" |
| 7 | Binary Search | Sorted array, "min X sao cho có thể Y", n ≤ 10⁹ |
| 8 | Recursion + Tree + D&C | "Cây", "đệ quy", "merge/quick sort" |
| 9 | Heap + BST | "Top K", "merge K", "median stream" |
| 10 | Graph (BFS/DFS/Topo/Union-Find) | "Đảo", "shortest path", "course schedule" |
| 11 | Backtracking | "Liệt kê tất cả", "permutations", "constraint satisfaction" |
| 12-13 | Dynamic Programming | "Min/max", "đếm số cách", overlapping subproblems |
| 14 | Bit + Trie | "XOR", "prefix search", "n ≤ 20" |
Đề: Thiết kế Least Recently Used Cache với capacity. Hỗ trợ:
get(key): trả value nếu có, -1 nếu không. O(1).put(key, value): thêm/cập nhật. Nếu đầy, evict key ít dùng nhất. O(1).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!
# 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
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]
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 ✓
Get/put đều O(1). Space O(capacity).
Đề: "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ú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.
Đây là điểm khởi đầu — chứ không phải đích đến. Hành trình tiếp tục.
Code is poetry — học pattern, không học bài.
"The expert in anything was once a beginner."
Nhấn T hoặc Esc để đóng