Coding DSA Interview At Big Tech kèm lời giải

Phạm Ngọc Lâm — ex-Senior Software Engineer @ TikTok / Grab, co-founder EngineerPro

Lê Quang Hoà — ex-Tech Lead @ TikTok, co-founder EngineerPro

2026

Mục tiêu project: tổng hợp các câu hỏi DSA coding interview tiêu biểu nhất ở các công ty Big Tech, kèm lời giải Python 3, phân tích độ phức tạp, bẫy phỏng vấn thường gặp, và bài tự luyện liên quan để bạn đọc luyện tập có hệ thống theo pattern, không phải học vẹt từng bài.

Quy mô: 288 bài tập (267 bài giải đầy đủ + 21 bài recap cross-reference qua các chương) · 44 pattern · Lời giải Python 3 chuẩn LC.

📚 Cuốn sách dựa trên nội dung khoá học — các topic được biên soạn theo lộ trình của khoá DSA Coding Interview đang được giảng dạy tại EngineerPro.

Tuy nhiên, cuốn sách không thể thay thế việc rèn luyện tư duy thuật toán cũng như cách thức học và hiểu sâu về thuật toán mà bạn có được khi tham gia khoá học trực tiếp tại EngineerPro.

Các bạn quan tâm về khoá học vui lòng nhắn tin qua fanpage để được tư vấn:

Project này được bảo trợ bởi EngineerPro.

EngineerPro là cộng đồng và nền tảng đào tạo hướng đến việc giúp kỹ sư phần mềm Việt Nam bứt phá vào Big Tech và các công ty quốc tế — từ phỏng vấn coding, system design, đến career growth. Cuốn sách này là một trong những đóng góp của chúng tôi vào hệ sinh thái học tập tiếng Việt cho engineers.

How to find us:

Về tác giả

Phạm Ngọc Lâm

Phạm Ngọc Lâm

ex-Senior Software Engineer @ TikTok · Grab

Co-founder EngineerPro

in LinkedIn 🌐 Portfolio

Lê Quang Hoà

Lê Quang Hoà

ex-Tech Lead @ TikTok

Co-founder EngineerPro

in LinkedIn

Cả hai tác giả đã trải qua hàng trăm cuộc phỏng vấn technical với vai trò cả interviewer lẫn interviewee tại TikTok, Grab và các công ty công nghệ hàng đầu khác. Cuốn sách này là chắt lọc từ những trải nghiệm thật ở cả hai phía của bàn phỏng vấn.

🙏 Lời cảm ơn

Cuốn sách này lấy cảm hứng từ các chủ đề DSA mà nhiều mentor xuất sắc tại EngineerPro đã giảng dạy trong các khoá học và mentor session suốt nhiều năm qua. Hai tác giả xin gửi lời cảm ơn chân thành đến đội ngũ giảng viên đã đồng hành cùng cộng đồng:

  • Chị Lam Đỗ — ex-Software Engineer @ Meta
  • Anh Lê Chương — Senior Software Engineer @ Google
  • Anh Trần Khánh Hiệp — Software Engineer @ Spotify
  • Anh Tùng Trần — Senior SWE @ Pendle · ex-Senior SWE @ Shopee
  • Anh Quang Hoàng — Senior Software Engineer @ Google
  • Anh Kyle Nguyễn — Senior Engineer @ Citadel
  • Anh Tùng Lâm — Senior Software Engineer @ Shopee
  • Anh Hiếu — Senior Software Engineer @ Acronis · ex-SWE @ Shopee
  • … và nhiều anh/chị giảng viên khác trong cộng đồng EngineerPro.

Phần mở đầu

Video: ví dụ một buổi phỏng vấn coding

Trước khi đi vào nội dung chính, mời bạn xem một buổi phỏng vấn coding mẫu do EngineerPro thực hiện. Video giúp bạn hình dung luồng diễn ra thực tế của một vòng phỏng vấn: cách interviewer đặt câu hỏi, cách ứng viên clarify, phân tích, code và trao đổi follow-up.

Ví dụ một buổi phỏng vấn coding — EngineerPro Xem trên YouTube

▶ Xem cả playlist các buổi phỏng vấn mẫu khác trên kênh YouTube EngineerPro.

Mẹo xem: lần đầu xem hãy quan sát cách ứng viên trình bày, không cần hiểu hết thuật toán. Lần thứ 2 quay lại sau khi đã đọc xong Chương 1–5, bạn sẽ nhận ra rất nhiều pattern quen thuộc.


0.1 Lời nói đầu

Chào mừng bạn đến với Coding DSA Interview At Big Tech kèm lời giải.

Cuốn sách này được viết với một niềm tin đơn giản: phỏng vấn coding ở Big Tech không phải là cuộc thi đố vui — đó là một bộ kỹ năng có thể học và rèn luyện một cách có hệ thống.

Hầu hết các tài liệu phỏng vấn hiện có thường rơi vào 1 trong 2 thái cực: - Quá học thuật (textbook DSA), thiếu thực tế phỏng vấn. - Quá “mẹo vặt” (300 LeetCode), thiếu khung tư duy hệ thống.

Cuốn sách này cố gắng đứng ở giữa: học PATTERN, không học bài. Mỗi chương tập trung vào 1 pattern, 5-18 bài tiêu biểu, kèm cách trình bày trên whiteboard và bẫy thường gặp khi phỏng vấn thật. Khi bạn nắm được pattern, bài mới chỉ là biến thể.

Đối tượng độc giả: - Sinh viên CNTT chuẩn bị thực tập / new-grad ở Big Tech. - Lập trình viên đã đi làm muốn chuyển việc lên FAANG-tier. - Người tự học LeetCode đang bí cách tiếp cận có hệ thống.

Chúc bạn pass mọi vòng phỏng vấn!

Cách dùng cuốn sách này

Có 3 cách tiếp cận tuỳ thời gian + kinh nghiệm:

Đọc tuần tự (khuyến nghị cho người mới): Chương 1 → 44 theo thứ tự. Mỗi chương bạn phải tự code trước khi xem lời giải. Hết Level 1 (Chương 1–20) bạn đã có nền tảng tốt cho phỏng vấn entry-level Big Tech.

Đọc theo pattern (nếu đã có nền): lướt mục lục, đọc chương nào còn yếu. Mỗi chương đứng độc lập, có tham chiếu chéo (cross-reference) sang chương khác rõ ràng.

Ôn gấp trước onsite: xem Roadmap học dưới đây.

Roadmap học

Roadmap 1 tuần (chuẩn bị onsite gấp): - Ngày 1-2: Frontmatter (0.1-0.5) + Phụ lục D (50 bài must-do). - Ngày 3: Ôn Chương 1, 2, 5, 6 (Array, String, Binary Search, Hash) — toàn bộ Easy/Medium tier. - Ngày 4: Chương 7, 9, 10, 11 (Linked List, Graph, BFS, DFS). - Ngày 5: Chương 17, 18, 27 (D&C, Monotonic, Sliding Window). - Ngày 6: Chương 28, 29 (Backtracking, DP) — 2 chương dài và quan trọng nhất. - Ngày 7: Mock interview + đọc Phụ lục E (Behavioral).

Roadmap 2 tuần (đã biết DSA, refresh): - Tuần 1: Level 1 (Chương 1-20) — 3 chương/ngày. - Tuần 2: Level 2 (Chương 21-32) — 2 chương/ngày + mock interview cuối tuần.

Roadmap 6 tuần (người mới): - Tuần 1-2: Frontmatter + Level 1 (Ch 1-10) — 1 chương/ngày, tự code mỗi bài. - Tuần 3-4: Level 1 (Ch 11-20) — 1 chương/ngày. - Tuần 5: Level 2 (Ch 21-32) — 2 chương/ngày, đọc kỹ pattern. - Tuần 6: Level 3 (Ch 33-44) — 2 chương/ngày, không cần thuộc hết — biết tồn tại.

Skip nếu thiếu thời gian (priority chương quan trọng): bỏ qua Ch 20 (Prime), Ch 31 (Game Theory), Ch 33-36 (MST/Hash/KMP/Z) — niche pattern, hiếm trong phỏng vấn entry/mid.

Tự đánh giá level: - Easy < 30 phút mỗi bài → Level 1 đủ. - Medium 30-60 phút → đọc tới Level 2. - Hard > 60 phút hoặc bí thường xuyên → đọc cả Level 3.

⚠️ Đừng đọc code trước khi tự lên kế hoạch. Sách trình bày lời giải sau mục “Hướng tiếp cận” có lý do — bạn phải vật lộn với bài trước khi xem đáp án; đó là cách não bộ hấp thu pattern hiệu quả nhất.


0.2 Quy trình một buổi phỏng vấn coding (UMPIRE)

UMPIRE = framework 6 bước giúp bạn không “đóng băng” khi nhận đề:

U — Understand (5 phút)

M — Match (2 phút)

P — Plan (5 phút)

I — Implement (15 phút)

R — Review (3 phút)

E — Evaluate (2 phút)

Mẹo tâm lý: Interviewer muốn thấy quá trình tư duy, không phải lời giải hoàn hảo ngay từ đầu. Hãy nói thành lời (think out loud).


0.3 Big-O trong 30 phút

Định nghĩa nhanh

f(n) = O(g(n)) ↔︎ tồn tại c > 0, n₀ sao cho f(n) <= c·g(n) với mọi n >= n₀.

Trong phỏng vấn: bỏ qua hằng số, bỏ qua thành phần thấp hơn. 3n² + 100n + 5 = O(n²).

Bảng “cheatsheet”

Notation Tên Ví dụ
O(1) Constant Hash lookup, push/pop stack
O(log n) Logarithm Binary search
O(n) Linear Duyệt mảng
O(n log n) Linearithmic Sort, segment tree build
O(n²) Quadratic Brute force 2 vòng for
O(2^n) Exponential Subset bruteforce
O(n!) Factorial Permutation bruteforce

Quy tắc tính toán

  1. Sequential (a();b();): O(a + b), lấy max.
  2. Nested loops: O(n × n) = O(n²).
  3. Recursion:
  4. Master theorem cho T(n) = aT(n/b) + f(n):

Amortized analysis

Một số op đôi khi chậm, trung bình nhanh: - list.append() Python: O(1) amortized (mặc dù dynamic resize). - Hash table với open addressing: O(1) amortized.

Bẫy thường gặp

Các bound thực tế trong phỏng vấn

n Cho phép
n ≤ 10 O(n!) brute force
n ≤ 20 O(2^n) bitmask
n ≤ 5000 O(n²) OK
n ≤ 10^5 O(n log n) hoặc O(n)
n ≤ 10^7 O(n) strict
n ≤ 10^9 O(log n) (search on answer)

0.4 Python 3 cheat-sheet cho phỏng vấn

Data structures

# List
lst = [1, 2, 3]
lst.append(x); lst.pop()      # O(1) cả 2
lst.insert(0, x); lst.pop(0)  # O(n) - tránh!
sorted_lst = sorted(lst)      # O(n log n), trả về list mới
lst.sort()                    # in-place
lst[::-1]                     # reverse, O(n)
lst[a:b]                      # slicing, O(b-a) copy

# Dict
d = {}; d[k] = v               # O(1) amortized
from collections import defaultdict, Counter
dd = defaultdict(list)
cnt = Counter("anagram")       # {'a':3, 'n':1, 'g':1, 'r':1, 'm':1}
cnt.most_common(2)             # [('a',3), ('n',1)]

# Set
s = {1, 2, 3}
s.add(x); s.discard(x)         # O(1)
s & t; s | t; s - t            # giao/hợp/hiệu

# Deque (double-ended queue) — dùng cho BFS, sliding window
from collections import deque
dq = deque()
dq.append(x); dq.appendleft(x)
dq.pop(); dq.popleft()         # tất cả O(1)

# Heap (min-heap)
import heapq
h = []
heapq.heappush(h, x)
heapq.heappop(h)              # O(log n)
heapq.heapify(lst)            # O(n)
heapq.nsmallest(k, lst)       # O(n log k)

Idioms quan trọng

# Enumerate
for i, x in enumerate(lst):
    pass

# Zip
for a, b in zip(lst1, lst2):
    pass

# Comprehension
[x*2 for x in lst if x > 0]
{x: i for i, x in enumerate(lst)}

# Bisect
from bisect import bisect_left, bisect_right
idx = bisect_left(sorted_lst, x)

# Functools
from functools import cache, reduce
@cache
def f(n):
    ...
reduce(lambda a, b: a + b, lst, 0)

# Itertools
from itertools import combinations, permutations, product
list(combinations([1,2,3], 2))    # [(1,2),(1,3),(2,3)]

Pitfalls (CRITICAL — đọc kỹ!)

Số học & division: - dict[k] raise KeyError nếu k không có → dùng dict.get(k, default) hoặc defaultdict. - int / int = float; phải dùng // cho integer division. - -7 // 2 = -4 (floor), không phải -3 (truncate toward 0). Truncate: int(-7/2). - -7 % 2 = 1 trong Python (luôn ≥ 0), khác với C/C++/Java. Cẩn thận khi modulo số âm trong prefix sum.

Heap & comparison: - heapq Python chỉ có min-heap. Muốn max-heap → push -x. - Heap so sánh tuple element-by-element: (priority, idx, payload)idx làm tiebreaker khi priority bằng nhau (vì payload có thể không hashable / comparable, ví dụ ListNode). - heappush heap với tuple chứa object không comparable (như ListNode) sẽ raise TypeError khi priority bằng nhau.

Recursion & cache: - Python recursion limit mặc định ~1000. Set sys.setrecursionlimit(10**6) cho graph/tree lớn. - Python không có tail call optimization → đệ quy đệ quy đệ quy ⇒ stack overflow. Chuyển sang iterative khi n > 10^5. - @functools.cache chỉ work với arguments hashable. list / dict / set → không cache được. Dùng tuple(...) / frozenset(...) để wrap. - @cache trên self.method lưu cache global (qua mọi instance). Dùng @cached_property nếu cache theo instance.

Mutable defaults & references: - Default mutable argument: def f(x=[]) — bug nghiêm trọng (mọi call share cùng list). Dùng def f(x=None): x = x or []. - result.append(path) — append reference, không copy. Cần result.append(path.copy()) hoặc result.append(path[:]).

Sort & stability: - sorted() Python là Timsort, stable, O(n log n). Tận dụng stability để sort multi-key bằng nhiều lần sort. - Sort custom: Python 3 không có cmp= arg. Dùng key=... hoặc functools.cmp_to_key(...).

Integer & overflow: - Python int vô hạn → không cần lo overflow như Java/C++. Nhưng cố ý truncate (32-bit) khi đề yêu cầu (LC 7, LC 8, LC 50).


0.5 Cách trình bày code trên whiteboard / CoderPad

Nguyên tắc vàng

  1. Nói thành lời (“think out loud”). Im lặng khiến interviewer khó theo dõi tư duy của bạn — họ cần nghe được luồng suy nghĩ để giúp khi bạn bí, và để đánh giá cách bạn tiếp cận vấn đề chứ không chỉ kết quả cuối.
  2. Bắt đầu từ ví dụ cụ thể. Vẽ input ra giấy, chạy thuật toán bằng tay từng bước.
  3. Code theo hướng top-down. Viết hàm chính def solve(...) trước, gọi tới các hàm phụ (helper), rồi mới cài đặt hàm phụ sau.
  4. Đặt tên biến rõ ràng:
  5. Đừng cố viết ngắn cho ngắn. Code rõ ràng quan trọng hơn 1 dòng “thông minh”.

Khi bí

Sau khi xong

Lưu ý về tác phong khi phỏng vấn

Mẫu lời nói cho từng giai đoạn

Giai đoạn Câu mẫu
Làm rõ đề “Cho em xác nhận lại đề: input là …, output là …, có ràng buộc gì thêm không?”
Brute force “Trước hết em mô tả cách trực tiếp: duyệt mọi cặp, độ phức tạp O(n²)…”
Tối ưu “Em thấy có thể dùng hash map để giảm thao tác tra cứu xuống O(1)…”
“Em đang lưỡng lự giữa hash map và sorted array. Anh/chị có gợi ý nào không?”
Xong “Lời giải chạy O(n) thời gian và O(n) bộ nhớ. Để em chạy thử vài edge case…”

Chương 1 — Array

Array (mảng) là cấu trúc dữ liệu cơ bản nhất nhưng cũng là pattern xuất hiện nhiều nhất trong phỏng vấn coding ở Big Tech. Phần lớn các kỹ thuật ở chương sau (two pointers, sliding window, prefix sum, monotonic stack, …) đều bắt nguồn từ việc thao tác trên array. Mục tiêu của chương: thành thạo các thao tác in-place, two-pass, và bắt đầu hình thành thói quen “nghĩ về index thay vì nghĩ về phần tử”.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Template code

from typing import List

def two_pass_pattern(nums: List[int]) -> List[int]:
    """Mẫu 2-lượt: pass 1 gom thông tin, pass 2 dùng thông tin đó."""
    n = len(nums)
    aux = [0] * n
    # pass 1: tính prefix / suffix / count
    for i in range(n):
        aux[i] = ...   # tuỳ bài
    # pass 2: dùng aux để ra kết quả
    out = [0] * n
    for i in range(n):
        out[i] = ...   # tuỳ bài
    return out


def two_pointers_in_place(nums: List[int]) -> int:
    """Mẫu two pointers in-place: slow = vị trí ghi, fast = vị trí đọc."""
    slow = 0
    for fast in range(len(nums)):
        if condition(nums[fast]):
            nums[slow] = nums[fast]
            slow += 1
    return slow  # độ dài phần "hợp lệ" sau khi nén

Bài tự luyện cuối chương


1.1 Two Sum (LC 1)

Đề bài

Cho một mảng số nguyên nums và một số nguyên target. Hãy trả về chỉ số của hai phần tử trong nums sao cho tổng của chúng bằng target.

Bạn có thể giả định mỗi input có đúng một đáp án, và không được dùng cùng một phần tử hai lần.

Ví dụ

Input:  nums = [2, 7, 11, 15], target = 9
Output: [0, 1]
Giải thích: nums[0] + nums[1] == 9.

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — O(n²). Duyệt mọi cặp (i, j) với i < j và kiểm tra nums[i] + nums[j] == target. Dễ code nhưng sẽ TLE khi n lớn.

Tối ưu — Hash map một lượt — O(n). Khi đứng tại chỉ số i, ta cần tìm xem có j < i nào để nums[j] == target - nums[i] hay không. Dùng dict để lưu {giá_trị: chỉ_số} của các phần tử đã thấy.

Mẹo trình bày: Luôn bắt đầu bằng brute force, nói rõ độ phức tạp của nó, sau đó nói: “Em nghĩ có thể thay phép tìm tuyến tính O(n) bằng hash map tra cứu O(1), nhờ đó tổng độ phức tạp giảm từ O(n²) xuống O(n)…” — interviewer rất thích luồng tư duy này.

Code Python 3

from typing import List

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        seen: dict[int, int] = {}
        for i, x in enumerate(nums):
            complement = target - x
            if complement in seen:
                return [seen[complement], i]
            seen[x] = i
        return []  # theo đề bài, dòng này không bao giờ chạy

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


1.2 Best Time to Buy and Sell Stock (LC 121)

Đề bài

Cho mảng prices với prices[i] là giá cổ phiếu vào ngày thứ i. Bạn được mua một lần rồi bán một lần sau đó (không được bán trước khi mua). Hãy trả về lợi nhuận lớn nhất có thể, hoặc 0 nếu không có giao dịch nào có lãi.

Ví dụ

Input:  prices = [7, 1, 5, 3, 6, 4]
Output: 5
Giải thích: mua ngày 2 (giá 1), bán ngày 5 (giá 6), lợi nhuận = 6 - 1 = 5.

Input:  prices = [7, 6, 4, 3, 1]
Output: 0
Giải thích: giá giảm liên tục, không có giao dịch nào có lãi.

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — O(n²). Duyệt mọi cặp (i, j) với i < j và lấy max(prices[j] - prices[i]). TLE với n = 10^5.

Tối ưu — một lượt, O(n). Khi đứng tại ngày i và quyết định “sẽ bán hôm nay”, lợi nhuận tối ưu là prices[i] - min(prices[0..i-1]). Vậy ta chỉ cần duy trì min_so_far khi duyệt và cập nhật best mỗi bước.

Hình minh hoạ với prices = [7, 1, 5, 3, 6, 4]:

day        :   0    1    2    3    4    5
prices     :   7    1    5    3    6    4
                    │              │
                    │ mua ở đây    │ bán ở đây
                    ▼              ▼
min_so_far :   7    1    1    1    1    1
profit_now :   0    0    4    2    5    3   (= prices[i] - min_so_far)
best       :   0    0    4    4    5    5   ← đáp án = 5
                              ▲
                              giữ nguyên vì 2 < 4

Mindset: Đây là DP một biến. State min_so_far chính là cách rút gọn mảng dp[i] = min(prices[0..i]) về O(1) space — kỹ thuật sẽ gặp đi gặp lại ở các chương DP sau.

Code Python 3

from typing import List
import math

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        min_so_far = math.inf
        best = 0
        for p in prices:
            min_so_far = min(min_so_far, p)
            best = max(best, p - min_so_far)
        return best

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


1.3 Product of Array Except Self (LC 238)

Đề bài

Cho một mảng numsn số nguyên, trả về mảng answer cùng độ dài, sao cho answer[i] là tích của tất cả phần tử của nums trừ nums[i].

Ràng buộc đặc biệt: - Không được dùng phép chia. - Phải chạy trong O(n) time.

Ví dụ

Input:  nums = [1, 2, 3, 4]
Output: [24, 12, 8, 6]
Giải thích:
  answer[0] = 2*3*4 = 24
  answer[1] = 1*3*4 = 12
  answer[2] = 1*2*4 = 8
  answer[3] = 1*2*3 = 6

Input:  nums = [-1, 1, 0, -3, 3]
Output: [0, 0, 9, 0, 0]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — O(n²). Với mỗi i, duyệt lại toàn mảng để tính tích — đề bài đã cấm sẵn.

Phép chia — O(n) nhưng bị cấm. Tính total = product(nums) rồi answer[i] = total / nums[i]. Cấm vì khi nums[i] == 0 ta chia cho 0; hơn nữa, nhiều ngôn ngữ không có integer-exact division.

Tối ưu — Prefix product + Suffix product — O(n) time, O(1) extra space (không tính output).

Quan sát: answer[i] = (∏ nums[0..i-1]) * (∏ nums[i+1..n-1]). Gọi 2 lượng: - left[i] = tích nums[0..i-1] (left product, left[0] = 1). - right[i] = tích nums[i+1..n-1] (right product, right[n-1] = 1). Khi đó answer[i] = left[i] * right[i].

Để đạt O(1) extra space, ta dùng chính mảng answer: - Lượt 1 (trái → phải): điền answer[i] = left[i]. - Lượt 2 (phải → trái): nhân answer[i] *= right, vừa duyệt vừa cập nhật biến right.

Hình minh hoạ với nums = [1, 2, 3, 4]:

              i=0     i=1     i=2     i=3
nums      :  [  1  ,   2  ,   3  ,   4  ]

                 ┌─────────────┐
                 │  prefix →   │   (tích các phần tử BÊN TRÁI i)
                 ▼             ▼
left[i]   :  [  1  ,   1  ,   2  ,   6  ]
              (rỗng) (1)   (1·2) (1·2·3)

                         ┌─────────────┐
                         │   ← suffix  │  (tích các phần tử BÊN PHẢI i)
                         ▼             ▼
right[i]  :  [ 24  ,  12  ,   4  ,   1  ]
            (2·3·4)(3·4)  (4)  (rỗng)

                         ↓  nhân từng vị trí  ↓

answer[i] :  [ 24  ,  12  ,   8  ,   6  ]
              1·24   1·12   2·4    6·1

Trong code thực, ta không lưu cả 2 mảng leftright — chỉ dùng answer cho lượt prefix, rồi dùng 1 biến right rolling từ phải sang trái để nhân vào answer ngay tại chỗ.

Code Python 3

from typing import List

class Solution:
    def productExceptSelf(self, nums: List[int]) -> List[int]:
        n = len(nums)
        answer = [1] * n

        # Lượt 1: answer[i] = tích các phần tử ở bên trái i.
        left = 1
        for i in range(n):
            answer[i] = left
            left *= nums[i]

        # Lượt 2: nhân thêm tích bên phải, dùng biến right rolling.
        right = 1
        for i in range(n - 1, -1, -1):
            answer[i] *= right
            right *= nums[i]

        return answer

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


1.4 Move Zeroes (LC 283)

Đề bài

Cho mảng nums, hãy di chuyển tất cả số 0 về cuối mảng, giữ nguyên thứ tự tương đối của các phần tử khác 0. Phải làm in-place, không được tạo mảng phụ.

Ví dụ

Input:  nums = [0, 1, 0, 3, 12]
Output: [1, 3, 12, 0, 0]

Input:  nums = [0]
Output: [0]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — O(n) time, O(n) extra space. Tạo mảng phụ chứa các số khác 0, sau đó pad số 0 cho đủ n. Đề bài cấm dùng mảng phụ — bị loại.

Tối ưu — Two pointers — O(n) time, O(1) extra space. Dùng 2 con trỏ: - slow = vị trí tiếp theo để ghi một số khác 0. - fast = vị trí đang đọc trong mảng.

Pass 1: với mỗi fast, nếu nums[fast] != 0nums[slow] = nums[fast], tăng slow. Pass 2: từ slow đến hết mảng, gán 0.

Hình minh hoạ với nums = [0, 1, 0, 3, 12]:

                    Lượt 1: dồn các số khác 0 về đầu
                    ──────────────────────────────────

Khởi tạo :  [ 0 , 1 , 0 , 3 , 12]      slow=0  fast=0
              S
              F

fast=0:  nums[0]=0, bỏ qua
         [ 0 , 1 , 0 , 3 , 12]         slow=0  fast=1
           S
               F

fast=1:  nums[1]=1≠0 → ghi nums[0]=1, slow++
         [ 1 , 1 , 0 , 3 , 12]         slow=1  fast=2
               S
                   F

fast=2:  nums[2]=0, bỏ qua             slow=1  fast=3
fast=3:  nums[3]=3≠0 → ghi nums[1]=3, slow++
         [ 1 , 3 , 0 , 3 , 12]         slow=2  fast=4
                   S
                       F

fast=4:  nums[4]=12≠0 → ghi nums[2]=12, slow++
         [ 1 , 3 ,12 , 3 , 12]         slow=3  fast=hết

                    Lượt 2: từ slow đến hết, gán 0
                    ──────────────────────────────────

         [ 1 , 3 ,12 , 0 , 0 ]    ← đáp án
                       ▲   ▲
                   gán 0  gán 0

Cách này tối thiểu hoá số phép ghi về đúng n (vì mỗi ô được ghi đúng 1 lần). Có một biến thể dùng swap ngay khi đi qua, code ngắn hơn nhưng số ghi gấp đôi.

Code Python 3

from typing import List

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        slow = 0
        # Lượt 1: dồn các số khác 0 về đầu.
        for fast in range(len(nums)):
            if nums[fast] != 0:
                nums[slow] = nums[fast]
                slow += 1
        # Lượt 2: phần còn lại gán 0.
        for i in range(slow, len(nums)):
            nums[i] = 0

Phân tích độ phức tạp

Bình luận

slow = 0
for fast in range(len(nums)):
    if nums[fast] != 0:
        nums[slow], nums[fast] = nums[fast], nums[slow]
        slow += 1

Bài tự luyện liên quan


1.5 Container With Most Water (LC 11)

Đề bài

Cho mảng height với height[i] là chiều cao của cột thứ i. Hãy chọn ra 2 cột i < j sao cho lượng nước chứa được giữa chúng là lớn nhất.

Lượng nước = min(height[i], height[j]) * (j - i).

Ví dụ

Input:  height = [1, 8, 6, 2, 5, 4, 8, 3, 7]
Output: 49
Giải thích: chọn cột 1 (cao 8) và cột 8 (cao 7) → 7 * (8-1) = 49.

Input:  height = [1, 1]
Output: 1

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — O(n²). Duyệt mọi cặp (i, j) và lấy max. TLE với n = 10^5.

Tối ưu — Two pointers, O(n). Đặt l = 0, r = n - 1. Tại mỗi bước, diện tích hiện tại = min(height[l], height[r]) * (r - l).

Câu hỏi cốt lõi: dịch con trỏ nào? — Dịch con trỏ ở bên thấp hơn.

Tại sao? Diện tích bị giới hạn bởi cột thấp hơn. Nếu dịch con trỏ ở cột cao hơn vào trong, khoảng cách giảm và cột thấp vẫn là bottleneck → diện tích chỉ có thể giảm hoặc bằng. Còn nếu dịch con trỏ ở cột thấp hơn, ta có cơ hội (không bảo đảm) gặp một cột cao hơn để diện tích tăng.

Proof bằng “loại trừ”: Khi cố định cột thấp (giả sử bên trái) và dịch cột bên phải vào, mọi cặp (l, r' < r) đều có diện tích ≤ height[l] * (r - l). Do đó các cặp này không thể là đáp án nếu chưa tốt hơn cái hiện tại — ta “loại trừ” chúng cùng lúc và chỉ cần dịch l.

Hình minh hoạ với height = [1, 8, 6, 2, 5, 4, 8, 3, 7]:

         ▓                                ▓
   8     ▓       ▓                        |          ← chiều cao 8
   7     ▓       ▓                        ▓
   6     ▓   ▓   ▓               ▓        ▓
   5     ▓   ▓   ▓       ▓       ▓        ▓
   4     ▓   ▓   ▓       ▓   ▓   ▓        ▓
   3     ▓   ▓   ▓       ▓   ▓   ▓    ▓   ▓
   2     ▓   ▓   ▓   ▓   ▓   ▓   ▓    ▓   ▓
   1 ▓   ▓   ▓   ▓   ▓   ▓   ▓   ▓    ▓   ▓
       └─┴───┴───┴───┴───┴───┴───┴────┴───┴─
index: 0   1   2   3   4   5   6    7   8
       L                                  R

Bảng trace (★ = đáp án tốt nhất tại thời điểm đó):

  bước │  L   R │ min(h[L], h[R]) │ width │  area │ move
  ─────┼────────┼─────────────────┼───────┼───────┼──────────────
   1   │  0   8 │       1         │   8   │    8  │ h[L]<h[R] → L++
   2   │  1   8 │       7         │   7   │  49 ★ │ h[L]>=h[R] → R--
   3   │  1   7 │       3         │   6   │   18  │ → R--
   4   │  1   6 │       8         │   5   │   40  │ → R--
   5   │  1   5 │       4         │   4   │   16  │ → R--
   6   │  1   4 │       5         │   3   │   15  │ → R--
   7   │  1   3 │       2         │   2   │    4  │ → R--
   8   │  1   2 │       6         │   1   │    6  │ → R--
   ─── │  1   1 │       stop      │       │       │

Đáp án: 49 (cặp cột index 1 và 8, cao 8 và 7).

Code Python 3

from typing import List

class Solution:
    def maxArea(self, height: List[int]) -> int:
        l, r = 0, len(height) - 1
        best = 0
        while l < r:
            h = min(height[l], height[r])
            best = max(best, h * (r - l))
            # Luôn dịch con trỏ ở phía thấp hơn.
            if height[l] < height[r]:
                l += 1
            else:
                r -= 1
        return best

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


1.6 Rotate Array (LC 189)

Đề bài

Cho mảng nums và một số nguyên không âm k. Hãy xoay mảng sang phải k bước.

Ví dụ

Input:  nums = [1, 2, 3, 4, 5, 6, 7], k = 3
Output: [5, 6, 7, 1, 2, 3, 4]
Giải thích: xoay phải 1 bước → [7,1,2,3,4,5,6]; 2 bước → [6,7,1,2,3,4,5]; 3 bước → [5,6,7,1,2,3,4].

Input:  nums = [-1, -100, 3, 99], k = 2
Output: [3, 99, -1, -100]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — Xoay 1 bước, lặp k lần — O(n·k). TLE.

Mảng phụ — O(n) time, O(n) space. new[(i + k) % n] = nums[i], rồi copy new về nums. Đơn giản nhưng vi phạm follow-up O(1) space.

Tối ưu — Three Reverses, O(n) time, O(1) extra space. Quan sát ví dụ với n = 7, k = 3: - Reverse toàn bộ: [7, 6, 5, 4, 3, 2, 1]. - Reverse [0..k-1]: [5, 6, 7, 4, 3, 2, 1]. - Reverse [k..n-1]: [5, 6, 7, 1, 2, 3, 4]. ✓

Trực giác: Khi reverse toàn bộ, phần “đáng lẽ ra cuối” giờ đứng đầu nhưng bị đảo ngược. Hai lần reverse con tiếp theo “sửa” lại thứ tự bên trong mỗi khối.

Hình minh hoạ với n = 7, k = 3:

Input         :  [ 1   2   3   4 │ 5   6   7 ]
                                  ▲
                  k=3 phần tử cuối cần "nhảy" lên đầu

──────────────────────────────────────────────────

Bước 1: reverse toàn bộ [0..6]
                  ◀═══════════════════════════▶
                 [ 7   6   5   4   3   2   1 ]
                   ↑               ↑
                  (5,6,7 đảo)     (1,2,3,4 đảo)

Bước 2: reverse [0..k-1] = [0..2]   (sửa lại 3 phần tử đầu)
                  ◀═══════▶
                 [ 5   6   7 │ 4   3   2   1 ]
                                ↑
                  3 phần tử đầu  4 phần tử cuối vẫn đảo
                  đã đúng        cần sửa tiếp

Bước 3: reverse [k..n-1] = [3..6]   (sửa lại 4 phần tử cuối)
                              ◀═══════════════▶
                 [ 5   6   7 │ 1   2   3   4 ]   ← đáp án ✓

Code Python 3

from typing import List

class Solution:
    def rotate(self, nums: List[int], k: int) -> None:
        n = len(nums)
        k %= n  # luôn normalize trước

        def reverse(left: int, right: int) -> None:
            while left < right:
                nums[left], nums[right] = nums[right], nums[left]
                left += 1
                right -= 1

        reverse(0, n - 1)        # đảo toàn bộ
        reverse(0, k - 1)        # đảo phần đầu (k phần tử)
        reverse(k, n - 1)        # đảo phần đuôi

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Array decision checklist (trước khi viết code)

Câu hỏi Nếu YES Nếu NO
Cho phép mutate input không? In-place (Move Zeroes, Rotate) Tạo array kết quả
Cần giữ thứ tự gốc? Two-pointer cùng chiều Có thể swap tự do
Trả về index hay value? Cẩn thận khi sort: lưu (value, index)
Có số 0 / số âm? Product Except Self không dùng được division Có thể prefix×suffix bình thường
Cần O(1) bộ nhớ? 3-reverse trick, in-place marker Có thể dùng hash/extra array

Cửa ngõ sang pattern khác

Recap Rotate Array — so sánh 3 cách

Cách Time Space Khi nào chọn
Extra array O(n) O(n) Dễ viết, ít bug nhất; khi RAM dư
3-reverse O(n) O(1) Mặc định trong phỏng vấn — đẹp & ngắn
Cyclic replacement (GCD) O(n) O(1) Khi interviewer hỏi follow-up “không reverse”

Chương 2 — String

Chuỗi thực ra là mảng các ký tự — tất cả kỹ thuật ở Chương 1 (two pointers, in-place, prefix) đều áp dụng được. Tuy nhiên, string có 2 đặc thù riêng: (i) phải xử lý bảng mã (chỉ ASCII hay full Unicode?), và (ii) trong Python, chuỗi là immutable — không sửa được tại chỗ, mọi thao tác “đổi ký tự” thực ra phải chuyển sang list rồi ''.join.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Template code

from collections import Counter
from typing import List

def two_pointers_in_string(s: str) -> bool:
    """Mẫu two pointers: kiểm tra điều kiện đối xứng / cặp."""
    l, r = 0, len(s) - 1
    while l < r:
        if not check(s[l], s[r]):
            return False
        l += 1
        r -= 1
    return True


def count_chars(s: str) -> dict[str, int]:
    """Bảng đếm ký tự — gần như mọi bài string đều dùng."""
    return Counter(s)

Bài tự luyện cuối chương


2.1 Valid Anagram (LC 242)

Đề bài

Cho hai chuỗi st. Trả về True nếu tanagram của s (cùng các ký tự, cùng số lần xuất hiện, chỉ khác thứ tự), ngược lại False.

Ví dụ

Input:  s = "anagram", t = "nagaram"
Output: True

Input:  s = "rat", t = "car"
Output: False

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — sắp xếp, O(n log n). sorted(s) == sorted(t). Code 1 dòng, nhưng O(n log n) về thời gian và O(n) về bộ nhớ (vì sorted trả về list).

Tối ưu — Counter một lượt, O(n). Đếm ký tự trong s, sau đó duyệt t và trừ. Nếu có ký tự nào âm hoặc kết thúc với mọi count = 0 → đúng anagram.

Tối ưu hơn nữa — bảng cố định 26 phần tử, O(1) extra space (theo bảng mã). Vì chỉ có 26 chữ cái, ta dùng int[26] (hoặc list 26 phần tử) thay cho dict. Bộ nhớ thực tế là O(1) (không phụ thuộc n).

Code Python 3

from collections import Counter

class Solution:
    def isAnagram(self, s: str, t: str) -> bool:
        if len(s) != len(t):
            return False
        return Counter(s) == Counter(t)


class SolutionFast:
    """Bảng cố định 26 chữ cái — O(1) bộ nhớ thực tế."""
    def isAnagram(self, s: str, t: str) -> bool:
        if len(s) != len(t):
            return False
        count = [0] * 26
        for ch in s:
            count[ord(ch) - ord('a')] += 1
        for ch in t:
            count[ord(ch) - ord('a')] -= 1
            if count[ord(ch) - ord('a')] < 0:
                return False
        return True

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


2.2 Valid Palindrome (LC 125)

Đề bài

Cho chuỗi s. Coi là palindrome nếu sau khi chuyển toàn bộ chữ in hoa → thường và bỏ tất cả ký tự không phải chữ-và-số thì chuỗi đọc xuôi và ngược giống nhau. Trả về True / False.

Ví dụ

Input:  s = "A man, a plan, a canal: Panama"
Output: True
Giải thích: sau lọc → "amanaplanacanalpanama" — đọc xuôi và ngược giống nhau.

Input:  s = "race a car"
Output: False
Giải thích: sau lọc → "raceacar" — không palindrome.

Input:  s = " "
Output: True
Giải thích: chuỗi rỗng coi là palindrome.

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — lọc rồi so sánh đảo ngược — O(n) time, O(n) space. filtered = ''.join(ch.lower() for ch in s if ch.isalnum()), rồi filtered == filtered[::-1]. Đơn giản, nhưng tốn O(n) bộ nhớ phụ.

Tối ưu — Two pointers in-place — O(n) time, O(1) space. Hai con trỏ l (đầu) và r (cuối), bỏ qua ký tự không phải alphanumeric ở mỗi bên, rồi so sánh s[l].lower() == s[r].lower(). Nếu khác → False.

Code Python 3

class Solution:
    def isPalindrome(self, s: str) -> bool:
        l, r = 0, len(s) - 1
        while l < r:
            while l < r and not s[l].isalnum():
                l += 1
            while l < r and not s[r].isalnum():
                r -= 1
            if s[l].lower() != s[r].lower():
                return False
            l += 1
            r -= 1
        return True

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


2.3 Longest Common Prefix (LC 14)

Đề bài

Cho mảng các chuỗi strs. Hãy trả về tiền tố chung dài nhất. Nếu không có tiền tố chung, trả về "".

Ví dụ

Input:  strs = ["flower", "flow", "flight"]
Output: "fl"

Input:  strs = ["dog", "racecar", "car"]
Output: ""
Giải thích: không có ký tự nào chung ngay từ vị trí đầu.

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cách 1 — Vertical scan, O(S) với S là tổng độ dài. Duyệt từng vị trí cột i = 0, 1, 2, .... Tại mỗi i, kiểm tra ký tự strs[0][i] có khớp với strs[j][i] cho mọi j không. Nếu có chuỗi nào hết hoặc khác → trả về strs[0][:i].

Cách 2 — Horizontal scan. Lấy prefix = strs[0], sau đó với mỗi chuỗi tiếp theo, rút ngắn prefix cho đến khi nó là tiền tố của chuỗi đó.

Cách 3 — Sort + so sánh 2 đầu, O(n log n · L). Sort mảng theo thứ tự lexicographic. Tiền tố chung dài nhất chính là tiền tố chung của strs[0]strs[-1]. Hay nhưng không tối ưu time.

Mình giới thiệu vertical scan vì nó là cách dễ nhất để diễn đạt trên whiteboard và có thể early-exit ngay khi gặp mismatch đầu tiên.

Code Python 3

from typing import List

class Solution:
    def longestCommonPrefix(self, strs: List[str]) -> str:
        if not strs:
            return ""
        for i, ch in enumerate(strs[0]):
            for s in strs[1:]:
                if i >= len(s) or s[i] != ch:
                    return strs[0][:i]
        return strs[0]  # toàn bộ strs[0] là tiền tố chung

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


2.4 String to Integer / atoi (LC 8)

Đề bài

Cài đặt hàm atoi (ASCII to Integer) chuyển một chuỗi thành số nguyên 32-bit có dấu. Quy tắc:

  1. Bỏ qua khoảng trắng đầu chuỗi.
  2. Đọc dấu + hoặc - (tuỳ chọn).
  3. Đọc các ký tự số liên tiếp cho đến khi gặp ký tự không phải số.
  4. Áp dấu vào kết quả.
  5. Clamp vào phạm vi int 32-bit: [-2^31, 2^31 - 1].
  6. Trả về 0 nếu không đọc được số nào (ví dụ chuỗi toàn chữ).

Ví dụ

Input:  s = "42"
Output: 42

Input:  s = "   -42"
Output: -42                (bỏ space đầu, đọc dấu '-', rồi đọc "42")

Input:  s = "4193 with words"
Output: 4193               (dừng tại khoảng trắng sau "4193"; phần "with words" bị bỏ)

Input:  s = "words and 987"
Output: 0                  (gặp 'w' đầu tiên ngay sau khi bỏ space → không đọc được số nào)

Input:  s = "-91283472332"
Output: -2147483648        (= INT_MIN, clamp vì -91283472332 < -2^31)

Input:  s = "+-12"
Output: 0                  (đã đọc dấu '+', sau đó gặp '-' không phải digit → fail ngay)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cách 1 — Quy trình tuần tự, dùng index chạy qua chuỗi. 4 bước rõ ràng: skip space → đọc dấu → đọc số → clamp. Mỗi bước có biến trạng thái riêng.

Cách 2 — Finite State Machine (FSM). Mô hình trạng thái giúp code gọn hơn và dễ mở rộng khi đề bài thêm yêu cầu (số thực, scientific notation, …). Rất đáng học vì đây là pattern chung cho mọi bài parser (Chương 32).

Hình minh hoạ FSM:

       blank        sign        digit       khác
   ┌───────────────────────────────────────────────┐
S  │  start  →  start    signed   in_number   end │
T  │  signed →  end      end      in_number   end │
A  │ in_num  →  end      end      in_number   end │
T  │  end    →  end      end      end         end │
E  └───────────────────────────────────────────────┘

Trạng thái:
  start     : đang skip space đầu
  signed    : đã đọc 1 dấu, chờ digit
  in_number : đang đọc các chữ số
  end       : đã kết thúc, mọi ký tự sau bị ignore

Code Python 3

INT_MAX = 2**31 - 1   # 2147483647
INT_MIN = -2**31      # -2147483648

class Solution:
    """Cách 1 — quy trình tuần tự."""

    def myAtoi(self, s: str) -> int:
        i, n = 0, len(s)

        # 1. Bỏ space đầu.
        while i < n and s[i] == ' ':
            i += 1

        # 2. Đọc dấu (tuỳ chọn).
        sign = 1
        if i < n and s[i] in '+-':
            sign = -1 if s[i] == '-' else 1
            i += 1

        # 3. Đọc các chữ số.
        result = 0
        while i < n and s[i].isdigit():
            result = result * 10 + (ord(s[i]) - ord('0'))
            # Tối ưu: có thể early-clamp ngay đây để khỏi overflow.
            if result > 2**31:   # vượt rất nhiều
                break
            i += 1

        # 4. Áp dấu và clamp.
        result *= sign
        return max(INT_MIN, min(INT_MAX, result))


class SolutionFSM:
    """Cách 2 — Finite State Machine. Dễ extend khi đề thêm yêu cầu."""

    table = {
        'start':     {'blank': 'start',  'sign': 'signed',   'digit': 'in_num', 'other': 'end'},
        'signed':    {'blank': 'end',    'sign': 'end',      'digit': 'in_num', 'other': 'end'},
        'in_num':    {'blank': 'end',    'sign': 'end',      'digit': 'in_num', 'other': 'end'},
        'end':       {'blank': 'end',    'sign': 'end',      'digit': 'end',    'other': 'end'},
    }

    @staticmethod
    def _kind(ch: str) -> str:
        if ch == ' ':           return 'blank'
        if ch in '+-':          return 'sign'
        if ch.isdigit():        return 'digit'
        return 'other'

    def myAtoi(self, s: str) -> int:
        state = 'start'
        sign = 1
        result = 0
        for ch in s:
            state = self.table[state][self._kind(ch)]
            if state == 'in_num':
                result = result * 10 + int(ch)
                result = min(result, INT_MAX + 1)  # chặn overflow sớm
            elif state == 'signed':
                sign = -1 if ch == '-' else 1
            elif state == 'end':
                break
        return max(INT_MIN, min(INT_MAX, sign * result))

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


2.5 Group Anagrams (LC 49)

Đề bài

Cho mảng chuỗi strs. Hãy gom các chuỗi là anagram của nhau vào cùng một nhóm. Trả về danh sách các nhóm (thứ tự nhóm và thứ tự trong nhóm không quan trọng).

Ví dụ

Input:  strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
Output: [["eat", "tea", "ate"], ["tan", "nat"], ["bat"]]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Ý tưởng cốt lõi: Hai chuỗi là anagram ↔︎ có cùng “chữ ký”. Ta dùng dict {chữ_ký: list các chuỗi} để gom.

Cách 1 — Chữ ký = sorted(s), O(n · k log k). key = ''.join(sorted(s)). Hai anagram sẽ có cùng sorted form.

Cách 2 — Chữ ký = tuple count 26 chữ, O(n · k). key = tuple(Counter(s)[ch] for ch in 'abcdefghijklmnopqrstuvwxyz'). Tránh được phép sort O(k log k) nhưng tuple 26 phần tử có overhead.

Hình minh hoạ với ["eat", "tea", "tan", "ate", "nat", "bat"]:

str    sorted_key   bucket
─────  ───────────  ─────────────────────
"eat"   "aet"  ──┐
"tea"   "aet"  ──┤───►  bucket "aet" = ["eat", "tea", "ate"]
"ate"   "aet"  ──┘
"tan"   "ant"  ──┐
"nat"   "ant"  ──┤───►  bucket "ant" = ["tan", "nat"]
"bat"   "abt"  ──────►  bucket "abt" = ["bat"]

Code Python 3

from collections import defaultdict
from typing import List

class Solution:
    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
        groups: dict[str, list[str]] = defaultdict(list)
        for s in strs:
            key = ''.join(sorted(s))
            groups[key].append(s)
        return list(groups.values())


class SolutionCount:
    """Chữ ký = tuple count 26 chữ — không cần sort."""

    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
        groups: dict[tuple, list[str]] = defaultdict(list)
        for s in strs:
            count = [0] * 26
            for ch in s:
                count[ord(ch) - ord('a')] += 1
            groups[tuple(count)].append(s)
        return list(groups.values())

Phân tích độ phức tạp

Cách Time Space
sorted-key O(n · k log k) O(n·k)
count-key O(n · k) O(n·k)

Với n = số chuỗi, k = độ dài chuỗi.

Bình luận

Bài tự luyện liên quan


2.6 Reverse Words in a String (LC 151)

Đề bài

Cho chuỗi s chứa nhiều từ cách nhau bởi ít nhất 1 dấu cách. Hãy đảo thứ tự các từ và trả về chuỗi kết quả, sao cho:

Ví dụ

Input:  s = "the sky is blue"
Output: "blue is sky the"

Input:  s = "  hello world  "
Output: "hello world"   (gọn space đầu/cuối + giữa)

Input:  s = "a good   example"
Output: "example good a"   (gộp nhiều space giữa thành 1)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cách 1 — Pythonic split-reverse-join, O(n). return ' '.join(reversed(s.split())). split() không tham số sẽ tự gộp nhiều space và bỏ space đầu/cuối — chính xác cái ta cần.

Cách 2 — Three Reverses (in-place trên mảng ký tự). Áp dụng đúng ý tưởng của bài Rotate Array (1.6): 1. Reverse toàn bộ chuỗi. 2. Reverse từng “từ” trong chuỗi đã reverse. 3. Dọn space (chỉ giữ 1 space giữa từ, bỏ space đầu/cuối).

Hình minh hoạ với s = "the sky is blue":

Input               :  "the sky is blue"

Bước 1: reverse toàn bộ
                       "eulb si yks eht"

Bước 2: reverse từng từ trong chuỗi đã đảo
                       "blue is sky the"   ← đáp án ✓

So sánh với Rotate Array: Rotate Array reverse trên đơn vị phần tử, Reverse Words reverse trên đơn vị “từ” (substring giữa các space). Same idea, khác mức trừu tượng.

Code Python 3

class Solution:
    """Cách 1 — Pythonic, ngắn gọn nhất."""

    def reverseWords(self, s: str) -> str:
        return ' '.join(reversed(s.split()))


class SolutionInPlace:
    """Cách 2 — Three reverses, in-place trên list ký tự."""

    def reverseWords(self, s: str) -> str:
        chars = list(s.strip())  # Python string immutable → phải chuyển list

        # 1. Reverse toàn bộ.
        self._reverse(chars, 0, len(chars) - 1)

        # 2. Reverse từng từ.
        start = 0
        for i in range(len(chars) + 1):
            if i == len(chars) or chars[i] == ' ':
                self._reverse(chars, start, i - 1)
                start = i + 1

        # 3. Gộp nhiều space giữa các từ thành 1.
        return self._collapse_spaces(chars)

    @staticmethod
    def _reverse(arr: list, l: int, r: int) -> None:
        while l < r:
            arr[l], arr[r] = arr[r], arr[l]
            l += 1
            r -= 1

    @staticmethod
    def _collapse_spaces(chars: list) -> str:
        out, prev_space = [], False
        for ch in chars:
            if ch == ' ':
                if not prev_space and out:
                    out.append(' ')
                prev_space = True
            else:
                out.append(ch)
                prev_space = False
        if out and out[-1] == ' ':
            out.pop()
        return ''.join(out)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Giả thiết về ký tự — làm rõ ngay trước khi viết code

  1. Bảng chữ cái: lowercase a–z (26)? ASCII 128? Unicode? — Mảng đếm [26] chỉ dùng được khi đúng 26 chữ.
  2. Phân biệt hoa thường?"Aa" có là palindrome không? LC 125 lowercase trước; LC 5 không.
  3. Có ký tự không phải chữ-và-số? — Lọc bằng isalnum(), hay đề bài đã đảm bảo sạch?
  4. Có khoảng trắng đầu/cuối? — Gọi strip() trước khi parse số.

String pattern map

Pattern Khi gặp Chương
Counting (Counter, [26]) Anagram, frequency 02, 06
Two pointers (in/out) Palindrome, reverse 02, 26
Sliding window Substring với ràng buộc động 27
Parsing với stack/FSM atoi, calculator, Decode 08, 32
Pattern matching strStr, anagrams trong text 35, 36, 34
Hashing string Rabin-Karp, Distinct substrings 34

Group Anagrams — chọn key thế nào?

Bridge sang Chương 32 (String Parser)

LC 8 (atoi) là FSM nhỏ (4 trạng thái: start, sign, digits, overflow). Khi đề bài phức tạp hơn (Valid Number, Calculator) → đọc Chương 32.


Chương 3 — Recursion

Recursion (đệ quy) là ngôn ngữ tự nhiên để mô tả bài toán có cấu trúc tự tương tự — giải bài lớn bằng cách kết hợp lời giải của vài bài con nhỏ hơn. Chương này dạy bạn cảm nhận 3 thành phần của một lời giải đệ quy: (i) base case (điều kiện dừng), (ii) recursive case (gọi vào bài con nhỏ hơn), (iii) kết hợp kết quả từ các bài con. Khi 3 cái này “click”, bạn sẽ thấy DP, Backtracking, Tree, Graph DFS đều là cùng một ngôn ngữ.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

3 câu hỏi cần trả lời trước khi code đệ quy: 1. Trạng thái của hàm gồm những biến gì? (Cần đủ để định nghĩa “bài con”.) 2. Base case là gì? (Khi nào trả về luôn?) 3. Bước đệ quy chia bài lớn thành bài con thế nào, kết hợp kết quả ra sao?

Template code

from functools import cache

# 1) Đệ quy "thuần" (có thể chậm vì lặp lại bài con).
def recurse(state):
    if base_condition(state):
        return base_value
    result = combine(recurse(subproblem_1(state)),
                     recurse(subproblem_2(state)))
    return result


# 2) Top-down DP: thêm cache để O(số trạng thái).
@cache
def f(*state):
    if base_condition(*state):
        return base_value
    return combine(f(*sub1(*state)), f(*sub2(*state)))


# 3) Backtracking: liệt kê + undo.
def backtrack(path, choices):
    if is_solution(path):
        results.append(path.copy())
        return
    for c in choices:
        if not valid(c, path):
            continue
        path.append(c)
        backtrack(path, next_choices(choices, c))
        path.pop()              # undo — đặc trưng của backtracking

Bài tự luyện cuối chương


3.1 Fibonacci số thứ n (LC 509)

Đề bài

Tính số Fibonacci thứ n theo định nghĩa: F(0) = 0, F(1) = 1, và F(n) = F(n-1) + F(n-2) với n >= 2.

Ví dụ

Input:  n = 2  → 1
Input:  n = 3  → 2
Input:  n = 10 → 55

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cách 1 — Đệ quy thuần, O(2^n). Mỗi F(n) gọi 2 lần đệ quy. Cây gọi có ~2^n node → rất chậm.

Cách 2 — Memoization (top-down DP), O(n) time, O(n) space. Cache lại kết quả → mỗi F(k) tính đúng 1 lần.

Cách 3 — Iterative (bottom-up), O(n) time, O(1) space. Chỉ cần 2 biến prev, curr cuộn từ dưới lên.

Cách 4 — Matrix exponentiation, O(log n). Khi n lên đến 10^18.

Hình minh hoạ — cây gọi đệ quy cho F(5):

                F(5)
              /      \
          F(4)        F(3)
         /    \      /    \
       F(3)  F(2) F(2)   F(1)
       /  \   / \  / \
     F(2) F(1)...   ...   ← F(3), F(2), F(1) bị tính LẠI nhiều lần

→ Nếu memo cache mỗi F(k), số subproblem chỉ là n+1 (n=5 → 6 calls).
→ Nếu không memo, số calls ~ Fibonacci(n+1) ~ φ^n (exponential).

Code Python 3

from functools import cache

class Solution:
    """Cách 3 — iterative O(1) space, đáp án production."""
    def fib(self, n: int) -> int:
        if n < 2:
            return n
        prev, curr = 0, 1
        for _ in range(2, n + 1):
            prev, curr = curr, prev + curr
        return curr


class SolutionMemo:
    """Cách 2 — top-down DP."""
    @cache
    def fib(self, n: int) -> int:
        if n < 2:
            return n
        return self.fib(n - 1) + self.fib(n - 2)


class SolutionMatrix:
    """Cách 4 — Matrix exponentiation, O(log n)."""
    MOD = 10**9 + 7

    def fib(self, n: int) -> int:
        if n < 2:
            return n
        # [[F(n+1), F(n)], [F(n), F(n-1)]] = [[1,1],[1,0]] ^ n
        result, base = [[1, 0], [0, 1]], [[1, 1], [1, 0]]
        while n > 0:
            if n & 1:
                result = self._mul(result, base)
            base = self._mul(base, base)
            n >>= 1
        return result[0][1]  # F(n)

    def _mul(self, a, b):
        return [[(a[0][0]*b[0][0] + a[0][1]*b[1][0]) % self.MOD,
                 (a[0][0]*b[0][1] + a[0][1]*b[1][1]) % self.MOD],
                [(a[1][0]*b[0][0] + a[1][1]*b[1][0]) % self.MOD,
                 (a[1][0]*b[0][1] + a[1][1]*b[1][1]) % self.MOD]]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


3.2 Power(x, n) (LC 50)

Đề bài

Cài đặt hàm tính x^n với x là số thực và n là số nguyên (có thể âm).

Ví dụ

Input:  x = 2.00000, n = 10     → 1024.00000
Input:  x = 2.10000, n = 3      → 9.26100
Input:  x = 2.00000, n = -2     → 0.25

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cách 1 — Nhân tay, O(n). Loop nhân n lần. TLE khi n = 2^31.

Cách 2 — Fast Power (đệ quy / iterative), O(log n).

Quan sát đệ quy: - Nếu n == 0: trả 1. - Nếu n chẵn: x^n = (x^(n/2))^2. - Nếu n lẻ: x^n = x · x^(n-1).

Cho phép n âm: gọi đệ quy với n = -n rồi nghịch đảo.

Hình minh hoạ — cây gọi cho x^10:

                  x^10
                   │ chẵn → (x^5)^2
                   ▼
                  x^5
                   │ lẻ → x · x^4
                   ▼
                  x^4
                   │ chẵn → (x^2)^2
                   ▼
                  x^2
                   │ chẵn → (x^1)^2
                   ▼
                  x^1
                   │ lẻ → x · x^0
                   ▼
                  x^0 = 1

Tổng số phép nhân: ~ 2 log₂(10) ≈ 8  (vs. 10 phép của brute force)

Code Python 3

class Solution:
    """Đệ quy fast power."""

    def myPow(self, x: float, n: int) -> float:
        if n == 0:
            return 1.0
        if n < 0:
            return 1.0 / self.myPow(x, -n)
        half = self.myPow(x, n // 2)
        return half * half if n % 2 == 0 else half * half * x


class SolutionIter:
    """Iterative — tránh chiều sâu đệ quy. Đọc bit từ thấp lên cao."""

    def myPow(self, x: float, n: int) -> float:
        if n < 0:
            x, n = 1.0 / x, -n
        result = 1.0
        base = x
        while n > 0:
            if n & 1:
                result *= base
            base *= base
            n >>= 1
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


3.3 Reverse Linked List (đệ quy) (LC 206)

Đề bài

Cho head của một danh sách liên kết đơn. Hãy đảo ngược danh sách và trả về node đầu mới. (Bài này có 2 cách: iterative và đệ quy. Ở chương này tập trung bản đệ quy; bản iterative gặp lại ở Chương 7.)

Ví dụ

Input:  head = 1 → 2 → 3 → 4 → 5 → None  (singly linked list)
Output: 5 → 4 → 3 → 2 → 1 → None

Input:  head = None     (empty list)
Output: None

Ràng buộc

Clarifying questions

Hướng tiếp cận

Ý tưởng đệ quy: - Base case: nếu headNone hoặc head.nextNone → trả head. - Đệ quy: gọi reverseList(head.next) để đảo phần đuôi, được node cuối mới (chính là node cuối cũ → đầu mới sau khi đảo). - Kết hợp: lúc này head.next vẫn trỏ tới node cũ ngay sau head (chưa bị thay đổi vì đệ quy chỉ làm với phần đuôi). Ta gán head.next.next = headhead.next = None để khâu head vào cuối danh sách đã đảo.

Hình minh hoạ với 1 → 2 → 3 → None:

Gọi reverseList(1):
  reverseList(2):
    reverseList(3):
      base case → return 3            #  3 → None
    # tại đây head=2, head.next=3
    # phần đuôi đã đảo: 3 → None
    # ta cần khâu 2 vào sau 3:
    head.next.next = head              #  3 → 2
    head.next = None                   #  2 → None
    # giờ chuỗi: 3 → 2 → None, đầu mới = 3
    return 3
  # tại đây head=1, head.next=2
  # phần đuôi đã đảo: 3 → 2 → None
  # khâu 1 vào sau 2:
  head.next.next = head                #  2 → 1
  head.next = None                     #  1 → None
  # giờ chuỗi: 3 → 2 → 1 → None
  return 3

Code Python 3

class ListNode:
    def __init__(self, val: int = 0, next: 'ListNode | None' = None):
        self.val = val
        self.next = next


class Solution:
    def reverseList(self, head: ListNode | None) -> ListNode | None:
        if head is None or head.next is None:
            return head
        new_head = self.reverseList(head.next)
        head.next.next = head
        head.next = None
        return new_head

Phân tích độ phức tạp

Bình luận

prev = None
while head:
    nxt = head.next
    head.next = prev
    prev = head
    head = nxt
return prev

Bài tự luyện liên quan


3.4 Generate Parentheses (LC 22)

Đề bài

Cho số nguyên n, sinh tất cả các chuỗi dấu ngoặc đúng (well-formed) độ dài 2n.

Ví dụ

Input:  n = 3
Output: ["((()))", "(()())", "(())()", "()(())", "()()()"]

Input:  n = 1
Output: ["()"]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Ý tưởng: Sinh ký tự ( hoặc ) từng bước. Mỗi bước có 2 lựa chọn, nhưng phải đảm bảo tính hợp lệ: - Số ( đã đặt không vượt quá n. - Số ) đã đặt không vượt quá số ( đã đặt (vì sẽ tạo ngoặc không match).

Đệ quy với 2 counter: open_count, close_count. Khi len(path) == 2n → đẩy vào kết quả.

Hình minh hoạ — cây quyết định cho n = 2:

                       ""
                     /    \
                ( /        \ )  ✗ (close > open)
                   "("
                  /   \
              ( /      \ )
              "(("    "()"
               │        │
            ) ▼     ( / \ )  ✗
              "(()"   "()("
               │       │
            ) ▼     ) ▼
              "(())" ★  "()()" ★

Đáp án: ["(())", "()()"]
(✗ = nhánh bị cắt vì không hợp lệ)

Code Python 3

from typing import List

class Solution:
    def generateParenthesis(self, n: int) -> List[str]:
        result: list[str] = []

        def backtrack(path: list[str], open_cnt: int, close_cnt: int) -> None:
            if len(path) == 2 * n:
                result.append(''.join(path))
                return
            if open_cnt < n:
                path.append('(')
                backtrack(path, open_cnt + 1, close_cnt)
                path.pop()
            if close_cnt < open_cnt:
                path.append(')')
                backtrack(path, open_cnt, close_cnt + 1)
                path.pop()

        backtrack([], 0, 0)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


3.5 Permutations (LC 46)

Đề bài

Cho mảng nums các số phân biệt, trả về tất cả hoán vị có thể của chúng.

Ví dụ

Input:  nums = [1, 2, 3]
Output: [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Ý tưởng: Tại mỗi bước, chọn 1 số chưa dùng đẩy vào path. Khi path đủ n phần tử → 1 hoán vị hoàn chỉnh.

2 cách quản lý “đã dùng”: - Mảng used: bool[n]. - Hoặc dùng set các index đã dùng.

Hình minh hoạ — cây quyết định cho [1, 2, 3]:

                          [ ]
              ┌────────────┼────────────┐
            [1]           [2]          [3]
           /   \         /   \         /  \
        [1,2] [1,3]   [2,1] [2,3]   [3,1] [3,2]
          │     │       │     │       │     │
       [1,2,3][1,3,2] [2,1,3][2,3,1][3,1,2][3,2,1]

Tổng: 3 · 2 · 1 = 6 hoán vị.

Code Python 3

from typing import List

class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        result: list[list[int]] = []
        n = len(nums)
        used = [False] * n
        path: list[int] = []

        def backtrack() -> None:
            if len(path) == n:
                result.append(path.copy())
                return
            for i in range(n):
                if used[i]:
                    continue
                used[i] = True
                path.append(nums[i])
                backtrack()
                path.pop()
                used[i] = False

        backtrack()
        return result


class SolutionSwap:
    """Cách 2 — swap in-place, không cần mảng used."""

    def permute(self, nums: List[int]) -> List[List[int]]:
        result: list[list[int]] = []

        def backtrack(start: int) -> None:
            if start == len(nums):
                result.append(nums.copy())
                return
            for i in range(start, len(nums)):
                nums[start], nums[i] = nums[i], nums[start]
                backtrack(start + 1)
                nums[start], nums[i] = nums[i], nums[start]  # undo

        backtrack(0)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


3.6 Subsets (LC 78)

Đề bài

Cho mảng nums các số phân biệt. Trả về tất cả tập con có thể của nums (bao gồm tập rỗng và tập đầy đủ). Tổng cộng 2^n tập con.

Ví dụ

Input:  nums = [1, 2, 3]
Output: [[], [1], [2], [3], [1,2], [1,3], [2,3], [1,2,3]]

Input:  nums = [0]
Output: [[], [0]]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Có 3 cách kinh điển, đều nên biết.

Cách 1 — Backtracking “chọn / không chọn”. Tại mỗi index i, có 2 nhánh: thêm nums[i] vào path hoặc bỏ qua.

Cách 2 — Backtracking “bắt đầu từ start”. Mỗi node trong cây gọi result.append(path.copy()) (mọi prefix đều là tập con hợp lệ), rồi loop for i in range(start, n).

Cách 3 — Bit Mask iteration. Mỗi số từ 0 đến 2^n - 1 đại diện cho 1 tập con: bit i bật ↔︎ nums[i] trong tập. (Xem thêm Chương 21.)

Hình minh hoạ — cây “chọn / không chọn” cho [1, 2, 3]:

                            [ ]
                bỏ 1 /          \ chọn 1
                  [ ]             [1]
            bỏ 2 / \ chọn 2     bỏ 2 / \ chọn 2
                [ ] [2]            [1] [1,2]
          bỏ3/\ ... ...          ...  ...
           [ ] [3]

Mỗi LÁ của cây = 1 tập con. Cây có 2^3 = 8 lá.

Code Python 3

from typing import List

class Solution:
    """Cách 2 — gom mọi prefix là 1 tập con."""

    def subsets(self, nums: List[int]) -> List[List[int]]:
        result: list[list[int]] = []
        path: list[int] = []

        def backtrack(start: int) -> None:
            result.append(path.copy())          # mọi state đều là một subset
            for i in range(start, len(nums)):
                path.append(nums[i])
                backtrack(i + 1)
                path.pop()

        backtrack(0)
        return result


class SolutionBitmask:
    """Cách 3 — iterate qua 2^n bitmask."""

    def subsets(self, nums: List[int]) -> List[List[int]]:
        n = len(nums)
        result = []
        for mask in range(1 << n):
            subset = [nums[i] for i in range(n) if mask & (1 << i)]
            result.append(subset)
        return result


class SolutionPick:
    """Cách 1 — backtracking chọn / không chọn."""

    def subsets(self, nums: List[int]) -> List[List[int]]:
        result: list[list[int]] = []
        path: list[int] = []

        def backtrack(i: int) -> None:
            if i == len(nums):
                result.append(path.copy())
                return
            # nhánh không chọn nums[i]
            backtrack(i + 1)
            # nhánh chọn nums[i]
            path.append(nums[i])
            backtrack(i + 1)
            path.pop()

        backtrack(0)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Recursion vs DFS vs Backtracking vs Top-down DP

Thuộc tính Recursion DFS Backtracking Top-down DP
Mục tiêu Giải bài con tự gọi lại Duyệt graph/tree Liệt kê tất cả lời giải Giải tối ưu / đếm
undo state? Không bắt buộc Hiếm Bắt buộc (choose/unchoose) Không
memo? Có thể có Hiếm Hiếm (state phụ thuộc đường đi) Bắt buộc
Ví dụ Factorial, Fibonacci Number of Islands Permutations, N-Queens LCS, Coin Change

Mantra backtracking

def backtrack(path, choices):
    if is_goal(path):
        record(path); return
    for c in choices:
        if not feasible(path, c): continue
        path.append(c)          # choose
        backtrack(path, ...)    # explore
        path.pop()              # unchoose

Python recursion caveats


Chương 4 — Sorting

Sort tự nó là bài toán đã giải. Chương này không dạy bạn cài đặt quicksort — Python đã có sorted() rất tốt (Timsort, O(n log n) worst, stable). Cái cần học là: khi nào sort là tiền đề giải bài, và bí mật nằm ở hàm so sánh (comparator) tuỳ biến và việc duyệt mảng đã sort bằng pattern two pointers / sweep line.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

3 câu hỏi vàng: 1. Sort theo khoá nào? start, end, length, freq, ratio? 2. Sort xong duyệt thế nào? one-pass / two pointers / sweep line / heap? 3. Cần stable không? Python sorted mặc định là stable — đây là tài sản quý.

Template code

from functools import cmp_to_key
from typing import List

# 1) Sort theo khoá đơn giản
nums.sort(key=lambda x: x[0])

# 2) Sort theo nhiều khoá (tie-breaker)
nums.sort(key=lambda x: (x[0], -x[1]))    # x[0] tăng, x[1] giảm

# 3) Sort theo comparator tuỳ biến
def cmp(a, b) -> int:
    if a + b > b + a:   return -1   # a đứng trước
    if a + b < b + a:   return  1   # b đứng trước
    return 0
arr.sort(key=cmp_to_key(cmp))

# 4) Sweep line trên mảng các sự kiện
events = [(start, +1), (end, -1)]
events.sort()

Bài tự luyện cuối chương


4.1 Sort Colors / Dutch National Flag (LC 75)

Đề bài

Cho mảng nums chỉ chứa các giá trị 0, 1, 2 (đại diện cho 3 màu). Hãy sắp xếp nums sao cho cùng màu đứng cạnh nhau theo thứ tự 0 → 1 → 2. Phải làm in-place, không được dùng hàm sort của ngôn ngữ.

Ví dụ

Input:  nums = [2, 0, 2, 1, 1, 0]
Output: [0, 0, 1, 1, 2, 2]

Input:  nums = [2, 0, 1]
Output: [0, 1, 2]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cách 1 — Counting sort 2 lượt, O(n). Đếm số lượng 0, 1, 2 rồi ghi đè lại. Đơn giản nhưng đi 2 lượt.

Cách 2 — Dutch National Flag (Edsger Dijkstra), 1 lượt, O(n).

Giữ 3 con trỏ: - lo = ranh giới phải của vùng 0s (mọi phần tử ở [0..lo-1]0). - hi = ranh giới trái của vùng 2s (mọi phần tử ở [hi+1..n-1]2). - mid = con trỏ duyệt giữa hai vùng.

Bất biến: [0..lo-1] = 0, [lo..mid-1] = 1, [mid..hi] chưa xử lý, [hi+1..n-1] = 2.

Tại mỗi bước: - nums[mid] == 0 → swap với nums[lo], lo++, mid++. - nums[mid] == 1 → đã đúng vùng, mid++. - nums[mid] == 2 → swap với nums[hi], hi-- (mid không tăng vì giá trị mới từ hi xuống chưa được xử lý).

Hình minh hoạ với nums = [2, 0, 2, 1, 1, 0]:

                  lo  mid          hi
Khởi tạo  :  [ 2,  0,  2,  1,  1,  0 ]
                ↑   ↑                ↑
              lo=0 mid=0           hi=5

mid=0: nums[0]=2 → swap(0,5), hi--
              [ 0,  0,  2,  1,  1,  2 ]
                ↑   ↑           ↑
              lo=0 mid=0      hi=4

mid=0: nums[0]=0 → swap(lo,mid)=swap(0,0), lo++, mid++
              [ 0,  0,  2,  1,  1,  2 ]
                    ↑   ↑       ↑
                  lo=1 mid=1  hi=4

mid=1: nums[1]=0 → swap(1,1), lo++, mid++
              [ 0,  0,  2,  1,  1,  2 ]
                        ↑   ↑   ↑
                       lo=2 mid=2 hi=4

mid=2: nums[2]=2 → swap(2,4), hi--
              [ 0,  0,  1,  1,  2,  2 ]
                        ↑   ↑   ↑
                       lo=2 mid=2 hi=3

mid=2: nums[2]=1 → mid++
              [ 0,  0,  1,  1,  2,  2 ]
                        ↑       ↑
                       lo=2 mid=3 hi=3

mid=3: nums[3]=1 → mid++
              [ 0,  0,  1,  1,  2,  2 ]
                        ↑           ↑
                       lo=2  mid=4 hi=3   ← mid > hi → dừng

Kết quả : [0, 0, 1, 1, 2, 2]  ✓

Code Python 3

from typing import List

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        lo, mid, hi = 0, 0, len(nums) - 1
        while mid <= hi:
            if nums[mid] == 0:
                nums[lo], nums[mid] = nums[mid], nums[lo]
                lo += 1
                mid += 1
            elif nums[mid] == 1:
                mid += 1
            else:  # nums[mid] == 2
                nums[mid], nums[hi] = nums[hi], nums[mid]
                hi -= 1
                # KHÔNG tăng mid — giá trị từ hi chưa biết là gì

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


4.2 Merge Intervals (LC 56)

Đề bài

Cho mảng các khoảng intervals[i] = [start_i, end_i]. Hãy gộp tất cả các khoảng giao nhau lại thành các khoảng không giao nhau, và trả về kết quả.

Ví dụ

Input:  intervals = [[1,3], [2,6], [8,10], [15,18]]
Output: [[1,6], [8,10], [15,18]]
Giải thích: [1,3] và [2,6] giao nhau → gộp thành [1,6].

Input:  intervals = [[1,4], [4,5]]
Output: [[1,5]]
Giải thích: [1,4] và [4,5] coi là giao (chia sẻ điểm 4).

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force. Lặp đi lặp lại tìm cặp giao nhau và gộp. O(n²) hoặc tệ hơn.

Tối ưu — Sort + 1 lượt — O(n log n).

Sort theo start tăng dần. Sau đó duyệt, giữ last = khoảng cuối cùng đã thêm vào kết quả. Với khoảng cur tiếp theo: - Nếu cur.start <= last.end → giao nhau, mở rộng last.end = max(last.end, cur.end). - Nếu không → push cur thành khoảng mới.

Hình minh hoạ với [[1,3], [2,6], [8,10], [15,18]]:

Trục số:
   1   3   5   7   9  11  13  15  17  19
   |   |   |   |   |   |   |   |   |   |
   ├───┤                                       [1,3]
       ├──────────┤                            [2,6]
                       ├───┤                   [8,10]
                                       ├───┤   [15,18]

Sau khi sort theo start: [[1,3], [2,6], [8,10], [15,18]]

Duyệt:
  Push [1,3]                              result = [[1,3]]
  cur=[2,6], 2 <= 3 → mở rộng [1, max(3,6)] = [1,6]
                                           result = [[1,6]]
  cur=[8,10], 8 > 6 → push                result = [[1,6], [8,10]]
  cur=[15,18], 15 > 10 → push             result = [[1,6], [8,10], [15,18]]

Kết quả: [[1,6], [8,10], [15,18]]

Code Python 3

from typing import List

class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        intervals.sort(key=lambda x: x[0])
        result: list[list[int]] = []
        for cur in intervals:
            if result and cur[0] <= result[-1][1]:
                result[-1][1] = max(result[-1][1], cur[1])
            else:
                result.append(cur[:])   # copy để khỏi share reference
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


4.3 Largest Number (LC 179)

Đề bài

Cho mảng số nguyên không âm nums. Hãy ghép chúng (theo thứ tự nào đó) lại thành một chuỗi sao cho số tạo ra là lớn nhất. Trả về kết quả dưới dạng chuỗi (vì số có thể rất lớn).

Ví dụ

Input:  nums = [10, 2]
Output: "210"

Input:  nums = [3, 30, 34, 5, 9]
Output: "9534330"

Input:  nums = [0, 0]
Output: "0"   (không phải "00")

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — thử mọi hoán vị O(n! · n). TLE khi n đủ lớn.

Tối ưu — Sort với comparator tuỳ biến — O(n log n · L) với L = độ dài tối đa.

Insight: Để quyết định a đứng trước b hay sau, chỉ cần so sánh 2 cách ghép: str(a) + str(b) vs str(b) + str(a) — chuỗi nào lớn hơn thì cách đó “tốt hơn”.

Tại sao đúng? Quan hệ “ghép nào lớn hơn” có tính bắc cầu — chứng minh chặt qua trường hợp Lexicographic của các chuỗi ghép, đảm bảo tồn tại thứ tự sort hợp lệ.

Ví dụ: a = 3, b = 30"330" > "303"3 đứng trước 30.

Code Python 3

from functools import cmp_to_key
from typing import List

class Solution:
    def largestNumber(self, nums: List[int]) -> str:
        strs = [str(x) for x in nums]

        def cmp(a: str, b: str) -> int:
            if a + b > b + a:   return -1   # a đứng trước
            if a + b < b + a:   return  1   # b đứng trước
            return 0

        strs.sort(key=cmp_to_key(cmp))
        result = ''.join(strs)
        # edge case: [0, 0, 0] → tránh ra "000"
        return '0' if result[0] == '0' else result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


4.4 Meeting Rooms II (LC 253)

Đề bài

Cho mảng các khoảng intervals[i] = [start_i, end_i] đại diện cho các cuộc họp. Tìm số phòng tối thiểu cần thiết để chứa tất cả.

(Tức là, tại mọi thời điểm, có tối đa mấy cuộc họp đang diễn ra đồng thời?)

Ví dụ

Input:  intervals = [[0,30], [5,10], [15,20]]
Output: 2
Giải thích: tại t=5, [0,30] và [5,10] cùng diễn ra → cần 2 phòng.

Input:  intervals = [[7,10], [2,4]]
Output: 1
Giải thích: 2 cuộc họp không overlap, 1 phòng dùng được cả 2.

Ràng buộc

Clarifying questions

Hướng tiếp cận

Có 3 cách hay, đều xứng đáng biết:

Cách 1 — Heap (priority queue) — O(n log n).

Sort theo start. Duyệt từng cuộc họp, dùng min-heap chứa end_time của các cuộc đang diễn ra. Khi cuộc mới đến (start): - Nếu heap top có end <= start → cuộc cũ đã xong → pop ra (tái dùng phòng). - Push end của cuộc mới.

Kích thước heap tại mỗi thời điểm = số phòng đang dùng → max của size là đáp án.

Cách 2 — Sweep line / Chronological — O(n log n).

Tạo 2 mảng: starts (đã sort) và ends (đã sort). Duyệt 2 con trỏ: nếu starts[i] < ends[j] → cuộc mới bắt đầu trước cuộc cũ kết thúc → cần thêm phòng (rooms++, i++); ngược lại → giải phóng (i++ thì sao đây — sai). Đúng ra: i++ khi starts[i] < ends[j]j++ khi starts[i] >= ends[j].

Cách 3 — Event-driven, O(n log n).

Mỗi cuộc tạo 2 sự kiện: (start, +1)(end, -1). Sort tất cả sự kiện (ưu tiên -1 trước +1 nếu cùng time → ưu tiên đóng phòng). Duyệt và giữ cur/peak.

Hình minh hoạ với [[0,30], [5,10], [15,20]] — heap-based:

Sort by start: [[0,30], [5,10], [15,20]]

Bước 1: cuộc [0,30]    heap = [30]     → rooms = 1
Bước 2: cuộc [5,10]    top=30 > 5 → giữ; push 10  heap = [10, 30]  → rooms = 2 ★
Bước 3: cuộc [15,20]   top=10 <= 15 → pop 10; push 20  heap = [20, 30] → rooms = 2

Trục số:
   0   5  10  15  20  25  30
   |   |   |   |   |   |   |
   ├───────────────────────┤    [0, 30] dùng phòng A
       ├───┤                    [5, 10] dùng phòng B
                ├───┤            [15, 20] tái dùng phòng B

Code Python 3

import heapq
from typing import List

class Solution:
    def minMeetingRooms(self, intervals: List[List[int]]) -> int:
        if not intervals:
            return 0
        intervals.sort(key=lambda x: x[0])
        heap: list[int] = []   # min-heap of end times
        for start, end in intervals:
            if heap and heap[0] <= start:
                heapq.heappop(heap)
            heapq.heappush(heap, end)
        return len(heap)


class SolutionEvents:
    """Cách event-driven — clean cho follow-up."""

    def minMeetingRooms(self, intervals: List[List[int]]) -> int:
        events = []
        for s, e in intervals:
            events.append((s, +1))
            events.append((e, -1))
        # Khi cùng time: ưu tiên -1 trước +1 (cuộc kết thúc thì phòng giải phóng trước cuộc mới)
        events.sort(key=lambda x: (x[0], x[1]))

        cur = peak = 0
        for _, delta in events:
            cur += delta
            peak = max(peak, cur)
        return peak

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


4.5 Custom Sort String (LC 791)

Đề bài

Cho hai chuỗi order (toàn các ký tự phân biệt) và s. Hãy sắp xếp lại s sao cho thứ tự các ký tự xuất hiện trong order được tôn trọng. Các ký tự không nằm trong order có thể đặt ở bất cứ đâu trong kết quả.

Ví dụ

Input:  order = "cba", s = "abcd"
Output: "cbad"
Giải thích: trong order, c < b < a. Các ký tự d không xuất hiện trong order
            nên đặt đâu cũng được.

Input:  order = "bcafg", s = "abcd"
Output: "bcad"

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cách 1 — Sort với comparator theo bảng index, O(|s| log |s|).

Tạo dict priority = {ch: i for i, ch in enumerate(order)}. Mỗi ký tự không có trong order cho priority lớn vô cùng (ví dụ 26). Rồi sort s theo priority này.

Cách 2 — Counter + emit theo order, O(|s|).

Đếm Counter(s), sau đó duyệt qua từng ký tự trong order và “in ra” đúng số lần. Cuối cùng nối thêm các ký tự còn lại (không trong order).

Cách 2 không cần sort, nhanh hơn, và rất tự nhiên — interviewer thường mong đợi cách này.

Code Python 3

from collections import Counter

class Solution:
    """Cách 2 — Counter + emit theo order."""

    def customSortString(self, order: str, s: str) -> str:
        cnt = Counter(s)
        parts: list[str] = []
        # 1. Phần các ký tự thuộc order, theo đúng thứ tự order.
        for ch in order:
            if ch in cnt:
                parts.append(ch * cnt.pop(ch))
        # 2. Các ký tự còn lại (không trong order) — thứ tự không quan trọng.
        for ch, c in cnt.items():
            parts.append(ch * c)
        return ''.join(parts)


class SolutionSort:
    """Cách 1 — comparator theo bảng index."""

    def customSortString(self, order: str, s: str) -> str:
        priority = {ch: i for i, ch in enumerate(order)}
        return ''.join(sorted(s, key=lambda ch: priority.get(ch, 26)))

Phân tích độ phức tạp

Cách Time Space
Counter O(|s| + |order|) O(1)
Sort key O(|s| log |s|) O(|s|)

Bình luận

Bài tự luyện liên quan


4.6 Wiggle Sort (LC 280)

Đề bài

Cho mảng nums. Sắp xếp lại để thoả mãn:

nums[0] <= nums[1] >= nums[2] <= nums[3] >= nums[4] <= ...

Tức là vị trí lẻ luôn >= vị trí kề bên trái và phải.

Ví dụ

Input:  nums = [3, 5, 2, 1, 6, 4]
Output: [3, 5, 1, 6, 2, 4]   (một trong nhiều đáp án hợp lệ)

Input:  nums = [6, 6, 5, 6, 3, 8]
Output: [6, 6, 5, 6, 3, 8]   (đã thoả mãn)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cách 1 — Sort rồi swap cặp, O(n log n). Sort tăng dần, sau đó với mỗi cặp (i, i+1) với i lẻ → swap. Bài giải đúng nhưng không tối ưu.

Cách 2 — Greedy 1 lượt, O(n).

Quan sát điều kiện: tại mỗi vị trí i, - Nếu i lẻ (1, 3, 5, …): nums[i] >= nums[i-1]. - Nếu i chẵn (2, 4, 6, …): nums[i] <= nums[i-1].

Duyệt từ i = 1, nếu vi phạm thì swap nums[i] với nums[i-1]. Tại sao swap đảm bảo không phá quan hệ trước đó? Vì ta chỉ sửa phần tử ở vị trí i-1 (làm nó nhỏ hơn hoặc lớn hơn), và quan hệ giữa nums[i-2]nums[i-1] ở bước trước đã đảm bảo “biên” phù hợp.

Hình minh hoạ với nums = [3, 5, 2, 1, 6, 4]:

Index :   0   1   2   3   4   5
Input :  [3,  5,  2,  1,  6,  4]
            lẻ  chẵn lẻ  chẵn lẻ
            ≥   ≤    ≥   ≤    ≥

i=1 (lẻ):  cần nums[1] >= nums[0]   5 >= 3 ✓
i=2 (chẵn):cần nums[2] <= nums[1]   2 <= 5 ✓
i=3 (lẻ):  cần nums[3] >= nums[2]   1 >= 2 ✗ → swap
           [3, 5, 1, 2, 6, 4]
i=4 (chẵn):cần nums[4] <= nums[3]   6 <= 2 ✗ → swap
           [3, 5, 1, 6, 2, 4]
i=5 (lẻ):  cần nums[5] >= nums[4]   4 >= 2 ✓

Kết quả : [3, 5, 1, 6, 2, 4]  ✓

Code Python 3

from typing import List

class Solution:
    def wiggleSort(self, nums: List[int]) -> None:
        for i in range(1, len(nums)):
            should_be_greater = (i % 2 == 1)
            if (should_be_greater and nums[i] < nums[i - 1]) or \
               (not should_be_greater and nums[i] > nums[i - 1]):
                nums[i], nums[i - 1] = nums[i - 1], nums[i]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Sorting mua gì / mất gì

Mua: - Đưa thứ tự về monotonic → cho phép two-pointer, binary search, sweep. - Hợp nhóm các phần tử “giống nhau” cạnh nhau (anagram, intervals).

Mất: - Mất index gốc → nếu output yêu cầu index, lưu (value, idx) trước. - Mutate input — làm rõ với interviewer trước khi sort. - O(n log n), không miễn phí.

Largest Number (LC 179) — bẫy comparator

Meeting Rooms II — heap vs sweep

Heap Sweep line
Tư duy Phòng nào trống sớm nhất → tái dùng Đếm overlap tại mỗi mốc thời gian
Code heapq + sort theo start Sort events (time, ±1)
Output yêu cầu Số phòng max Số phòng max
Khi mở rộng Dễ trả về schedule (phòng nào lúc nào) Khó trả về schedule

Wiggle Sort: LC 280 vs 324


Chương 5 — Binary Search

Binary Search (tìm kiếm nhị phân) tưởng dễ — “chia đôi mảng đã sort” — nhưng thực tế là bug magnet số một trong phỏng vấn. Knuth từng viết: “trong khi ý tưởng đơn giản, viết đúng nó là chuyện khó hơn ta tưởng”. Chương này dạy bạn một template duy nhất áp dụng cho mọi biến thể: tìm equal, tìm boundary, tìm trên rotated, search on answer, … — sẽ gặp lại ở Chương 25 (Advanced Binary Search).

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Mẫu suy nghĩ chuẩn: 1. Không gian tìm kiếm là gì? (index, giá trị, đáp án). 2. Hàm check(mid) trả True/False thế nào? Liệu nó đơn điệu không? 3. Đáp án là ranh giới nào? First True hay last False?

Template code

def lower_bound(nums: list[int], target: int) -> int:
    """Trả về vị trí đầu tiên có nums[i] >= target. Nếu không có, trả về len(nums)."""
    lo, hi = 0, len(nums)        # [lo, hi)  — half-open
    while lo < hi:
        mid = (lo + hi) // 2
        if nums[mid] < target:
            lo = mid + 1
        else:
            hi = mid
    return lo


def upper_bound(nums: list[int], target: int) -> int:
    """Trả về vị trí đầu tiên có nums[i] > target."""
    lo, hi = 0, len(nums)
    while lo < hi:
        mid = (lo + hi) // 2
        if nums[mid] <= target:
            lo = mid + 1
        else:
            hi = mid
    return lo


def binary_search_answer(check, lo: int, hi: int) -> int:
    """Tìm giá trị nhỏ nhất trong [lo, hi] thoả check(x)=True (check đơn điệu F→T)."""
    while lo < hi:
        mid = (lo + hi) // 2
        if check(mid):
            hi = mid
        else:
            lo = mid + 1
    return lo

Mẹo cuối cùng: mình luôn dùng nửa khoảng đóng-mở [lo, hi) và điều kiện vòng lặp lo < hi. Cách này đồng bộ với bisect của Python và ít bug off-by-one hơn cách lo <= hi.

Bài tự luyện cuối chương


5.1 Binary Search cơ bản (LC 704)

Đề bài

Cho mảng nums đã sort tăng dần và số target. Trả về chỉ số của target trong nums, hoặc -1 nếu không có. Phải chạy O(log n).

Ví dụ

Input:  nums = [-1, 0, 3, 5, 9, 12], target = 9
Output: 4

Input:  nums = [-1, 0, 3, 5, 9, 12], target = 2
Output: -1

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — O(n). Duyệt tuần tự — không thoả O(log n).

Tối ưu — Binary Search half-open, O(log n).

Duy trì [lo, hi). Tại mỗi vòng: - mid = (lo + hi) // 2. - Nếu nums[mid] == target → trả mid. - Nếu nums[mid] < target → đáp án (nếu có) ở [mid+1, hi)lo = mid + 1. - Nếu nums[mid] > target → đáp án ở [lo, mid)hi = mid.

Hình minh hoạ với nums = [-1, 0, 3, 5, 9, 12], target = 9:

index :     0    1    2    3    4    5
nums  :  [ -1,   0,   3,   5,   9,  12 ]

Vòng 1: lo=0, hi=6  → mid=3, nums[3]=5 < 9 → lo = mid+1 = 4
Vòng 2: lo=4, hi=6  → mid=5, nums[5]=12 > 9 → hi = mid = 5
Vòng 3: lo=4, hi=5  → mid=4, nums[4]=9 == 9 → trả 4  ✓

Code Python 3

from typing import List

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        lo, hi = 0, len(nums)    # [lo, hi)
        while lo < hi:
            mid = (lo + hi) // 2
            if nums[mid] == target:
                return mid
            if nums[mid] < target:
                lo = mid + 1
            else:
                hi = mid
        return -1

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


5.2 Search Insert Position (LC 35)

Đề bài

Cho mảng nums đã sort, không có duplicate, và target. Trả về: - Chỉ số của target nếu có trong mảng. - Vị trí mà target sẽ được chèn vào nếu không có (giữ mảng vẫn sort).

Phải O(log n).

Ví dụ

Input:  nums = [1, 3, 5, 6], target = 5   → 2
Input:  nums = [1, 3, 5, 6], target = 2   → 1
Input:  nums = [1, 3, 5, 6], target = 7   → 4   (chèn cuối)
Input:  nums = [1, 3, 5, 6], target = 0   → 0   (chèn đầu)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Đây chính là lower_bound! Vị trí đầu tiên có nums[i] >= target — ý nghĩa: “đây là vị trí target sẽ đứng nếu được chèn”.

Code Python 3

from typing import List

class Solution:
    def searchInsert(self, nums: List[int], target: int) -> int:
        lo, hi = 0, len(nums)
        while lo < hi:
            mid = (lo + hi) // 2
            if nums[mid] < target:
                lo = mid + 1
            else:
                hi = mid
        return lo

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


5.3 First Bad Version (LC 278)

Đề bài

Cho n phiên bản, đánh số từ 1 đến n. Có một phiên bản “bad” và mọi phiên bản sau nó cũng bad. Bạn được cấp hàm API isBadVersion(int) trả True/False. Hãy tìm phiên bản bad đầu tiên với số lần gọi API tối thiểu.

Ví dụ

n = 5, bad version = 4
gọi isBadVersion(3) → False
gọi isBadVersion(5) → True
gọi isBadVersion(4) → True
→ trả 4

Ràng buộc

Clarifying questions

Hướng tiếp cận

Đây là search on monotonic predicate. Pattern hoàn hảo cho template binary_search_answer:

Hình minh hoạ với n = 7, bad = 4:

version :  1     2     3     4     5     6     7
check  :   F     F     F     T     T     T     T
                              ↑
                       cần tìm vị trí này

Vòng 1: lo=1, hi=7  → mid=4, check(4)=T → hi=4
Vòng 2: lo=1, hi=4  → mid=2, check(2)=F → lo=3
Vòng 3: lo=3, hi=4  → mid=3, check(3)=F → lo=4
lo == hi → trả lo = 4  ✓

Code Python 3

def isBadVersion(v: int) -> bool: ...     # API có sẵn

class Solution:
    def firstBadVersion(self, n: int) -> int:
        lo, hi = 1, n      # closed interval [lo, hi]
        while lo < hi:
            mid = lo + (hi - lo) // 2      # tránh overflow ở các ngôn ngữ khác
            if isBadVersion(mid):
                hi = mid
            else:
                lo = mid + 1
        return lo

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


5.4 Find First and Last Position of Element (LC 34)

Đề bài

Cho mảng nums đã sort tăng dần (có thể có duplicate) và target. Trả về [first, last] — chỉ số đầu và cuối của các vị trí target trong mảng. Nếu không có, trả [-1, -1]. Phải O(log n).

Ví dụ

Input:  nums = [5, 7, 7, 8, 8, 10], target = 8
Output: [3, 4]

Input:  nums = [5, 7, 7, 8, 8, 10], target = 6
Output: [-1, -1]

Input:  nums = [], target = 0
Output: [-1, -1]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Dùng 2 lần binary search: - first = lower_bound(target) — vị trí đầu tiên >= target. - last = upper_bound(target) - 1 — vị trí cuối cùng <= target.

Sau đó kiểm tra xem first có hợp lệ và nums[first] == target không.

Hình minh hoạ với nums = [5, 7, 7, 8, 8, 10], target = 8:

index :     0    1    2    3    4    5
nums  :  [  5,   7,   7,   8,   8,  10 ]
                          ↑    ↑
                       first  last

lower_bound(8) = 3   (vị trí đầu tiên >= 8)
upper_bound(8) = 5   (vị trí đầu tiên > 8)
last           = 4   (= upper - 1)

Trả [3, 4]

Code Python 3

from typing import List

class Solution:
    def searchRange(self, nums: List[int], target: int) -> List[int]:
        def lower_bound(t: int) -> int:
            lo, hi = 0, len(nums)
            while lo < hi:
                mid = (lo + hi) // 2
                if nums[mid] < t:
                    lo = mid + 1
                else:
                    hi = mid
            return lo

        first = lower_bound(target)
        if first == len(nums) or nums[first] != target:
            return [-1, -1]
        last = lower_bound(target + 1) - 1
        return [first, last]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


5.5 Search in Rotated Sorted Array (LC 33)

Đề bài

Cho mảng nums đã sort tăng dần và các giá trị phân biệt, sau đó bị xoay tại một pivot bí ẩn (k lần xoay phải, k chưa biết). Cho target, trả về index hoặc -1. Phải O(log n).

Ví dụ

Input:  nums = [4, 5, 6, 7, 0, 1, 2], target = 0
Output: 4

Input:  nums = [4, 5, 6, 7, 0, 1, 2], target = 3
Output: -1

Input:  nums = [1], target = 0
Output: -1

Ràng buộc

Clarifying questions

Hướng tiếp cận

Quan sát then chốt: Khi chia mảng bị rotated tại mid, luôn có ít nhất một nửa ([lo, mid] hoặc [mid, hi]) là sorted đúng (không bị rotate).

Quy trình mỗi vòng: 1. Tính mid. 2. Nếu nums[mid] == target → trả mid. 3. Xác định nửa nào đang sorted: - Nếu nums[lo] <= nums[mid] → nửa trái sorted. - Ngược lại → nửa phải sorted. 4. Kiểm tra target có nằm trong nửa sorted không (so sánh bằng <, >): - Có → search nửa sorted. - Không → search nửa còn lại.

Hình minh hoạ với nums = [4, 5, 6, 7, 0, 1, 2], target = 0:

index :   0    1    2    3    4    5    6
nums  :  [4,   5,   6,   7,   0,   1,   2]
          lo                            hi

Vòng 1: lo=0, hi=6, mid=3, nums[mid]=7
  nums[lo]=4 <= nums[mid]=7 → nửa trái [0..3] = [4,5,6,7] đã sort.
  target=0 nằm trong [4..7]? Không (0 < 4) → đi nửa phải.
  → lo = mid + 1 = 4

Vòng 2: lo=4, hi=6, mid=5, nums[mid]=1
  nums[lo]=0 <= nums[mid]=1 → nửa trái [4..5] = [0,1] đã sort.
  target=0 nằm trong [0..1]? Có → đi nửa trái.
  → hi = mid - 1 = 4

Vòng 3: lo=4, hi=4, mid=4, nums[mid]=0 == target → trả 4  ✓

Code Python 3

from typing import List

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        lo, hi = 0, len(nums) - 1        # closed interval
        while lo <= hi:
            mid = (lo + hi) // 2
            if nums[mid] == target:
                return mid

            # Nửa trái [lo..mid] đã sort?
            if nums[lo] <= nums[mid]:
                if nums[lo] <= target < nums[mid]:
                    hi = mid - 1     # target ở nửa trái
                else:
                    lo = mid + 1
            # Ngược lại: nửa phải [mid..hi] đã sort
            else:
                if nums[mid] < target <= nums[hi]:
                    lo = mid + 1     # target ở nửa phải
                else:
                    hi = mid - 1

        return -1

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


5.6 Sqrt(x) (LC 69)

Đề bài

Cho số nguyên không âm x. Trả về căn bậc hai làm tròn xuống của x, tức là số nguyên r lớn nhất sao cho r * r <= x.

Không được dùng hàm built-in sqrt.

Ví dụ

Input:  x = 4   → 2
Input:  x = 8   → 2   (vì 2² = 4 ≤ 8 < 9 = 3²)
Input:  x = 0   → 0
Input:  x = 1   → 1

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cách 1 — Binary Search trên đáp án — O(log x).

Tìm số nguyên lớn nhất r thoả r² <= x. Tương đương với “last True” trong dãy đơn điệu [T, T, ..., T, F, F, ...] (T = r² <= x).

Khoảng tìm kiếm: [0, x] (hoặc [0, x//2 + 1] để tiết kiệm).

Cách 2 — Newton’s Method — O(log x) nhưng hằng số nhỏ hơn.

Lặp r = (r + x/r) / 2 cho đến khi r² <= x < (r+1)². Hội tụ rất nhanh (quadratic convergence) — đây là cách sqrt được cài trong nhiều thư viện chuẩn.

Hình minh hoạ — Binary Search cho x = 8:

   r :    0    1    2    3    4    5    6    7    8
  r² :    0    1    4    9   16   25   36   49   64
                    ↑
                last r với r² <= 8

Vòng 1: lo=0, hi=8   mid=4, 16 > 8 → hi = 3
Vòng 2: lo=0, hi=3   mid=1,  1 <= 8 → answer=1, lo=2
Vòng 3: lo=2, hi=3   mid=2,  4 <= 8 → answer=2, lo=3
Vòng 4: lo=3, hi=3   mid=3,  9 > 8 → hi=2  → lo > hi, dừng

Trả 2  ✓

Code Python 3

class Solution:
    """Cách 1 — Binary Search."""

    def mySqrt(self, x: int) -> int:
        if x < 2:
            return x
        lo, hi = 1, x // 2 + 1
        answer = 0
        while lo <= hi:
            mid = (lo + hi) // 2
            if mid * mid <= x:
                answer = mid
                lo = mid + 1
            else:
                hi = mid - 1
        return answer


class SolutionNewton:
    """Cách 2 — Newton's Method, hằng số nhỏ hơn."""

    def mySqrt(self, x: int) -> int:
        if x < 2:
            return x
        r = x
        while r * r > x:
            r = (r + x // r) // 2
        return r

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Template invariants (chọn 1 và bám)

Closed interval [lo, hi]:

lo, hi = 0, n - 1
while lo <= hi:
    mid = (lo + hi) // 2
    if check(mid): return mid
    elif too_small(mid): lo = mid + 1
    else: hi = mid - 1
return -1

Half-open [lo, hi) — first-true (lower_bound):

lo, hi = 0, n          # hi không inclusive
while lo < hi:
    mid = (lo + hi) // 2
    if pred(mid): hi = mid
    else: lo = mid + 1
return lo                # vị trí đầu tiên thỏa

Checklist trước khi submit

  1. lo, hi khởi tạo đúng (đặc biệt khi search on answer: lo = min, hi = max hoặc max+1).
  2. Điều kiện vòng lặp khớp với loại interval (<= hay <).
  3. Cập nhật mid+1 / mid-1 / mid đúng — tránh infinite loop.
  4. Trường hợp không tìm thấy trả về cái gì? -1, n, lo?
  5. Overflow (lo + hi) // 2 an toàn ở Python; ở Java/C++ dùng lo + (hi - lo) // 2.

Sqrt overflow note (ngôn ngữ khác)

Python int không tràn. Java/C++: mid * mid có thể tràn int32. Dùng (long) mid * mid hoặc so sánh mid <= x / mid để tránh nhân.


Chương 6 — Hash Table

Hash Table (bảng băm) là “vũ khí thần kỳ” của phỏng vấn coding: nó biến nhiều bài O(n²) thành O(n). Triết lý: đổi bộ nhớ lấy thời gian — chấp nhận thêm O(n) bộ nhớ phụ để có look-up O(1). Chương này dạy bạn nhận diện khi nào nên và khi nào không nên dùng hash, cùng 6 bài kinh điển rất hay gặp ở Big Tech.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Khi không nên dùng hash: - Cần thứ tự sort → dùng SortedSet/TreeMap (Python: sortedcontainers). - Cần O(1) worst-case (không phải amortized) → hash bị adversary tấn công collision. - Khoá là loại phức tạp (list, dict) → phải convert sang tuple/frozenset.

Template code

from collections import Counter, defaultdict
from typing import List

# 1) Counter: đếm tần suất
cnt = Counter(nums)               # {value: count}
top3 = cnt.most_common(3)         # 3 phần tử thường gặp nhất

# 2) defaultdict(list): nhóm theo khoá
groups: dict[str, list[int]] = defaultdict(list)
for i, v in enumerate(arr):
    groups[v].append(i)

# 3) Prefix sum + dict: tìm subarray
prefix_index = {0: -1}            # prefix_sum -> index sớm nhất
cur = 0
for i, x in enumerate(arr):
    cur += x
    if cur - target in prefix_index:
        # tìm thấy subarray có sum = target
        ...
    if cur not in prefix_index:
        prefix_index[cur] = i

Bài tự luyện cuối chương


6.1 Contains Duplicate (LC 217)

Đề bài

Cho mảng nums. Trả về True nếu có ít nhất 1 phần tử xuất hiện ≥ 2 lần, ngược lại False.

Ví dụ

Input:  nums = [1, 2, 3, 1]   → True
Input:  nums = [1, 2, 3, 4]   → False
Input:  nums = []             → False

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — O(n²). So sánh mọi cặp.

Sort — O(n log n), O(1) extra space. Sort rồi check 2 phần tử kề.

Hash set — O(n) time, O(n) space — đáp án phổ biến nhất.

One-liner Pythonic: return len(set(nums)) != len(nums).

Code Python 3

from typing import List

class Solution:
    def containsDuplicate(self, nums: List[int]) -> bool:
        seen: set[int] = set()
        for x in nums:
            if x in seen:
                return True
            seen.add(x)
        return False

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


6.2 Longest Consecutive Sequence (LC 128)

Đề bài

Cho mảng nums không sort. Trả về độ dài của dãy số liên tiếp (consecutive integers, có thể không đứng cạnh trong mảng) dài nhất. Phải chạy O(n).

Ví dụ

Input:  nums = [100, 4, 200, 1, 3, 2]
Output: 4
Giải thích: dãy [1, 2, 3, 4] dài 4.

Input:  nums = [0, 3, 7, 2, 5, 8, 4, 6, 0, 1]
Output: 9
Giải thích: dãy [0, 1, 2, 3, 4, 5, 6, 7, 8].

Input:  nums = []
Output: 0

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — O(n³). Với mỗi phần tử, đếm số x, x+1, x+2, ... có trong mảng.

Sort — O(n log n). Sort, đếm dãy liên tiếp. Đơn giản nhưng không thoả O(n).

Tối ưu — Hash Set + “chỉ bắt đầu từ điểm đầu dãy” — O(n).

Insight then chốt: Một số xđiểm bắt đầu dãy ↔︎ x - 1 không có trong mảng. Vậy chỉ với những x thoả điều kiện này, ta mới đếm dãy x, x+1, x+2, ... bằng cách lookup hash. Mỗi phần tử bị “đếm tới” tối đa 1 lần trên toàn quá trình → tổng O(n).

Hình minh hoạ với nums = [100, 4, 200, 1, 3, 2]:

Set: {100, 4, 200, 1, 3, 2}

Duyệt từng x trong set:
  x=100: 99 không có trong set → là điểm bắt đầu
         Đếm: 100 ✓, 101 ✗  → length 1
  x=4:   3 CÓ trong set → SKIP (sẽ được đếm khi bắt đầu từ 1)
  x=200: 199 không có → là điểm bắt đầu
         Đếm: 200 ✓, 201 ✗  → length 1
  x=1:   0 không có → là điểm bắt đầu
         Đếm: 1 ✓, 2 ✓, 3 ✓, 4 ✓, 5 ✗  → length 4  ★
  x=3:   2 CÓ → SKIP
  x=2:   1 CÓ → SKIP

Tổng: max length = 4.

Chìa khoá: mỗi phần tử của dãy 1-2-3-4 chỉ được "duyệt forward" đúng 1 lần
            (khi x=1). Tổng công ~ O(n).

Code Python 3

from typing import List

class Solution:
    def longestConsecutive(self, nums: List[int]) -> int:
        num_set = set(nums)
        best = 0
        for x in num_set:
            # Chỉ bắt đầu khi x là điểm đầu dãy (x-1 không có).
            if x - 1 not in num_set:
                cur = x
                length = 1
                while cur + 1 in num_set:
                    cur += 1
                    length += 1
                best = max(best, length)
        return best

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


6.3 Top K Frequent Elements (LC 347)

Đề bài

Cho mảng nums và số nguyên k. Trả về k phần tử thường gặp nhất (output order tuỳ ý).

Ví dụ

Input:  nums = [1, 1, 1, 2, 2, 3], k = 2
Output: [1, 2]

Input:  nums = [1], k = 1
Output: [1]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cách 1 — Counter + sort — O(n log n). Đơn giản nhưng không thoả gợi ý LC.

Cách 2 — Min-heap kích thước k — O(n log k). Duy trì heap kích thước k; khi vượt thì pop phần tử ít tần suất nhất.

Cách 3 — Bucket sort theo frequency — O(n)xịn nhất.

Frequency tối đa là n → tạo n + 1 bucket, bucket i chứa các phần tử có frequency i. Duyệt bucket từ cao xuống thấp, gom k phần tử.

Hình minh hoạ với nums = [1,1,1,2,2,3], k=2:

Bước 1: Counter → {1:3, 2:2, 3:1}

Bước 2: Bucket sort theo frequency (n=6, có 7 bucket 0..6):
  bucket[0] = []
  bucket[1] = [3]      # số 3 xuất hiện 1 lần
  bucket[2] = [2]      # số 2 xuất hiện 2 lần
  bucket[3] = [1]      # số 1 xuất hiện 3 lần
  bucket[4..6] = []

Bước 3: Duyệt bucket từ index 6 xuống:
  i=6: rỗng
  i=5: rỗng
  i=4: rỗng
  i=3: [1] → result = [1]
  i=2: [2] → result = [1, 2]   đủ k=2, dừng

Output: [1, 2]

Code Python 3

import heapq
from collections import Counter
from typing import List

class Solution:
    """Cách 3 — Bucket sort, O(n)."""

    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        cnt = Counter(nums)
        n = len(nums)
        buckets: list[list[int]] = [[] for _ in range(n + 1)]
        for x, freq in cnt.items():
            buckets[freq].append(x)
        result: list[int] = []
        for freq in range(n, 0, -1):
            for x in buckets[freq]:
                result.append(x)
                if len(result) == k:
                    return result
        return result


class SolutionHeap:
    """Cách 2 — Min-heap kích thước k, O(n log k)."""

    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        cnt = Counter(nums)
        # heapq.nlargest dùng heap-based partial sort
        return heapq.nlargest(k, cnt.keys(), key=cnt.get)

Phân tích độ phức tạp

Cách Time Space
Counter + sort O(n log n) O(n)
Min-heap O(n log k) O(n)
Bucket sort O(n) O(n)

Bình luận

Bài tự luyện liên quan


6.4 Subarray Sum Equals K (LC 560)

Đề bài

Cho mảng số nguyên nums và số k. Trả về số lượng subarray (liên tục) có tổng đúng bằng k.

Ví dụ

Input:  nums = [1, 1, 1], k = 2
Output: 2
Giải thích: 2 subarray [1,1] (vị trí 0..1 và 1..2).

Input:  nums = [1, 2, 3], k = 3
Output: 2
Giải thích: [1,2] và [3].

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — O(n²). Với mỗi i, tính prefix sum dần và check == k. Chấp nhận được nhưng không tối ưu.

Tối ưu — Prefix sum + Hash map — O(n).

Đặt P[i] = tổng nums[0..i-1] (P[0] = 0). Khi đó tổng subarray nums[j..i-1] = P[i] - P[j]. Subarray có tổng k ↔︎ P[i] - P[j] = k ↔︎ P[j] = P[i] - k.

→ Duyệt và đếm số j < i thoả P[j] == cur - k. Dùng dict đếm {prefix_sum: số lần xuất hiện}.

Hình minh hoạ với nums = [3, 4, 7, 2, -3, 1, 4, 2], k = 7:

i    :   0    1    2    3    4    5    6    7
nums :   3    4    7    2   -3    1    4    2
P[i+1]:  3    7   14   16   13   14   18   20
                  ↑                   ↑
                P[3]=14            P[7]=18  ←  P[7]-P[3]=4? KHÔNG, =4

Tay xét:
P[]   = [0, 3, 7, 14, 16, 13, 14, 18, 20]
counts = {0:1}                        cur=0
i=0  cur=3   (3-7=-4 not in counts)  add 3 → {0:1, 3:1}
i=1  cur=7   (7-7= 0 in counts: +1)  add 7 → {0:1, 3:1, 7:1}   answer=1
i=2  cur=14  (14-7=7 in counts: +1)  add 14 → ...              answer=2
i=3  cur=16  (16-7=9 not in counts)                            answer=2
i=4  cur=13  (13-7=6 not in counts)                            answer=2
i=5  cur=14  (14-7=7 in counts: +1)  cur cũ đã có → counts[14]+=1
                                                              answer=3
i=6  cur=18  (18-7=11 not in counts)                          answer=3
i=7  cur=20  (20-7=13 in counts: +1)                          answer=4

Đáp án: 4 subarray có tổng = 7.

Code Python 3

from collections import defaultdict
from typing import List

class Solution:
    def subarraySum(self, nums: List[int], k: int) -> int:
        counts: dict[int, int] = defaultdict(int)
        counts[0] = 1            # prefix sum 0 đã xuất hiện 1 lần (rỗng)
        cur = 0
        result = 0
        for x in nums:
            cur += x
            result += counts[cur - k]    # bao nhiêu j thoả P[j] = cur - k
            counts[cur] += 1
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


6.5 Isomorphic Strings (LC 205)

Đề bài

Hai chuỗi st gọi là đồng cấu (isomorphic) nếu tồn tại một song ánh giữa các ký tự của st sao cho thay thế từng ký tự trong s theo ánh xạ đó cho ra t.

Ví dụ

Input:  s = "egg", t = "add"   → True
Giải thích: e→a, g→d (bijection).

Input:  s = "foo", t = "bar"   → False
Giải thích: o phải map cả vào a và r (không là hàm).

Input:  s = "paper", t = "title"   → True

Input:  s = "badc", t = "baba"   → False
Giải thích: d→a và c→a, hai ký tự khác map vào cùng 1 → vi phạm song ánh.

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cách 1 — Dùng 2 dict (map cả 2 chiều).

Duyệt cùng lúc 2 chuỗi: - Nếu s[i] đã trong s2t → check s2t[s[i]] == t[i]. - Nếu chưa → kiểm tra t[i] chưa nằm trong t2s (tránh nhiều s map vào cùng 1 t). - Lưu cặp (s[i], t[i]) vào cả 2 dict.

Cách 2 — Thay bằng “first occurrence index”.

Một chuỗi có thể được “chuẩn hoá” bằng cách thay mỗi ký tự bằng vị trí xuất hiện đầu tiên của nó. Hai chuỗi isomorphic ↔︎ chuẩn hoá xong giống nhau.

Ví dụ "egg"[0, 1, 1], "add"[0, 1, 1] → bằng nhau → True.

Cách 1 trực quan hơn, cách 2 đẹp về mặt thuật toán. Cả 2 đều O(n).

Code Python 3

class Solution:
    """Cách 1 — 2 dict, kiểm tra song ánh."""

    def isIsomorphic(self, s: str, t: str) -> bool:
        if len(s) != len(t):
            return False
        s2t: dict[str, str] = {}
        t2s: dict[str, str] = {}
        for a, b in zip(s, t):
            if a in s2t:
                if s2t[a] != b:
                    return False
            else:
                if b in t2s:        # b đã được map từ ký tự khác
                    return False
                s2t[a] = b
                t2s[b] = a
        return True


class SolutionNormalize:
    """Cách 2 — chuẩn hoá theo first-occurrence index."""

    def isIsomorphic(self, s: str, t: str) -> bool:
        return self._normalize(s) == self._normalize(t)

    @staticmethod
    def _normalize(s: str) -> list[int]:
        idx: dict[str, int] = {}
        out: list[int] = []
        for ch in s:
            if ch not in idx:
                idx[ch] = len(idx)
            out.append(idx[ch])
        return out

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


6.6 LRU Cache (LC 146)

Đề bài

Thiết kế Least Recently Used (LRU) Cache với 2 operations đều O(1):

Ví dụ

Input (LC-style operation arrays):
  ops  = ["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"]
  args = [[2],        [1,1], [2,2], [1],   [3,3], [2],   [4,4], [1],   [3],   [4]]

Output: [null, null, null, 1, null, -1, null, -1, 3, 4]

Trace từng bước (capacity = 2; cuối phải = MRU, trái = LRU):

  LRUCache(2)         → null;   cache = {}                    (LRU ← → MRU)
  put(1, 1)           → null;   cache = {1=1}
  put(2, 2)           → null;   cache = {1=1, 2=2}
  get(1)              → 1;      cache = {2=2, 1=1}   (1 vừa dùng → MRU)
  put(3, 3)           → null;   cache = {1=1, 3=3}   (evict 2: LRU)
  get(2)              → -1;     (key 2 không còn)
  put(4, 4)           → null;   cache = {3=3, 4=4}   (evict 1)
  get(1)              → -1
  get(3)              → 3;      cache = {4=4, 3=3}
  get(4)              → 4;      cache = {3=3, 4=4}

Ràng buộc

Clarifying questions

Hướng tiếp cận

Yêu cầu cốt lõi: O(1) cho cả getput ↔︎ cần đồng thời: - Hash map: key → reference đến node (cho O(1) lookup). - Doubly Linked List (DLL): thứ tự truy cập (MRU ở 1 đầu, LRU ở đầu kia). Cho phép xoá node bất kỳ trong O(1) nếu có reference.

Mỗi get(k): - Nếu k trong map → lấy node, di chuyển node lên đầu DLL (= MRU), trả value. - Ngược lại trả -1.

Mỗi put(k, v): - Nếu k đã có → update value, di chuyển lên đầu. - Nếu không → thêm node mới ở đầu. Nếu vượt capacity → xoá node cuối (LRU) và xoá khỏi map.

Cách Pythonic — dùng OrderedDict (đã hỗ trợ sẵn 2 yêu cầu trên):

OrderedDict Python được triển khai bên dưới như hash map kết hợp doubly linked list. Lớp này có 2 method đắt giá cho LRU: move_to_end(key)popitem(last=False) (pop đầu).

Hình minh hoạ — DLL state qua các operation:

capacity = 2
                    Head (MRU)              Tail (LRU)
                          │                       │
                          ▼                       ▼
put(1,1):   DLL:    1                              cache = {1: node1}
put(2,2):   DLL:    2 ─── 1                       cache = {1: ., 2: .}
get(1)=1:   DLL:    1 ─── 2     (1 → MRU)
put(3,3):   DLL:    3 ─── 1     (xoá 2 vì LRU)
get(2)=-1
put(4,4):   DLL:    4 ─── 3     (xoá 1)
get(1)=-1
get(3)=3:   DLL:    3 ─── 4
get(4)=4:   DLL:    4 ─── 3

Code Python 3

from collections import OrderedDict

class LRUCache:
    """Cách Pythonic — OrderedDict đã có sẵn DLL + hash."""

    def __init__(self, capacity: int):
        self.cap = capacity
        self.cache: OrderedDict[int, int] = OrderedDict()

    def get(self, key: int) -> int:
        if key not in self.cache:
            return -1
        self.cache.move_to_end(key)        # đẩy thành MRU (cuối)
        return self.cache[key]

    def put(self, key: int, value: int) -> None:
        if key in self.cache:
            self.cache.move_to_end(key)
        self.cache[key] = value
        if len(self.cache) > self.cap:
            self.cache.popitem(last=False)  # pop LRU ở đầu


# ─────────────────────────────────────────────────────────────
# Phiên bản tự tay (interview-friendly): dict + doubly linked list.
# Trình bày khi interviewer hỏi: "Implement LRU không dùng built-in."

class _Node:
    __slots__ = ("key", "val", "prev", "next")

    def __init__(self, key: int = 0, val: int = 0):
        self.key, self.val = key, val
        self.prev: "_Node | None" = None
        self.next: "_Node | None" = None


class LRUCacheManual:
    def __init__(self, capacity: int):
        self.cap = capacity
        self.cache: dict[int, _Node] = {}
        # Dùng 2 sentinel head/tail để code rút gọn (không phải check None).
        self.head, self.tail = _Node(), _Node()
        self.head.next = self.tail
        self.tail.prev = self.head

    def _remove(self, node: _Node) -> None:
        node.prev.next = node.next
        node.next.prev = node.prev

    def _add_to_front(self, node: _Node) -> None:
        node.prev = self.head
        node.next = self.head.next
        self.head.next.prev = node
        self.head.next = node

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

    def put(self, key: int, value: int) -> None:
        if key in self.cache:
            node = self.cache[key]
            node.val = value
            self._remove(node)
            self._add_to_front(node)
            return
        if len(self.cache) == self.cap:
            lru = self.tail.prev          # LRU = sát tail
            self._remove(lru)
            del self.cache[lru.key]
        new_node = _Node(key, value)
        self.cache[key] = new_node
        self._add_to_front(new_node)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Khi hash KHÔNG đủ

Yêu cầu Hash đủ? Thay thế
Lookup O(1), không cần thứ tự
Cần thứ tự duyệt OrderedDict / sorted list
Range query [l, r] Fenwick / Segment Tree (Chương 22)
Top-k frequent Partial Heap (Chương 15)
Nearest neighbor Sorted set / BST
Subarray sum có số âm ✅ Prefix sum + hash
Subarray sum không âm Thường dùng sliding window (27)

LRU 2 cách

Longest Consecutive — vì sao O(n)?


Chương 7 — Linked List

Linked List (danh sách liên kết) là cấu trúc “đơn giản về lý thuyết, phức tạp về code”. Mỗi node trỏ tới node kế tiếp, vậy thôi — nhưng để code không bug, bạn cần thuộc lòng 5 trick nhỏ: dummy head, two pointers, đảo in-place, split-by-pivot, và phép gắn pointer chéo. Hết chương này, bạn sẽ thấy LL không còn đáng sợ.

Chương này có 12 bài — gấp đôi các chương cơ bản — vì pattern LL có nhiều biến thể quan trọng, từ Easy (Reverse, Merge, Cycle) đến Hard (Reverse k-Group, Sort, Reorder).

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

5 trick phải thuộc lòng:

  1. Dummy head: tạo 1 node giả dummy.next = head, dùng prev = dummy. Tránh hàng tá if head is None.
  2. Two pointers (slow / fast): tìm giữa (1×, 2× tốc độ), phát hiện chu trình (Floyd), tìm offset từ cuối.
  3. Reverse in-place: 3 con trỏ prev / curr / nxt.
  4. Split → process → merge: pattern cho merge-sort, palindrome check, reorder.
  5. Pointer relinking: khi gắn a.next = b, luôn nhớ rời a khỏi vị trí cũ trước (cập nhật cả pointer “đi vào” và “đi ra” của a).

Định dạng input (áp dụng cho TẤT CẢ bài trong chương)

Mọi bài trong chương 7 (và 3.3, 15.5) đều dùng ListNode chuẩn của LeetCode:

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

Template code

class ListNode:
    def __init__(self, val: int = 0, next: "ListNode | None" = None):
        self.val = val
        self.next = next


def use_dummy(head: ListNode | None) -> ListNode | None:
    """Mẫu dummy head — bài hay có chèn/xoá node ở đầu."""
    dummy = ListNode(0, head)
    prev = dummy
    while prev.next:
        # ... thao tác trên prev.next ...
        prev = prev.next
    return dummy.next      # head có thể đã đổi


def find_middle(head: ListNode | None) -> ListNode | None:
    """Slow/fast pointers — tìm middle (LC 876)."""
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    return slow


def reverse(head: ListNode | None) -> ListNode | None:
    """Reverse iterative — 3 con trỏ."""
    prev, curr = None, head
    while curr:
        curr.next, prev, curr = prev, curr, curr.next
    return prev

Bài tự luyện cuối chương


7.1 Reverse Linked List (LC 206) — bản iterative

Đề bài

Cho head của linked list đơn. Đảo ngược danh sách và trả về head mới. (Bài này đã có bản đệ quy ở Chương 3.3 — phần này tập trung vào bản iterative với O(1) space.)

Ví dụ

Input:  head = 1 → 2 → 3 → 4 → 5 → None  (singly linked list)
Output: 5 → 4 → 3 → 2 → 1 → None

Ràng buộc

Clarifying questions

Hướng tiếp cận

3 con trỏ: - prev = node ngay trước curr trong danh sách kết quả. - curr = node đang xử lý. - nxt = sao lưu curr.next trước khi sửa.

Mỗi vòng: lật curr.next về prev, rồi dịch prev, curr về phía trước.

Hình minh hoạ với 1 → 2 → 3 → None:

Bắt đầu :   prev = None
            curr → 1 → 2 → 3 → None
                    
Vòng 1:     nxt = 2
            curr.next = prev   →   None ← 1   2 → 3 → None
            prev = curr = 1, curr = 2

Vòng 2:     nxt = 3
            curr.next = prev   →   None ← 1 ← 2   3 → None
            prev = 2, curr = 3

Vòng 3:     nxt = None
            curr.next = prev   →   None ← 1 ← 2 ← 3
            prev = 3, curr = None  → dừng

Trả prev = 3, danh sách: 3 → 2 → 1 → None

Code Python 3

class Solution:
    def reverseList(self, head: ListNode | None) -> ListNode | None:
        prev, curr = None, head
        while curr:
            nxt = curr.next
            curr.next = prev
            prev = curr
            curr = nxt
        return prev

Pythonic 1 dòng inside loop: curr.next, prev, curr = prev, curr, curr.next. Tuple unpacking đánh giá RHS trước, không cần nxt tạm.

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


7.2 Merge Two Sorted Lists (LC 21)

Đề bài

Cho 2 head của 2 linked list đã sort tăng dần. Trả về head của list gộp lại (cũng sort tăng).

Ví dụ

Input:  l1 = 1 → 2 → 4,  l2 = 1 → 3 → 4
Output: 1 → 1 → 2 → 3 → 4 → 4

Ràng buộc

Clarifying questions

Hướng tiếp cận

Iterative — dùng dummy head. Tạo dummytail = dummy. Mỗi vòng, gắn tail.next vào node nhỏ hơn của 2 list, dịch tail. Cuối cùng gắn phần đuôi còn dư.

Đệ quy (rất gọn nhưng O(n) stack):

if not l1: return l2
if not l2: return l1
if l1.val <= l2.val:
    l1.next = self.mergeTwoLists(l1.next, l2)
    return l1
else:
    l2.next = self.mergeTwoLists(l1, l2.next)
    return l2

Code Python 3

class Solution:
    def mergeTwoLists(self, l1: ListNode | None, l2: ListNode | None) -> ListNode | None:
        dummy = ListNode()
        tail = dummy
        while l1 and l2:
            if l1.val <= l2.val:
                tail.next, l1 = l1, l1.next
            else:
                tail.next, l2 = l2, l2.next
            tail = tail.next
        tail.next = l1 if l1 else l2     # gắn phần đuôi còn dư
        return dummy.next

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


7.3 Linked List Cycle (LC 141)

Đề bài

Cho head của linked list. Trả về True nếu có chu trình, ngược lại False. Yêu cầu: O(1) extra space.

Ví dụ

Input:  head = [3, 2, 0, -4], cycle bắt đầu ở index 1
        3 → 2 → 0 → -4
            ↑________|
Output: True

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — Hash set, O(n) space. Lưu các node đã thấy.

Tối ưu — Floyd’s Tortoise and Hare, O(1) space.

Hai con trỏ slow (1×) và fast (2×). Nếu có chu trình, fast sẽ “đuổi kịp” slow trong vòng tròn (mỗi bước khoảng cách giữa 2 giảm 1). Nếu không, fast hết đường.

Hình minh hoạ — slow/fast trên chu trình:

Linked list:  3 → 2 → 0 → -4 → ⟲ (về 2)

Bước 0:  slow=3, fast=3
Bước 1:  slow=2, fast=0
Bước 2:  slow=0, fast=2     (fast đã quay vòng)
Bước 3:  slow=-4, fast=-4   ★ gặp nhau → return True

Trên chu trình dài L, slow đi 1 bước, fast đi 2 bước → khoảng cách
giảm 1 mỗi bước → tối đa L bước thì gặp.

Code Python 3

class Solution:
    def hasCycle(self, head: ListNode | None) -> bool:
        slow = fast = head
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
            if slow is fast:
                return True
        return False

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


7.4 Middle of the Linked List (LC 876)

Đề bài

Cho head. Trả về node giữa của linked list. Nếu có 2 node giữa (list chẵn), trả về cái thứ 2.

Ví dụ

Input:  head = 1 → 2 → 3 → 4 → 5     (singly linked list)
Output: node có value 3 (giữa, thuộc về nửa sau khi length chẵn)
Input:  head = 1 → 2 → 3 → 4 → 5 → 6   (singly linked list, length chẵn)
Output: node có value 4 (giữa thứ 2)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — 2 lượt, O(n). Đếm độ dài, rồi đi đến giữa.

Tối ưu — Slow/fast 1 lượt, O(n). Khi fast chạm cuối, slow ở giữa.

Code Python 3

class Solution:
    def middleNode(self, head: ListNode | None) -> ListNode | None:
        slow = fast = head
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
        return slow

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


7.5 Remove Nth Node From End of List (LC 19)

Đề bài

Cho head và số n. Xoá node thứ n tính từ cuối (1-indexed) và trả về head có thể đã đổi.

Ví dụ

Input:  1 → 2 → 3 → 4 → 5, n = 2     → 1 → 2 → 3 → 5  (xoá node 4)
Input:  1, n = 1                     → None
Input:  1 → 2, n = 1                 → 1

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — 2 lượt. Đếm độ dài L, sau đó xoá node thứ L - n từ đầu.

Tối ưu — 1 lượt, two pointers cách nhau n. - Tạo dummy để xử lý case xoá head. - fast đi n bước trước. - Sau đó slowfast cùng đi đến khi fast.next is None. Lúc đó slow.next chính là node cần xoá.

Hình minh hoạ với 1 → 2 → 3 → 4 → 5, n = 2:

Bắt đầu:  dummy → 1 → 2 → 3 → 4 → 5 → None
          slow                            
          fast                            

Sau khi fast đi n=2 bước:
          dummy → 1 → 2 → 3 → 4 → 5 → None
          slow        fast              

Cùng đi đến khi fast.next == None:
          dummy → 1 → 2 → 3 → 4 → 5 → None
                          slow      fast

slow.next = 4 → cần xoá. slow.next = slow.next.next.
          dummy → 1 → 2 → 3 → 5 → None  ✓

Code Python 3

class Solution:
    def removeNthFromEnd(self, head: ListNode | None, n: int) -> ListNode | None:
        dummy = ListNode(0, head)
        slow = fast = dummy
        for _ in range(n):
            fast = fast.next
        while fast.next:
            slow = slow.next
            fast = fast.next
        slow.next = slow.next.next
        return dummy.next

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


7.6 Palindrome Linked List (LC 234)

Đề bài

Cho head của linked list đơn. Trả về True nếu giá trị các node tạo thành chuỗi palindrome. Yêu cầu: O(n) time, O(1) space.

Ví dụ

Input:  head = 1 → 2 → 2 → 1     → Output: True   (palindrome)
Input:  head = 1 → 2             → Output: False  (1 ≠ 2)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — copy ra mảng + two pointers, O(n) space. Đơn giản, nhưng vi phạm O(1) space.

Tối ưu — Split + Reverse half + Compare, O(1) space. 1. Tìm middle (slow/fast). 2. Đảo nửa sau (in-place). 3. So sánh từng node giữa nửa đầu và nửa sau đã đảo. 4. (Optional) Khôi phục nửa sau (trong phỏng vấn thường khỏi cần).

Hình minh hoạ với 1 → 2 → 3 → 2 → 1:

Bước 1: tìm middle (slow ở node 3)
              1 → 2 → 3 → 2 → 1
                      ↑ slow

Bước 2: đảo nửa sau (từ slow.next = 2):
              1 → 2 → 3   1 → 2
              (nửa đầu)   (nửa sau đã đảo)

Bước 3: so sánh từng node:
              1 vs 1 ✓
              2 vs 2 ✓
        → True

Code Python 3

class Solution:
    def isPalindrome(self, head: ListNode | None) -> bool:
        # 1. Tìm middle.
        slow = fast = head
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next

        # 2. Đảo nửa sau (bắt đầu từ slow).
        prev, curr = None, slow
        while curr:
            curr.next, prev, curr = prev, curr, curr.next

        # 3. So sánh.
        left, right = head, prev
        while right:    # nửa sau đã đảo có thể ngắn hơn 1 node
            if left.val != right.val:
                return False
            left = left.next
            right = right.next
        return True

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


7.7 Add Two Numbers (LC 2)

Đề bài

Cho 2 linked list đại diện 2 số nguyên không âm, digits lưu ngược (digit hàng đơn vị ở head). Trả về linked list = tổng 2 số (cũng theo dạng ngược).

Ví dụ

Input:  l1 = 2 → 4 → 3   (đại diện 342)
        l2 = 5 → 6 → 4   (đại diện 465)
Output: 7 → 0 → 8         (đại diện 807 = 342 + 465)

Input:  l1 = 9 → 9 → 9 → 9 → 9 → 9 → 9
        l2 = 9 → 9 → 9 → 9
Output: 8 → 9 → 9 → 9 → 0 → 0 → 0 → 1

Ràng buộc

Clarifying questions

Hướng tiếp cận

Mô phỏng cộng “tay”: đi đồng thời 2 list, giữ biến carry. Mỗi vòng: - total = (l1.val if l1 else 0) + (l2.val if l2 else 0) + carry - digit = total % 10, carry = total // 10. - Push digit vào kết quả; dịch l1, l2.

Vòng lặp dừng khi cả 2 cạn carry == 0.

Code Python 3

class Solution:
    def addTwoNumbers(self, l1: ListNode | None, l2: ListNode | None) -> ListNode | None:
        dummy = ListNode()
        tail = dummy
        carry = 0
        while l1 or l2 or carry:
            total = (l1.val if l1 else 0) + (l2.val if l2 else 0) + carry
            carry, digit = divmod(total, 10)
            tail.next = ListNode(digit)
            tail = tail.next
            if l1: l1 = l1.next
            if l2: l2 = l2.next
        return dummy.next

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


7.8 Copy List with Random Pointer (LC 138)

Đề bài

Cho linked list mà mỗi node ngoài next còn có random — trỏ tới node bất kỳ trong list (hoặc None). Hãy deep copy danh sách (mỗi node mới là 1 instance riêng, các random trỏ đúng vào node mới tương ứng).

Ví dụ

Input (LC-style):
  head = [[7, null], [13, 0], [11, 4], [10, 2], [1, 0]]
  (mỗi phần tử [val, random_index]; random_index là chỉ số 0-based của node
   mà `random` trỏ tới, hoặc null nếu `random = None`)

  Tương ứng linked list:
        node 0      node 1      node 2      node 3      node 4
        val=7   →   val=13  →   val=11  →   val=10  →   val=1  →  None
        random:                                                    (next pointers)
          [0] → None
          [1] → node 0  (val 7)
          [2] → node 4  (val 1)
          [3] → node 2  (val 11)
          [4] → node 0  (val 7)

Output: deep copy của list trên — cùng val và cùng cấu trúc random,
        nhưng MỌI node là instance MỚI (không chia sẻ với input).

Output ở dạng LC array:
  [[7, null], [13, 0], [11, 4], [10, 2], [1, 0]]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cách 1 — Hash map “old → new”, O(n) time, O(n) space.

Lượt 1: tạo các node mới, lưu old_to_new[old] = new. Lượt 2: với mỗi old, gán new.next = old_to_new[old.next]new.random = old_to_new[old.random].

Cách 2 — Interweave, O(n) time, O(1) extra space.

Trick rất nổi tiếng: 1. Lượt 1: chèn mỗi node copy ngay sau node gốc: A → A' → B → B' → C → C'. 2. Lượt 2: với mỗi node gốc A: gán A'.random = A.random.next (vì A.random.next chính là copy của A.random). 3. Lượt 3: tách 2 list ra.

Hình minh hoạ — Cách 2 với 3 node A, B, C:

Lượt 1 (chèn copy):
  A → A' → B → B' → C → C'

Lượt 2 (gán random):
  Giả sử A.random = C
  → A'.random = A.random.next = C.next = C'   (copy của C)  ✓

Lượt 3 (tách):
  Original:  A → B → C
  Copy:      A' → B' → C'

Code Python 3

class Node:
    def __init__(self, val: int = 0, next=None, random=None):
        self.val = val
        self.next = next
        self.random = random


class Solution:
    """Cách 1 — Hash map. Code dễ debug nhất."""

    def copyRandomList(self, head: "Node | None") -> "Node | None":
        if not head:
            return None
        old_to_new: dict[Node, Node] = {}
        # Lượt 1: tạo node copy.
        cur = head
        while cur:
            old_to_new[cur] = Node(cur.val)
            cur = cur.next
        # Lượt 2: gán next/random.
        cur = head
        while cur:
            old_to_new[cur].next = old_to_new.get(cur.next)
            old_to_new[cur].random = old_to_new.get(cur.random)
            cur = cur.next
        return old_to_new[head]


class SolutionInterleave:
    """Cách 2 — Interweave, O(1) extra space."""

    def copyRandomList(self, head: "Node | None") -> "Node | None":
        if not head:
            return None
        # 1. Chèn copy ngay sau mỗi node gốc.
        cur = head
        while cur:
            cur.next = Node(cur.val, cur.next)
            cur = cur.next.next
        # 2. Gán random cho copy.
        cur = head
        while cur:
            if cur.random:
                cur.next.random = cur.random.next
            cur = cur.next.next
        # 3. Tách 2 list.
        new_head = head.next
        cur, copy = head, new_head
        while cur:
            cur.next = copy.next
            cur = cur.next
            copy.next = cur.next if cur else None
            copy = copy.next
        return new_head

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


7.9 Reverse Nodes in k-Group (LC 25)

Đề bài

Cho head và số k. Đảo ngược từng nhóm k node liên tiếp trong list. Nếu số node còn lại không đủ k thì giữ nguyên. Yêu cầu: O(1) extra space.

Ví dụ

Input:  1 → 2 → 3 → 4 → 5, k = 2
Output: 2 → 1 → 4 → 3 → 5    (nhóm cuối chỉ có 1 node → giữ)

Input:  1 → 2 → 3 → 4 → 5, k = 3
Output: 3 → 2 → 1 → 4 → 5

Ràng buộc

Clarifying questions

Hướng tiếp cận

Quy trình: 1. Đi k bước để xác định đuôi nhóm. Nếu không đủ k → break. 2. Đảo nhóm trong khoảng [head_nhóm, đuôi_nhóm]. 3. Khâu đầu nhóm đã đảo vào prev_group_tail, đuôi nhóm đã đảo trỏ tới next_group_head. 4. Cập nhật prev_group_tail để tiếp tục nhóm sau.

Hình minh hoạ với 1 → 2 → 3 → 4 → 5, k = 2:

Bắt đầu:    dummy → 1 → 2 → 3 → 4 → 5 → None
            prev

Nhóm 1: [1, 2]. Đảo → [2, 1].
            dummy → 2 → 1 → 3 → 4 → 5 → None
                        ↑
                       prev (= 1, tail của nhóm vừa đảo)

Nhóm 2: [3, 4]. Đảo → [4, 3].
            dummy → 2 → 1 → 4 → 3 → 5 → None
                                ↑
                               prev (= 3)

Nhóm 3: [5]. Chỉ 1 node → không đủ k=2 → giữ.
            dummy → 2 → 1 → 4 → 3 → 5 → None   ✓

Code Python 3

class Solution:
    def reverseKGroup(self, head: ListNode | None, k: int) -> ListNode | None:
        dummy = ListNode(0, head)
        prev_group_tail = dummy

        while True:
            # 1. Tìm đuôi nhóm — đi k bước.
            kth = prev_group_tail
            for _ in range(k):
                kth = kth.next
                if not kth:
                    return dummy.next      # không đủ k → giữ nguyên

            group_next = kth.next
            # 2. Đảo từ prev_group_tail.next đến kth.
            prev, curr = group_next, prev_group_tail.next
            while curr is not group_next:
                curr.next, prev, curr = prev, curr, curr.next
            # 3. Khâu nhóm đã đảo vào.
            old_head = prev_group_tail.next
            prev_group_tail.next = kth
            prev_group_tail = old_head     # bây giờ là đuôi của nhóm đã đảo

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


7.10 Sort List (LC 148)

Đề bài

Sort linked list tăng dần với O(n log n) time và O(1) extra space (theo follow-up — chỉ tính call stack/aux, không tính node).

Ví dụ

Input:  head = 4 → 2 → 1 → 3   (singly linked list)
Output: 1 → 2 → 3 → 4

Ràng buộc

Clarifying questions

Hướng tiếp cận

O(n log n) ⇒ quicksort, mergesort, heapsort. Quicksort khó áp cho LL, heapsort cần O(n) extra. Mergesort là tự nhiên nhất:

  1. Split list thành 2 nửa (dùng slow/fast tìm middle, ngắt slow.next).
  2. Recurse trên cả 2 nửa.
  3. Merge 2 list đã sort (bài 7.2).

Top-down (đệ quy) đẹp về code nhưng tốn O(log n) stack. Bottom-up (iterative) đạt thật sự O(1) space — nhưng code phức tạp. Trong phỏng vấn, top-down là đủ tốt.

Hình minh hoạ với 4 → 2 → 1 → 3:

                   sort([4, 2, 1, 3])
                  /                  \
        sort([4, 2])             sort([1, 3])
         /        \                /        \
    [4]            [2]         [1]            [3]
       \          /                \          /
       merge → [2, 4]              merge → [1, 3]
                       \              /
                       merge → [1, 2, 3, 4]   ✓

Code Python 3

class Solution:
    def sortList(self, head: ListNode | None) -> ListNode | None:
        if not head or not head.next:
            return head

        # 1. Split: tìm middle, ngắt đôi.
        slow, fast = head, head.next
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
        mid = slow.next
        slow.next = None

        # 2. Recurse.
        left = self.sortList(head)
        right = self.sortList(mid)

        # 3. Merge.
        return self._merge(left, right)

    @staticmethod
    def _merge(l1: ListNode | None, l2: ListNode | None) -> ListNode | None:
        dummy = ListNode()
        tail = dummy
        while l1 and l2:
            if l1.val <= l2.val:
                tail.next, l1 = l1, l1.next
            else:
                tail.next, l2 = l2, l2.next
            tail = tail.next
        tail.next = l1 or l2
        return dummy.next

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


7.11 LRU Cache (LC 146) — recap

Bài này đã được giải đầy đủ ở Chương 6 — Hash Table (mục 6.6). Ở đây chúng ta chỉ tóm tắt nhanh pattern Doubly Linked List vốn là điểm nhấn của LL.

Đề bài

Thiết kế LRU Cache với get(key)put(key, value) đều O(1). Khi vượt capacity → xoá key ít dùng gần nhất.

Ràng buộc

Clarifying questions

Hướng tiếp cận: Doubly Linked List + Hash Map

LRU yêu cầu O(1) cho cả: - Lookup theo key → hash map. - Di chuyển 1 phần tử bất kỳ về đầu → doubly linked list (chỉ DLL mới cho phép unlink O(1) khi có reference).

Hình minh hoạ DLL state khi LRU eviction

capacity = 2
Head sentinel — MRU end                LRU end — Tail sentinel
       │                                                  │
       ▼                                                  ▼
     ┌───┐    ┌──────┐    ┌──────┐    ┌──────┐    ┌───┐
     │ H │ ↔ │ K=4  │ ↔ │ K=3  │ ↔ │ K=1  │ ↔ │ T │
     └───┘    └──────┘    └──────┘    └──────┘    └───┘
                                          ▲
                          khi vượt capacity, xoá node này
                          (Tail.prev = LRU)

Hash map:  {1: node1, 3: node3, 4: node4}

Code Python 3 (pattern DLL)

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


# Hai sentinel head/tail giúp tránh hàng tá check None.
head, tail = Node(), Node()
head.next, tail.prev = tail, head

def _remove(node):              # O(1) — chỉ cần reference
    node.prev.next = node.next
    node.next.prev = node.prev

def _add_to_front(node):
    node.prev = head
    node.next = head.next
    head.next.prev = node
    head.next = node

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


7.12 Reorder List (LC 143)

Đề bài

Cho linked list L: L0 → L1 → ... → Ln-1. Hãy sắp xếp lại thành:

L0 → Ln-1 → L1 → Ln-2 → L2 → Ln-3 → ...

In-place, không được tạo node mới.

Ví dụ

Input:  head = 1 → 2 → 3 → 4
Output: 1 → 4 → 2 → 3 (mutate in-place)

Input:  head = 1 → 2 → 3 → 4 → 5
Output: 1 → 5 → 2 → 4 → 3   (mutate in-place)

Ràng buộc

Clarifying questions

Hướng tiếp cận

3 bước chuẩn — vẫn là pattern “split → reverse → merge”:

  1. Tìm middle (slow/fast).
  2. Đảo nửa sau (in-place).
  3. Merge xen kẽ nửa đầu và nửa sau đã đảo.

Hình minh hoạ với 1 → 2 → 3 → 4 → 5:

Bước 1: tìm middle (slow ở 3)
            1 → 2 → 3 → 4 → 5

Bước 2: cắt đôi và đảo nửa sau:
            1 → 2 → 3      5 → 4
            (nửa đầu)      (nửa sau đã đảo)

Bước 3: merge xen kẽ:
            Lấy 1 từ trái → 1
            Lấy 5 từ phải → 1, 5
            Lấy 2 từ trái → 1, 5, 2
            Lấy 4 từ phải → 1, 5, 2, 4
            Lấy 3 từ trái → 1, 5, 2, 4, 3

Output: 1 → 5 → 2 → 4 → 3  ✓

Code Python 3

class Solution:
    def reorderList(self, head: ListNode | None) -> None:
        if not head or not head.next:
            return

        # 1. Tìm middle.
        slow = fast = head
        while fast.next and fast.next.next:
            slow = slow.next
            fast = fast.next.next

        # 2. Đảo nửa sau (từ slow.next).
        second = slow.next
        slow.next = None      # cắt đôi
        prev, curr = None, second
        while curr:
            curr.next, prev, curr = prev, curr, curr.next
        second = prev          # head của nửa sau đã đảo

        # 3. Merge xen kẽ.
        first = head
        while second:
            tmp1, tmp2 = first.next, second.next
            first.next = second
            second.next = tmp1
            first, second = tmp1, tmp2

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Pointer safety checklist

  1. Trước khi cắt a.next = b, đã lưu a.next cũ chưa?
  2. dummy/sentinel trỏ vào head chưa? (Cần khi head có thể đổi.)
  3. Vòng lặp while cur and cur.next: chú ý điều kiện kép cho 2 nút cuối.
  4. Sau khi reverse hoặc split, tail cũ đã .next = None chưa? (Tránh cycle.)
  5. Edge cases: list rỗng (head = None), 1 phần tử, k > len.

Dummy/sentinel pattern (cốt lõi)

dummy = ListNode(0, head)
prev = dummy
# ... thao tác trên prev.next ...
return dummy.next

Dùng cho: Remove Nth From End, Merge Two Sorted, Partition, Odd Even, Reverse K-Group.

LRU (LC 146) bridge


Chương 8 — Queue + Stack

Stack (LIFO) và Queue (FIFO) là cặp đôi “ngược nhau”. Stack giải được mọi bài có cấu trúc lồng (parentheses, recursion, nested structure), Queue phục vụ duyệt theo lớp (BFS, sliding window). Chương này tập trung stack — bài queue thuần sẽ gặp lại ở Chương 10 (BFS). Cuối chương có 1 bài teaser về monotonic stack — pattern mạnh sẽ được khai thác sâu ở Chương 18.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Stack rất hợp khi: - Có cấu trúc lồng / balanced (ngoặc, tag HTML, nested expression). - Cần “undo” — bước trước phải xử lý xong sau bước hiện tại (DFS iterative). - Bài “next greater element”, “largest rectangle”, “daily temperatures” → monotonic stack (Chương 18).

Queue rất hợp khi: - Cần duyệt theo lớp / BFS (Chương 10). - Bài “sliding window max” → monotonic deque (Chương 18). - Producer-consumer, task scheduler.

Template code

from collections import deque
from typing import List

# 1) Stack với list (built-in trong Python, O(1) amortized).
stack: List[int] = []
stack.append(x)        # push
top = stack[-1]        # peek
val = stack.pop()      # pop

# 2) Queue với collections.deque — O(1) push/pop ở cả 2 đầu.
queue: deque[int] = deque()
queue.append(x)        # enqueue (đẩy vào cuối)
val = queue.popleft()  # dequeue (lấy đầu)

# 3) Stack lưu (index, value) — pattern monotonic.
stack: list[tuple[int, int]] = []        # (index, value)
for i, v in enumerate(arr):
    while stack and stack[-1][1] < v:
        idx, _ = stack.pop()
        # ... xử lý idx ...
    stack.append((i, v))

Bài tự luyện cuối chương


8.1 Valid Parentheses (LC 20)

Đề bài

Cho chuỗi s chỉ chứa ()[]{}. Trả về True nếu chuỗi hợp lệ: - Mỗi dấu ngoặc mở có dấu đóng tương ứng. - Các dấu ngoặc đóng đúng thứ tự (LIFO).

Ví dụ

Input:  s = "()"            → Output: True
Input:  s = "()[]{}"        → Output: True
Input:  s = "(]"            → Output: False
Input:  s = "([)]"          → Output: False    (lồng sai)
Input:  s = "{[]}"          → Output: True

Ràng buộc

Clarifying questions

Hướng tiếp cận

Stack pattern kinh điển. Duyệt từng ký tự: - Nếu là ngoặc mở → push. - Nếu là ngoặc đóng → check stack top có phải cặp tương ứng không. Nếu không, hoặc stack rỗng → return False. Nếu có → pop.

Cuối cùng stack phải rỗng (mọi ngoặc đã match).

Hình minh hoạ với s = "{[()]}":

ch   action          stack sau action
─────────────────────────────────────
{    push            ['{']
[    push            ['{', '[']
(    push            ['{', '[', '(']
)    pop, match (    ['{', '[']
]    pop, match [    ['{']
}    pop, match {    []

Stack rỗng → True ✓

Code Python 3

class Solution:
    def isValid(self, s: str) -> bool:
        pairs = {')': '(', ']': '[', '}': '{'}
        stack: list[str] = []
        for ch in s:
            if ch in pairs:                # ngoặc đóng
                if not stack or stack.pop() != pairs[ch]:
                    return False
            else:                          # ngoặc mở
                stack.append(ch)
        return not stack

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


8.2 Min Stack (LC 155)

Đề bài

Thiết kế stack hỗ trợ 4 thao tác đều O(1): - push(x) - pop() - top() — peek - getMin() — trả về min hiện có trong stack

Ví dụ

Input (LC-style operation arrays):
  ops  = ["MinStack","push","push","push","getMin","pop","top","getMin"]
  args = [[],        [-2],   [0],  [-3],  [],     [],   [],   []]

Output: [null, null, null, null, -3, null, 0, -2]

Giải thích từng bước:
  MinStack()  → khởi tạo
  push(-2);  push(0);  push(-3)
  getMin()   → -3
  pop()      → bỏ -3
  top()      → 0
  getMin()   → -2

Ràng buộc

Clarifying questions

Hướng tiếp cận

Vấn đề: getMin() O(1) ⇒ phải lưu min đâu đó. Nhưng khi pop min ra, phải biết min mới — đây là cốt lõi.

Cách 1 — Aux stack song song giữ min_so_far.

Stack phụ mins mỗi ô = min của tất cả các phần tử dưới (kể cả nó) trong main stack. Khi push, push min(x, mins[-1]). Khi pop, pop cả 2 stack.

Cách 2 — 1 stack lưu (value, current_min).

Tương đương cách 1 nhưng gộp thành tuple. Cùng overhead.

Hình minh hoạ — Cách 1 với push(-2), push(0), push(-3), pop():

push(-2):  stack=[-2]      mins=[-2]
push(0):   stack=[-2, 0]   mins=[-2, -2]    (min(0, -2)=-2)
push(-3):  stack=[-2,0,-3] mins=[-2,-2,-3]   (min(-3, -2)=-3)

getMin() → mins[-1] = -3   ✓
pop():    stack=[-2, 0]    mins=[-2, -2]
getMin() → mins[-1] = -2   ✓

Code Python 3

class MinStack:
    def __init__(self):
        self.stack: list[int] = []
        self.mins: list[int] = []

    def push(self, val: int) -> None:
        self.stack.append(val)
        cur_min = val if not self.mins else min(val, self.mins[-1])
        self.mins.append(cur_min)

    def pop(self) -> None:
        self.stack.pop()
        self.mins.pop()

    def top(self) -> int:
        return self.stack[-1]

    def getMin(self) -> int:
        return self.mins[-1]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


8.3 Implement Queue using Stacks (LC 232)

Đề bài

Thiết kế Queue (FIFO) chỉ dùng 2 stack. Hỗ trợ push, pop, peek, empty.

Ví dụ

Input (LC-style operation arrays):
  ops  = ["MyQueue","push","push","peek","pop","empty"]
  args = [[],       [1],   [2],   [],    [],   []]

Output: [null, null, null, 1, 1, false]

Giải thích từng bước:
  MyQueue()  → khởi tạo
  push(1);  push(2)
  peek()    → 1   (FIFO: phần tử đẩy vào trước ra trước)
  pop()     → 1
  empty()   → false

Ràng buộc

Clarifying questions

Hướng tiếp cận

Ý tưởng — 2 stack: in (input) và out (output). - push(x): push vào in. - pop / peek: nếu out rỗng → “đổ” toàn bộ in sang out (đảo thứ tự vì stack). Sau đó pop/peek từ out.

Phân tích amortized: Mỗi phần tử bị move giữa 2 stack tối đa 1 lần. Worst-case 1 op = O(n), nhưng amortized = O(1).

Hình minh hoạ với push(1), push(2), push(3), pop(), push(4), pop():

push(1): in=[1]              out=[]
push(2): in=[1, 2]           out=[]
push(3): in=[1, 2, 3]        out=[]

pop(): out rỗng → đổ in sang out
       in=[]                  out=[3, 2, 1]    (1 ở top)
       out.pop() → 1
       in=[]                  out=[3, 2]

push(4): in=[4]              out=[3, 2]

pop(): out không rỗng → out.pop() → 2
       in=[4]                out=[3]

→ FIFO: 1 ra trước (đúng thứ tự push)

Code Python 3

class MyQueue:
    def __init__(self):
        self.in_st: list[int] = []
        self.out_st: list[int] = []

    def push(self, x: int) -> None:
        self.in_st.append(x)

    def pop(self) -> int:
        self._shift()
        return self.out_st.pop()

    def peek(self) -> int:
        self._shift()
        return self.out_st[-1]

    def empty(self) -> bool:
        return not self.in_st and not self.out_st

    def _shift(self) -> None:
        """Khi out rỗng, đổ toàn bộ in sang out."""
        if not self.out_st:
            while self.in_st:
                self.out_st.append(self.in_st.pop())

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


8.4 Evaluate Reverse Polish Notation (LC 150)

Đề bài

Cho mảng tokens biểu diễn biểu thức Reverse Polish Notation (postfix). Mỗi token là số nguyên hoặc 1 trong 4 phép + - * /. Trả về kết quả (chia lấy phần nguyên hướng về 0).

Ví dụ

Input:  tokens = ["2","1","+","3","*"]
Output: 9
Giải thích: (2 + 1) * 3 = 9.

Input:  tokens = ["4","13","5","/","+"]
Output: 6
Giải thích: 4 + (13 / 5) = 4 + 2 = 6.

Input:  tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]
Output: 22

Ràng buộc

Clarifying questions

Hướng tiếp cận

RPN ↔︎ stack — kinh điển. Duyệt: - Số → push. - Phép → pop 2 phần tử (b lấy trước, a lấy sau), tính a op b, push kết quả.

Cuối cùng stack còn 1 phần tử = kết quả.

Hình minh hoạ với ["2","1","+","3","*"]:

token   action                      stack
─────────────────────────────────────────────
"2"     push 2                      [2]
"1"     push 1                      [2, 1]
"+"     pop b=1, a=2; push 3        [3]
"3"     push 3                      [3, 3]
"*"     pop b=3, a=3; push 9        [9]

Kết quả: 9

Code Python 3

from typing import List
import operator

class Solution:
    OPS = {
        '+': operator.add,
        '-': operator.sub,
        '*': operator.mul,
        '/': lambda a, b: int(a / b),   # chia hướng về 0
    }

    def evalRPN(self, tokens: List[str]) -> int:
        stack: list[int] = []
        for tk in tokens:
            if tk in self.OPS:
                b = stack.pop()
                a = stack.pop()
                stack.append(self.OPS[tk](a, b))
            else:
                stack.append(int(tk))
        return stack[0]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


8.5 Daily Temperatures (LC 739) — teaser monotonic stack

Đề bài

Cho mảng temperatures các nhiệt độ. Với mỗi ngày i, tìm xem bao nhiêu ngày sau đó mới có nhiệt độ cao hơn ngày i. Nếu không có, trả 0.

Ví dụ

Input:  temperatures = [73, 74, 75, 71, 69, 72, 76, 73]
Output: [1, 1, 4, 2, 1, 1, 0, 0]
Giải thích:
  day 0: 73 → ngày 1 (74) cao hơn → 1
  day 2: 75 → phải đợi tới ngày 6 (76) → 6-2=4
  day 6: 76 không có gì cao hơn → 0

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — O(n²). Với mỗi i, quét forward tìm cái lớn hơn. TLE khi n = 10^5.

Tối ưu — Monotonic decreasing stack — O(n).

Ý tưởng: Duy trì stack các index mà temperature giảm dần (từ đáy lên đỉnh). Khi gặp ngày i có nhiệt cao hơn top của stack → đó chính là “answer” cho ngày ở top. Pop và ghi result[top] = i - top.

Hình minh hoạ với [73, 74, 75, 71, 69, 72, 76, 73]:

i  temp   action                                stack (idx)    result
─────────────────────────────────────────────────────────────────────────
0  73     push 0                                [0]            [_, _, _, _, _, _, _, _]
1  74     74 > 73 → pop 0, result[0]=1-0=1     [1]            [1, _, _, _, _, _, _, _]
          push 1
2  75     75 > 74 → pop 1, result[1]=2-1=1     [2]            [1, 1, _, _, _, _, _, _]
          push 2
3  71     71 < 75 → push 3                     [2, 3]
4  69     69 < 71 → push 4                     [2, 3, 4]
5  72     72 > 69 → pop 4, result[4]=5-4=1     [2, 3, 5]
          72 > 71 → pop 3, result[3]=5-3=2
          push 5
6  76     76 > 72 → pop 5, result[5]=6-5=1     [6]
          76 > 75 → pop 2, result[2]=6-2=4
          push 6
7  73     73 < 76 → push 7                     [6, 7]

Còn lại trong stack: [6, 7] → result giữ 0.
Kết quả: [1, 1, 4, 2, 1, 1, 0, 0]  ✓

Stack invariant: index trong stack có temperature giảm dần từ đáy lên đỉnh. Mỗi index được push 1 lần, pop tối đa 1 lần → tổng O(n).

Code Python 3

from typing import List

class Solution:
    def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
        n = len(temperatures)
        result = [0] * n
        stack: list[int] = []   # các index có temperature giảm dần
        for i, t in enumerate(temperatures):
            while stack and temperatures[stack[-1]] < t:
                j = stack.pop()
                result[j] = i - j
            stack.append(i)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


8.6 Decode String (LC 394)

Đề bài

Cho chuỗi s mã hoá theo dạng k[encoded_string] — nghĩa là encoded_string sẽ được lặp k lần. Decode chuỗi.

Ví dụ

Input:  s = "3[a]2[bc]"
Output: "aaabcbc"

Input:  s = "3[a2[c]]"
Output: "accaccacc"   (lồng)

Input:  s = "2[abc]3[cd]ef"
Output: "abcabccdcdcdef"

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cấu trúc lồng ⇒ dùng stack. Mỗi khi gặp [, ta “lưu” số k và chuỗi đang build vào stack, rồi reset chuỗi build. Khi gặp ], pop ra (prev_str, k) và nối prev_str + k * cur_str.

Cách 2 — Đệ quy. Mỗi k[...] thành 1 lần gọi đệ quy. Code gọn hơn nhưng tốn stack (đệ quy có thể vượt giới hạn với chuỗi lồng sâu).

Hình minh hoạ — Cách stack với "3[a2[c]]":

ch    action                                     stack         cur
─────────────────────────────────────────────────────────────────
'3'   k = 3                                      []           ""
'['   push (k, cur); reset k, cur                [(3, "")]    ""
'a'   cur += 'a'                                 [(3, "")]    "a"
'2'   k = 2                                      [(3, "")]    "a"
'['   push (k=2, cur="a"); reset k, cur          [(3, ""), (2, "a")]  ""
'c'   cur += 'c'                                 [(3, ""), (2, "a")]  "c"
']'   (prev_k=2, prev_str="a") = pop;
      cur = prev_str + prev_k * cur = "a" + "cc" [(3, "")]    "acc"
']'   (prev_k=3, prev_str="") = pop;
      cur = "" + 3 * "acc" = "accaccacc"         []           "accaccacc"

Kết quả: "accaccacc"  ✓

Code Python 3

class Solution:
    def decodeString(self, s: str) -> str:
        stack: list[tuple[str, int]] = []   # (prev_str, prev_k)
        cur, k = "", 0
        for ch in s:
            if ch.isdigit():
                k = k * 10 + int(ch)
            elif ch == '[':
                stack.append((cur, k))
                cur, k = "", 0
            elif ch == ']':
                prev_str, prev_k = stack.pop()
                cur = prev_str + cur * prev_k
            else:
                cur += ch
        return cur


class SolutionRecursive:
    """Cách 2 — đệ quy. Code clean nhưng tốn stack."""

    def decodeString(self, s: str) -> str:
        self.i = 0
        return self._decode(s)

    def _decode(self, s: str) -> str:
        result = ""
        k = 0
        while self.i < len(s) and s[self.i] != ']':
            ch = s[self.i]
            if ch.isdigit():
                k = k * 10 + int(ch)
                self.i += 1
            elif ch == '[':
                self.i += 1
                inner = self._decode(s)
                result += k * inner
                k = 0
                self.i += 1     # skip ']'
            else:
                result += ch
                self.i += 1
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Stack mental models

Mục đích Stack chứa gì Bài ví dụ
Match cặp đối xứng Mở ngoặc / token chờ đóng LC 20, 1249
Lưu token trước đó chưa hoàn tất Số / chuỗi cần “expand” sau LC 394 Decode
Undo / context Phép tính cha LC 224 Calculator
Monotonic Index/value tăng/giảm Chương 18
Iterative DFS Frame call Tree iterative inorder

Decode String (LC 394) — trace 3[a2[c]]

Bước Char num cur numStack strStack
0 3 3 "" [] []
1 [ 0 "" [3] [""]
2 a 0 "a" [3] [""]
3 2 2 "a" [3] [""]
4 [ 0 "" [3,2] ["", "a"]
5 c 0 "c" [3,2] ["", "a"]
6 ] 0 "acc" (a + c×2) [3] [""]
7 ] 0 "accaccacc" (×3) [] []

RPN (LC 150) — thứ tự operand

Pop b trước, a sau, tính a op b. Nhầm thứ tự là bug điển hình với -/.

Min Stack so sánh


Chương 9 — Graph

Graph là cấu trúc dữ liệu trừu tượng nhất nhưng cũng phổ biến nhất trong đời thực: bạn bè, đường đi, dependency, … Chương này giới thiệu biểu diễn graph và 6 bài phổ thông về graph. Hai kỹ thuật duyệt BFS, DFS sẽ được đào sâu ở Chương 10 và 11; chương này dùng cả 2 ở mức cơ bản để bạn làm quen.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

3 câu hỏi trước khi code: 1. Hướng / vô hướng? Directed cần cân nhắc thêm chu trình. 2. Có trọng số? Nếu có → cân nhắc Dijkstra (Chương 30), không thì BFS/DFS đủ. 3. Đặc tính đặc biệt? DAG → topo sort, bipartite, planar, …

Biểu diễn graph

from collections import defaultdict
from typing import List

# 1) Adjacency List — hầu hết bài dùng cái này
graph: dict[int, list[int]] = defaultdict(list)
for u, v in edges:
    graph[u].append(v)
    graph[v].append(u)    # bỏ dòng này nếu directed

# 2) Edge List — input "raw"
edges: list[tuple[int, int]] = [(0, 1), (1, 2), ...]

# 3) Adjacency Matrix — chỉ khi V nhỏ (≤ 1000) và mật độ cao
adj = [[0] * n for _ in range(n)]
for u, v in edges:
    adj[u][v] = 1

Khi nào dùng cái nào?

Biểu diễn Lookup (u, v) Duyệt láng giềng u Bộ nhớ
Adj list O(deg(u)) O(deg(u)) O(V + E)
Edge list O(E) O(E) O(E)
Adj matrix O(1) O(V) O(V²)

Mặc định dùng adjacency list. Chỉ chuyển sang matrix khi cần kiểm tra edge O(1)V nhỏ.

Template code

from collections import defaultdict, deque

# DFS đệ quy
def dfs(node: int, visited: set[int], graph: dict) -> None:
    if node in visited:
        return
    visited.add(node)
    for neighbor in graph[node]:
        dfs(neighbor, visited, graph)

# DFS iterative bằng stack
def dfs_iter(start: int, graph: dict) -> set[int]:
    visited = set()
    stack = [start]
    while stack:
        node = stack.pop()
        if node in visited:
            continue
        visited.add(node)
        for nb in graph[node]:
            if nb not in visited:
                stack.append(nb)
    return visited

# BFS bằng queue
def bfs(start: int, graph: dict) -> set[int]:
    visited = {start}
    queue = deque([start])
    while queue:
        node = queue.popleft()
        for nb in graph[node]:
            if nb not in visited:
                visited.add(nb)
                queue.append(nb)
    return visited

Bài tự luyện cuối chương


9.1 Find if Path Exists in Graph (LC 1971)

Đề bài

Cho n đỉnh đánh số 0..n-1 và mảng cạnh vô hướng edges[i] = [u, v]. Cho sourcedestination. Trả về True nếu có đường đi giữa chúng.

Ví dụ

Input:  n = 3, edges = [[0,1],[1,2],[2,0]], source = 0, destination = 2
Output: True

Input:  n = 6, edges = [[0,1],[0,2],[3,5],[5,4],[4,3]], source = 0, destination = 5
Output: False

Ràng buộc

Clarifying questions

Hướng tiếp cận

3 cách kinh điển, đều O(V + E): 1. BFS — duyệt theo lớp, return ngay khi gặp destination. 2. DFS — đệ quy hoặc stack. 3. Union Find (Chương 24) — gộp 2 đỉnh thành 1 root nếu có cạnh; check find(source) == find(destination). Đẹp khi đề bài có nhiều query “có đường đi giữa u, v?”.

Code Python 3

from collections import defaultdict, deque
from typing import List

class Solution:
    """BFS — sạch nhất cho 1 query."""

    def validPath(self, n: int, edges: List[List[int]],
                  source: int, destination: int) -> bool:
        if source == destination:
            return True
        graph = defaultdict(list)
        for u, v in edges:
            graph[u].append(v)
            graph[v].append(u)

        visited = {source}
        queue = deque([source])
        while queue:
            node = queue.popleft()
            for nb in graph[node]:
                if nb == destination:
                    return True
                if nb not in visited:
                    visited.add(nb)
                    queue.append(nb)
        return False

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


9.2 Clone Graph (LC 133)

Đề bài

Cho node của một undirected connected graph. Mỗi Nodeval: intneighbors: list[Node]. Hãy deep copy graph: tạo bản sao mà mỗi node mới là instance riêng, các neighbors trỏ vào node mới tương ứng.

Ví dụ

Input:  adjList = [[2,4], [1,3], [2,4], [1,3]]
        (adjList[i-1] = danh sách neighbor của node i, node value 1-indexed
         giống LC; tham số API thực chất là Node = adjList[0] = node 1)

        Tương ứng graph:
            1 ── 2
            │    │
            4 ── 3

Output: [[2,4], [1,3], [2,4], [1,3]]
        (cùng cấu trúc adjacency, nhưng MỌI Node trong output là instance MỚI;
         không có Node nào dùng chung với input)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Pattern y hệt LC 138 (Copy List with Random Pointer, bài 7.8): dùng hash map original_to_copy để tránh tạo trùng và xử lý chu trình.

BFS / DFS đều OK — quan trọng là check visited qua dict.

Hình minh hoạ với graph 1—2—3—4—1:

Bắt đầu: visit 1.
  cloned = {1: Node(1)}
  queue = [1]

Pop 1:  hàng xóm = [2, 4]
  Tạo Node(2), Node(4); thêm vào cloned.
  cloned[1].neighbors = [cloned[2], cloned[4]]
  queue = [2, 4]

Pop 2:  hàng xóm = [1, 3]
  cloned[1] đã có; tạo Node(3).
  cloned[2].neighbors = [cloned[1], cloned[3]]
  queue = [4, 3]

Pop 4:  hàng xóm = [1, 3]
  cả 2 đã có trong cloned.
  cloned[4].neighbors = [cloned[1], cloned[3]]

Pop 3:  hàng xóm = [2, 4]
  cả 2 đã có.
  cloned[3].neighbors = [cloned[2], cloned[4]]

→ Trả cloned[1] làm head của bản copy.

Code Python 3

from collections import deque

class Node:
    def __init__(self, val: int = 0, neighbors: "list[Node] | None" = None):
        self.val = val
        self.neighbors = neighbors if neighbors is not None else []


class Solution:
    """BFS với hash map."""

    def cloneGraph(self, node: "Node | None") -> "Node | None":
        if not node:
            return None

        cloned: dict["Node", "Node"] = {node: Node(node.val)}
        queue: deque["Node"] = deque([node])

        while queue:
            cur = queue.popleft()
            for nb in cur.neighbors:
                if nb not in cloned:
                    cloned[nb] = Node(nb.val)
                    queue.append(nb)
                cloned[cur].neighbors.append(cloned[nb])

        return cloned[node]


class SolutionDFS:
    """DFS đệ quy — code ngắn hơn."""

    def cloneGraph(self, node: "Node | None") -> "Node | None":
        cloned: dict["Node", "Node"] = {}

        def dfs(cur: "Node") -> "Node":
            if cur in cloned:
                return cloned[cur]
            copy = Node(cur.val)
            cloned[cur] = copy           # phải set TRƯỚC khi đệ quy neighbors
            copy.neighbors = [dfs(nb) for nb in cur.neighbors]
            return copy

        return dfs(node) if node else None

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


9.3 Number of Connected Components (LC 323)

Đề bài

Cho n đỉnh đánh số 0..n-1 và mảng cạnh vô hướng. Đếm số thành phần liên thông.

Ví dụ

Input:  n = 5, edges = [[0,1],[1,2],[3,4]]
Output: 2
Giải thích: 2 component {0,1,2} và {3,4}.

Input:  n = 5, edges = [[0,1],[1,2],[2,3],[3,4]]
Output: 1

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cách 1 — Duyệt DFS/BFS từng đỉnh chưa thăm — O(V + E). Cho mỗi đỉnh chưa thăm, tăng counter và DFS/BFS đánh dấu cả component.

Cách 2 — Union Find — O((V + E) · α(V)). Gom các đỉnh có cạnh thành cùng root. Đếm số root khác nhau.

Code Python 3

from collections import defaultdict
from typing import List

class Solution:
    def countComponents(self, n: int, edges: List[List[int]]) -> int:
        graph = defaultdict(list)
        for u, v in edges:
            graph[u].append(v)
            graph[v].append(u)

        visited = [False] * n
        count = 0

        def dfs(node: int) -> None:
            visited[node] = True
            for nb in graph[node]:
                if not visited[nb]:
                    dfs(nb)

        for i in range(n):
            if not visited[i]:
                count += 1
                dfs(i)
        return count


class SolutionUF:
    """Union Find — đẹp khi đề bài có nhiều query."""

    def countComponents(self, n: int, edges: List[List[int]]) -> int:
        parent = list(range(n))

        def find(x: int) -> int:
            while parent[x] != x:
                parent[x] = parent[parent[x]]   # path compression
                x = parent[x]
            return x

        def union(x: int, y: int) -> bool:
            px, py = find(x), find(y)
            if px == py:
                return False
            parent[px] = py
            return True

        components = n
        for u, v in edges:
            if union(u, v):
                components -= 1
        return components

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


9.4 Course Schedule (LC 207)

Đề bài

numCourses khoá học đánh số 0..numCourses-1. prerequisites[i] = [a, b] nghĩa là muốn học khoá a thì phải hoàn thành khoá b trước. Trả về True nếu có thể học hết tất cả khoá, ngược lại False.

Ví dụ

Input:  numCourses = 2, prerequisites = [[1, 0]]
Output: True

Input:  numCourses = 2, prerequisites = [[1, 0], [0, 1]]
Output: False    (vòng tròn: 1 cần 0, 0 cần 1)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Phát biểu lại: Tạo directed graph b → a (b phải xong trước a). Câu hỏi: graph có chu trình không? Nếu không có chu trình → có thể học hết.

Cách 1 — DFS với 3 trạng thái (white / gray / black).

Nếu DFS gặp gray → tìm thấy back-edge → có chu trình.

Cách 2 — Topological Sort BFS (Kahn’s algorithm).

Đếm indegree mỗi node. Đẩy các node indegree == 0 vào queue. Pop và giảm indegree của các neighbors. Nếu cuối cùng đếm được numCourses node → DAG; ngược lại có chu trình.

Topological sort là cốt lõi của Chương 13. Ở đây giới thiệu sớm vì Course Schedule là kinh điển ứng dụng nó.

Hình minh hoạ — DFS 3 màu với cycle 0 → 1 → 0:

Bắt đầu: tất cả white.
DFS(0):
  mark 0 = gray.
  visit 1 (white):
    DFS(1):
      mark 1 = gray.
      visit 0 (gray!) → back-edge phát hiện → return False (có chu trình)

Code Python 3

from collections import defaultdict, deque
from typing import List

class Solution:
    """DFS 3-color."""

    WHITE, GRAY, BLACK = 0, 1, 2

    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        graph = defaultdict(list)
        for a, b in prerequisites:
            graph[b].append(a)            # b → a

        color = [self.WHITE] * numCourses

        def has_cycle(node: int) -> bool:
            if color[node] == self.GRAY:
                return True               # back-edge
            if color[node] == self.BLACK:
                return False              # đã xong, không có cycle từ đây
            color[node] = self.GRAY
            for nb in graph[node]:
                if has_cycle(nb):
                    return True
            color[node] = self.BLACK
            return False

        for i in range(numCourses):
            if has_cycle(i):
                return False
        return True


class SolutionKahn:
    """BFS topo sort (Kahn's algorithm)."""

    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        graph = defaultdict(list)
        indeg = [0] * numCourses
        for a, b in prerequisites:
            graph[b].append(a)
            indeg[a] += 1

        queue = deque(i for i, d in enumerate(indeg) if d == 0)
        processed = 0
        while queue:
            node = queue.popleft()
            processed += 1
            for nb in graph[node]:
                indeg[nb] -= 1
                if indeg[nb] == 0:
                    queue.append(nb)

        return processed == numCourses

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


9.5 Is Graph Bipartite? (LC 785)

Đề bài

Cho graph vô hướng dạng adjacency list graph[i] = [hàng xóm của i]. Trả về True nếu graph là bipartite — có thể chia các đỉnh thành 2 tập sao cho mọi cạnh nối 2 đỉnh ở khác tập.

Ví dụ

Input:  graph = [[1,2,3],[0,2],[0,1,3],[0,2]]
Output: False
Giải thích: 0—1—2—0 là tam giác → không thể 2-color.

Input:  graph = [[1,3],[0,2],[1,3],[0,2]]
Output: True
Giải thích: tập A = {0, 2}, tập B = {1, 3}.

Ràng buộc

Clarifying questions

Hướng tiếp cận

Equivalent: Graph bipartite ↔︎ tô được 2 màu sao cho không có 2 đỉnh kề nhau cùng màu.

BFS/DFS với 2-color: Bắt đầu mỗi component, tô đỉnh đầu màu 0. Khi BFS/DFS sang hàng xóm, tô màu ngược. Nếu gặp hàng xóm đã có màu giống → return False.

Hình minh hoạ — graph tam giác (không bipartite):

       0
      / \
     1───2

BFS từ 0:
  color[0] = 0.
  Sang 1: color[1] = 1 (ngược màu 0).
  Sang 2: color[2] = 1 (ngược màu 0).
  Từ 1, sang 2: color[2] đã là 1 == color[1] = 1 → MÂU THUẪN → False

Code Python 3

from collections import deque
from typing import List

class Solution:
    def isBipartite(self, graph: List[List[int]]) -> bool:
        n = len(graph)
        color = [-1] * n        # -1 = chưa tô

        for start in range(n):
            if color[start] != -1:
                continue
            # BFS component chứa start.
            color[start] = 0
            queue = deque([start])
            while queue:
                node = queue.popleft()
                for nb in graph[node]:
                    if color[nb] == -1:
                        color[nb] = 1 - color[node]
                        queue.append(nb)
                    elif color[nb] == color[node]:
                        return False
        return True

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


9.6 Evaluate Division (LC 399)

Đề bài

Cho mảng các đẳng thức equations[i] = [Ai, Bi] và mảng values[i] nghĩa là Ai / Bi = values[i]. Cho mảng query queries[j] = [Cj, Dj]. Hãy trả lời mỗi query với giá trị Cj / Dj, hoặc -1 nếu không xác định được.

Ví dụ

Input:  equations = [["a","b"], ["b","c"]]
        values    = [2.0, 3.0]
        queries   = [["a","c"], ["b","a"], ["a","e"], ["a","a"], ["x","x"]]
Output: [6.0, 0.5, -1.0, 1.0, -1.0]
Giải thích:
  a/c = a/b * b/c = 2 * 3 = 6
  b/a = 1/(a/b) = 0.5
  a/e không xác định (e không có trong equations)
  a/a = 1
  x/x: x không có trong equations → -1

Ràng buộc

Clarifying questions

Hướng tiếp cận

Insight: mỗi đẳng thức A / B = k ↔︎ trong directed graph có 2 cạnh: - A → B trọng số k. - B → A trọng số 1/k.

Khi đó C / D = tích trọng số dọc theo bất kỳ đường đi nào từ C đến D. Nếu không có đường đi → trả -1.

DFS trên graph trọng số là đủ. Tối ưu hơn: Union Find với trọng số (xem Chương 24).

Hình minh hoạ với equations a/b=2, b/c=3:

Graph trọng số:
            ── 2 ──>          ── 3 ──>
       a               b               c
            <─ 0.5 ──         <─ 1/3 ─

Query a/c: DFS từ a:
  a → b (× 2), tiếp b → c (× 3) → tổng 2 * 3 = 6.  ✓

Code Python 3

from collections import defaultdict
from typing import List

class Solution:
    def calcEquation(
        self, equations: List[List[str]],
        values: List[float], queries: List[List[str]]
    ) -> List[float]:
        graph: dict[str, dict[str, float]] = defaultdict(dict)
        for (a, b), v in zip(equations, values):
            graph[a][b] = v
            graph[b][a] = 1.0 / v

        def dfs(src: str, dst: str, visited: set[str]) -> float:
            if src not in graph or dst not in graph:
                return -1.0
            if src == dst:
                return 1.0
            visited.add(src)
            for nb, weight in graph[src].items():
                if nb in visited:
                    continue
                sub = dfs(nb, dst, visited)
                if sub != -1.0:
                    return weight * sub
            return -1.0

        return [dfs(c, d, set()) for c, d in queries]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Graph problem diagnosis

Triệu chứng đề bài Pattern phù hợp Chương
“Có đường từ A đến B?” BFS / DFS / Union Find 10, 11, 24
“Số cụm/đảo” DFS / Union Find 12, 24
“Thứ tự thực hiện với ràng buộc” Topological sort 13
“Shortest path trọng số dương Dijkstra (Chương 30)
“Shortest path 0/1 edges 0-1 BFS / BFS thường
“All-pairs shortest” Floyd-Warshall O(V³)
“Nhỏ nhất kết nối tất cả” MST (Chương 33)
“Bottleneck min/max trên path” Kruskal + DSU / BS + BFS (37)
“Bipartite?” BFS/DFS 2-color

Clone Graph (LC 133) — mapping diagram

old:  1 — 2
       \  /
        3 — 4

old_to_new = {1:1', 2:2', 3:3', 4:4'}   (dict cũ → bản sao)
clone(node):
    if node in old_to_new: return old_to_new[node]
    new = Node(node.val)
    old_to_new[node] = new       # ĐẶT TRƯỚC khi đệ quy → tránh vòng
    for nei in node.neighbors:
        new.neighbors.append(clone(nei))
    return new

Evaluate Division (LC 399) — weighted DFS

Course Schedule ở chương này = teaser

Bài đầy đủ Topological sort xem Chương 13. Chương này chỉ trình bày DFS detect cycle.


Chương 10 — Breadth-First Search (BFS)

BFS duyệt graph theo lớp (level by level). Đặc tính then chốt: nếu mọi cạnh có trọng số bằng nhau (= 1), BFS từ source cho ra đường đi ngắn nhất đến mọi đỉnh khác. Đây là lý do BFS xuất hiện rất nhiều ở các bài “shortest path in unweighted graph”, “minimum steps”, “minimum transformations”, …

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Phân biệt với DFS: - BFS: tìm path ngắn nhất, duyệt theo lớp. - DFS: thám hiểm sâu (path đầu tiên đến đích), check connectivity, đếm components.

Template code

from collections import deque

# 1) BFS chuẩn — đường đi ngắn nhất từ start tới target
def bfs_shortest(start, target, neighbors_fn) -> int:
    if start == target:
        return 0
    visited = {start}
    queue = deque([(start, 0)])         # (node, distance)
    while queue:
        node, dist = queue.popleft()
        for nb in neighbors_fn(node):
            if nb == target:
                return dist + 1
            if nb not in visited:
                visited.add(nb)
                queue.append((nb, dist + 1))
    return -1


# 2) BFS theo lớp — không cần lưu distance trong queue
def bfs_by_level(start, neighbors_fn):
    visited = {start}
    queue = deque([start])
    level = 0
    while queue:
        size = len(queue)
        for _ in range(size):
            node = queue.popleft()
            # ... xử lý node ở level này ...
            for nb in neighbors_fn(node):
                if nb not in visited:
                    visited.add(nb)
                    queue.append(nb)
        level += 1


# 3) Multi-source BFS — đẩy nhiều nguồn vào queue cùng lúc
def multi_source(sources: list, neighbors_fn):
    queue = deque(sources)
    visited = set(sources)
    while queue:
        ...

Bài tự luyện cuối chương


10.1 Binary Tree Level Order Traversal (LC 102)

Đề bài

Cho root của một binary tree. Trả về level order traversal dưới dạng danh sách các list (mỗi list chứa các node ở 1 level từ trên xuống, trái sang phải).

Ví dụ

Input:  root = [3, 9, 20, null, null, 15, 7]   (LC level-order serialize)

        Cây thực tế:
              3
             / \
            9   20
               /  \
              15   7

Output: [[3], [9, 20], [15, 7]]

Ràng buộc

Clarifying questions

Hướng tiếp cận

BFS theo lớp. Mỗi vòng outer = 1 level. Đầu mỗi vòng, ghi size = len(queue), sau đó pop đúng size node — đó là toàn bộ level hiện tại.

Hình minh hoạ:

Init:  queue = [3]
Level 0: size=1
  Pop 3. result.append([3]). Push 9, 20.
  queue = [9, 20]

Level 1: size=2
  Pop 9 → null children.
  Pop 20 → push 15, 7.
  result.append([9, 20]).
  queue = [15, 7]

Level 2: size=2
  Pop 15, 7 → null children.
  result.append([15, 7]).
  queue = []

→ [[3], [9, 20], [15, 7]]

Code Python 3

from collections import deque
from typing import List, Optional

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val, self.left, self.right = val, left, right


class Solution:
    def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
        if not root:
            return []
        result: List[List[int]] = []
        queue = deque([root])
        while queue:
            size = len(queue)
            level_vals: list[int] = []
            for _ in range(size):
                node = queue.popleft()
                level_vals.append(node.val)
                if node.left:
                    queue.append(node.left)
                if node.right:
                    queue.append(node.right)
            result.append(level_vals)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


10.2 Rotting Oranges (LC 994) — Multi-source BFS

Đề bài

Cho lưới grid với các giá trị: - 0 = ô trống - 1 = quả tươi - 2 = quả thối

Mỗi phút, mỗi quả thối làm 4 ô kề bên (lên/xuống/trái/phải) có quả tươi trở thành thối. Trả về số phút tối thiểu để không còn quả tươi, hoặc -1 nếu không khả thi.

Ví dụ

Input:  grid = [[2,1,1],
                [1,1,0],
                [0,1,1]]
        (0 = empty, 1 = fresh orange, 2 = rotten)
Output: 4   (số phút để mọi orange thối)

Input:  grid = [[2,1,1],
                [0,1,1],
                [1,0,1]]
Output: -1   (quả tươi ở (2,0) bị cô lập, không bao giờ thối)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Insight: Mỗi quả thối là một nguồn lan toả. Tất cả nguồn lan đồng thời mỗi phút → multi-source BFS.

Quy trình: 1. Đẩy tất cả quả thối ban đầu vào queue cùng lúc (level 0). 2. BFS theo lớp — mỗi level tăng minutes thêm 1. 3. Đếm số quả tươi ban đầu. Mỗi lần thối thêm một quả → giảm count. 4. Cuối: nếu còn quả tươi → -1, ngược lại → minutes.

Hình minh hoạ với grid 3x3:

Init:                t=0:              t=1:              t=4:
[2, 1, 1]            [2, 1, 1]         [2, 2, 1]         [2, 2, 2]
[1, 1, 0]            [1, 1, 0]         [2, 1, 0]         [2, 2, 0]
[0, 1, 1]            [0, 1, 1]         [0, 1, 1]         [0, 2, 2]
                                       (4 quả tươi)      (xong)

Hàng động BFS:
Queue chứa các ô (i, j) cùng level → mỗi vòng outer pop hết queue rồi push
hàng xóm. Số vòng outer = số phút.

Code Python 3

from collections import deque
from typing import List

class Solution:
    def orangesRotting(self, grid: List[List[int]]) -> int:
        rows, cols = len(grid), len(grid[0])
        queue: deque[tuple[int, int]] = deque()
        fresh = 0
        for r in range(rows):
            for c in range(cols):
                if grid[r][c] == 2:
                    queue.append((r, c))
                elif grid[r][c] == 1:
                    fresh += 1

        if fresh == 0:
            return 0

        minutes = 0
        dirs = [(-1, 0), (1, 0), (0, -1), (0, 1)]
        while queue and fresh > 0:
            minutes += 1
            for _ in range(len(queue)):
                r, c = queue.popleft()
                for dr, dc in dirs:
                    nr, nc = r + dr, c + dc
                    if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
                        grid[nr][nc] = 2
                        fresh -= 1
                        queue.append((nr, nc))

        return minutes if fresh == 0 else -1

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


10.3 Word Ladder (LC 127)

Đề bài

Cho beginWord, endWord, và wordList (các từ cùng độ dài). Mỗi bước biến đổi: thay đúng 1 ký tự trong word hiện tại sao cho từ mới vẫn nằm trong wordList. Trả về số bước tối thiểu để biến beginWordendWord (bao gồm cả 2 đầu). Trả 0 nếu không khả thi.

Ví dụ

Input:  beginWord = "hit", endWord = "cog"
        wordList = ["hot","dot","dog","lot","log","cog"]
Output: 5
Giải thích: hit → hot → dot → dog → cog  (độ dài 5)

Input:  beginWord = "hit", endWord = "cog"
        wordList = ["hot","dot","dog","lot","log"]
Output: 0    (cog không trong wordList)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Mô hình hoá: Mỗi từ là 1 đỉnh; có cạnh giữa 2 từ nếu chúng khác nhau đúng 1 ký tự. Bài thành shortest path trong undirected graph → BFS.

Tối ưu sinh hàng xóm: Thay vì so sánh từ cur với mọi từ trong wordList (O(N·L) mỗi node) — quá chậm — ta sinh hàng xóm bằng cách thay từng vị trí ký tự bằng a..z (O(26·L) mỗi node).

Hình minh hoạ với "hit" → "cog":

Level 1: hit
Level 2: hot                (đổi i→o)
Level 3: dot, lot           (đổi h→d/l)
Level 4: dog, log           (đổi t→g)
Level 5: cog                ★ đáp án (5 bước)

Code Python 3

from collections import deque
from typing import List

class Solution:
    def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
        word_set = set(wordList)
        if endWord not in word_set:
            return 0

        queue = deque([(beginWord, 1)])
        visited = {beginWord}
        while queue:
            word, steps = queue.popleft()
            if word == endWord:
                return steps
            for i in range(len(word)):
                for ch in 'abcdefghijklmnopqrstuvwxyz':
                    if ch == word[i]:
                        continue
                    next_word = word[:i] + ch + word[i + 1:]
                    if next_word in word_set and next_word not in visited:
                        visited.add(next_word)
                        queue.append((next_word, steps + 1))
        return 0

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


10.4 Open the Lock (LC 752)

Đề bài

Cho khoá 4 ô số 0000. Mỗi bước: xoay 1 ô số lên hoặc xuống 1 đơn vị (vòng 0..9). Cho deadends (các tổ hợp không được đến) và target. Trả về số bước tối thiểu để đến target, hoặc -1 nếu không khả thi.

Ví dụ

Input:  deadends = ["0201","0101","0102","1212","2002"], target = "0202"
Output: 6
Giải thích: 0000 → 1000 → 1100 → 1200 → 1201 → 1202 → 0202
            (không trùng deadend nào)

Ràng buộc

Clarifying questions

Hướng tiếp cận

State space implicit graph. Mỗi state là chuỗi 4 chữ số → 10^4 = 10000 state. Từ mỗi state có 8 transition (4 ô × 2 chiều).

BFS: bắt đầu từ "0000", BFS đến target. Skip các state trong deadends.

Hình minh hoạ một phần BFS:

Level 0: 0000
Level 1: 1000, 9000, 0100, 0900, 0010, 0090, 0001, 0009  (8 hàng xóm)
Level 2: ... (mỗi node 8 hàng xóm, trừ những cái đã visited / deadend)
...
Level 6: 0202  ★

Code Python 3

from collections import deque
from typing import List

class Solution:
    def openLock(self, deadends: List[str], target: str) -> int:
        dead = set(deadends)
        if "0000" in dead:
            return -1
        if target == "0000":
            return 0

        def neighbors(state: str):
            for i in range(4):
                d = int(state[i])
                for delta in (-1, 1):
                    new_d = (d + delta) % 10
                    yield state[:i] + str(new_d) + state[i + 1:]

        visited = {"0000"}
        queue = deque([("0000", 0)])
        while queue:
            state, steps = queue.popleft()
            for nb in neighbors(state):
                if nb in dead or nb in visited:
                    continue
                if nb == target:
                    return steps + 1
                visited.add(nb)
                queue.append((nb, steps + 1))
        return -1

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


10.5 Shortest Path in Binary Matrix (LC 1091)

Đề bài

Cho ma trận vuông n × n chứa 0 (đi được) và 1 (vật cản). Tìm đường đi ngắn nhất từ (0,0) đến (n-1,n-1), đi được 8 hướng (4 trục + 4 chéo). Độ dài đường đi = số ô đi qua (kể cả start và end). Trả -1 nếu không đi được.

Ví dụ

Input:  grid = [[0,0,0],
                [1,1,0],
                [1,1,0]]
Output: 4
Giải thích: (0,0) → (0,1) → (1,2) → (2,2)

Ràng buộc

Clarifying questions

Hướng tiếp cận

BFS từ (0,0) với 8 hướng. Mỗi cạnh trọng số 1 (mỗi bước = 1 ô).

Code Python 3

from collections import deque
from typing import List

class Solution:
    def shortestPathBinaryMatrix(self, grid: List[List[int]]) -> int:
        n = len(grid)
        if grid[0][0] != 0 or grid[n - 1][n - 1] != 0:
            return -1
        if n == 1:
            return 1

        dirs = [(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]
        queue = deque([(0, 0, 1)])     # (r, c, steps)
        grid[0][0] = 1                  # mark visited
        while queue:
            r, c, steps = queue.popleft()
            for dr, dc in dirs:
                nr, nc = r + dr, c + dc
                if 0 <= nr < n and 0 <= nc < n and grid[nr][nc] == 0:
                    if (nr, nc) == (n - 1, n - 1):
                        return steps + 1
                    grid[nr][nc] = 1
                    queue.append((nr, nc, steps + 1))
        return -1

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


10.6 Snakes and Ladders (LC 909)

Đề bài

Cho bàn cờ n × n đánh số ô theo zigzag (như cờ rắn rồng). board[i][j] = -1 nghĩa là ô bình thường; nếu >= 1, đó là rắn/thang đưa bạn đến ô số đó.

Mỗi bước, từ ô hiện tại bạn được tung xúc xắc (6 mặt) đi 1..6 bước; nếu ô đến có rắn/thang, tự động đi tiếp đến ô đích. Tìm số lần tung tối thiểu để đến ô n*n cuối cùng. Trả -1 nếu không thể.

Ví dụ

Input:  board =
        [[-1,-1,-1,-1,-1,-1],
         [-1,-1,-1,-1,-1,-1],
         [-1,-1,-1,-1,-1,-1],
         [-1,35,-1,-1,13,-1],
         [-1,-1,-1,-1,-1,-1],
         [-1,15,-1,-1,-1,-1]]
Output: 4

Ràng buộc

Clarifying questions

Hướng tiếp cận

State space: mỗi ô đánh số 1..n². Từ ô s, có thể đi đến ô s+1, s+2, ..., s+6 (rồi nhảy nếu có rắn/thang). Mỗi cạnh = 1 lần tung → BFS ra số tung tối thiểu.

Trick zigzag → tọa độ: - Hàng (từ dưới): (label - 1) // n. - Cột tuỳ hướng hàng: chẵn từ dưới thì trái → phải, lẻ thì phải → trái.

Code Python 3

from collections import deque
from typing import List

class Solution:
    def snakesAndLadders(self, board: List[List[int]]) -> int:
        n = len(board)
        def label_to_pos(label: int) -> tuple[int, int]:
            quot, rem = divmod(label - 1, n)
            row = n - 1 - quot
            col = rem if quot % 2 == 0 else n - 1 - rem
            return row, col

        target = n * n
        visited = {1}
        queue = deque([(1, 0)])     # (square, throws)
        while queue:
            square, throws = queue.popleft()
            for d in range(1, 7):
                nxt = square + d
                if nxt > target:
                    break
                r, c = label_to_pos(nxt)
                if board[r][c] != -1:
                    nxt = board[r][c]
                if nxt == target:
                    return throws + 1
                if nxt not in visited:
                    visited.add(nxt)
                    queue.append((nxt, throws + 1))
        return -1

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

BFS state design (đa dạng hơn bạn nghĩ)

State Bài tiêu biểu
node Shortest path unweighted graph
(r, c) Grid (Number of Islands, 01 Matrix)
word Word Ladder
(r, c, k_remaining) Shortest Path with K Obstacles
board_serialized Sliding Puzzle, Open Lock
bitmask_visited Shortest Path Visiting All Nodes
(node, parity) Bipartite, chẵn/lẻ bước

Word Ladder — neighbor generation

Snakes & Ladders — 1D ↔︎ 2D

Board n×n serpentine: index i (1..n²) → tọa độ:

row_from_bottom = (i - 1) // n     # 0 = bottom row
col_in_row      = (i - 1) % n
r = n - 1 - row_from_bottom
c = col_in_row if row_from_bottom % 2 == 0 else n - 1 - col_in_row

Bug điển hình: quên đảo chiều hàng lẻ, hoặc index 0/1.

Distance: level BFS vs lưu trong queue


Chương 11 — Depth-First Search (DFS)

DFS đi sâu nhất có thể trước khi quay lui. Đây là pattern tự nhiên cho mọi bài cây / đồ thị có cấu trúc đệ quy: depth, path sum, validate, LCA, … Chương này tập trung DFS trên cây — pattern dễ thuộc lòng và rất hay trong phỏng vấn. DFS trên grid sẽ ở Chương 12 (Island Matrix).

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

3 mẫu DFS trên cây: 1. Top-down: truyền state xuống (vd. path_so_far, current_sum). 2. Bottom-up: leaf trả giá trị lên, node tổng hợp từ con. 3. Hỗn hợp: vừa truyền xuống vừa nhận lên.

Định dạng input (áp dụng cho TẤT CẢ bài tree trong chương)

Mọi bài trong chương 10/11/22 (tree) dùng TreeNode chuẩn của LeetCode:

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

Template code

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val, self.left, self.right = val, left, right


# 1) Bottom-up: trả về giá trị từ con
def dfs_bottom_up(node) -> int:
    if not node:
        return 0
    left = dfs_bottom_up(node.left)
    right = dfs_bottom_up(node.right)
    return combine(node.val, left, right)


# 2) Top-down: truyền state xuống
def dfs_top_down(node, state) -> None:
    if not node:
        return
    new_state = update(state, node.val)
    if is_leaf(node):
        # ... ghi kết quả ...
        return
    dfs_top_down(node.left, new_state)
    dfs_top_down(node.right, new_state)


# 3) Iterative DFS bằng stack
def dfs_iter(root):
    stack = [root]
    while stack:
        node = stack.pop()
        if not node:
            continue
        # ... visit node ...
        stack.append(node.right)
        stack.append(node.left)     # left lên top trước

Bài tự luyện cuối chương


11.1 Maximum Depth of Binary Tree (LC 104)

Đề bài

Cho root của binary tree. Trả về độ sâu lớn nhất (số node trên path dài nhất từ root đến lá).

Ví dụ

Input:  root = [3, 9, 20, null, null, 15, 7]   (LC level-order serialize)

        Cây thực tế:
              3
             / \
            9   20
               /  \
              15   7

Output: 3

Ràng buộc

Clarifying questions

Hướng tiếp cận

Bottom-up một dòng:

depth(node) = 1 + max(depth(left), depth(right)), base case rỗng → 0.

Code Python 3

class Solution:
    def maxDepth(self, root) -> int:
        if not root:
            return 0
        return 1 + max(self.maxDepth(root.left), self.maxDepth(root.right))

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


11.2 Path Sum II (LC 113)

Đề bài

Cho root và số targetSum. Trả về tất cả đường đi root-to-leaf có tổng giá trị bằng targetSum.

Ví dụ

Input:  root = [5, 4, 8, 11, null, 13, 4, 7, 2, null, null, 5, 1]
        targetSum = 22

        Cây thực tế:
                  5
                 / \
                4   8
               /   / \
              11  13  4
             /  \    / \
            7    2  5   1

Output: [[5,4,11,2], [5,8,4,5]]

Ràng buộc

Clarifying questions

Hướng tiếp cận

DFS top-down + backtracking: - Đi xuống mỗi node, giảm target còn lại và append node vào path. - Tại leaf: nếu target == leaf.val → copy path vào kết quả. - Khi quay lên (sau khi duyệt xong các con) → path.pop() để khôi phục trạng thái.

Hình minh hoạ — đường đi 5 → 4 → 11 → 2 cho target = 22:

DFS(5, target=22, path=[]):
  path=[5], remaining=17
  DFS(4, 17):
    path=[5,4], remaining=13
    DFS(11, 13):
      path=[5,4,11], remaining=2
      DFS(7, 2):  leaf, 7 != 2 → bỏ
                  pop → path=[5,4,11]
      DFS(2, 2):  leaf, 2 == 2 → ADD [5,4,11,2] vào result
                  pop → path=[5,4,11]
      pop → path=[5,4]
    pop → path=[5]
  ...

Code Python 3

from typing import List, Optional

class Solution:
    def pathSum(self, root: Optional["TreeNode"], targetSum: int) -> List[List[int]]:
        result: list[list[int]] = []
        path: list[int] = []

        def dfs(node, remaining: int) -> None:
            if not node:
                return
            path.append(node.val)
            remaining -= node.val
            if not node.left and not node.right and remaining == 0:
                result.append(path.copy())
            else:
                dfs(node.left, remaining)
                dfs(node.right, remaining)
            path.pop()                  # backtrack

        dfs(root, targetSum)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


11.3 All Paths From Source to Target (LC 797)

Đề bài

Cho DAG graph (mảng adjacency list, node i có hàng xóm graph[i]). Trả về tất cả đường đi từ node 0 đến node n - 1.

Ví dụ

Input:  graph = [[1,2],[3],[3],[]]
        # đồ thị:  0 → 1 → 3
        #          0 → 2 → 3
Output: [[0,1,3], [0,2,3]]

Ràng buộc

Clarifying questions

Hướng tiếp cận

DAG ⇒ không có chu trình ⇒ không cần visited. DFS từ 0, mỗi đến n-1 ghi path.

Code Python 3

from typing import List

class Solution:
    def allPathsSourceTarget(self, graph: List[List[int]]) -> List[List[int]]:
        n = len(graph)
        result: list[list[int]] = []
        path: list[int] = [0]

        def dfs(node: int) -> None:
            if node == n - 1:
                result.append(path.copy())
                return
            for nb in graph[node]:
                path.append(nb)
                dfs(nb)
                path.pop()

        dfs(0)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


11.4 Validate Binary Search Tree (LC 98)

Đề bài

Cho root. Kiểm tra cây có phải BST hợp lệ không, theo định nghĩa: - Mọi node con trái: value strictly less than node hiện tại. - Mọi node con phải: value strictly greater than node hiện tại. - Cả 2 subtree đều là BST.

Ví dụ

Input:  root = [2, 1, 3]
        Cây thực tế:
              2
             / \
            1   3
Output: true

Input:  root = [5, 1, 4, null, null, 3, 6]
        Cây thực tế:
              5
             / \
            1   4
               / \
              3   6
Output: false
        (node 3 ở subtree phải của 5, nhưng 3 < 5 → vi phạm BST)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cách sai phổ biến — chỉ check node.left.val < node.val < node.right.val. Sai vì BST yêu cầu TOÀN BỘ subtree trái < node, không chỉ child trực tiếp.

Ví dụ ngược:

    5
   / \
  1   4
     / \
    3   6

Tại node 5: 4 < 5 (OK), 1 < 5 (OK). Tại node 4: 3 < 4 < 6 (OK). Local check pass, nhưng 3 < 5 trong subtree phải → sai.

Cách đúng — DFS truyền (low, high) bound:

dfs(node, low, high): node phải thoả low < node.val < high. Khi đi xuống: - Trái: bound mới (low, node.val). - Phải: bound mới (node.val, high).

Cách 2 — Inorder traversal phải sắp xếp tăng strict.

BST inorder = sequence sorted. Duyệt inorder, kiểm tra mỗi value > value trước.

Code Python 3

import math
from typing import Optional

class Solution:
    """Cách 1 — DFS với bound (low, high)."""

    def isValidBST(self, root: Optional["TreeNode"]) -> bool:

        def dfs(node, low: float, high: float) -> bool:
            if not node:
                return True
            if not (low < node.val < high):
                return False
            return dfs(node.left, low, node.val) and \
                   dfs(node.right, node.val, high)

        return dfs(root, -math.inf, math.inf)


class SolutionInorder:
    """Cách 2 — inorder traversal."""

    def isValidBST(self, root) -> bool:
        self.prev = -math.inf

        def inorder(node) -> bool:
            if not node:
                return True
            if not inorder(node.left):
                return False
            if node.val <= self.prev:
                return False
            self.prev = node.val
            return inorder(node.right)

        return inorder(root)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


11.5 House Robber III (LC 337)

Đề bài

Cho root của binary tree (LC level-order serialize, vd [3,2,3,null,3,null,1]). Nhà ở mỗi node có giá trị node.val. Tên trộm không thể trộm 2 nhà kề nhau (parent ↔︎ child). Trả về số tiền tối đa trộm được.

Ví dụ

Input:  root = [3, 2, 3, null, 3, null, 1]
        Cây thực tế:
              3
             / \
            2   3
             \   \
              3   1
Output: 7
        (trộm 3 + 3 + 1 = 7)

Input:  root = [3, 4, 5, 1, 3, null, 1]
        Cây thực tế:
              3
             / \
            4   5
           / \   \
          1   3   1
Output: 9
        (trộm 4 + 5 = 9)

Ràng buộc

Clarifying questions

Hướng tiếp cận

DP trên cây — mỗi node trả về 2 giá trị: - rob_this = max tiền nếu trộm node này (con không trộm). - skip_this = max tiền nếu không trộm node này (con tuỳ ý).

Quan hệ: - rob_this = node.val + left.skip + right.skip - skip_this = max(left.rob, left.skip) + max(right.rob, right.skip)

Đáp án = max(root.rob, root.skip).

Hình minh hoạ:

       3
      / \
     2   3
      \   \
       3   1

DFS bottom-up:
  node 3 (lá phải-phải): rob=3, skip=0
  node 3 (lá phải-trái): rob=3, skip=0
  node 1 (lá phải-phải-right): rob=1, skip=0

  node 2: rob = 2 + 0 (no left) + 0 (3 skip) = 2
          skip = 0 + max(3, 0) = 3
  node 3 (right): rob = 3 + max(0, 0)(no left) + 0 = 3
                  skip = 0 + max(1, 0) = 1

  node 3 (root): rob = 3 + 3 (no rob của 2) + 1 (no rob của 3-right) = 7
                 skip = max(2,3) + max(3,1) = 3 + 3 = 6

Đáp án: max(7, 6) = 7

Code Python 3

from typing import Optional, Tuple

class Solution:
    def rob(self, root: Optional["TreeNode"]) -> int:

        def dfs(node) -> Tuple[int, int]:
            """Trả về (rob_this, skip_this)."""
            if not node:
                return 0, 0
            l_rob, l_skip = dfs(node.left)
            r_rob, r_skip = dfs(node.right)
            rob_this = node.val + l_skip + r_skip
            skip_this = max(l_rob, l_skip) + max(r_rob, r_skip)
            return rob_this, skip_this

        return max(dfs(root))

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


11.6 Lowest Common Ancestor of a Binary Tree (LC 236)

Đề bài

Cho root của binary tree (không phải BST) và 2 node p, q. Tìm lowest common ancestor (LCA) — node thấp nhất có cả pq trong subtree của nó.

Ví dụ

Input:  root = [3, 5, 1, 6, 2, 0, 8, null, null, 7, 4]
        Cây thực tế:
              3
             / \
            5   1
           / \ / \
          6  2 0  8
            / \
           7   4

Input:  p = 5, q = 1   → Output: 3
Input:  p = 5, q = 4   → Output: 5   (5 là tổ tiên của chính nó)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Insight tinh tế: Tại mỗi node: - Nếu node == p hoặc node == q → trả về node luôn. - Đệ quy trên leftright. - Nếu cả 2 đệ quy đều trả về non-None → node là LCA. - Nếu chỉ 1 cái trả non-None → trả cái đó (nghĩa là cả p và q đều ở 1 bên).

Code Python 3

from typing import Optional

class Solution:
    def lowestCommonAncestor(self, root, p, q):
        if not root or root is p or root is q:
            return root

        left = self.lowestCommonAncestor(root.left, p, q)
        right = self.lowestCommonAncestor(root.right, p, q)

        if left and right:
            return root          # p và q ở 2 nhánh → root là LCA
        return left if left else right

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Traversal order cheat sheet

Traversal Khi nào dùng
Pre-order (root → L → R) Serialize, clone, copy
In-order (L → root → R) BST sorted output, kth smallest
Post-order (L → R → root) Tổng hợp từ con (tree DP, diameter)
Level-order (BFS) Theo tầng, distance

DFS return value design (cốt lõi tree DP)

def dfs(node):
    if not node: return base
    L = dfs(node.left)
    R = dfs(node.right)
    # combine L, R với node.val → ans cho subtree này
    # CẬP NHẬT đáp số toàn cục nếu cần
    return result_to_pass_up

Validate BST — global bounds

House Robber III — (rob, skip) trace

Cây:

    3
   / \
  2   3
   \   \
    3   1

Post-order trả về (rob_this, skip_this): - Lá 3 (left của 2): (3, 0). - Lá 1 (right của 3): (1, 0). - Node 2: rob = 2 + 0 = 2, skip = max(3,0) = 3(2, 3). - Node 3 (right of root): rob = 3 + 0 = 3, skip = max(1,0) = 1(3, 1). - Root 3: rob = 3 + 3 + 1 = 7, skip = max(2,3) + max(3,1) = 3 + 3 = 6max(7,6) = 7.

LCA variants — phân biệt

Loại cây Cách
Binary tree thường Đệ quy bottom-up, trả node nếu chứa p hoặc q (LC 236)
BST So sánh value với root, đi 1 nhánh (LC 235) — O(log n)
parent pointer Hash các tổ tiên của p, đi từ q lên

Chương 12 — Island Matrix Traversal

Grid 2D thực ra là graph ngầm: mỗi ô là 1 node, 4 ô kề (lên/xuống/trái/ phải) là cạnh. Mọi bài “đảo” / “tô màu vùng” / “flood fill” đều là DFS/BFS trên graph này. Chương này dạy bạn 4 trick đặc trưng cho grid: (1) flood fill, (2) multi-source BFS từ biên, (3) reverse thinking (đánh dấu cái không cần), (4) mutate input để mark visited.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Template code

from collections import deque
from typing import List

DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]

# 1) Flood fill DFS (đệ quy)
def flood_fill(grid: List[List[int]], r: int, c: int, marker: int) -> int:
    rows, cols = len(grid), len(grid[0])
    if not (0 <= r < rows and 0 <= c < cols) or grid[r][c] != 1:
        return 0
    grid[r][c] = marker             # mark visited
    size = 1
    for dr, dc in DIRS:
        size += flood_fill(grid, r + dr, c + dc, marker)
    return size


# 2) Multi-source BFS từ tất cả biên hoặc tất cả ô đặc biệt
def multi_source_bfs(grid, sources):
    queue = deque(sources)
    visited = set(sources)
    while queue:
        r, c = queue.popleft()
        for dr, dc in DIRS:
            nr, nc = r + dr, c + dc
            if 0 <= nr < rows and 0 <= nc < cols and (nr, nc) not in visited:
                visited.add((nr, nc))
                queue.append((nr, nc))
    return visited

Bài tự luyện cuối chương


12.1 Number of Islands (LC 200)

Đề bài

Cho grid m × n với '1' = đất, '0' = nước. Đảo là vùng đất liên thông (4 hướng). Đếm số đảo.

Ví dụ

Input:
[["1","1","1","1","0"],
 ["1","1","0","1","0"],
 ["1","1","0","0","0"],
 ["0","0","0","0","0"]]
Output: 1   (cả vùng "1" liên thông)

Input:
[["1","1","0","0","0"],
 ["1","1","0","0","0"],
 ["0","0","1","0","0"],
 ["0","0","0","1","1"]]
Output: 3   (góc trái-trên, giữa, góc phải-dưới)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Pattern flood fill kinh điển. Duyệt mỗi ô: - Nếu là '1' và chưa visited → tăng counter, gọi DFS/BFS đánh dấu toàn bộ đảo này thành visited (đổi '1''0' để khỏi dùng set riêng).

Hình minh hoạ với grid 4×5 thứ 2:

Bước 1, ô (0,0) = '1' → DFS:
[1 1 0 0 0]      [* * 0 0 0]
[1 1 0 0 0]  →   [* * 0 0 0]
[0 0 1 0 0]      [0 0 1 0 0]
[0 0 0 1 1]      [0 0 0 1 1]
                 (đảo 1 đánh dấu)

Bước 2, gặp ô (2,2) = '1' → DFS (chỉ đánh dấu chính nó):
[* * 0 0 0]
[* * 0 0 0]
[0 0 * 0 0]
[0 0 0 1 1]

Bước 3, gặp ô (3,3) = '1' → DFS:
[* * 0 0 0]
[* * 0 0 0]
[0 0 * 0 0]
[0 0 0 * *]

Đếm: 3 đảo

Code Python 3

from typing import List

class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        if not grid or not grid[0]:
            return 0
        rows, cols = len(grid), len(grid[0])
        DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]

        def dfs(r: int, c: int) -> None:
            if not (0 <= r < rows and 0 <= c < cols) or grid[r][c] != '1':
                return
            grid[r][c] = '0'              # mark visited
            for dr, dc in DIRS:
                dfs(r + dr, c + dc)

        count = 0
        for r in range(rows):
            for c in range(cols):
                if grid[r][c] == '1':
                    count += 1
                    dfs(r, c)
        return count

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


12.2 Max Area of Island (LC 695)

Đề bài

Cùng grid với 0/1. Trả về diện tích lớn nhất của một đảo (số ô ‘1’ của nó), hoặc 0 nếu không có đảo nào.

Ví dụ

Input:
[[0,0,1,0,0,0,0,1,0,0,0,0,0],
 [0,0,0,0,0,0,0,1,1,1,0,0,0],
 [0,1,1,0,1,0,0,0,0,0,0,0,0],
 [0,1,0,0,1,1,0,0,1,0,1,0,0],
 [0,1,0,0,1,1,0,0,1,1,1,0,0],
 [0,0,0,0,0,0,0,0,0,0,1,0,0],
 [0,0,0,0,0,0,0,1,1,1,0,0,0],
 [0,0,0,0,0,0,0,1,1,0,0,0,0]]
Output: 6

Ràng buộc

Clarifying questions

Hướng tiếp cận

Variant của bài 12.1, nhưng DFS trả về size thay vì void. Lấy max qua các lần khởi động DFS.

Code Python 3

from typing import List

class Solution:
    def maxAreaOfIsland(self, grid: List[List[int]]) -> int:
        if not grid:
            return 0
        rows, cols = len(grid), len(grid[0])
        DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]

        def dfs(r: int, c: int) -> int:
            if not (0 <= r < rows and 0 <= c < cols) or grid[r][c] != 1:
                return 0
            grid[r][c] = 0
            return 1 + sum(dfs(r + dr, c + dc) for dr, dc in DIRS)

        best = 0
        for r in range(rows):
            for c in range(cols):
                if grid[r][c] == 1:
                    best = max(best, dfs(r, c))
        return best

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


12.3 Surrounded Regions (LC 130)

Đề bài

Cho grid chứa 'X''O'. Lật mọi vùng 'O' được bao quanh hoàn toàn bởi 'X' (vùng không chạm biên grid) thành 'X'. Vùng 'O' chạm biên được giữ nguyên.

Ví dụ

Input:  board = [['X','X','X','X'],
                 ['X','O','O','X'],
                 ['X','X','O','X'],
                 ['X','O','X','X']]
Output: board = [['X','X','X','X'],
                 ['X','X','X','X'],
                 ['X','X','X','X'],
                 ['X','O','X','X']]

(mutate in-place; 'O' ở (3,1) chạm biên dưới → giữ;
 cụm 'O' bên trong bị bao quanh → lật thành 'X')

Ràng buộc

Clarifying questions

Hướng tiếp cận

Reverse thinking — đây là pattern cực hay: - Thay vì tìm vùng 'O' bị bao, ta tìm vùng 'O' chạm biên (rất dễ). - Đánh dấu chúng (vd. đổi tạm thành '#'). - Cuối cùng: - '#''O' (giữ). - 'O' còn lại → 'X' (lật).

Hình minh hoạ:

Grid ban đầu:               Sau DFS từ các 'O' biên (đánh dấu '#'):
X X X X                     X X X X
X O O X                     X O O X     (O ở (1,1),(1,2) KHÔNG chạm biên,
X X O X         →           X X O X       không bị mark)
X O X X                     X # X X     (O ở (3,1) chạm biên → mark)

Quét cuối:
'#' → 'O'; 'O' → 'X':
X X X X
X X X X
X X X X
X O X X

Code Python 3

from typing import List

class Solution:
    def solve(self, board: List[List[str]]) -> None:
        if not board:
            return
        rows, cols = len(board), len(board[0])

        def dfs(r: int, c: int) -> None:
            if not (0 <= r < rows and 0 <= c < cols) or board[r][c] != 'O':
                return
            board[r][c] = '#'
            dfs(r + 1, c); dfs(r - 1, c); dfs(r, c + 1); dfs(r, c - 1)

        # 1. DFS từ mọi 'O' ở biên — mark thành '#'.
        for r in range(rows):
            dfs(r, 0)
            dfs(r, cols - 1)
        for c in range(cols):
            dfs(0, c)
            dfs(rows - 1, c)

        # 2. Lật: '#' → 'O' (giữ); 'O' → 'X' (lật).
        for r in range(rows):
            for c in range(cols):
                if board[r][c] == 'O':
                    board[r][c] = 'X'
                elif board[r][c] == '#':
                    board[r][c] = 'O'

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


12.4 Pacific Atlantic Water Flow (LC 417)

Đề bài

Cho ma trận chiều cao heights[i][j] đại diện chiều cao đảo. Mép trái và mép trên giáp Thái Bình Dương; mép phải và mép dưới giáp Đại Tây Dương. Nước chảy từ ô (r, c) sang ô kề có chiều cao ≤ (r, c).

Trả về tất cả (r, c) mà nước từ đó có thể chảy ra cả 2 đại dương.

Ví dụ

Input:  heights = [[1,2,2,3,5],
                   [3,2,3,4,4],
                   [2,4,5,3,1],
                   [6,7,1,4,5],
                   [5,1,1,2,4]]
Output: [[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],[4,0]]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Forward thinking — TLE. Với mỗi ô, BFS xem chảy được ra biên nào. Worst-case O((mn)²).

Reverse thinking — O(m · n).

Thay vì hỏi “ô nào chảy được ra biển?”, ta hỏi “biển có thể vươn lên tới ô nào?”. Biển vươn lên cao theo quy tắc: chỉ vào ô có chiều cao ô hiện tại.

Hình minh hoạ — Pacific reach (P) và Atlantic reach (A) trên ma trận 5x5:

P P P P P/A                   A A A A A
P . . . A             P/A . . . A
P . . . A             P . . . A
P/A . . . A             P . . . A
P/A A A A A             P P P P P

Giao P ∩ A = các ô có cả 2 → đáp án.

Code Python 3

from typing import List

class Solution:
    def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]:
        if not heights:
            return []
        rows, cols = len(heights), len(heights[0])
        pacific: set[tuple[int, int]] = set()
        atlantic: set[tuple[int, int]] = set()

        def dfs(r: int, c: int, visited: set, prev_h: int) -> None:
            if (r, c) in visited:
                return
            if not (0 <= r < rows and 0 <= c < cols):
                return
            if heights[r][c] < prev_h:    # biển không vươn được vào ô thấp hơn
                return
            visited.add((r, c))
            for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
                dfs(r + dr, c + dc, visited, heights[r][c])

        # Pacific: biên trái + biên trên.
        for r in range(rows):
            dfs(r, 0, pacific, heights[r][0])
        for c in range(cols):
            dfs(0, c, pacific, heights[0][c])
        # Atlantic: biên phải + biên dưới.
        for r in range(rows):
            dfs(r, cols - 1, atlantic, heights[r][cols - 1])
        for c in range(cols):
            dfs(rows - 1, c, atlantic, heights[rows - 1][c])

        return [[r, c] for r, c in pacific & atlantic]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


12.5 Walls and Gates (LC 286)

Đề bài

Cho rooms: ma trận m × n các số nguyên với 3 giá trị có ý nghĩa: - -1 = tường (cản đường). - 0 = cổng (gate). - INF (= 2³¹ - 1) = phòng trống.

Điền vào mỗi phòng trống khoảng cách ngắn nhất (số bước 4 hướng) đến cổng gần nhất. Nếu phòng không đến được cổng nào, giữ INF.

Ví dụ

Input:  rooms = [[INF, -1,  0, INF],
                 [INF,INF,INF, -1],
                 [INF, -1,INF, -1],
                 [  0, -1,INF,INF]]
        (-1 = tường, 0 = cổng, INF = phòng trống)

Output: rooms = [[3, -1, 0, 1],
                 [2,  2, 1,-1],
                 [1, -1, 2,-1],
                 [0, -1, 3, 4]]
        (mutate in-place; mỗi ô = khoảng cách 4 hướng tới cổng gần nhất)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Multi-source BFS — đẩy tất cả các cổng vào queue cùng lúc. Lan toả ra ngoài, mỗi ô đặt distance = số bước từ cổng gần nhất.

Vì sao multi-source > BFS từng cổng? Multi-source O(mn), BFS từng cổng O(gates · mn)gates có thể O(mn).

Code Python 3

from collections import deque
from typing import List

class Solution:
    def wallsAndGates(self, rooms: List[List[int]]) -> None:
        if not rooms:
            return
        rows, cols = len(rooms), len(rooms[0])
        queue: deque[tuple[int, int]] = deque()
        for r in range(rows):
            for c in range(cols):
                if rooms[r][c] == 0:
                    queue.append((r, c))

        DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]
        while queue:
            r, c = queue.popleft()
            for dr, dc in DIRS:
                nr, nc = r + dr, c + dc
                # Chỉ "ghi" vào ô INF — vì BFS, lần đầu ghi đã là min.
                if 0 <= nr < rows and 0 <= nc < cols and rooms[nr][nc] == 2147483647:
                    rooms[nr][nc] = rooms[r][c] + 1
                    queue.append((nr, nc))

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


12.6 01 Matrix (LC 542)

Đề bài

Cho ma trận mat chỉ chứa 01. Trả về ma trận cùng kích thước, mỗi ô = khoảng cách (số bước 4 hướng) đến số 0 gần nhất.

Ví dụ

Input:  mat = [[0,0,0],
               [0,1,0],
               [1,1,1]]
Output:       [[0,0,0],
               [0,1,0],
               [1,2,1]]
        (mỗi ô = khoảng cách Manhattan tới ô '0' gần nhất)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Y hệt Walls and Gates: multi-source BFS từ tất cả ô 0. Distance ban đầu của ô 0 = 0, các ô 1 chưa biết.

Cách khác — DP 2 lượt, O(m · n): - Lượt 1 (trên-trái → dưới-phải): dp[r][c] = min(dp[r-1][c], dp[r][c-1]) + 1. - Lượt 2 (dưới-phải → trên-trái): dp[r][c] = min(dp[r][c], dp[r+1][c]+1, dp[r][c+1]+1).

Cả 2 đều O(m · n). BFS trực quan hơn; DP gọn space.

Code Python 3

from collections import deque
from typing import List

class Solution:
    def updateMatrix(self, mat: List[List[int]]) -> List[List[int]]:
        rows, cols = len(mat), len(mat[0])
        INF = float('inf')
        dist = [[INF] * cols for _ in range(rows)]

        queue: deque[tuple[int, int]] = deque()
        for r in range(rows):
            for c in range(cols):
                if mat[r][c] == 0:
                    dist[r][c] = 0
                    queue.append((r, c))

        DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]
        while queue:
            r, c = queue.popleft()
            for dr, dc in DIRS:
                nr, nc = r + dr, c + dc
                if 0 <= nr < rows and 0 <= nc < cols and dist[nr][nc] > dist[r][c] + 1:
                    dist[nr][nc] = dist[r][c] + 1
                    queue.append((nr, nc))
        return dist


class SolutionDP:
    """Cách DP 2 lượt — gọn space (in-place khi cho phép)."""

    def updateMatrix(self, mat: List[List[int]]) -> List[List[int]]:
        rows, cols = len(mat), len(mat[0])
        INF = rows + cols + 1
        dist = [[0 if mat[r][c] == 0 else INF
                 for c in range(cols)] for r in range(rows)]
        # Lượt 1: trên-trái → dưới-phải.
        for r in range(rows):
            for c in range(cols):
                if dist[r][c] == 0:
                    continue
                top = dist[r-1][c] if r > 0 else INF
                left = dist[r][c-1] if c > 0 else INF
                dist[r][c] = min(top, left) + 1
        # Lượt 2: dưới-phải → trên-trái.
        for r in range(rows - 1, -1, -1):
            for c in range(cols - 1, -1, -1):
                if dist[r][c] == 0:
                    continue
                bot = dist[r+1][c] + 1 if r < rows - 1 else INF
                right = dist[r][c+1] + 1 if c < cols - 1 else INF
                dist[r][c] = min(dist[r][c], bot, right)
        return dist

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Matrix traversal checklist

  1. Direction array: dirs = [(-1,0),(1,0),(0,-1),(0,1)] (4-conn) hoặc 8-conn.
  2. Bounds: 0 <= nr < R and 0 <= nc < C.
  3. Visited: in-place mark (đổi '1' → '0' hoặc #) hay set?
  4. Mutation OK? Hỏi interviewer; nếu không, dùng visited 2D.
  5. Stack overflow với DFS đệ quy: grid 1000×1000 có thể vượt limit. Dùng iterative stack hoặc BFS.

Boundary-first technique

Cho bài Surrounded Regions (LC 130) và Pacific Atlantic (LC 417): - “Cell không thoả điều kiện” = cell kết nối với biên. - Seed BFS/DFS từ biên, đánh dấu cell reach được; cell còn lại là cell bị bao quanh.

Multi-source BFS

Cho 01 Matrix (LC 542) và Walls and Gates (LC 286): - Bỏ tất cả nguồn vào queue ban đầu (cell 0 cho 542, cổng 0 cho 286). - BFS level → distance lan ra. Mỗi cell được visit một lầnO(R·C).

In-place mark vs visited set

Tiêu chí In-place Set/2D bool
Bộ nhớ phụ O(1) O(R·C)
Mutate input? Không
Concurrency/restore Khó Dễ
Ưu tiên Khi cho phép & cần O(1) extra Khi grid immutable hoặc cần re-run

Chương 13 — Topological Sort

Topological Sort sắp xếp đỉnh của DAG (Directed Acyclic Graph) sao cho mọi cạnh u → v thì u đứng trước v trong thứ tự. Đây là pattern bắt buộc cho mọi bài “hoàn thành theo thứ tự phụ thuộc”: build system, task scheduler, course prerequisites, …

Mục tiêu chương

Sau chương này, bạn sẽ:

Quy ước hướng cạnh

Một trong những lỗi phổ biến nhất khi giải topo sort là vẽ cạnh sai chiều. Vì lý do này, toàn bộ sách dùng một quy ước duy nhất:

Mô tả thực tế              Cạnh trong graph        Indegree
─────────────────────────────────────────────────────────────
"a must come before b"     a → b                   indeg[b] += 1
"b depends on a"           a → b                   indeg[b] += 1
"a is prerequisite of b"   a → b                   indeg[b] += 1
─────────────────────────────────────────────────────────────
LC 207/210 input:          prerequisites[i] = [course, prereq]
                           tức là [b, a] dạng "to do b, must do a"
                           → cạnh a → b (prereq → course)
─────────────────────────────────────────────────────────────
LC 269 Alien Dict:         words[i] < words[i+1] theo lex
                           → ký tự khác nhau đầu tiên: c1 < c2
                           → cạnh c1 → c2

Kahn’s invariant: Pop node có indeg == 0 ↔︎ “không còn ai phải xong trước nó”.

Mọi bài Topo trong cuốn sách dùng convention u → v nghĩa là u xong trước v. Khi gặp đề có wording khác, bước đầu tiên nên là vẽ 2–3 cạnh ra giấy để kiểm tra rằng hướng cạnh trong code khớp đúng với mô tả của đề bài.

Khi nào dùng pattern này?

2 thuật toán kinh điển:

  1. Kahn’s algorithm (BFS) — đếm indegree, đẩy node có indeg=0 vào queue.
  2. DFS với post-order — duyệt DFS, push node vào stack khi xong; reverse stack.

Cả 2 đều O(V + E). Mặc định mình dùng Kahn vì nó dễ extend cho “min levels”.

Template code

from collections import defaultdict, deque
from typing import List

def topo_sort_kahn(n: int, edges: List[tuple]) -> List[int]:
    graph = defaultdict(list)
    indeg = [0] * n
    for u, v in edges:
        graph[u].append(v)
        indeg[v] += 1

    queue = deque(i for i in range(n) if indeg[i] == 0)
    order: list[int] = []
    while queue:
        u = queue.popleft()
        order.append(u)
        for v in graph[u]:
            indeg[v] -= 1
            if indeg[v] == 0:
                queue.append(v)

    return order if len(order) == n else []     # rỗng = có chu trình

Bài tự luyện cuối chương


13.1 Course Schedule II (LC 210)

Đề bài

Cho numCourses khoá và prerequisites[i] = [a, b] (học a cần b xong trước). Trả về thứ tự học hợp lệ, hoặc [] nếu có chu trình.

Ví dụ

Input:  numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]
Output: [0, 1, 2, 3]   (hoặc [0, 2, 1, 3])
Giải thích:
  Cạnh: 0 → 1, 0 → 2, 1 → 3, 2 → 3.

Ràng buộc

Clarifying questions

Hướng tiếp cận

Kahn’s algorithm — y hệt template. Khi pop node, push vào order. Cuối: nếu len(order) == numCourses → trả order; ngược lại có chu trình.

Hình minh hoạ với [[1,0],[2,0],[3,1],[3,2]]:

Graph:        0
             / \
            1   2
             \ /
              3

indeg ban đầu: [0, 1, 1, 2]
queue = [0]                   (indeg 0)

Pop 0 → order=[0]
  giảm indeg[1], indeg[2] → [_, 0, 0, 2]
  queue = [1, 2]

Pop 1 → order=[0, 1]
  giảm indeg[3] → [_, _, _, 1]

Pop 2 → order=[0, 1, 2]
  giảm indeg[3] → [_, _, _, 0]
  queue = [3]

Pop 3 → order=[0, 1, 2, 3]

len(order)=4=numCourses → trả [0, 1, 2, 3]  ✓

Code Python 3

from collections import defaultdict, deque
from typing import List

class Solution:
    def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
        graph = defaultdict(list)
        indeg = [0] * numCourses
        for a, b in prerequisites:
            graph[b].append(a)
            indeg[a] += 1

        queue = deque(i for i, d in enumerate(indeg) if d == 0)
        order: list[int] = []
        while queue:
            node = queue.popleft()
            order.append(node)
            for nb in graph[node]:
                indeg[nb] -= 1
                if indeg[nb] == 0:
                    queue.append(nb)

        return order if len(order) == numCourses else []

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


13.2 Alien Dictionary (LC 269)

Đề bài

Một ngôn ngữ ngoài hành tinh dùng chữ cái Latin nhưng thứ tự khác. Cho danh sách words đã sắp xếp theo thứ tự đó. Hãy tìm 1 thứ tự chữ cái hợp lệ (string các chữ cái). Trả "" nếu mâu thuẫn.

Ví dụ

Input:  words = ["wrt","wrf","er","ett","rftt"]
        (mảng từ đã sort theo thứ tự alphabet của ngôn ngữ ngoài hành tinh)
Output: "wertf"  (1 thứ tự chữ cái hợp lệ; có thể có nhiều đáp án)
Giải thích:
  wrt < wrf  → t < f
  wrf < er   → w < e
  er  < ett  → r < t
  ett < rftt → e < r
  → topo: w < e < r < t < f

Input:  words = ["z","x","z"]
Output: ""             (mâu thuẫn cyclic: z<x từ cặp 1 nhưng x<z từ cặp 2)

Ràng buộc

Clarifying questions

Hướng tiếp cận

2 bước:

  1. Trích quan hệ thứ tự từ các cặp (words[i], words[i+1]) liền kề:
  2. Topological sort trên các quan hệ thu được.

Hình minh hoạ với ["wrt","wrf","er","ett","rftt"]:

Cặp 1: wrt vs wrf
  vị trí khác đầu: index 2 → t < f
  → cạnh t → f

Cặp 2: wrf vs er
  vị trí khác đầu: index 0 → w < e
  → cạnh w → e

Cặp 3: er vs ett
  vị trí khác đầu: index 1 → r < t
  → cạnh r → t

Cặp 4: ett vs rftt
  vị trí khác đầu: index 0 → e < r
  → cạnh e → r

Graph: w → e → r → t → f
Topo: w, e, r, t, f → "wertf"

Code Python 3

from collections import defaultdict, deque
from typing import List

class Solution:
    def alienOrder(self, words: List[str]) -> str:
        # Khởi tạo indeg cho tất cả ký tự xuất hiện.
        indeg = {ch: 0 for w in words for ch in w}
        graph = defaultdict(set)

        # Trích quan hệ từ các cặp kề nhau.
        for i in range(len(words) - 1):
            w1, w2 = words[i], words[i + 1]
            # Edge case mâu thuẫn tiền tố.
            if len(w1) > len(w2) and w1.startswith(w2):
                return ""
            for c1, c2 in zip(w1, w2):
                if c1 != c2:
                    if c2 not in graph[c1]:
                        graph[c1].add(c2)
                        indeg[c2] += 1
                    break

        # Kahn's.
        queue = deque(ch for ch, d in indeg.items() if d == 0)
        order: list[str] = []
        while queue:
            ch = queue.popleft()
            order.append(ch)
            for nb in graph[ch]:
                indeg[nb] -= 1
                if indeg[nb] == 0:
                    queue.append(nb)

        return ''.join(order) if len(order) == len(indeg) else ""

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


13.3 Minimum Height Trees (LC 310)

Đề bài

Input: n (số đỉnh) và edges: List[List[int]] — danh sách n-1 cạnh [u, v] mô tả tree vô hướng. Đỉnh đánh số 0..n-1.

Tìm tất cả root có thể chọn để chiều cao tree là min. Trả về danh sách root đó (có thể có 1 hoặc 2).

Ví dụ

Input:  n=6, edges = [[0,3],[1,3],[2,3],[4,3],[5,4]]
Tree:    0   1   2
          \  |  /
           \ | /
             3
             |
             4
             |
             5
Output: [3, 4]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Insight: Centroid của tree (gồm 1 hoặc 2 node) tối thiểu hoá chiều cao. Cách tìm: BFS từ các lá, “gọt vỏ” dần.

Quy trình: 1. Xây graph + tính degree. 2. Đẩy mọi lá (degree == 1) vào queue. 3. Lặp: pop một lớp lá, giảm degree hàng xóm, lá mới (degree == 1) → queue. 4. Khi còn ≤ 2 node → đó là centroid(s).

Hình minh hoạ:

Lớp đầu — lá: [0, 1, 2, 5]
Gọt → còn lại: [3, 4]
                3 (degree=1 sau gọt), 4 (degree=1 sau gọt)
→ ≤ 2 node → centroids = [3, 4]

Code Python 3

from collections import defaultdict, deque
from typing import List

class Solution:
    def findMinHeightTrees(self, n: int, edges: List[List[int]]) -> List[int]:
        if n == 1:
            return [0]
        graph = defaultdict(set)
        for u, v in edges:
            graph[u].add(v)
            graph[v].add(u)

        leaves = deque(i for i in range(n) if len(graph[i]) == 1)
        remaining = n
        while remaining > 2:
            size = len(leaves)
            remaining -= size
            for _ in range(size):
                leaf = leaves.popleft()
                nb = next(iter(graph[leaf]))    # lá có đúng 1 hàng xóm
                graph[nb].remove(leaf)
                if len(graph[nb]) == 1:
                    leaves.append(nb)
        return list(leaves)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


13.4 Sort Items by Groups Respecting Dependencies (LC 1203)

Đề bài

Cho n items, mỗi item thuộc 1 group (group[i] = -1 nếu chưa thuộc nhóm nào, sẽ phân nhóm riêng). Cho beforeItems[i] = các item phải làm trước item i. Hãy sắp xếp items sao cho: - Tôn trọng beforeItems. - Các item cùng group đứng liền nhau.

Trả [] nếu không khả thi.

Ví dụ

Input:  n=8, m=2, group=[-1,-1,1,0,0,1,0,-1], beforeItems=[[],[6],[5],[6],[3,6],[],[],[]]
Output: [6,3,4,1,5,2,0,7]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Hai lần topo sort: 1. Sort các group với nhau (item-level edge i → j mà khác group → group-edge). 2. Trong mỗi group, sort các item của nó. 3. Concat kết quả: dùng order group, mỗi group ghi tất cả item của nó theo order item.

Pre-processing: mọi item có group[i] == -1 → gán group mới riêng để tránh “không nhóm” ảnh hưởng.

Code Python 3

from collections import defaultdict, deque
from typing import List

class Solution:
    def sortItems(
        self, n: int, m: int,
        group: List[int], beforeItems: List[List[int]]
    ) -> List[int]:
        # Gán group riêng cho item -1.
        for i in range(n):
            if group[i] == -1:
                group[i] = m
                m += 1

        item_graph = defaultdict(list)
        item_indeg = [0] * n
        group_graph = defaultdict(set)
        group_indeg = defaultdict(int)

        for cur, befores in enumerate(beforeItems):
            for prev in befores:
                item_graph[prev].append(cur)
                item_indeg[cur] += 1
                if group[prev] != group[cur]:
                    if group[cur] not in group_graph[group[prev]]:
                        group_graph[group[prev]].add(group[cur])
                        group_indeg[group[cur]] += 1

        def topo(nodes, graph, indeg) -> List[int]:
            queue = deque(x for x in nodes if indeg[x] == 0)
            out: list = []
            while queue:
                x = queue.popleft()
                out.append(x)
                for nb in graph[x]:
                    indeg[nb] -= 1
                    if indeg[nb] == 0:
                        queue.append(nb)
            return out if len(out) == len(nodes) else []

        item_order = topo(range(n), item_graph, item_indeg)
        if not item_order:
            return []
        group_order = topo(range(m), group_graph, group_indeg)
        if not group_order:
            return []

        # Gom theo group theo thứ tự group, các item trong group giữ thứ tự item_order.
        bucket: dict[int, list[int]] = defaultdict(list)
        for item in item_order:
            bucket[group[item]].append(item)
        result: list[int] = []
        for g in group_order:
            result.extend(bucket[g])
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


13.5 Sequence Reconstruction (LC 444)

Đề bài

Cho nums (1 hoán vị của 1..n) và sequences (list các sub-sequence). Hãy kiểm tra nums có phải là topological order duy nhất suy ra từ sequences hay không.

Ví dụ

Input:  nums = [1, 2, 3], sequences = [[1,2],[1,3]]
Output: False
Giải thích: từ [1,2] và [1,3] → có thể là [1,2,3] hoặc [1,3,2] → không unique.

Input:  nums = [1, 2, 3], sequences = [[1,2],[1,3],[2,3]]
Output: True

Ràng buộc

Clarifying questions

Hướng tiếp cận

Chạy Kahn’s. Để unique, mỗi level chỉ có đúng 1 node indeg == 0 — nếu có ≥ 2 ⇒ có nhiều topo order ⇒ False. Đồng thời thứ tự pop phải khớp nums.

Code Python 3

from collections import defaultdict, deque
from typing import List

class Solution:
    def sequenceReconstruction(self, nums: List[int], sequences: List[List[int]]) -> bool:
        n = len(nums)
        graph = defaultdict(set)
        indeg = [0] * (n + 1)
        for seq in sequences:
            for i in range(len(seq) - 1):
                u, v = seq[i], seq[i + 1]
                if v not in graph[u]:
                    graph[u].add(v)
                    indeg[v] += 1

        queue = deque(i for i in range(1, n + 1) if indeg[i] == 0)
        idx = 0
        while queue:
            if len(queue) > 1:
                return False              # >1 lựa chọn → không unique
            x = queue.popleft()
            if nums[idx] != x:
                return False              # khác thứ tự nums
            idx += 1
            for nb in graph[x]:
                indeg[nb] -= 1
                if indeg[nb] == 0:
                    queue.append(nb)
        return idx == n

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


13.6 Parallel Courses (LC 1136)

Đề bài

Cho n khoá học và relations [a, b] (học a xong rồi học b). Mỗi semester bạn có thể học bất kỳ số khoá miễn đã hoàn thành prerequisite. Trả về số semester tối thiểu để học hết, hoặc -1 nếu có chu trình.

Ví dụ

Input:  n=3, relations=[[1,3],[2,3]]
Output: 2
Giải thích: Semester 1 học [1,2], semester 2 học [3]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Kahn’s BFS, nhưng đếm theo level (semester). Mỗi vòng outer of BFS xử lý toàn bộ queue hiện tại = các khoá có thể học cùng semester.

Code Python 3

from collections import defaultdict, deque
from typing import List

class Solution:
    def minimumSemesters(self, n: int, relations: List[List[int]]) -> int:
        graph = defaultdict(list)
        indeg = [0] * (n + 1)
        for u, v in relations:
            graph[u].append(v)
            indeg[v] += 1

        queue = deque(i for i in range(1, n + 1) if indeg[i] == 0)
        taken = 0
        semesters = 0
        while queue:
            semesters += 1
            for _ in range(len(queue)):
                u = queue.popleft()
                taken += 1
                for v in graph[u]:
                    indeg[v] -= 1
                    if indeg[v] == 0:
                        queue.append(v)
        return semesters if taken == n else -1

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Topo + DP framing (cầu nối sang Chương 43)

Sequence Reconstruction (LC 444) — vì sao queue phải luôn có ≤ 1 phần tử?

Alien Dictionary (LC 269) — invalid prefix case

Nếu w_iprefix của w_{i-1} (vd ["abc", "ab"]), từ điển không hợp lệ → return "". Kiểm tra TRƯỚC khi build edges (đừng quên break đúng chỗ).

Sort Items by Groups (LC 1203) — DAG 2 lớp

items 5,6 ∈ groupA   items 7,8 ∈ groupB   items 9 ∈ -1 (riêng)

Item DAG:  5 → 6,  7 → 8,  6 → 7   (intra + cross-group)
Group DAG: A → B   (vì 6 → 7 mà 6 ∈ A, 7 ∈ B)

→ Topo group order → trong mỗi group topo item order.

Chương 14 — Interval

Interval (khoảng [start, end]) là pattern bao trùm nhiều bài calendar/scheduling/booking quan trọng. Hai chương 4 (Sorting) đã chạm qua Merge IntervalsMeeting Rooms II; chương này xoáy sâu vào 8 mẫu thao tác trên interval (merge, insert, intersection, overlap, free time) — đây là pattern không thể tránh khi phỏng vấn các công ty lịch (Google Calendar) và đặt phòng (Airbnb, Booking).

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

4 thao tác chuẩn trên 2 interval A = [a₁, a₂], B = [b₁, b₂]:

1. Tách rời (disjoint):   A.end < B.start  →  A trước B, không giao
2. Chạm điểm (touch):     A.end == B.start →  liền kề, có thể merge tuỳ đề
3. Giao một phần:         A.start < B.start ≤ A.end < B.end
4. Chứa nhau (contain):   A.start ≤ B.start ≤ B.end ≤ A.end

Template code

from typing import List

# 1) Merge 2 interval có giao nhau
def merge_two(a, b):
    return [min(a[0], b[0]), max(a[1], b[1])]


# 2) Check overlap (kể cả chỉ chạm điểm)
def overlaps(a, b) -> bool:
    return a[0] <= b[1] and b[0] <= a[1]


# 3) Sweep line: cùng pattern cho mọi bài "đếm overlap tối đa"
events: List[tuple[int, int]] = []
for s, e in intervals:
    events.append((s, +1))       # mở
    events.append((e, -1))       # đóng
events.sort()
cur = peak = 0
for _, delta in events:
    cur += delta
    peak = max(peak, cur)

Bài tự luyện cuối chương


14.1 Merge Intervals (LC 56) — recap

Đã giải đầy đủ ở Chương 4.2 dưới góc Sorting. Ở đây mình tóm tắt nhanh dưới lens “interval” và mở rộng follow-up.

Đề bài

Gộp các khoảng giao nhau. [1,3][2,6][1,6].

Hướng tiếp cận

Sort theo start. Duyệt 1 lượt, giữ last = interval cuối đã thêm vào kết quả. Nếu cur.start <= last.endlast.end = max(last.end, cur.end); ngược lại push cur mới.

Code Python 3

class Solution:
    def merge(self, intervals):
        intervals.sort(key=lambda x: x[0])
        result = []
        for cur in intervals:
            if result and cur[0] <= result[-1][1]:
                result[-1][1] = max(result[-1][1], cur[1])
            else:
                result.append(cur[:])
        return result

Phân tích độ phức tạp

Bình luận thêm cho góc interval

Bài tự luyện liên quan


14.2 Insert Interval (LC 57)

Đề bài

Cho mảng intervals đã sort theo start và không giao nhau. Chèn newInterval vào và merge nếu cần.

Ví dụ

Input:  intervals = [[1,3],[6,9]], newInterval = [2,5]
Output: [[1,5],[6,9]]

Input:  intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8]
Output: [[1,2],[3,10],[12,16]]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cách 1 — O(n) duyệt 1 lượt, 3 giai đoạn.

  1. Trước newInterval: đẩy hết các interval có end < newInterval.start.
  2. Giao nhau: với các interval có start <= newInterval.end, mở rộng newInterval (start = min, end = max). Cuối giai đoạn, push newInterval.
  3. Sau newInterval: đẩy phần còn lại.

Cách 2 — Concat + merge (gọi lại bài 14.1). Đơn giản nhưng O(n log n) cho sort thừa.

Hình minh hoạ với intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], new = [4,8]:

Trục số:
1   3   5   6  7  8  10        12      16
├─┤ ├───┤ ├─┤  ├──┤            ├──────┤
        ├──────────┤  new = [4, 8]

Giai đoạn 1 (end < 4):  [1, 2]
                         result = [[1,2]]

Giai đoạn 2 (start <= 8):
  [3, 5]: mở rộng newInterval = [min(4,3), max(8,5)] = [3, 8]
  [6, 7]: mở rộng = [3, 8]
  [8, 10]: mở rộng = [3, 10]
  Push [3, 10]
                         result = [[1,2], [3,10]]

Giai đoạn 3: còn [12, 16]
                         result = [[1,2], [3,10], [12,16]]  ✓

Code Python 3

from typing import List

class Solution:
    def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]:
        result: list[list[int]] = []
        i, n = 0, len(intervals)
        # 1) Trước newInterval.
        while i < n and intervals[i][1] < newInterval[0]:
            result.append(intervals[i])
            i += 1
        # 2) Giao nhau — mở rộng newInterval.
        while i < n and intervals[i][0] <= newInterval[1]:
            newInterval[0] = min(newInterval[0], intervals[i][0])
            newInterval[1] = max(newInterval[1], intervals[i][1])
            i += 1
        result.append(newInterval)
        # 3) Sau newInterval.
        while i < n:
            result.append(intervals[i])
            i += 1
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


14.3 Non-overlapping Intervals (LC 435)

Đề bài

Cho mảng intervals. Trả về số interval tối thiểu cần xoá để các interval còn lại không giao nhau.

Ví dụ

Input:  intervals = [[1,2],[2,3],[3,4],[1,3]]
Output: 1
Giải thích: xoá [1,3] → còn [1,2],[2,3],[3,4] không giao nhau.

Input:  intervals = [[1,2],[1,2],[1,2]]
Output: 2

Input:  intervals = [[1,2],[2,3]]
Output: 0              (đã không giao, không cần xoá)

(mỗi phần tử [start, end] biểu diễn khoảng nửa-mở [start, end))

Ràng buộc

Clarifying questions

Hướng tiếp cận

Greedy — sort theo end tăng dần. Giữ interval có end nhỏ nhất → giữ được càng nhiều “không gian” về sau cho các interval tiếp theo.

Pseudocode: - Sort theo end. - Giữ last_end = -∞. Với mỗi interval [s, e]: - Nếu s >= last_end → giữ (không overlap), last_end = e. - Ngược lại → đếm xoá.

Tại sao sort theo end, không phải start? Greedy hoạt động vì: “luôn chọn interval có end sớm nhất” cho phép phần còn lại có nhiều “free time” hơn — chứng minh quy nạp.

Hình minh hoạ với [[1,2],[2,3],[3,4],[1,3]]:

Sort theo end:  [[1,2], [2,3], [1,3], [3,4]]
                       end=2   end=3   end=3   end=4

Duyệt:
  [1,2]: start=1 >= -inf → giữ; last_end=2          giữ: 1
  [2,3]: start=2 >= 2    → giữ; last_end=3          giữ: 2
  [1,3]: start=1 < 3     → xoá                       xoá: 1
  [3,4]: start=3 >= 3    → giữ; last_end=4          giữ: 3

Giữ 3, xoá 1 → đáp án = 1.

Code Python 3

from typing import List

class Solution:
    def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
        if not intervals:
            return 0
        intervals.sort(key=lambda x: x[1])
        kept = 1
        last_end = intervals[0][1]
        for s, e in intervals[1:]:
            if s >= last_end:
                kept += 1
                last_end = e
        return len(intervals) - kept

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


14.4 Meeting Rooms II (LC 253) — recap

Đã giải đầy đủ ở Chương 4.4. Ở đây chỉ tóm tắt và liên hệ.

Đề bài

Tìm số phòng tối thiểu để chứa tất cả meeting.

Hướng tiếp cận

3 cách (heap, sweep line events, chronological 2-pointer) — tất cả đều O(n log n). Sweep line là pattern interval ngôn ngữ chuẩn.

Code Python 3

from typing import List

class Solution:
    def minMeetingRooms(self, intervals: List[List[int]]) -> int:
        events = [(s, +1) for s, _ in intervals] + [(e, -1) for _, e in intervals]
        events.sort(key=lambda x: (x[0], x[1]))
        cur = peak = 0
        for _, d in events:
            cur += d
            peak = max(peak, cur)
        return peak

Phân tích độ phức tạp

Bình luận thêm

Bài tự luyện liên quan


14.5 Minimum Number of Arrows to Burst Balloons (LC 452)

Đề bài

Cho mảng các balloon [x_start, x_end] (mỗi balloon là 1 interval trên trục x). Một mũi tên bắn thẳng đứng tại x = X sẽ làm nổ tất cả balloon có x_start <= X <= x_end. Tìm số mũi tên tối thiểu để nổ hết.

Ví dụ

Input:  points = [[10,16],[2,8],[1,6],[7,12]]
        (mỗi phần tử [xstart, xend] biểu diễn 1 quả bóng nằm trong khoảng đóng [xstart, xend])
Output: 2   (cần ít nhất 2 mũi tên: bắn x=6 nổ [1,6] và [2,8]; bắn x=11 nổ [7,12] và [10,16])
Giải thích:
  1 mũi tại x = 6 nổ [1,6] và [2,8].
  1 mũi tại x = 11 nổ [7,12] và [10,16].

Ràng buộc

Clarifying questions

Hướng tiếp cận

Tương đương bài 14.3 (Non-overlapping Intervals): mỗi mũi tên ứng với 1 nhóm balloon có giao chung. Đếm nhóm = số mũi tên.

Greedy sort theo end y hệt 14.3: - Sort balloons theo end. - Giữ last_end = -∞. Với mỗi balloon [s, e]: - Nếu s > last_end → cần mũi mới; last_end = e. - Ngược lại → balloon này được nổ chung với mũi hiện tại.

Code Python 3

from typing import List

class Solution:
    def findMinArrowShots(self, points: List[List[int]]) -> int:
        if not points:
            return 0
        points.sort(key=lambda x: x[1])
        arrows = 1
        last_end = points[0][1]
        for s, e in points[1:]:
            if s > last_end:        # không giao → cần mũi mới
                arrows += 1
                last_end = e
        return arrows

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


14.6 Employee Free Time (LC 759)

Đề bài

Cho schedule[i] = danh sách interval đại diện thời gian bận của employee i. Trả về tất cả interval free chung cho tất cả employee, sắp xếp tăng dần. (Không tính khoảng trước người đầu tiên bận và sau người cuối kết thúc.)

Ví dụ

Input:  schedule = [[[1,2],[5,6]],[[1,3]],[[4,10]]]
Output: [[3, 4]]
Giải thích:
  Hợp các bận: [1,3] (gồm [1,2] + [1,3]), [4,10] (gồm [5,6] + [4,10]).
  Free chung giữa các khoảng bận: [3, 4].

Ràng buộc

Clarifying questions

Hướng tiếp cận

Bước 1: Gộp tất cả interval bận thành 1 list không phụ thuộc employee. Bước 2: Sort theo start, merge (như bài 14.1). Bước 3: Kẽ hở giữa các merged intervals = free time.

Code Python 3

from typing import List

class Interval:
    def __init__(self, start: int = 0, end: int = 0):
        self.start, self.end = start, end


class Solution:
    def employeeFreeTime(self, schedule: "List[List[Interval]]") -> "List[Interval]":
        all_busy: list[tuple[int, int]] = []
        for emp_sched in schedule:
            for iv in emp_sched:
                all_busy.append((iv.start, iv.end))
        all_busy.sort()

        merged: list[list[int]] = []
        for s, e in all_busy:
            if merged and s <= merged[-1][1]:
                merged[-1][1] = max(merged[-1][1], e)
            else:
                merged.append([s, e])

        free = []
        for i in range(1, len(merged)):
            if merged[i - 1][1] < merged[i][0]:
                free.append(Interval(merged[i - 1][1], merged[i][0]))
        return free

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Interval convention checklist

  1. Đóng [s, e] hay nửa mở [s, e)?
  2. Sort theo start (Merge, Insert) hay sort theo end (Greedy, Min Arrows)?
  3. Sweep line tie-break: với event tại cùng thời điểm t:

Employee Free Time — visual

e1: |==1==|       |==3==|
e2:    |==2==|       |==4==|
sort all → merge ⇒ busy: [1∪2] [3∪4]
free = complement giữa các busy block

Recap lens (vì sao Merge & Meeting xuất hiện lại)


Chương 15 — Heap

Heap (Priority Queue) trả lời nhanh câu hỏi “phần tử lớn/nhỏ nhất bây giờ là gì?”. Hai thao tác cốt lõi pushpop đều O(log n). Heap giải gọn các bài: top-k, median trên data stream, merge k lists, scheduling. Pattern phổ biến nhất: heap kích thước k để duy trì k phần tử tốt nhất.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Python heapq chỉ có min-heap. Muốn max-heap → push -x.

Template code

import heapq

# 1) Min-heap cơ bản
heap: list[int] = []
heapq.heappush(heap, x)
top = heap[0]              # peek (không pop)
val = heapq.heappop(heap)

# 2) Max-heap bằng cách negate
heapq.heappush(heap, -x)
val = -heapq.heappop(heap)

# 3) K phần tử nhỏ nhất / lớn nhất từ list
smallest_k = heapq.nsmallest(k, arr)        # O(n log k)
largest_k = heapq.nlargest(k, arr)

# 4) Heapify O(n) — nhanh hơn n lần push
heapq.heapify(arr)

# 5) Heap với key tuỳ ý — push tuple (priority, payload)
heapq.heappush(heap, (priority, payload))

Bài tự luyện cuối chương


15.1 Kth Largest Element in an Array (LC 215)

Đề bài

Cho mảng nums và số k. Trả về phần tử lớn thứ k (1-indexed, theo thứ tự giảm dần). Có duplicate.

Ví dụ

Input:  nums = [3, 2, 1, 5, 6, 4], k = 2     → 5
Input:  nums = [3, 2, 3, 1, 2, 4, 5, 5, 6], k = 4 → 4

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cách 1 — Sort, O(n log n). sorted(nums)[-k]. Đơn giản nhất.

Cách 2 — Min-heap kích thước k, O(n log k).

Duyệt mảng, đẩy vào heap. Khi heap > k → pop. Cuối cùng heap[0] là Kth largest.

Cách 3 — Quickselect, O(n) trung bình.

Variant của quicksort: partition quanh pivot, recurse chỉ vào nửa chứa Kth.

Code Python 3

import heapq
from typing import List

class Solution:
    """Cách 2 — heap kích thước k."""

    def findKthLargest(self, nums: List[int], k: int) -> int:
        heap: list[int] = []
        for x in nums:
            heapq.heappush(heap, x)
            if len(heap) > k:
                heapq.heappop(heap)
        return heap[0]


class SolutionQS:
    """Cách 3 — quickselect, O(n) trung bình."""

    def findKthLargest(self, nums: List[int], k: int) -> int:
        # Kth largest = (n-k)-th smallest (0-indexed).
        target = len(nums) - k

        def partition(lo: int, hi: int) -> int:
            pivot = nums[hi]
            store = lo
            for i in range(lo, hi):
                if nums[i] < pivot:
                    nums[store], nums[i] = nums[i], nums[store]
                    store += 1
            nums[store], nums[hi] = nums[hi], nums[store]
            return store

        lo, hi = 0, len(nums) - 1
        while True:
            p = partition(lo, hi)
            if p == target:
                return nums[p]
            elif p < target:
                lo = p + 1
            else:
                hi = p - 1

Phân tích độ phức tạp

Cách Time Space
Sort O(n log n) O(1)
Heap size k O(n log k) O(k)
Quickselect O(n) avg, O(n²) worst O(1)

Bình luận

Bài tự luyện liên quan


15.2 Top K Frequent Words (LC 692)

Đề bài

Cho mảng words và số k. Trả về k từ thường gặp nhất. Nếu hoà tần suất, từ nào lexicographically smaller ưu tiên.

Ví dụ

Input:  words = ["i", "love", "leetcode", "i", "love", "coding"], k = 2
Output: ["i", "love"]
Giải thích: "i" và "love" cùng tần suất 2. "i" < "love" trong từ điển.

Input:  k = 4
Output: ["the", "is", "sunny", "day"]   (tie → sort lex)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Mấu chốt: comparator tie-break — tần suất cao hơn ưu tiên, nếu hoà thì từ lex nhỏ hơn ưu tiên.

Cách 1 — Sort, O(n log n). sorted(cnt.keys(), key=lambda w: (-cnt[w], w))[:k].

Cách 2 — Min-heap kích thước k, O(n log k).

Heap chứa tuple (freq, word). Vì heap min, ta muốn: - Tần suất thấp ở top (pop trước) → push thẳng freq. - Hoà → từ lex lớn hơn ở top (pop trước) → push word.

Trick: với từng tuple, ta dùng (-freq, word) rồi sort… không, dùng custom wrapper cũng được. Cách 1 đơn giản hơn nhiều.

Code Python 3

import heapq
from collections import Counter
from typing import List

class Solution:
    """Cách 1 — sort. Sạch nhất cho bài này."""

    def topKFrequent(self, words: List[str], k: int) -> List[str]:
        cnt = Counter(words)
        return sorted(cnt.keys(), key=lambda w: (-cnt[w], w))[:k]


class SolutionHeap:
    """Cách 2 — min-heap kích thước k."""

    def topKFrequent(self, words: List[str], k: int) -> List[str]:
        cnt = Counter(words)
        # Trong min-heap, muốn pop ra cái "thua kém" trước.
        # "Thua kém" = freq thấp hơn HOẶC (freq bằng + word lex lớn hơn).
        # Dùng tuple (freq, -word_chars) thì không tiện vì word là string.
        # Cách: dùng wrapper class.

        class Wrap:
            __slots__ = ("freq", "word")
            def __init__(self, f, w): self.freq, self.word = f, w
            def __lt__(self, other):
                if self.freq != other.freq:
                    return self.freq < other.freq      # min-heap → freq nhỏ ở top
                return self.word > other.word          # tie → word lex LỚN ở top
            def __eq__(self, other):
                return (self.freq, self.word) == (other.freq, other.word)

        heap: list[Wrap] = []
        for w, f in cnt.items():
            heapq.heappush(heap, Wrap(f, w))
            if len(heap) > k:
                heapq.heappop(heap)
        # Pop ra → ngược lại để có thứ tự đúng.
        return [heapq.heappop(heap).word for _ in range(k)][::-1]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


15.3 Find Median from Data Stream (LC 295)

Đề bài

Thiết kế class: - addNum(num): thêm num vào data stream. - findMedian(): trả về median hiện tại.

Median của n số: phần tử giữa (sorted), hoặc trung bình 2 phần tử giữa nếu n chẵn.

Ví dụ

Input (LC-style operation arrays):
  ops  = ["MedianFinder", "addNum", "addNum", "findMedian", "addNum", "findMedian"]
  args = [[],             [1],      [2],      [],           [3],      []]

Output: [null, null, null, 1.5, null, 2.0]

Giải thích:
  MedianFinder()    → khởi tạo, null
  addNum(1)         → null;  stream = [1]
  addNum(2)         → null;  stream = [1, 2]
  findMedian()      → 1.5    (mean của 2 phần tử giữa)
  addNum(3)         → null;  stream = [1, 2, 3]
  findMedian()      → 2.0    (phần tử giữa của 3 số sorted)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Trick 2 heap: - low = max-heap chứa nửa nhỏ (dưới median). - high = min-heap chứa nửa lớn (trên median).

Invariant: - Mọi phần tử trong low ≤ mọi phần tử trong high. - len(low) == len(high) hoặc len(low) == len(high) + 1.

addNum: 1. Push vào low (max-heap → push -num). 2. Đẩy top của low sang high (rebalance). 3. Nếu len(high) > len(low) → đẩy top của high về lại low.

findMedian: - len(low) > len(high)low[0] (negated). - Bằng nhau → trung bình của 2 top.

Hình minh hoạ với stream 1, 2, 3:

addNum(1):
  push low=-1. low=[-1]
  move -low[0]=1 to high. low=[], high=[1]
  len(high) > len(low) → move 1 back to low. low=[-1], high=[]

  median: low[0]=-(-1)=1.0

addNum(2):
  push -2 to low. low=[-1, -2] → heap-balanced [-2, -1] (max-heap → 2 on top)
  Hmm cần check: max-heap đơn giản, top = max = 2.
  move 2 to high. low=[-1], high=[2]
  len(high) <= len(low) OK.

  median: (low[0] + high[0]) / 2 = (1 + 2) / 2 = 1.5  ✓

addNum(3):
  push -3 to low. After push: low contains -1 and -3.
  Top of max-heap = 1 (smallest negated → 1).
  move 1 to high. low=[-3] (i.e., values {3}), high=[1, 2]
  Hmm, now high has 2 items, low has 1.
  Wait that's wrong invariant. Let me redo.

Thực ra trick đúng:

addNum(num):
  if not low or num <= -low[0]:
      heappush(low, -num)
  else:
      heappush(high, num)
  # rebalance: |len(low) - len(high)| <= 1, low >= high in size
  if len(low) > len(high) + 1:
      heappush(high, -heappop(low))
  elif len(high) > len(low):
      heappush(low, -heappop(high))

Code Python 3

import heapq

class MedianFinder:
    def __init__(self):
        self.low: list[int] = []   # max-heap (negated)
        self.high: list[int] = []  # min-heap

    def addNum(self, num: int) -> None:
        if not self.low or num <= -self.low[0]:
            heapq.heappush(self.low, -num)
        else:
            heapq.heappush(self.high, num)
        # Rebalance.
        if len(self.low) > len(self.high) + 1:
            heapq.heappush(self.high, -heapq.heappop(self.low))
        elif len(self.high) > len(self.low):
            heapq.heappush(self.low, -heapq.heappop(self.high))

    def findMedian(self) -> float:
        if len(self.low) > len(self.high):
            return float(-self.low[0])
        return (-self.low[0] + self.high[0]) / 2

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


15.4 K Closest Points to Origin (LC 973)

Đề bài

Cho mảng points, mỗi điểm [x, y]. Trả về k điểm gần origin nhất (Euclidean distance).

Ví dụ

Input:  points = [[1,3], [-2,2]], k = 1
Output: [[-2, 2]]
Giải thích: dist²(1,3) = 10, dist²(-2,2) = 8.

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cách heap size k — O(n log k). Max-heap chứa k điểm gần nhất; khi vượt k, pop điểm xa nhất.

Lưu ý: dùng dist² thay vì sqrt(dist) — tránh float, giữ thứ tự không đổi.

Code Python 3

import heapq
from typing import List

class Solution:
    def kClosest(self, points: List[List[int]], k: int) -> List[List[int]]:
        heap: list[tuple[int, list[int]]] = []   # (-dist², point) — max-heap
        for p in points:
            d2 = p[0] ** 2 + p[1] ** 2
            heapq.heappush(heap, (-d2, p))
            if len(heap) > k:
                heapq.heappop(heap)
        return [p for _, p in heap]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


15.5 Merge k Sorted Lists (LC 23)

Đề bài

Input: lists: List[Optional[ListNode]] — mảng k head của k singly linked list đã sort tăng dần. Gộp tất cả thành 1 linked list sort và trả về head mới.

Ví dụ

Input:  lists = [1→4→5, 1→3→4, 2→6]
Output: 1→1→2→3→4→4→5→6

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cách 1 — Min-heap k-way merge, O(N log k).

Heap chứa (val, idx, node) của head mỗi list. Pop cái nhỏ nhất, push hàng tiếp theo từ cùng list. Lặp đến hết.

idx là tiebreaker — Python không so sánh ListNode được khi val bằng.

Cách 2 — Merge từng cặp (Divide & Conquer), O(N log k).

Mergesort kiểu tournament: ghép cặp lists rồi merge, lặp lại. Cùng độ phức tạp.

Code Python 3

import heapq
from typing import List, Optional

class ListNode:
    def __init__(self, val=0, next=None):
        self.val, self.next = val, next


class Solution:
    """Cách 1 — min-heap k-way merge."""

    def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
        heap: list[tuple[int, int, ListNode]] = []
        for i, head in enumerate(lists):
            if head:
                heapq.heappush(heap, (head.val, i, head))

        dummy = ListNode()
        tail = dummy
        while heap:
            val, i, node = heapq.heappop(heap)
            tail.next = node
            tail = node
            if node.next:
                heapq.heappush(heap, (node.next.val, i, node.next))
        return dummy.next

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


15.6 Task Scheduler (LC 621)

Đề bài

Cho mảng tasks (chữ cái A-Z) và số n. Mỗi đơn vị thời gian thực hiện 1 task hoặc idle. 2 task giống nhau phải cách nhau ít nhất n đơn vị. Trả về số đơn vị thời gian tối thiểu để hoàn thành tất cả.

Ví dụ

Input:  tasks = ["A","A","A","B","B","B"], n = 2
Output: 8
Lịch:   A → B → idle → A → B → idle → A → B

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cách 1 — Greedy + max-heap, O(N log 26).

Mỗi tick: - Lấy task có freq cao nhất (heap top). Decrement freq, đẩy vào “cooldown queue” (end_time, freq). - Khi cooldown_queue.front().end_time <= now → pop và đẩy lại vào heap.

Cách 2 — Công thức trực tiếp, O(N).

Cho f_max = freq cao nhất, count_max = số task có freq này. Đáp án = max(n_total, (f_max - 1) * (n + 1) + count_max).

Trực giác: Task có freq cao nhất tạo ra f_max - 1 khoảng có độ dài n+1, cộng count_max cuối ở “row cuối”.

Code Python 3

from collections import Counter
from typing import List

class Solution:
    """Cách 2 — công thức O(N)."""

    def leastInterval(self, tasks: List[str], n: int) -> int:
        cnt = Counter(tasks)
        f_max = max(cnt.values())
        count_max = sum(1 for v in cnt.values() if v == f_max)
        slots = (f_max - 1) * (n + 1) + count_max
        return max(slots, len(tasks))


import heapq
from collections import deque

class SolutionHeap:
    """Cách 1 — heap + cooldown queue."""

    def leastInterval(self, tasks: List[str], n: int) -> int:
        heap = [-c for c in Counter(tasks).values()]   # max-heap (negated)
        heapq.heapify(heap)
        cooldown: deque[tuple[int, int]] = deque()     # (available_time, neg_count)
        time = 0
        while heap or cooldown:
            time += 1
            if heap:
                neg = heapq.heappop(heap) + 1          # giảm freq 1 đơn vị
                if neg < 0:
                    cooldown.append((time + n, neg))
            if cooldown and cooldown[0][0] == time:
                _, neg = cooldown.popleft()
                heapq.heappush(heap, neg)
        return time

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Heap vs Sort vs Quickselect

Yêu cầu Heap O(n log k) Sort O(n log n) Quickselect O(n) avg
Top-k khi n lớn, k nhỏ ✅ Tốt nhất OK ✅ Khi không cần thứ tự
Cần k phần tử đã sắp Cần sort thêm sau
Streaming (data đến từng phần)
Đảm bảo worst-case ❌ (worst O(n²))
Interview preference Mặc định, dễ giải thích Khi n nhỏ Khi push “linear time”

Median Finder invariant

max_heap (lo)      min_heap (hi)
  …,3,5,7,8  ←     ←  9,10,12,…
top = 8                top = 9

Task Scheduler 2 cách

Top K Frequent Words tie-break

Sort theo (-freq, word): tần số giảm dần, từ điển tăng dần.


Chương 16 — Greedy

Greedy = tham lam = mỗi bước chọn cái tốt nhất tại chỗ, hy vọng cộng dồn lại được kết quả tối ưu toàn cục. Pattern lừa dối ở chỗ: Greedy hợp lệ rất khó chứng minh. Trong phỏng vấn, bạn vừa phải đoán đúng “luật tham” vừa phải justify ngắn gọn vì sao nó tối ưu. Chương này dạy 6 bài kinh điển — học để có mẫu reasoning.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Trick chứng minh Greedy: 1. Exchange argument: giả sử có nghiệm tối ưu khác greedy, “hoán đổi” để biến nó thành greedy mà không xấu đi. 2. Cấu trúc matroid: ít gặp trong phỏng vấn nhưng đẹp về lý thuyết.

Khi Greedy sai → DP cứu: nếu hoán đổi không bảo toàn tối ưu, phải xét toàn cục → DP.

Template code

def greedy_template(items):
    items.sort(key=...)         # 90% bài greedy phải sort trước
    result = 0
    for x in items:
        if local_condition(x):
            result += x
            # ... cập nhật state ...
    return result

Bài tự luyện cuối chương


16.1 Jump Game (LC 55)

Đề bài

Cho mảng nums, nums[i] = số bước tối đa bạn có thể nhảy từ vị trí i. Bắt đầu ở i = 0. Trả về True nếu có thể tới i = n - 1.

Ví dụ

Input:  nums = [2, 3, 1, 1, 4]   → True
Giải thích: 0 → 1 → 4 hoặc 0 → 2 → 3 → 4

Input:  nums = [3, 2, 1, 0, 4]   → False
Giải thích: tới index 3 thì kẹt (nums[3]=0).

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — DFS từ 0, O(2^n). TLE.

Tối ưu — Greedy 1 lượt, O(n).

Duy trì farthest = vị trí xa nhất có thể tới đến bây giờ. Tại mỗi i: - Nếu i > farthest → không tới được i → return False. - Cập nhật farthest = max(farthest, i + nums[i]). - Nếu farthest >= n - 1 → return True.

Hình minh hoạ với [2, 3, 1, 1, 4]:

index :   0   1   2   3   4
nums  :   2   3   1   1   4

i=0: farthest = max(0, 0+2) = 2
i=1: 1 <= 2 OK; farthest = max(2, 1+3) = 4 → >= n-1=4 → True ✓

Code Python 3

from typing import List

class Solution:
    def canJump(self, nums: List[int]) -> bool:
        farthest = 0
        for i, x in enumerate(nums):
            if i > farthest:
                return False
            farthest = max(farthest, i + x)
            if farthest >= len(nums) - 1:
                return True
        return True

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


16.2 Jump Game II (LC 45)

Đề bài

Cùng thiết lập như 16.1, nhưng giả sử luôn tới được n - 1. Trả về số bước nhảy tối thiểu.

Ví dụ

Input:  nums = [2, 3, 1, 1, 4]
Output: 2
Giải thích: 0 → 1 → 4.

Input:  nums = [2, 3, 0, 1, 4]
Output: 2

Ràng buộc

Clarifying questions

Hướng tiếp cận

BFS theo lớp. Mỗi “level” của BFS = các vị trí có thể tới sau k bước.

Implementation Greedy 1 lượt: - current_end = ranh giới của level hiện tại. - farthest = xa nhất tới được trong level hiện tại. - Khi i == current_end (kết thúc level), bump jumps += 1 và set current_end = farthest.

Hình minh hoạ với [2, 3, 1, 1, 4]:

i=0:  farthest = max(0, 0+2) = 2
      i == current_end (=0) → jumps=1, current_end=2
i=1:  farthest = max(2, 1+3) = 4
i=2:  farthest = max(4, 2+1) = 4
      i == current_end (=2) → jumps=2, current_end=4
i=3:  ... (đã quá đủ, ta dừng khi current_end >= n-1)

Đáp án: 2  ✓

Code Python 3

from typing import List

class Solution:
    def jump(self, nums: List[int]) -> int:
        jumps = 0
        current_end = 0
        farthest = 0
        for i in range(len(nums) - 1):
            farthest = max(farthest, i + nums[i])
            if i == current_end:
                jumps += 1
                current_end = farthest
                if current_end >= len(nums) - 1:
                    break
        return jumps

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


16.3 Gas Station (LC 134)

Đề bài

n trạm xăng vòng tròn, trạm igas[i] xăng. Đi từ trạm i tới i+1 tốn cost[i]. Tìm chỉ số trạm bắt đầu sao cho đi hết vòng được, hoặc -1 nếu không thể.

Ví dụ

Input:  gas = [1,2,3,4,5], cost = [3,4,5,1,2]
Output: 3
Giải thích: bắt đầu từ trạm 3.

Input:  gas = [2,3,4], cost = [3,4,3]
Output: -1

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — thử mỗi start, O(n²). Có thể TLE.

Tối ưu Greedy — O(n).

Điều kiện cần & đủ: sum(gas) >= sum(cost). Nếu vi phạm → -1.

Khi điều kiện thoả, chỉ tồn tại đúng 1 nghiệm (nếu mảng phân biệt). Tìm bằng: - Duyệt, giữ tank = balance hiện tại. - Nếu tank < 0 tại trạm i → mọi start ∈ [last_start..i] đều fail. Set start = i + 1, reset tank = 0.

Hình minh hoạ với gas = [1,2,3,4,5], cost = [3,4,5,1,2]:

i :    0     1     2     3     4
gas:   1     2     3     4     5
cost:  3     4     5     1     2
diff: -2    -2    -2    +3    +3

tank tích luỹ:
  i=0: tank = -2 → negative, reset start=1, tank=0
  i=1: tank = -2 → reset start=2, tank=0
  i=2: tank = -2 → reset start=3, tank=0
  i=3: tank = +3 OK
  i=4: tank = +6 OK

sum(diff) = -2-2-2+3+3 = 0 >= 0 → có nghiệm.
Đáp án: start=3 ✓

Code Python 3

from typing import List

class Solution:
    def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
        if sum(gas) < sum(cost):
            return -1
        start = 0
        tank = 0
        for i in range(len(gas)):
            tank += gas[i] - cost[i]
            if tank < 0:
                start = i + 1
                tank = 0
        return start

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


16.4 Assign Cookies (LC 455)

Đề bài

Cho mảng g[] (greed factor của trẻ) và s[] (size của cookie). Trẻ i hài lòng nếu nhận được cookie j với s[j] >= g[i]. Mỗi trẻ tối đa 1 cookie, mỗi cookie tối đa 1 trẻ. Tìm max số trẻ hài lòng.

Ví dụ

Input:  g = [1, 2, 3], s = [1, 1]   → 1
Input:  g = [1, 2], s = [1, 2, 3]   → 2

Ràng buộc

Clarifying questions

Hướng tiếp cận

Greedy kinh điển. Sort cả 2 mảng tăng dần. Two pointers i (trẻ), j (cookie). Đi qua cookie từ nhỏ đến lớn: nếu s[j] >= g[i] → trẻ i được cookie → i++; j++ luôn.

Justification: Đưa cookie nhỏ nhất đủ thoả cho trẻ greed nhỏ nhất — không “phí” cookie lớn. Exchange argument: nếu nghiệm tối ưu khác greedy, có thể đổi cookie giữa 2 trẻ mà không xấu đi.

Code Python 3

from typing import List

class Solution:
    def findContentChildren(self, g: List[int], s: List[int]) -> int:
        g.sort()
        s.sort()
        i = j = 0
        while i < len(g) and j < len(s):
            if s[j] >= g[i]:
                i += 1
            j += 1
        return i

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


16.5 Partition Labels (LC 763)

Đề bài

Cho chuỗi s. Chia s thành nhiều phần tối đa sao cho mỗi ký tự chỉ xuất hiện trong đúng 1 phần. Trả về độ dài của các phần.

Ví dụ

Input:  s = "ababcbacadefegdehijhklij"
Output: [9, 7, 8]
Giải thích:
  "ababcbaca" - chứa a, b, c.
  "defegde"   - chứa d, e, f, g.
  "hijhklij"  - chứa h, i, j, k, l.

Ràng buộc

Clarifying questions

Hướng tiếp cận

Greedy với “last occurrence”:

  1. Tìm last[ch] = chỉ số cuối của mỗi ký tự.
  2. Duyệt s, giữ end = max(end, last[s[i]]).

Hình minh hoạ với "ababcbacadefegdehijhklij":

last[a]=8, last[b]=5, last[c]=7, last[d]=14, last[e]=15, ...

i=0 (a): end = max(0, 8) = 8
i=1 (b): end = max(8, 5) = 8
...
i=8 (a): end = 8, i == end → phần 1: length 9 (0..8)
i=9 (d): end = 14
...
i=15 (e): end = 15, i == end → phần 2: length 7 (9..15)
...

Code Python 3

from typing import List

class Solution:
    def partitionLabels(self, s: str) -> List[int]:
        last = {ch: i for i, ch in enumerate(s)}
        result: list[int] = []
        start = end = 0
        for i, ch in enumerate(s):
            end = max(end, last[ch])
            if i == end:
                result.append(i - start + 1)
                start = i + 1
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


16.6 Candy (LC 135)

Đề bài

Cho mảng ratings. Phát kẹo cho n trẻ sao cho: 1. Mỗi trẻ ít nhất 1 kẹo. 2. Trẻ có rating cao hơn người hàng xóm (kề bên) thì được nhiều kẹo hơn.

Trả về số kẹo tối thiểu.

Ví dụ

Input:  ratings = [1, 0, 2]   → 5    (kẹo: [2, 1, 2])
Input:  ratings = [1, 2, 2]   → 4    (kẹo: [1, 2, 1])

Ràng buộc

Clarifying questions

Hướng tiếp cận

2 lượt qua mảng: - Lượt 1 (trái → phải): nếu ratings[i] > ratings[i-1]candies[i] = candies[i-1] + 1. - Lượt 2 (phải → trái): nếu ratings[i] > ratings[i+1]candies[i] = max(candies[i], candies[i+1] + 1).

Tổng candies = đáp án.

Hình minh hoạ với ratings = [1, 0, 2]:

ratings:    1   0   2

Lượt 1 (L→R), khởi tạo candies = [1, 1, 1]:
  i=1: r[1]=0 <= r[0]=1 → giữ 1
  i=2: r[2]=2 > r[1]=0  → candies[2] = candies[1]+1 = 2
  → candies = [1, 1, 2]

Lượt 2 (R→L):
  i=1: r[1]=0 <= r[2]=2 → giữ
  i=0: r[0]=1 > r[1]=0  → candies[0] = max(1, candies[1]+1) = max(1, 2) = 2
  → candies = [2, 1, 2]

Tổng: 2 + 1 + 2 = 5  ✓

Code Python 3

from typing import List

class Solution:
    def candy(self, ratings: List[int]) -> int:
        n = len(ratings)
        candies = [1] * n
        # Lượt 1.
        for i in range(1, n):
            if ratings[i] > ratings[i - 1]:
                candies[i] = candies[i - 1] + 1
        # Lượt 2.
        for i in range(n - 2, -1, -1):
            if ratings[i] > ratings[i + 1]:
                candies[i] = max(candies[i], candies[i + 1] + 1)
        return sum(candies)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

3 cách “justify greedy” trong phỏng vấn

  1. Exchange argument: Giả sử có lời giải tối ưu khác. Swap 1 lựa chọn của nó về greedy, chỉ ra cost không tăng. Lặp → tối ưu trùng greedy.
  2. Stay-ahead: Tại mọi bước k, lời giải greedy “tiến” ít nhất bằng mọi lời giải khác. Quy nạp → toàn cục tối ưu.
  3. Cut/Matroid property (cho MST, Greedy Choice): mọi đáp số tối ưu chứa được cạnh nhẹ nhất của một cut.

Jump Game (LC 55) — farthest-reach invariant

i:        0  1  2  3  4
nums:    [2, 3, 1, 1, 4]
reach:    2  4  4  4  ≥4 ✅

Gas Station (LC 134) — vì sao bỏ cả segment failed?

Nếu khởi đầu từ s mà fail tại i (tank âm), thì mọi điểm k trong [s, i] cũng fail khi xuất phát từ k (vì từ s đến k mình đã có dư xăng, vẫn không kéo được tới i+1). ⇒ Tiếp tục thử i+1.

Candy (LC 135) — 2-pass invariant


Chương 17 — Divide and Conquer

Divide and Conquer (D&C): chia bài thành các bài con độc lập, giải đệ quy, rồi kết hợp. Khác với DP (bài con có thể overlap), D&C bài con không trùng nhau. Các thuật toán nổi tiếng (merge sort, quicksort, fast power, closest pair) đều là D&C.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Master theorem cheat: - a = b, f(n) = O(n)T(n) = O(n log n) (merge sort). - a = 1, b = 2, f(n) = O(1)T(n) = O(log n) (binary search). - a = 2, b = 2, f(n) = O(n)T(n) = O(n log n).

Template code

def divide_conquer(arr, lo: int, hi: int):
    if lo >= hi:                    # base case
        return base_value(arr[lo])
    mid = (lo + hi) // 2
    left = divide_conquer(arr, lo, mid)
    right = divide_conquer(arr, mid + 1, hi)
    return combine(left, right)     # phần "conquer" — thường O(n)

Bài tự luyện cuối chương


17.1 Maximum Subarray — D&C version (LC 53)

Đề bài

Cho nums, tìm dãy con liên tụctổng max. Trả về tổng.

Ví dụ

Input:  nums = [-2,1,-3,4,-1,2,1,-5,4]
Output: 6 (dãy [4,-1,2,1])

Ràng buộc

Clarifying questions

Hướng tiếp cận

3 cách: 1. Kadane’s (DP 1D, O(n)) — chuẩn cho LC 53. 2. Prefix sum + min, O(n). 3. Divide & Conquer, O(n log n)focus của chương này.

D&C insight: chia mảng [lo, hi] làm 2 nửa [lo, mid], [mid+1, hi]. Max subarray hoặc: - Hoàn toàn trong nửa trái (đệ quy). - Hoàn toàn trong nửa phải (đệ quy). - Qua giữa — bắt đầu trong trái, kết thúc trong phải.

Crossing sum tính O(n): - left_max = max của nums[mid] + nums[mid-1] + ... cộng dồn về trái. - right_max = tương tự về phải. - crossing = left_max + right_max.

Max của 3 lựa chọn = đáp án bài con.

Code Python 3

from typing import List

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:

        def dc(lo: int, hi: int) -> int:
            if lo == hi:
                return nums[lo]
            mid = (lo + hi) // 2

            left = dc(lo, mid)
            right = dc(mid + 1, hi)

            # Crossing — bắt buộc bao gồm cả nums[mid] và nums[mid+1].
            left_max = -float('inf')
            cur = 0
            for i in range(mid, lo - 1, -1):
                cur += nums[i]
                left_max = max(left_max, cur)
            right_max = -float('inf')
            cur = 0
            for i in range(mid + 1, hi + 1):
                cur += nums[i]
                right_max = max(right_max, cur)
            crossing = left_max + right_max

            return max(left, right, crossing)

        return dc(0, len(nums) - 1)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


17.2 Merge Sort & Count Inversions (bài kinh điển)

Đề bài

Tính số inversion trong mảng nums: cặp (i, j) với i < jnums[i] > nums[j].

Ví dụ

Input:  nums = [2, 4, 1, 3, 5]
Output: 3
(số cặp i < j với nums[i] > nums[j]; cụ thể: (2,1), (4,1), (4,3))

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — O(n²). Đếm mọi cặp.

D&C bằng Merge Sort — O(n log n).

Trong khi merge 2 nửa đã sort: - Khi lấy từ nửa phải (vì nums_right[j] < nums_left[i]), mọi phần tử còn lại trong nửa trái đều tạo inversion với nums_right[j]. → cộng (len_left - i) vào counter.

Hình minh hoạ với [2, 4, 1, 3, 5]:

Chia:    [2, 4, 1]  |  [3, 5]
Đệ quy   [2, 4]  [1]    [3]  [5]
trái:    [2] [4]
         merge [2,4]: 0 inversion
       merge [2,4] với [1]:
         lấy 1 từ phải → còn 2 phần tử [2,4] trong trái lớn hơn → +2 inversion
         kết quả: [1, 2, 4]; total = 2

phải:    merge [3] [5]: 0 inversion → [3, 5]

merge [1, 2, 4] với [3, 5]:
  1 (trái), 2 (trái), 4 (trái), 3 (phải) — khi lấy 3, 4 vẫn ở trái còn lớn → +1
  4, 5 → 5 từ phải, không còn trái → 0 inv

Tổng: 2 + 1 = 3  ✓

Code Python 3

from typing import List

class Solution:
    def countInversions(self, nums: List[int]) -> int:

        def merge_sort(arr: List[int]) -> tuple[List[int], int]:
            if len(arr) <= 1:
                return arr, 0
            mid = len(arr) // 2
            left, inv_l = merge_sort(arr[:mid])
            right, inv_r = merge_sort(arr[mid:])
            merged, inv_split = merge(left, right)
            return merged, inv_l + inv_r + inv_split

        def merge(L: List[int], R: List[int]) -> tuple[List[int], int]:
            i = j = inv = 0
            out: List[int] = []
            while i < len(L) and j < len(R):
                if L[i] <= R[j]:
                    out.append(L[i]); i += 1
                else:
                    out.append(R[j]); j += 1
                    inv += len(L) - i        # các L[i..] đều > R[j]
            out.extend(L[i:])
            out.extend(R[j:])
            return out, inv

        _, total = merge_sort(nums)
        return total

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


17.3 Quickselect — Kth Largest (LC 215) — recap

Đã giải đầy đủ ở Chương 15.1. Ở đây tóm tắt lens D&C.

Đề bài

Cho nums, tìm phần tử lớn thứ k (1-indexed). Pattern D&C giải O(n) trung bình.

Ví dụ

Input:  nums = [3, 2, 1, 5, 6, 4], k = 2
Output: 5

Input:  nums = [3, 2, 3, 1, 2, 4, 5, 5, 6], k = 4
Output: 4

Hướng tiếp cận

Quickselect là D&C đặc biệt — đệ quy chỉ 1 nửa (nửa chứa Kth) thay vì cả 2. Vì vậy: - T(n) = T(n/2) + O(n) = O(n) trung bình (không phải O(n log n) như Quicksort). - Worst-case O(n²) nếu pivot xấu — tránh bằng random pivot.

Lưu ý chiều index: partition bên dưới sắp theo thứ tự tăng dần, nên pvị trí thứ p từ nhỏ nhất (0-based). Kth largest đứng ở vị trí len(nums) - k (0-based) sau khi sort. Vì vậy hàm public LC nhận k 1-indexed và chuyển thành target = len(nums) - k trước khi gọi quickselect.

Code Python 3

import random
from typing import List


class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        # Kth largest 1-indexed ⇔ vị trí (n - k) 0-based sau khi sort tăng dần.
        target = len(nums) - k
        return self._quickselect(nums, 0, len(nums) - 1, target)

    def _quickselect(self, arr: List[int], lo: int, hi: int, target: int) -> int:
        while lo <= hi:
            p = self._partition(arr, lo, hi)
            if p == target:
                return arr[p]
            elif p < target:
                lo = p + 1
            else:
                hi = p - 1
        return -1  # unreachable theo đề bài

    def _partition(self, arr: List[int], lo: int, hi: int) -> int:
        # Random pivot để tránh worst-case O(n^2) trên input đã sort.
        rand = random.randint(lo, hi)
        arr[rand], arr[hi] = arr[hi], arr[rand]
        pivot = arr[hi]
        store = lo
        for i in range(lo, hi):
            if arr[i] < pivot:
                arr[store], arr[i] = arr[i], arr[store]
                store += 1
        arr[store], arr[hi] = arr[hi], arr[store]
        return store

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


17.4 Pow(x, n) (LC 50) — recap

Đã giải đầy đủ ở Chương 3.2. Ở đây tóm tắt lens D&C.

Đề bài

Tính x^n với x thực, n nguyên (có thể âm). Pattern D&C cho O(log n).

Ví dụ

Input:  x = 2.00000, n = 10
Output: 1024.00000

Input:  x = 2.10000, n = 3
Output: 9.26100

Input:  x = 2.00000, n = -2
Output: 0.25000   (= 1 / 2^2)

Hướng tiếp cận

x^n = (x^(n/2))^2 (chẵn) hoặc x · (x^((n-1)/2))^2 (lẻ). Mỗi bước chia nửa nT(n) = T(n/2) + O(1) = O(log n).

Code Python 3

class Solution:
    def myPow(self, x: float, n: int) -> float:
        if n == 0: return 1.0
        if n < 0: return 1.0 / self.myPow(x, -n)
        half = self.myPow(x, n // 2)
        return half * half if n % 2 == 0 else half * half * x

Phân tích độ phức tạp

Bài tự luyện liên quan


17.5 Different Ways to Add Parentheses (LC 241)

Đề bài

Cho biểu thức s chứa số và toán tử +, -, *. Trả về tất cả giá trị có thể có sau khi thêm dấu ngoặc theo các cách khác nhau.

Ví dụ

Input:  s = "2*3-4*5"
Output: [-34, -14, -10, -10, 10]
Giải thích:
  (2*(3-(4*5))) = -34
  ((2*3)-(4*5)) = -14
  ((2*(3-4))*5) = -10
  (2*((3-4)*5)) = -10
  (((2*3)-4)*5) = 10

Ràng buộc

Clarifying questions

Hướng tiếp cận

Tại mỗi toán tử op, chia s thành 2 phần (trái, phải). Đệ quy tính all values của mỗi phần. Kết hợp: với mỗi (l, r) cặp giá trị, thêm l op r vào kết quả.

Memoize theo substring để tránh tính lại.

Hình minh hoạ với "2*3-4":

"2*3-4":
  Tại '*' (pos 1):
    Trái = "2" → [2]
    Phải = "3-4" → đệ quy:
      Tại '-' (pos 1):
        Trái = "3" → [3]
        Phải = "4" → [4]
        → [3 - 4] = [-1]
      → "3-4" có [-1]
    Kết hợp: 2 * -1 = -2

  Tại '-' (pos 3):
    Trái = "2*3" → đệ quy → [6]
    Phải = "4" → [4]
    → [6 - 4] = [2]

  → "2*3-4" có [-2, 2]

Code Python 3

from functools import cache
from typing import List

class Solution:
    def diffWaysToCompute(self, s: str) -> List[int]:

        @cache
        def compute(expr: str) -> tuple[int, ...]:
            if expr.isdigit():
                return (int(expr),)
            result: list[int] = []
            for i, ch in enumerate(expr):
                if ch in '+-*':
                    for l in compute(expr[:i]):
                        for r in compute(expr[i + 1:]):
                            if ch == '+': result.append(l + r)
                            elif ch == '-': result.append(l - r)
                            else: result.append(l * r)
            return tuple(result)

        return list(compute(s))

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


17.6 Closest Pair of Points (bài kinh điển)

Đề bài

Cho n điểm trên mặt phẳng, tìm cặp 2 điểm gần nhất (Euclidean distance). Phải O(n log n).

Ví dụ

Input:  points = [(0,0), (1,1), (4,5), (2,2), (8,8)]
Output: ((0,0), (1,1))  dist = sqrt(2)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — O(n²). So mọi cặp.

D&C — O(n log n).

  1. Sort points theo x.
  2. Chia làm 2 nửa tại x_mid.
  3. Đệ quy tìm d_left, d_right.
  4. d = min(d_left, d_right).
  5. Strip check: xét các điểm có |x - x_mid| < d, sort theo y, mỗi điểm chỉ cần so với ≤ 7 điểm kế tiếp trong strip (hình học chứng minh).
  6. Trả về min.

Đây là bài kinh điển trong giáo trình CS — phần lớn không xuất hiện trên LeetCode chuẩn, nhưng được hỏi ở phỏng vấn Big Tech như một bài tối ưu hoá brute force O(n²).

Code Python 3

import math
from typing import List, Tuple

Point = Tuple[float, float]

class Solution:
    def closestPair(self, points: List[Point]) -> float:
        # Sort by x trước.
        pts_x = sorted(points)

        def dist(p: Point, q: Point) -> float:
            return math.hypot(p[0] - q[0], p[1] - q[1])

        def dc(pts: List[Point]) -> float:
            n = len(pts)
            if n <= 3:
                return min(dist(pts[i], pts[j])
                           for i in range(n) for j in range(i + 1, n))
            mid = n // 2
            mid_x = pts[mid][0]
            d_left = dc(pts[:mid])
            d_right = dc(pts[mid:])
            d = min(d_left, d_right)

            # Strip.
            strip = sorted([p for p in pts if abs(p[0] - mid_x) < d],
                           key=lambda p: p[1])
            for i in range(len(strip)):
                # Chứng minh: chỉ cần kiểm tra ≤ 7 láng giềng theo y.
                for j in range(i + 1, min(i + 8, len(strip))):
                    d = min(d, dist(strip[i], strip[j]))
            return d

        return dc(pts_x)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

D&C recurrence framework

solve(P):
    if |P| ≤ threshold: brute()
    chia P → P1, P2 (gần đều)
    A1 = solve(P1)
    A2 = solve(P2)
    return combine(A1, A2, cross_information)

Recurrence tree (merge sort / inversion count)

n  ━━━ split ━━━ n/2, n/2 ━━━ split ━━━ n/4, n/4, n/4, n/4 ━━━ ...
                                                              depth = log n
combine cost = O(n) mỗi tầng × log n tầng ⇒ O(n log n)

Quickselect vs Sort vs Heap (LC 215 — Kth Largest)

Quickselect Heap size k Sort
Avg O(n) O(n log k) O(n log n)
Worst O(n²) (random pivot ↘) O(n log k) O(n log n)
Tại chỗ ✗ extra heap
Code trung bình ngắn ngắn nhất

Closest Pair (LC tham khảo) — thiết lập


Chương 18 — Monotonic Queue + Stack

Monotonic stack/deque = stack/deque mà các phần tử giữ đơn điệu (tăng hoặc giảm) khi đi qua. Đây là “vũ khí” mạnh nhất để giải các bài “next greater/smaller”, “range max/min in sliding window”, “largest rectangle”. Bài 8.5 (Daily Temperatures) đã teaser một chút — chương này đi sâu.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Khi nào tăng, khi nào giảm? - Stack tăng dần (top là max của stack): hữu ích cho “previous smaller”. - Stack giảm dần (top là min): cho “previous greater” / “next greater”.

Invariant của Monotonic Stack/Deque

Sự khác biệt giữa “stack thường” và “monotonic stack” là invariant — quy luật luôn đúng tại mọi thời điểm. Hiểu invariant = hiểu cả pattern.

Monotonic decreasing stack (giảm dần từ đáy → đỉnh)

Invariant tại mọi thời điểm:
   bottom → top
   [v0  ,  v1  ,  v2  ,  ...  ,  vk]
   v0 ≥ v1 ≥ v2 ≥ ... ≥ vk         (giảm dần)

Khi thêm value mới x:
   - Trong khi stack[-1] < x: pop (vì stack[-1] không còn cơ hội là "next greater")
   - Push x

Diễn giải:
   • Stack giữ các "ứng viên đang chờ next greater".
   • Khi gặp x lớn hơn top → top tìm được answer (= x) → pop.
   • Mỗi index push 1 lần, pop tối đa 1 lần → O(n) total.

Ví dụ trace với arr = [73, 74, 75, 71, 69, 72, 76]:

Bước  i   v   Action                                Stack (index, value)
─────────────────────────────────────────────────────────────────────────
init                                                []
1     0   73  push                                  [(0, 73)]
2     1   74  74 > 73 → pop (0,73), result[0]=74    [(1, 74)]
3     2   75  75 > 74 → pop (1,74), result[1]=75    [(2, 75)]
4     3   71  push                                  [(2, 75), (3, 71)]
5     4   69  push                                  [(2, 75), (3, 71), (4, 69)]
6     5   72  72 > 69 → pop (4,69), result[4]=72    [(2, 75), (3, 71), (5, 72)]
                72 > 71 → pop (3,71), result[3]=72
7     6   76  76 > 72 → pop (5,72), result[5]=76    [(6, 76)]
                76 > 75 → pop (2,75), result[2]=76

Result: [74, 75, 76, 72, 72, 76, -1]
        Mỗi index push 1× và pop ≤ 1× → O(n).

Monotonic deque (sliding window max)

Invariant tại mọi thời điểm (deque giảm dần từ head → tail):

   head                              tail
   [i0  ,  i1  ,  i2  ,  ...  ,  ik]
   arr[i0] ≥ arr[i1] ≥ ... ≥ arr[ik]
   
   • Head luôn là MAX của window hiện tại.
   • Tail là phần tử mới nhất.

Khi window slide (thêm index r, có thể bỏ index l):
   1. Pop từ tail nếu arr[tail] ≤ arr[r]   (vô dụng — r mới hơn + lớn hơn)
   2. Push r vào tail
   3. Pop head nếu head ≤ r - k             (ngoài window)
   4. Max = arr[head]

Ví dụ trace với arr = [1, 3, -1, -3, 5, 3, 6, 7], k = 3:

i  v   deque (index)  Pop tail/head?            Max khi i≥k-1=2
──────────────────────────────────────────────────────────────────
0  1   [0]            push 0
1  3   [1]            pop 0 (arr[0]=1 ≤ 3); push 1
2 -1   [1, 2]         push 2                     arr[1]=3
3 -3   [1, 2, 3]      push 3 (head 1 vẫn ≤ 3-k=0?  
                       Không, 1 > 0, ok)         arr[1]=3
4  5   [4]            pop 3,2,1 (đều ≤ 5)       arr[4]=5
5  3   [4, 5]         push 5                     arr[4]=5
6  6   [6]            pop 5 (3 ≤ 6); pop 4 (5 ≤ 6)
                       Wait — index 4 với arr[4]=5
                       5 ≤ 6 → pop. push 6.       arr[6]=6
7  7   [7]            pop 6                      arr[7]=7

Output: [3, 3, 5, 5, 6, 7]  ✓

Invariant này giúp gỡ lỗi rất nhanh: nếu deque không còn monotonic, hoặc phần tử ở đầu deque nằm ngoài window, thì chắc chắn code có bug.

Template code

# 1) Monotonic decreasing stack — Next Greater Element
def next_greater(arr: list[int]) -> list[int]:
    n = len(arr)
    result = [-1] * n
    stack: list[int] = []           # index, value giảm dần
    for i, v in enumerate(arr):
        while stack and arr[stack[-1]] < v:
            result[stack.pop()] = v
        stack.append(i)
    return result


# 2) Monotonic deque — Sliding Window Max
from collections import deque

def sliding_max(arr: list[int], k: int) -> list[int]:
    dq: deque[int] = deque()         # index, value giảm dần ở head→tail
    out = []
    for i, v in enumerate(arr):
        while dq and arr[dq[-1]] <= v:
            dq.pop()
        dq.append(i)
        if dq[0] <= i - k:
            dq.popleft()
        if i >= k - 1:
            out.append(arr[dq[0]])
    return out

Bài tự luyện cuối chương


18.1 Daily Temperatures (LC 739) — recap

Đã giải đầy đủ ở Chương 8.5. Pattern: monotonic decreasing stack, mỗi phần tử push/pop đúng 1 lần → O(n).

Code Python 3

class Solution:
    def dailyTemperatures(self, t):
        result = [0] * len(t)
        stack = []
        for i, v in enumerate(t):
            while stack and t[stack[-1]] < v:
                j = stack.pop()
                result[j] = i - j
            stack.append(i)
        return result

Phân tích độ phức tạp

Bài tự luyện liên quan


18.2 Next Greater Element II (LC 503)

Đề bài

Cho mảng vòng tròn nums. Với mỗi phần tử, tìm số đầu tiên lớn hơn nó tính theo chiều kim đồng hồ (có thể vòng lại). Không có → -1.

Ví dụ

Input:  nums = [1, 2, 1]   → [2, -1, 2]
Giải thích: 1 (idx 0) → 2; 2 → không có; 1 (idx 2) → 2 (vòng lại).

Ràng buộc

Clarifying questions

Hướng tiếp cận

Trick mảng vòng: duyệt 2n bước, dùng i % n để truy cập value. Stack monotonic giữ index thật (0..n-1) và chỉ pop ở vòng đầu (vì vòng 2 lan toả).

Code Python 3

from typing import List

class Solution:
    def nextGreaterElements(self, nums: List[int]) -> List[int]:
        n = len(nums)
        result = [-1] * n
        stack: list[int] = []
        for i in range(2 * n):
            cur = nums[i % n]
            while stack and nums[stack[-1]] < cur:
                result[stack.pop()] = cur
            if i < n:
                stack.append(i)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


18.3 Largest Rectangle in Histogram (LC 84)

Đề bài

Cho mảng heights[] (chiều cao mỗi cột width 1). Tìm diện tích hình chữ nhật lớn nhất trong histogram.

Ví dụ

Input:  heights = [2, 1, 5, 6, 2, 3]
Output: 10
Giải thích: chọn cột [5, 6] → 2 * 5 = 10.

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — O(n²). Với mỗi cột i, expand sang 2 bên đến khi gặp cột thấp hơn.

Monotonic increasing stack — O(n).

Stack chứa index, các height tăng dần từ đáy lên đỉnh. Khi gặp h[i] nhỏ hơn h[stack top], đó là dấu hiệu cột top kết thúc bên phảii. Pop top, tính diện tích: - height = h[top]. - width = i - stack[-1] - 1 (nếu stack rỗng sau pop, width = i).

Hình minh hoạ với heights = [2, 1, 5, 6, 2, 3]:

heights:    2   1   5   6   2   3
                       █
              █        █
              █        █
              █        █   █
              █        █   █   █
          █   █    █   █   █   █
       _________________________________
              0   1   2   3   4   5

Stack tracing (sentinel -1):
i=0  h=2: push 0     stack=[0]
i=1  h=1: h[0]=2 > 1 → pop 0. height=2, width=1-(-1)-1=1, area=2
         push 1     stack=[1]
i=2  h=5: push 2     stack=[1, 2]
i=3  h=6: push 3     stack=[1, 2, 3]
i=4  h=2: h[3]=6 > 2 → pop 3. height=6, width=4-2-1=1, area=6
         h[2]=5 > 2 → pop 2. height=5, width=4-1-1=2, area=10  ★
         push 4     stack=[1, 4]
i=5  h=3: push 5     stack=[1, 4, 5]

Hết mảng. Sentinel right (i=6, h=0):
  pop 5. height=3, width=6-4-1=1, area=3
  pop 4. height=2, width=6-1-1=4, area=8
  pop 1. height=1, width=6-(-1)-1=6, area=6

Max = 10  ✓

Code Python 3

from typing import List

class Solution:
    def largestRectangleArea(self, heights: List[int]) -> int:
        stack: list[int] = [-1]      # sentinel
        max_area = 0
        for i, h in enumerate(heights):
            while stack[-1] != -1 and heights[stack[-1]] >= h:
                height = heights[stack.pop()]
                width = i - stack[-1] - 1
                max_area = max(max_area, height * width)
            stack.append(i)
        # Drain stack với sentinel right = n.
        n = len(heights)
        while stack[-1] != -1:
            height = heights[stack.pop()]
            width = n - stack[-1] - 1
            max_area = max(max_area, height * width)
        return max_area

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


18.4 Sliding Window Maximum (LC 239)

Đề bài

Cho mảng nums và số k. Cửa sổ k phần tử trượt từ trái sang phải. Trả về max trong mỗi vị trí cửa sổ.

Ví dụ

Input:  nums = [1,3,-1,-3,5,3,6,7], k = 3
Output: [3, 3, 5, 5, 6, 7]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — O(n · k). TLE.

Monotonic Deque — O(n).

Deque chứa index, các value giảm dần từ head → tail. Khi mới đến i: 1. Pop từ tail nếu value <= nums[i] (chúng vô dụng — i mới hơn và lớn hơn). 2. Push i vào tail. 3. Pop từ head nếu index ngoài cửa sổ (dq[0] <= i - k). 4. Khi i >= k - 1, nums[dq[0]] là max.

Hình minh hoạ với nums = [1,3,-1,-3,5,3,6,7], k = 3:

i  v  deque (index)  state                       max khi i>=k-1=2
─────────────────────────────────────────────────────────────────
0  1  [0]            v=1
1  3  [1]            v=3 > 1, pop 0
2 -1  [1, 2]                                     nums[1]=3
3 -3  [1, 2, 3]      check head: 1 > 3-k=0 OK   nums[1]=3
4  5  [4]            5 > -3,-1,3 → pop tất     nums[4]=5
5  3  [4, 5]                                     nums[4]=5
6  6  [6]            6 > 3,5 → pop              nums[6]=6
7  7  [7]            7 > 6 → pop                nums[7]=7

Output: [3, 3, 5, 5, 6, 7]  ✓

Code Python 3

from collections import deque
from typing import List

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        dq: deque[int] = deque()
        result: list[int] = []
        for i, v in enumerate(nums):
            while dq and nums[dq[-1]] <= v:
                dq.pop()
            dq.append(i)
            if dq[0] <= i - k:
                dq.popleft()
            if i >= k - 1:
                result.append(nums[dq[0]])
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


18.5 Sum of Subarray Minimums (LC 907)

Đề bài

Cho mảng arr. Trả về tổng min(subarray) qua tất cả subarray liên tục. Modulo 10^9 + 7.

Ví dụ

Input:  arr = [3, 1, 2, 4]
Output: 17
Subarrays:  [3],[1],[2],[4],[3,1],[1,2],[2,4],[3,1,2],[1,2,4],[3,1,2,4]
Mins:        3 + 1 + 2 + 4 + 1 + 1 + 2 + 1 + 1 + 1 = 17

Ràng buộc

Clarifying questions

Hướng tiếp cận

Insight chuyển bài:

Thay vì duyệt subarray, ta hỏi: mỗi phần tử arr[i] là min của bao nhiêu subarray?

arr[i] là min của subarray [l, r] ↔︎ l ∈ (prev_less[i], i]r ∈ [i, next_less[i]).

Vậy, số subarray có min = arr[i] = (i - prev_less[i]) * (next_less[i] - i).

Đáp án = Σ arr[i] * (i - prev_less[i]) * (next_less[i] - i).

Tính prev_less, next_less dùng monotonic stack O(n).

Cẩn thận với duplicate: dùng strict < ở 1 phía, <= ở phía kia để tránh đếm trùng.

Code Python 3

from typing import List

class Solution:
    MOD = 10**9 + 7

    def sumSubarrayMins(self, arr: List[int]) -> int:
        n = len(arr)
        # prev_less[i]: nearest index j < i with arr[j] < arr[i]; -1 nếu không có.
        prev_less = [-1] * n
        stack: list[int] = []
        for i in range(n):
            while stack and arr[stack[-1]] >= arr[i]:
                stack.pop()
            prev_less[i] = stack[-1] if stack else -1
            stack.append(i)
        # next_less[i]: nearest index j > i with arr[j] <= arr[i]; n nếu không có.
        next_less = [n] * n
        stack.clear()
        for i in range(n - 1, -1, -1):
            while stack and arr[stack[-1]] > arr[i]:
                stack.pop()
            next_less[i] = stack[-1] if stack else n
            stack.append(i)

        result = 0
        for i in range(n):
            left = i - prev_less[i]
            right = next_less[i] - i
            result = (result + arr[i] * left * right) % self.MOD
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


18.6 Remove K Digits (LC 402)

Đề bài

Cho chuỗi số num và số k. Xoá đúng k chữ số sao cho chuỗi kết quả là số nhỏ nhất có thể (giữ thứ tự các chữ số còn lại). Bỏ leading zero, chuỗi rỗng trả "0".

Ví dụ

Input:  num = "1432219", k = 3   → "1219"
Input:  num = "10200", k = 1     → "200"
Input:  num = "10", k = 2        → "0"

Ràng buộc

Clarifying questions

Hướng tiếp cận

Greedy + Monotonic increasing stack.

Duyệt num. Với mỗi digit d: - Trong khi stack không rỗng, top > d, và k > 0: pop top (xoá nó tốt hơn vì làm số nhỏ đi). - Push d.

Cuối: nếu còn k > 0 (chưa đủ xoá) → pop từ cuối stack (digit ở cuối luôn lớn nhất trong số còn lại, vì stack đang increasing).

Cuối cùng: bỏ leading zero, return.

Hình minh hoạ với num = "1432219", k = 3:

digit  stack       k    action
─────────────────────────────────────────────────────
'1'    [1]         3    push
'4'    [1, 4]      3    push (4 > top)
'3'    [1, 3]      2    pop 4 (4 > 3, k>0). push 3
'2'    [1, 2]      1    pop 3 (3 > 2). push 2
'2'    [1, 2, 2]   1    push
'1'    [1, 1]      0    pop 2 (2 > 1). push 1
'9'    [1, 1, 9]   0    push (k=0, không pop được)

Còn 7-0=7 digit chứ không, nguyên thuỷ 7 digit, k=3, kết quả phải 4 digit.
stack chứa [1, 1, 9]. Cộng với cái cuối '1' đã pop... wait, recount.

Actually 1432219 length=7, k=3, output length=4.
Let me redo more carefully:

i=0 '1': stack=[1]
i=1 '4': 4 > 1, push. stack=[1,4]
i=2 '3': 4 > 3, k=3 → pop. stack=[1]. now stack[-1]=1 < 3, push. stack=[1,3] k=2
i=3 '2': 3 > 2, k=2 → pop. stack=[1]. 1 < 2, push. stack=[1,2] k=1
i=4 '2': 2 not > 2, push. stack=[1,2,2]
i=5 '1': 2 > 1, k=1 → pop. stack=[1,2]. 2 > 1, k=0 → stop. push. stack=[1,2,1]
i=6 '9': k=0, no pop. push. stack=[1,2,1,9]

len=4 OK. Result "1219". ✓

Code Python 3

class Solution:
    def removeKdigits(self, num: str, k: int) -> str:
        stack: list[str] = []
        for d in num:
            while k > 0 and stack and stack[-1] > d:
                stack.pop()
                k -= 1
            stack.append(d)
        # Còn k? Pop từ cuối (digit lớn nhất nếu stack tăng).
        while k > 0:
            stack.pop()
            k -= 1
        # Bỏ leading zero.
        result = ''.join(stack).lstrip('0')
        return result if result else '0'

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Largest Rectangle (LC 84) — sentinel trace

Input: heights = [2, 1, 5, 6, 2, 3], thêm sentinel 0 cuối.

i  h[i]  stack (idx)   pop & compute       maxA
0   2    [0]                                0
1   1    pop 0: h=2, w=1-0  ⇒ 2             2
         [1]
2   5    [1,2]                              2
3   6    [1,2,3]                            2
4   2    pop 3: h=6, w=4-2-1=1 ⇒ 6          6
         pop 2: h=5, w=4-1-1=2 ⇒ 10        10
         [1,4]
5   3    [1,4,5]                           10
6   0    pop 5: h=3, w=6-4-1=1 ⇒ 3
         pop 4: h=2, w=6-1-1=4 ⇒ 8
         pop 1: h=1, w=6     ⇒ 6           10

Invariant: stack lưu các index có h tăng nghiêm ngặt; khi gặp h thấp hơn, các cột bên trái cao hơn “đóng cửa” rectangle của chúng.

Sum of Subarray Minimums (LC 907) — contribution proof

Mỗi a[i] đóng góp a[i] × left × right lần làm minimum, với: - left[i] = số phần tử bên trái mà a[i] vẫn là min (kể cả chính i). - right[i] = tương tự bên phải. - Tie-break: dùng < bên trái, bên phải (hoặc ngược) để mỗi subarray min đếm đúng 1 lần.

Remove K Digits (LC 402) — greedy proof


Chương 19 — Prefix Sum

Prefix Sum = P[i] = arr[0] + arr[1] + ... + arr[i-1]. Cho phép tính tổng subarray [l, r] trong O(1): P[r+1] - P[l]. Đây là gateway cho Chương 6.4 (Subarray Sum K) và rất nhiều bài liên quan đến tổng / số dư / đếm subarray theo điều kiện. Cũng là nền cho Fenwick Tree (Chương 22).

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Template code

# 1) Prefix sum 1D
P = [0] * (n + 1)
for i in range(n):
    P[i + 1] = P[i] + arr[i]

# Tổng arr[l..r] (inclusive):
sum_lr = P[r + 1] - P[l]


# 2) Prefix sum + hash đếm subarray = K (Subarray Sum K)
from collections import defaultdict
def count_subarray_sum_k(arr, k):
    counts = defaultdict(int)
    counts[0] = 1
    cur = result = 0
    for x in arr:
        cur += x
        result += counts[cur - k]
        counts[cur] += 1
    return result


# 3) Prefix sum 2D
P = [[0] * (cols + 1) for _ in range(rows + 1)]
for r in range(rows):
    for c in range(cols):
        P[r+1][c+1] = mat[r][c] + P[r][c+1] + P[r+1][c] - P[r][c]
# Tổng rect (r1, c1) → (r2, c2):
# P[r2+1][c2+1] - P[r1][c2+1] - P[r2+1][c1] + P[r1][c1]

Bài tự luyện cuối chương


19.1 Range Sum Query - Immutable (LC 303)

Đề bài

Thiết kế class: - NumArray(nums): khởi tạo với mảng số. - sumRange(left, right): trả về tổng nums[left..right] (inclusive).

Mảng không thay đổi sau khởi tạo. Yêu cầu sumRange chạy O(1).

Ví dụ

Input (LC-style operation arrays):
  ops  = ["NumArray",            "sumRange", "sumRange", "sumRange"]
  args = [[[-2, 0, 3, -5, 2, -1]], [0, 2],     [2, 5],     [0, 5]]

Output: [null, 1, -1, -3]

Giải thích:
  NumArray([-2, 0, 3, -5, 2, -1])  → null  (khởi tạo prefix sum nội bộ)
  sumRange(0, 2)                   → 1      (-2 + 0 + 3)
  sumRange(2, 5)                   → -1     (3 + (-5) + 2 + (-1))
  sumRange(0, 5)                   → -3     (tổng toàn mảng)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Precompute prefix sum trong __init__. Mỗi query O(1).

Code Python 3

from typing import List

class NumArray:
    def __init__(self, nums: List[int]):
        n = len(nums)
        self.P = [0] * (n + 1)
        for i in range(n):
            self.P[i + 1] = self.P[i] + nums[i]

    def sumRange(self, left: int, right: int) -> int:
        return self.P[right + 1] - self.P[left]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


19.2 Subarray Sum Equals K (LC 560) — recap

Đã giải đầy đủ ở Chương 6.4. Pattern: prefix sum + hash đếm cur - k.

Code Python 3

from collections import defaultdict
from typing import List

class Solution:
    def subarraySum(self, nums: List[int], k: int) -> int:
        counts = defaultdict(int)
        counts[0] = 1
        cur = result = 0
        for x in nums:
            cur += x
            result += counts[cur - k]
            counts[cur] += 1
        return result

Liên hệ với các bài khác

Phân tích độ phức tạp

Bài tự luyện liên quan


19.3 Continuous Subarray Sum (LC 523)

Đề bài

Cho mảng nums và số k. Trả về True nếu tồn tại subarray độ dài ≥ 2 có tổng là bội số của k (kể cả 0×k = 0).

Ví dụ

Input:  nums = [23, 2, 4, 6, 7], k = 6   → True
Giải thích: [2, 4] có tổng 6 = 1*6.

Input:  nums = [23, 2, 6, 4, 7], k = 6   → True
Giải thích: [23, 2, 6, 4, 7] = 42 = 7*6.

Input:  nums = [1, 2, 3], k = 5          → False

Ràng buộc

Clarifying questions

Hướng tiếp cận

Prefix sum mod K + hash.

Subarray nums[l..r] có tổng chia hết k ↔︎ P[r+1] % k == P[l] % k. Tìm 2 vị trí cùng modulo, khoảng cách ≥ 2.

Dict {remainder: earliest_index}. Với mỗi i, tính cur % k: - Nếu đã thấy remainder này trước đó tại ji - j >= 2 → True. - Nếu chưa thấy → ghi i.

Code Python 3

from typing import List

class Solution:
    def checkSubarraySum(self, nums: List[int], k: int) -> bool:
        # remainder -> earliest index where this prefix-sum remainder appeared.
        # Khởi tạo {0: -1} để cover trường hợp subarray bắt đầu từ index 0.
        first_idx: dict[int, int] = {0: -1}
        cur = 0
        for i, x in enumerate(nums):
            cur += x
            rem = cur % k
            if rem in first_idx:
                if i - first_idx[rem] >= 2:
                    return True
            else:
                first_idx[rem] = i
        return False

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


19.4 Range Sum Query 2D - Immutable (LC 304)

Đề bài

Thiết kế class NumMatrix(matrix) với phương thức sumRegion(row1, col1, row2, col2) trả tổng vùng chữ nhật bao quanh bởi 4 góc (row1, col1)(row2, col2) (inclusive cả hai đầu) trong O(1).

Ví dụ

Input (LC-style operation arrays):
  ops  = ["NumMatrix", "sumRegion", "sumRegion", "sumRegion"]
  args = [[[[3,0,1,4,2],
            [5,6,3,2,1],
            [1,2,0,1,5],
            [4,1,0,1,7],
            [1,0,3,0,5]]],
          [2, 1, 4, 3],
          [1, 1, 2, 2],
          [1, 2, 2, 4]]

Output: [null, 8, 11, 12]

Giải thích:
  NumMatrix(matrix)          → khởi tạo, return null
  sumRegion(2, 1, 4, 3)      → tổng vùng [2..4] × [1..3] = 8
  sumRegion(1, 1, 2, 2)      → tổng vùng [1..2] × [1..2] = 11
  sumRegion(1, 2, 2, 4)      → tổng vùng [1..2] × [2..4] = 12

Ràng buộc

Clarifying questions

Hướng tiếp cận

Prefix sum 2D: P[r+1][c+1] = tổng vùng [0,0] → [r,c].

Inclusion-exclusion để lấy tổng vùng [r1,c1] → [r2,c2]:

P[r2+1][c2+1] − P[r1][c2+1] − P[r2+1][c1] + P[r1][c1]

Hình minh hoạ:

P[r2+1][c2+1] = tổng cả vùng (0,0) đến (r2,c2)
  − P[r1][c2+1] = trừ phần trên (0,0) đến (r1-1, c2)
  − P[r2+1][c1] = trừ phần trái (0,0) đến (r2, c1-1)
  + P[r1][c1]  = cộng lại phần overlap đã trừ 2 lần

Code Python 3

from typing import List

class NumMatrix:
    def __init__(self, matrix: List[List[int]]):
        if not matrix or not matrix[0]:
            return
        rows, cols = len(matrix), len(matrix[0])
        self.P = [[0] * (cols + 1) for _ in range(rows + 1)]
        for r in range(rows):
            for c in range(cols):
                self.P[r + 1][c + 1] = (
                    matrix[r][c]
                    + self.P[r][c + 1]
                    + self.P[r + 1][c]
                    - self.P[r][c]
                )

    def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:
        return (
            self.P[row2 + 1][col2 + 1]
            - self.P[row1][col2 + 1]
            - self.P[row2 + 1][col1]
            + self.P[row1][col1]
        )

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


19.5 Product of Array Except Self (LC 238) — recap

Đã giải đầy đủ ở Chương 1.3. Liên hệ với prefix sum.

Code Python 3

from typing import List


class Solution:
    def productExceptSelf(self, nums: List[int]) -> List[int]:
        n = len(nums)
        result = [1] * n
        left = 1
        for i in range(n):
            result[i] = left
            left *= nums[i]
        right = 1
        for i in range(n - 1, -1, -1):
            result[i] *= right
            right *= nums[i]
        return result

Liên hệ pattern

Phân tích độ phức tạp

Bài tự luyện liên quan


19.6 Find Pivot Index (LC 724)

Đề bài

Cho nums. Tìm pivot index — chỉ số i mà tổng nums[0..i-1] == nums[i+1..n-1]. Trả index trái nhất, hoặc -1.

Ví dụ

Input:  nums = [1, 7, 3, 6, 5, 6]
Output: 3
Giải thích: tổng trái 1+7+3=11; tổng phải 5+6=11.

Input:  nums = [1, 2, 3]   → -1
Input:  nums = [2, 1, -1]  → 0    (trái = rỗng = 0; phải = 1 + -1 = 0)

Ràng buộc

Clarifying questions

Hướng tiếp cận

left[i] + nums[i] + right[i] = total. Pivot ↔︎ left[i] == right[i] ↔︎ left[i] = (total - nums[i]) / 2.

1 lượt: giữ left_sum. Tại i: pivot ↔︎ left_sum == total - left_sum - nums[i].

Code Python 3

from typing import List

class Solution:
    def pivotIndex(self, nums: List[int]) -> int:
        total = sum(nums)
        left_sum = 0
        for i, x in enumerate(nums):
            if left_sum == total - left_sum - x:
                return i
            left_sum += x
        return -1

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

P[0] = 0 — vì sao

arr:  [ a0, a1, a2, a3 ]
P:    [  0, a0, a0+a1, a0+a1+a2, a0+a1+a2+a3 ]
       P[0] P[1] P[2]   P[3]      P[4]

Tổng arr[l..r] (đóng, 0-index) = P[r+1] - P[l]. Nếu thiếu P[0] = 0, công thức cần case riêng cho l == 0.

Continuous Subarray Sum (LC 523) — distance condition

Modulo với số âm

Python % luôn trả [0, k): (-3) % 5 == 2. An toàn cho prefix mod. Java/C++: (-3) % 5 == -3 → cần ((x % k) + k) % k.

Product Except Self recap

Đã có code đầy đủ ở Chương 1.3. Recap ở đây nhấn mạnh: prefix productsuffix product, không dùng P[r+1] - P[l] mà là nhân, nên không cần P[0] = 1 riêng — code dùng left = 1, right = 1 ban đầu.


Chương 20 — Prime Number

Chương cuối của Level 1, mang tinh thần “number theory cơ bản”. Prime number không phải pattern lớn trong phỏng vấn Big Tech (Google đôi khi hỏi), nhưng học để có Sàng Eratosthenes trong tủ vũ khí — thuật toán cổ điển vẫn rất hữu dụng.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Template code

def is_prime(n: int) -> bool:
    """Trial division O(sqrt(n))."""
    if n < 2:
        return False
    if n < 4:
        return True
    if n % 2 == 0:
        return False
    i = 3
    while i * i <= n:
        if n % i == 0:
            return False
        i += 2
    return True


def sieve(n: int) -> list[bool]:
    """Sàng Eratosthenes — O(n log log n)."""
    is_p = [True] * (n + 1)
    is_p[0] = is_p[1] = False
    for i in range(2, int(n ** 0.5) + 1):
        if is_p[i]:
            for j in range(i * i, n + 1, i):
                is_p[j] = False
    return is_p


def prime_factors(n: int) -> list[int]:
    """Phân tích thừa số — O(sqrt(n))."""
    factors: list[int] = []
    d = 2
    while d * d <= n:
        while n % d == 0:
            factors.append(d)
            n //= d
        d += 1
    if n > 1:
        factors.append(n)
    return factors

Bài tự luyện cuối chương


20.1 Count Primes (LC 204)

Đề bài

Cho n. Trả về số nguyên tố nhỏ hơn n.

Ví dụ

Input:  n = 10   → 4
Giải thích: primes < 10 = {2, 3, 5, 7}.

Input:  n = 0    → 0
Input:  n = 1    → 0

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — trial division mỗi số, O(n sqrt n). TLE với n = 5·10^6.

Sàng Eratosthenes — O(n log log n).

is_prime[] mảng boolean. Với mỗi i từ 2, nếu is_prime[i] thật → mark tất cả bội số (i*i, i*i+i, i*i+2i, ...) là composite.

Hình minh hoạ với n = 30:

i=2: mark 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28
i=3: mark 9, 12, 15, 18, 21, 24, 27
i=4: composite → skip
i=5: mark 25
i=6..29: hoặc composite hoặc i² > 30 → skip

Còn lại: 2, 3, 5, 7, 11, 13, 17, 19, 23, 29 → 10 primes.

Code Python 3

class Solution:
    def countPrimes(self, n: int) -> int:
        if n < 2:
            return 0
        is_p = [True] * n
        is_p[0] = is_p[1] = False
        for i in range(2, int(n ** 0.5) + 1):
            if is_p[i]:
                # Bắt đầu từ i*i — các bội nhỏ hơn đã bị mark bởi prime nhỏ hơn.
                for j in range(i * i, n, i):
                    is_p[j] = False
        return sum(is_p)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


20.2 Ugly Number II (LC 264)

Đề bài

Ugly Number = số nguyên dương có dạng 2^a · 3^b · 5^c. Trả về ugly number thứ n. (1 cũng là ugly.)

Ví dụ

Input:  n = 10
Output: 12
Giải thích: 10 ugly đầu = 1, 2, 3, 4, 5, 6, 8, 9, 10, 12.

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force đếm và check từng số — quá chậm khi n lớn.

3 con trỏ DP — O(n).

ugly[k] = ugly thứ k. Mỗi ugly mới = min(ugly[i2]*2, ugly[i3]*3, ugly[i5]*5). Sau khi chọn, advance pointer tương ứng.

Hình minh hoạ:

ugly = [1]                       i2=i3=i5=0
n2=1*2=2, n3=1*3=3, n5=1*5=5     min=2 → ugly=[1,2], i2=1
n2=2*2=4, n3=3, n5=5             min=3 → ugly=[1,2,3], i3=1
n2=4, n3=2*3=6, n5=5             min=4 → ugly=[1,2,3,4], i2=2
n2=3*2=6, n3=6, n5=5             min=5 → i5=1
n2=6, n3=6, n5=2*5=10            min=6 → i2=3, i3=2
...

Code Python 3

class Solution:
    def nthUglyNumber(self, n: int) -> int:
        ugly = [1]
        i2 = i3 = i5 = 0
        while len(ugly) < n:
            next_ugly = min(ugly[i2] * 2, ugly[i3] * 3, ugly[i5] * 5)
            ugly.append(next_ugly)
            if next_ugly == ugly[i2] * 2:
                i2 += 1
            if next_ugly == ugly[i3] * 3:
                i3 += 1
            if next_ugly == ugly[i5] * 5:
                i5 += 1
        return ugly[-1]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


20.3 Prime Arrangements (LC 1175)

Đề bài

Cho n. Đếm số hoán vị của 1..n sao cho mọi số nguyên tố đứng tại vị trí prime (index 1-based). Modulo 10^9 + 7.

Ví dụ

Input:  n = 5
Output: 12
Giải thích: 5 vị trí, có 3 prime (2, 3, 5). Số prime trong 1..5 = 3.
            3! * 2! = 6 * 2 = 12.

Ràng buộc

Clarifying questions

Hướng tiếp cận

Counting: Gọi p = số prime ≤ n. Có p! cách xếp prime vào p vị trí prime, và (n - p)! cách xếp non-prime vào n - p vị trí non-prime.

Đáp án = p! * (n - p)! mod (10^9 + 7).

Code Python 3

from math import factorial

class Solution:
    MOD = 10**9 + 7

    def numPrimeArrangements(self, n: int) -> int:
        # Đếm prime ≤ n.
        if n < 2:
            return 1
        is_p = [True] * (n + 1)
        is_p[0] = is_p[1] = False
        for i in range(2, int(n ** 0.5) + 1):
            if is_p[i]:
                for j in range(i * i, n + 1, i):
                    is_p[j] = False
        p = sum(is_p)
        return (factorial(p) * factorial(n - p)) % self.MOD

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


20.4 Closest Prime Numbers in Range (LC 2523)

Đề bài

Cho left, right. Trả về cặp prime (p, q) với left <= p < q <= rightq - p min. Nếu hoà, lấy cặp có p nhỏ hơn. Không có → [-1, -1].

Ví dụ

Input:  left=10, right=19
Output: [11, 13]

Ràng buộc

Clarifying questions

Hướng tiếp cận

  1. Sàng tới right, lấy danh sách prime trong [left, right].
  2. Duyệt cặp kề nhau trong list, lấy min gap.

Code Python 3

from typing import List

class Solution:
    def closestPrimes(self, left: int, right: int) -> List[int]:
        is_p = [True] * (right + 1)
        is_p[0] = is_p[1] = False
        for i in range(2, int(right ** 0.5) + 1):
            if is_p[i]:
                for j in range(i * i, right + 1, i):
                    is_p[j] = False
        primes = [i for i in range(left, right + 1) if is_p[i]]
        if len(primes) < 2:
            return [-1, -1]
        best_gap = float('inf')
        result = [-1, -1]
        for i in range(len(primes) - 1):
            gap = primes[i + 1] - primes[i]
            if gap < best_gap:
                best_gap = gap
                result = [primes[i], primes[i + 1]]
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


20.5 Largest Component Size by Common Factor (LC 952)

Đề bài

Cho nums. Nối 2 phần tử cùng component nếu chúng chia sẻ ít nhất 1 thừa số nguyên tố > 1. Trả về kích thước component lớn nhất.

Ví dụ

Input:  nums = [4, 6, 15, 35]   → 4
Giải thích: 4,6 chia sẻ 2; 6,15 chia sẻ 3; 15,35 chia sẻ 5 → tất cả 1 component.

Ràng buộc

Clarifying questions

Hướng tiếp cận

Insight: thay vì union các phần tử trực tiếp (cần O(n²)), ta union phần tử với mỗi thừa số prime của nó. Hai phần tử cùng prime → cùng component.

Pseudocode: - Với mỗi x trong nums, phân tích thừa số x. Union x với mỗi thừa số. - Đếm size component.

Code Python 3

from collections import Counter
from typing import List

class DSU:
    def __init__(self): self.par = {}
    def find(self, x):
        if x not in self.par: self.par[x] = x
        while self.par[x] != x:
            self.par[x] = self.par[self.par[x]]
            x = self.par[x]
        return x
    def union(self, a, b):
        ra, rb = self.find(a), self.find(b)
        if ra != rb: self.par[ra] = rb


class Solution:
    def largestComponentSize(self, nums: List[int]) -> int:
        dsu = DSU()
        for x in nums:
            d = 2
            v = x
            while d * d <= v:
                if v % d == 0:
                    dsu.union(x, d)
                    while v % d == 0:
                        v //= d
                d += 1
            if v > 1:
                dsu.union(x, v)
        # Đếm: với mỗi phần tử nums (chỉ phần tử nums, không phải prime).
        counter = Counter(dsu.find(x) for x in nums)
        return max(counter.values())

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


20.6 Distinct Prime Factors of Product in Array (LC 2521)

Đề bài

Cho nums. Trả về số prime factor phân biệt của tích các phần tử.

Ví dụ

Input:  nums = [2, 4, 3, 7, 10, 6]
Output: 4
Giải thích: tích = 2·4·3·7·10·6 = 10080 = 2^5 · 3^2 · 5 · 7. 4 prime phân biệt.

Ràng buộc

Clarifying questions

Hướng tiếp cận

Không cần tính tích thực (có thể overflow). Chỉ cần với mỗi số, lấy các thừa số prime, gom hết vào set.

Code Python 3

from typing import List

class Solution:
    def distinctPrimeFactors(self, nums: List[int]) -> int:
        primes: set[int] = set()
        for x in nums:
            d = 2
            while d * d <= x:
                while x % d == 0:
                    primes.add(d)
                    x //= d
                d += 1
            if x > 1:
                primes.add(x)
        return len(primes)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Prime toolbox — chọn theo constraint

n (giới hạn) Phương pháp Time
Kiểm tra 1 số n ≤ 10¹² Trial division đến √n O(√n)
Đếm primes ≤ n, n ≤ 10⁷ Sieve of Eratosthenes O(n log log n)
Factor nhiều số ≤ n, n ≤ 10⁶ SPF (Smallest Prime Factor) sieve precompute O(n log log n), mỗi factor O(log n)
Factor 1 số n ≤ 10¹⁸ Pollard ρ + Miller-Rabin sub-exponential

Largest Component by Common Factor (LC 952)

Prime Arrangements (LC 1175) — counting modulo


Chương 21 — Bit Manipulation + Mask

Bit manipulation mở ra giải pháp O(1) cho nhiều bài tưởng O(n). Tricks XOR, AND, OR + bit-shift là ngôn ngữ thứ 2 của lập trình viên. Chương này dạy 6 bài kinh điển + bit tricks bạn cần thuộc. Bitmask sẽ được mở rộng ở Chương 40 (Bitmask DP) và 41 (Bitmask + Trie).

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Bit tricks cheat-sheet

# Set bit i:                     x |= (1 << i)
# Clear bit i:                   x &= ~(1 << i)
# Toggle bit i:                  x ^= (1 << i)
# Test bit i:                    (x >> i) & 1
# Lowest set bit (rightmost 1):  x & -x      (ví dụ x=12=0b1100 → 4=0b100)
# Pop lowest set bit:            x &= x - 1
# Count set bits:                bin(x).count('1')   # hoặc x.bit_count() Py3.10+

# Iterate qua subsets của mask:
sub = mask
while sub:
    # ... use sub ...
    sub = (sub - 1) & mask

# Kiểm tra power of 2:           x > 0 and (x & (x - 1)) == 0
# Đảo bit 32-bit:                xor với 0xFFFFFFFF

Bài tự luyện cuối chương


21.1 Single Number (LC 136)

Đề bài

Cho mảng nums, mỗi phần tử xuất hiện 2 lần trừ đúng 1 phần tử xuất hiện 1 lần. Tìm phần tử đó. O(n) time, O(1) space.

Ví dụ

Input:  nums = [2, 2, 1]            → Output: 1
Input:  nums = [4, 1, 2, 1, 2]      → Output: 4
(mọi phần tử xuất hiện đúng 2 lần trừ một phần tử duy nhất xuất hiện 1 lần)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Trick XOR: a XOR a = 0; a XOR 0 = a. XOR có tính giao hoán & kết hợp. XOR cả mảng → các cặp huỷ nhau, còn lại = phần tử cô độc.

Code Python 3

from typing import List
from functools import reduce
from operator import xor

class Solution:
    def singleNumber(self, nums: List[int]) -> int:
        return reduce(xor, nums)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


21.2 Number of 1 Bits (LC 191)

Đề bài

Cho số nguyên n. Trả về số bit 1 trong biểu diễn nhị phân (Hamming weight).

Ví dụ

Input:  n = 11 (0b1011)   → 3
Input:  n = 128 (0b10000000) → 1

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cách 1 — Loop bit, O(32). Cách 2 — Trick n &= n - 1, O(số bit 1). Mỗi lần n & (n-1) xoá bit 1 thấp nhất.

Hình minh hoạ — n = 12 = 0b1100:

n = 1100
n-1=1011
n & (n-1) = 1000  → count = 1

n = 1000
n-1=0111
n & (n-1) = 0000  → count = 2

n = 0, dừng. Tổng 2 bit 1.  ✓

Code Python 3

class Solution:
    def hammingWeight(self, n: int) -> int:
        count = 0
        while n:
            n &= n - 1
            count += 1
        return count

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


21.3 Counting Bits (LC 338)

Đề bài

Cho n. Trả về mảng result dài n + 1, result[i] = số bit 1 của i.

Ví dụ

Input:  n = 5
Output: [0, 1, 1, 2, 1, 2]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — gọi hammingWeight(i) cho mỗi i, O(n log n).

DP — O(n): result[i] = result[i >> 1] + (i & 1).

Trực giác: bit 1 của i = bit 1 của i >> 1 (đã trừ bit cuối) + bit cuối.

Hoặc: result[i] = result[i & (i-1)] + 1.

Code Python 3

from typing import List

class Solution:
    def countBits(self, n: int) -> List[int]:
        result = [0] * (n + 1)
        for i in range(1, n + 1):
            result[i] = result[i >> 1] + (i & 1)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


21.4 Sum of Two Integers (LC 371)

Đề bài

Tính a + b không dùng + hoặc -.

Ví dụ

Input:  a = 1, b = 2   → 3
Input:  a = 2, b = 3   → 5

Ràng buộc

Clarifying questions

Hướng tiếp cận

Insight: - a XOR b = tổng các bit không carry. - (a AND b) << 1 = carry. - Lặp đến khi carry = 0.

Code Python 3

class Solution:
    MASK = 0xFFFFFFFF

    def getSum(self, a: int, b: int) -> int:
        while b:
            carry = (a & b) << 1
            a = (a ^ b) & self.MASK
            b = carry & self.MASK
        # Xử lý số âm trong Python (int vô hạn).
        return a if a < 0x80000000 else ~(a ^ self.MASK)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


21.5 Bitwise AND of Numbers Range (LC 201)

Đề bài

Cho left, right. Trả về AND của tất cả số trong [left, right].

Ví dụ

Input:  left = 5 (0b101), right = 7 (0b111)
Output: 4 (0b100)
Giải thích: 5 & 6 & 7 = 100 & 110 & 111 = 100 = 4.

Ràng buộc

Clarifying questions

Hướng tiếp cận

Insight: AND của range = prefix chung của leftright ở dạng nhị phân, padded zero ở các bit thấp hơn.

Vì khi range qua nhiều giá trị, các bit thấp đều có lúc bằng 0 → AND = 0.

Cách: shift cả 2 sang phải đến khi left == right (tìm prefix chung), rồi shift trái lại.

Code Python 3

class Solution:
    def rangeBitwiseAnd(self, left: int, right: int) -> int:
        shifts = 0
        while left < right:
            left >>= 1
            right >>= 1
            shifts += 1
        return left << shifts

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


21.6 Maximum XOR of Two Numbers in an Array (LC 421)

Đề bài

Cho nums. Trả về XOR lớn nhất của 2 phần tử khác nhau.

Ví dụ

Input:  nums = [3, 10, 5, 25, 2, 8]
Output: 28
Giải thích: 5 XOR 25 = 28.

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — O(n²). Mọi cặp.

Trick “Greedy bit” — O(n · 32).

Xây kết quả từng bit từ cao xuống thấp. Tại bit b: - Giả sử kết quả từ bit cao đến b+1 đã là result. - Thử mở bit b: candidate = result | (1 << b). - Kiểm tra: tồn tại cặp (a, b) trong nums với (a ^ b) & mask == candidate không? (mask = các bit ≥ b) - Cách check: tạo prefixes = {x & mask for x in nums}. Với mỗi prefix p, check p ^ candidate in prefixes.

Nếu có → giữ candidate; không có → giữ result cũ.

Trie approach (Chương 41) cũng làm được — đó là extension.

Code Python 3

from typing import List

class Solution:
    def findMaximumXOR(self, nums: List[int]) -> int:
        result = 0
        mask = 0
        for b in range(31, -1, -1):
            mask |= (1 << b)
            prefixes = {x & mask for x in nums}
            candidate = result | (1 << b)
            for p in prefixes:
                if p ^ candidate in prefixes:
                    result = candidate
                    break
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Python signed int caveat (LC 371 Sum of Two Integers)

Python int vô hạn bit(-1) << 1 không tràn. Để mô phỏng C++ 32-bit:

MASK = 0xFFFFFFFF
INT_MIN_NEG = 0x80000000
while b:
    a, b = (a ^ b) & MASK, ((a & b) << 1) & MASK
return a if a < INT_MIN_NEG else ~(a ^ MASK)

Counting Bits — 2 recurrence

Range AND (LC 201) — common prefix

Bài LC 421 (Max XOR of Two Numbers) là gateway cho Binary Trie trong Chương 41 (Bitmask + Trie). Mỗi số = đường đi 32 bit trong trie; greedy chọn bit ngược lại để maximize XOR.


Chương 22 — Advanced Tree (BST, Segment Tree, Fenwick Tree)

Chương cây nâng cao gồm 3 cấu trúc chính: BST (Binary Search Tree), Segment Tree, và Fenwick Tree (Binary Indexed Tree). Cả 3 đều giải bài “range query với update” trong O(log n) per operation. Trong phỏng vấn Big Tech, BST hỏi rất nhiều; Segment/Fenwick xuất hiện ở vòng on-site Hard.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Lựa chọn nhanh: - Cần sum / prefix only → Fenwick (đơn giản hơn). - Cần min / max / GCD / complex merge → Segment Tree. - Range update lazy → Segment Tree with lazy propagation.

Template code

# 1) BST validate / search — xem Chương 11.4

# 2) Fenwick Tree (1-indexed)
class Fenwick:
    def __init__(self, n: int):
        self.n = n
        self.tree = [0] * (n + 1)

    def update(self, i: int, delta: int) -> None:    # 1-indexed
        while i <= self.n:
            self.tree[i] += delta
            i += i & -i

    def query(self, i: int) -> int:                  # prefix sum [1..i]
        s = 0
        while i > 0:
            s += self.tree[i]
            i -= i & -i
        return s

    def range_query(self, l: int, r: int) -> int:
        return self.query(r) - self.query(l - 1)


# 3) Segment Tree — sum, iterative version
class SegTree:
    def __init__(self, n: int):
        self.n = n
        self.tree = [0] * (2 * n)

    def update(self, i: int, val: int) -> None:
        i += self.n
        self.tree[i] = val
        while i > 1:
            i //= 2
            self.tree[i] = self.tree[2*i] + self.tree[2*i+1]

    def query(self, l: int, r: int) -> int:          # [l, r)
        res = 0
        l += self.n; r += self.n
        while l < r:
            if l & 1: res += self.tree[l]; l += 1
            if r & 1: r -= 1; res += self.tree[r]
            l //= 2; r //= 2
        return res

Bài tự luyện cuối chương


22.1 Validate BST (LC 98) — recap

Đã giải đầy đủ ở Chương 11.4. Pattern: DFS với bound (low, high) hoặc inorder check strictly increasing.

Liên hệ với chương này

Code Python 3 (recap)

import math

class Solution:
    def isValidBST(self, root) -> bool:
        def dfs(node, low: float, high: float) -> bool:
            if not node:
                return True
            if not (low < node.val < high):
                return False
            return dfs(node.left, low, node.val) and \
                   dfs(node.right, node.val, high)
        return dfs(root, -math.inf, math.inf)

Phân tích độ phức tạp

Bài tự luyện liên quan


22.2 Recover Binary Search Tree (LC 99)

Đề bài

Input: root của 1 BST (LC level-order serialize). Đúng 2 node của BST bị swap giá trị nhầm. Khôi phục BST (sửa giá trị) mà không thay đổi cấu trúc cây. Yêu cầu O(1) extra space (follow-up).

Ví dụ

Input:  root = [1, 3, null, null, 2]
        (LC level-order serialize)

        Cây ban đầu (KHÔNG phải BST hợp lệ):
              1
             / \
            3   *
             \
              2

Output: root sau khi recover = [3, 1, null, null, 2]
        Cây sau khi sửa (BST hợp lệ):
              3
             / \
            1   *
             \
              2

Giải thích: hàm mutate cây in-place; 2 node bị swap value là 1 và 3 →
            chỉ cần swap value của 2 node đó.

Ràng buộc

Clarifying questions

Hướng tiếp cận

Inorder traversal của BST đúng = sequence tăng strictly. Khi có 2 node swap, inorder sẽ có 2 vị trí vi phạm (a > b với a đứng trước b).

Sau khi tìm first, second → swap giá trị chúng.

Code Python 3

class Solution:
    def recoverTree(self, root) -> None:
        first = second = prev = None

        def inorder(node) -> None:
            nonlocal first, second, prev
            if not node:
                return
            inorder(node.left)
            if prev and prev.val > node.val:
                if not first:
                    first = prev
                second = node
            prev = node
            inorder(node.right)

        inorder(root)
        first.val, second.val = second.val, first.val

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


22.3 Serialize and Deserialize Binary Tree (LC 297)

Đề bài

Thiết kế 2 hàm: serialize(root) -> strdeserialize(str) -> root.

Ví dụ

Input:  root = [1, 2, 3, null, null, 4, 5]
        (LC level-order serialize)

        Cây thực tế:
              1
             / \
            2   3
               / \
              4   5

Output (serialize): "1,2,#,#,3,4,#,#,5,#,#"
        (preorder DFS; `#` đại diện cho node None)

Yêu cầu round-trip: deserialize(serialize(root)) tạo lại cây tương đương
(cùng cấu trúc + cùng giá trị) với root ban đầu.

Ràng buộc

Clarifying questions

Hướng tiếp cận

Preorder DFS với # cho None.

Code Python 3

class Codec:
    def serialize(self, root) -> str:
        parts: list[str] = []

        def go(node):
            if not node:
                parts.append('#')
                return
            parts.append(str(node.val))
            go(node.left)
            go(node.right)

        go(root)
        return ','.join(parts)

    def deserialize(self, data: str):
        tokens = iter(data.split(','))

        def build():
            tk = next(tokens)
            if tk == '#':
                return None
            node = TreeNode(int(tk))
            node.left = build()
            node.right = build()
            return node

        return build()

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


22.4 Binary Tree Maximum Path Sum (LC 124)

Đề bài

Cho root của binary tree. Path là dãy node nối nhau qua edge (không nhất thiết qua root). Tìm path có sum max.

Ví dụ

Input:  root = [1, 2, 3]
        Cây thực tế:
              1
             / \
            2   3
Output: 6
        (path 2 → 1 → 3, tổng = 6)

Input:  root = [-10, 9, 20, null, null, 15, 7]
        Cây thực tế:
              -10
              /  \
             9    20
                 /  \
                15   7
Output: 42
        (path 15 → 20 → 7, tổng = 42, bỏ qua root -10 vì không có lợi)

Ràng buộc

Clarifying questions

Hướng tiếp cận

DFS bottom-up: mỗi node trả về “đường đi từ node xuống một lá” tối đa (= node.val + max(left_path, right_path, 0)). Đồng thời cập nhật best với “đường đi qua chính node” = node.val + left_path + right_path.

max(..., 0) để có thể bỏ subtree có sum âm.

Code Python 3

class Solution:
    def maxPathSum(self, root) -> int:
        self.best = -float('inf')

        def dfs(node) -> int:
            if not node:
                return 0
            left = max(dfs(node.left), 0)
            right = max(dfs(node.right), 0)
            self.best = max(self.best, node.val + left + right)
            return node.val + max(left, right)

        dfs(root)
        return self.best

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


22.5 Count of Smaller Numbers After Self (LC 315)

Đề bài

Cho mảng nums. Trả về counts[i] = số phần tử nhỏ hơn nums[i] ở phía sau trong mảng.

Ví dụ

Input:  nums = [5, 2, 6, 1]
Output: [2, 1, 1, 0]
Giải thích:
  5 có {2, 1} nhỏ hơn sau → 2.
  2 có {1} → 1.
  6 có {1} → 1.
  1 có 0.

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force — O(n²). TLE.

Cách 1 — Merge sort + đếm inversion (Chương 17.2).

Khi merge 2 nửa đã sort, khi lấy phần tử từ nửa trái L[i], mọi phần tử đã được lấy ra trước từ nửa phải đều nhỏ hơn L[i] → đếm vào counts[i].

Cách 2 — Fenwick Tree, O(n log n).

Duyệt từ phải sang trái. Với mỗi nums[i]: - counts[i] = fenwick.query(rank(nums[i]) - 1) — số phần tử nhỏ hơn đã thấy. - fenwick.update(rank(nums[i]), +1).

rank = vị trí của nums[i] trong sorted unique → coordinate compression.

Code Python 3 (Fenwick)

from typing import List
from bisect import bisect_left

class Fenwick:
    def __init__(self, n): self.n = n; self.tree = [0] * (n + 1)
    def update(self, i, d=1):
        while i <= self.n:
            self.tree[i] += d
            i += i & -i
    def query(self, i):
        s = 0
        while i > 0:
            s += self.tree[i]
            i -= i & -i
        return s


class Solution:
    def countSmaller(self, nums: List[int]) -> List[int]:
        sorted_vals = sorted(set(nums))
        rank = {v: i + 1 for i, v in enumerate(sorted_vals)}  # 1-indexed
        f = Fenwick(len(sorted_vals))
        counts = [0] * len(nums)
        for i in range(len(nums) - 1, -1, -1):
            r = rank[nums[i]]
            counts[i] = f.query(r - 1)
            f.update(r, 1)
        return counts

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


22.6 Range Sum Query - Mutable (LC 307)

Đề bài

Thiết kế class: - NumArray(nums): khởi tạo. - update(index, val): set nums[index] = val. - sumRange(left, right): tổng nums[left..right].

Cả updatesumRange phải O(log n).

Ví dụ

Input (LC-style operation arrays):
  ops  = ["NumArray", "sumRange", "update", "sumRange"]
  args = [[[1, 3, 5]], [0, 2],    [1, 2],   [0, 2]]

Output: [null, 9, null, 8]

Giải thích:
  NumArray([1, 3, 5])  → khởi tạo, return null
  sumRange(0, 2)       → 1 + 3 + 5 = 9
  update(1, 2)         → đặt nums[1] = 2; mảng giờ là [1, 2, 5]
  sumRange(0, 2)       → 1 + 2 + 5 = 8

Ràng buộc

Clarifying questions

Hướng tiếp cận

Fenwick Tree là lựa chọn lý tưởng — code ngắn nhất cho bài này. Segment Tree cũng làm được nhưng dài hơn.

Code Python 3 (Fenwick)

from typing import List

class NumArray:
    def __init__(self, nums: List[int]):
        self.n = len(nums)
        self.nums = nums[:]
        self.tree = [0] * (self.n + 1)
        for i, x in enumerate(nums):
            self._add(i + 1, x)

    def _add(self, i: int, delta: int) -> None:
        while i <= self.n:
            self.tree[i] += delta
            i += i & -i

    def _prefix(self, i: int) -> int:
        s = 0
        while i > 0:
            s += self.tree[i]
            i -= i & -i
        return s

    def update(self, index: int, val: int) -> None:
        delta = val - self.nums[index]
        self.nums[index] = val
        self._add(index + 1, delta)

    def sumRange(self, left: int, right: int) -> int:
        return self._prefix(right + 1) - self._prefix(left)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Tree family map

Cấu trúc Hỗ trợ Khi nào dùng
Binary Tree (cây thường) Duyệt, đường đi LC 124, 543
BST (sorted invariant) Search, kth, range count LC 230, 938
Fenwick / BIT Prefix sum / count, point update LC 307, 315
Segment Tree Range query/update tổng quát LC 732, 218
Trie Prefix strings Chương 23, 41

Fenwick vs Segment Tree

Fenwick Segment Tree
Op cơ bản Prefix sum, point update Range sum, range min/max/gcd…
Code ~10 dòng ~40-60 dòng
Bộ nhớ n 4n
Lazy propagation Khó Dễ
Khi đủ dùng Khi chỉ cần prefix/sum Khi cần range query + update phức tạp

Count Smaller After Self (LC 315) — coordinate compression

Serialize / Deserialize (LC 297) — chọn traversal


Chương 23 — Trie

Trie (prefix tree) là cây mà mỗi node đại diện cho một ký tự, mỗi đường đi từ root đến node = 1 prefix. Trie giải gọn các bài “tìm word có prefix X”, “search với wildcard”, “longest common prefix nhiều query”. Pattern Bitmask + Trie sẽ ở Chương 41.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Template code

class TrieNode:
    __slots__ = ("children", "is_end")
    def __init__(self):
        self.children: dict[str, "TrieNode"] = {}
        self.is_end = False


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

    def insert(self, word: str) -> None:
        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: str) -> bool:
        node = self._find(word)
        return node is not None and node.is_end

    def startsWith(self, prefix: str) -> bool:
        return self._find(prefix) is not None

    def _find(self, s: str) -> "TrieNode | None":
        node = self.root
        for ch in s:
            if ch not in node.children:
                return None
            node = node.children[ch]
        return node

Bài tự luyện cuối chương


23.1 Implement Trie (LC 208)

Đề bài

Cài đặt class Trie với 3 method: insert, search, startsWith. Yêu cầu mỗi op O(length(word)).

Ví dụ

Input (LC-style operation arrays):
  ops  = ["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
  args = [[],     ["apple"], ["apple"], ["app"], ["app"],      ["app"],  ["app"]]

Output: [null, null, true, false, true, null, true]

Giải thích:
  Trie()                  → khởi tạo, return null
  insert("apple")         → null
  search("apple")         → true
  search("app")           → false  (chưa insert "app" riêng)
  startsWith("app")       → true   (vẫn là prefix của "apple")
  insert("app")           → null
  search("app")           → true

Ràng buộc

Clarifying questions

Hướng tiếp cận

Trie = cây mà mỗi node là 1 ký tự, đường đi root → node là 1 prefix. Insert: walk theo ký tự, tạo node mới khi cần, mark is_end ở node cuối. Search/StartsWith: walk theo ký tự, return False nếu thiếu node.

Code Python 3

class TrieNode:
    __slots__ = ("children", "is_end")
    def __init__(self):
        self.children: dict[str, "TrieNode"] = {}
        self.is_end = False


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

    def insert(self, word: str) -> None:
        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: str) -> bool:
        node = self._find(word)
        return node is not None and node.is_end

    def startsWith(self, prefix: str) -> bool:
        return self._find(prefix) is not None

    def _find(self, s: str) -> "TrieNode | None":
        node = self.root
        for ch in s:
            if ch not in node.children:
                return None
            node = node.children[ch]
        return node

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


23.2 Add and Search Word (LC 211)

Đề bài

Cài đặt class với addWord(word)search(word). search cho phép . thay cho bất kỳ ký tự nào.

Ví dụ

Input (LC-style operation arrays):
  ops  = ["WordDictionary", "addWord", "addWord", "addWord", "search", "search", "search", "search"]
  args = [[],               ["bad"],   ["dad"],   ["mad"],   ["pad"],  ["bad"],  [".ad"],  ["b.."]]

Output: [null, null, null, null, false, true, true, true]

Giải thích:
  WordDictionary()      → null
  addWord("bad")        → null
  addWord("dad")        → null
  addWord("mad")        → null
  search("pad")         → false   ("pad" chưa từng add)
  search("bad")         → true
  search(".ad")         → true    (`.` match 1 char bất kỳ → khớp "bad", "dad", "mad")
  search("b..")         → true    (khớp "bad")

Ràng buộc

Clarifying questions

Hướng tiếp cận

addWord y hệt Trie thường. search cần DFS đệ quy khi gặp .: thử tất cả children.

Code Python 3

class WordDictionary:
    def __init__(self):
        self.root: dict = {}

    def addWord(self, word: str) -> None:
        node = self.root
        for ch in word:
            node = node.setdefault(ch, {})
        node['$'] = True       # marker end

    def search(self, word: str) -> bool:
        def dfs(node: dict, i: int) -> bool:
            if i == len(word):
                return '$' in node
            ch = word[i]
            if ch == '.':
                return any(dfs(child, i + 1)
                           for k, child in node.items() if k != '$')
            if ch not in node:
                return False
            return dfs(node[ch], i + 1)

        return dfs(self.root, 0)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


23.3 Word Search II (LC 212)

Đề bài

Cho ma trận board và mảng words. Trả về tất cả word xuất hiện trên board (qua đường đi 4 hướng, không thăm 1 ô 2 lần).

Ví dụ

Input:  board = [["o","a","a","n"],
                 ["e","t","a","e"],
                 ["i","h","k","r"],
                 ["i","f","l","v"]]
        words = ["oath", "pea", "eat", "rain"]

Output: ["oath", "eat"]   (thứ tự trong output có thể tuỳ ý)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force: với mỗi word, DFS từ mỗi ô → O(W · m · n · 4^L). TLE.

Trie + DFS — O(m · n · 4^L) (không phụ thuộc số word).

Build Trie từ words. DFS từ mỗi ô, đồng hành với một con trỏ trên Trie. Khi gặp node có is_end → thêm word vào kết quả. Sau khi đã ghi nhận 1 word, nên xoá word đó khỏi Trie (đặt is_end = None) để tránh tìm trùng.

Code Python 3

from typing import List

class Solution:
    def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
        # 1. Build Trie.
        root: dict = {}
        for w in words:
            node = root
            for ch in w:
                node = node.setdefault(ch, {})
            node['$'] = w

        rows, cols = len(board), len(board[0])
        DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]
        result: list[str] = []

        def dfs(r: int, c: int, node: dict) -> None:
            ch = board[r][c]
            if ch not in node:
                return
            nxt = node[ch]
            if '$' in nxt:
                result.append(nxt['$'])
                del nxt['$']           # tránh add trùng
            board[r][c] = '#'
            for dr, dc in DIRS:
                nr, nc = r + dr, c + dc
                if 0 <= nr < rows and 0 <= nc < cols and board[nr][nc] != '#':
                    dfs(nr, nc, nxt)
            board[r][c] = ch
            if not nxt:
                del node[ch]            # prune

        for r in range(rows):
            for c in range(cols):
                dfs(r, c, root)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


23.4 Longest Word in Dictionary (LC 720)

Đề bài

Cho mảng words. Tìm word dài nhấtmọi prefix của nó cũng có trong mảng. Nếu hoà, lấy word lex smallest.

Ví dụ

Input:  words = ["w","wo","wor","worl","world"]
Output: "world"   (mọi prefix lồng nhau đều có trong words; có nhiều thì lấy lex nhỏ nhất)

Input:  words = ["a","banana","app","appl","ap","apply","apple"]
Output: "apple"
("apple" và "apply" đều có toàn bộ prefix lồng nhau trong words;
 "apple" < "apply" theo thứ tự lex → chọn "apple")

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cách 1 — Sort + set, O(N · L log N). Sort words. Duy trì valid set. Đi qua từng word: nếu mọi prefix của nó đã trong valid (chỉ cần check prefix length-1), thêm vào valid.

Cách 2 — Trie + BFS/DFS. Build Trie. DFS preorder, ưu tiên is_end. Chỉ đi sâu nếu is_end true.

Cách 1 ngắn gọn hơn cho bài này.

Code Python 3

from typing import List

class Solution:
    def longestWord(self, words: List[str]) -> str:
        words.sort()                          # lex
        valid: set[str] = {""}
        best = ""
        for w in words:
            if w[:-1] in valid:
                valid.add(w)
                if len(w) > len(best):
                    best = w
        return best

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


23.5 Replace Words (LC 648)

Đề bài

Cho dictionary (các “root”) và câu sentence. Với mỗi từ trong sentence, thay nó bằng root ngắn nhất trong dictionary là prefix của nó. Nếu không có root nào → giữ từ.

Ví dụ

Input:  dictionary = ["cat","bat","rat"]
        sentence = "the cattle was rattled by the battery"
Output: "the cat was rat by the bat"

Ràng buộc

Clarifying questions

Hướng tiếp cận

Trie. Insert tất cả root. Với mỗi word trong sentence, duyệt qua Trie: khi gặp is_end đầu tiên → thay bằng prefix tới đó.

Code Python 3

from typing import List

class Solution:
    def replaceWords(self, dictionary: List[str], sentence: str) -> str:
        root: dict = {}
        for w in dictionary:
            node = root
            for ch in w:
                node = node.setdefault(ch, {})
            node['$'] = True

        def replace(word: str) -> str:
            node = root
            prefix = []
            for ch in word:
                if '$' in node:
                    return ''.join(prefix)
                if ch not in node:
                    return word
                prefix.append(ch)
                node = node[ch]
            return ''.join(prefix) if '$' in node else word

        return ' '.join(replace(w) for w in sentence.split())

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


23.6 Stream of Characters (LC 1032)

Đề bài

Thiết kế class StreamChecker với constructor lấy danh sách words. Method query(letter) trả về True nếu suffix của stream hiện tại match một word trong list.

Ví dụ

Input (LC-style operation arrays):
  ops  = ["StreamChecker", "query", "query", "query", "query", "query", "query"]
  args = [[["cd","f","kl"]], ["a"],  ["b"],   ["c"],   ["d"],   ["e"],   ["f"]]

Output: [null, false, false, false, true, false, true]

Giải thích:
  StreamChecker(["cd","f","kl"])
  query('a')  → false  (stream = "a")
  query('b')  → false  (stream = "ab")
  query('c')  → false  (stream = "abc")
  query('d')  → true   (suffix "cd" của stream "abcd" match)
  query('e')  → false
  query('f')  → true   (suffix "f" match)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Insight: suffix khó tìm theo prefix Trie. Reverse trick — build Trie trên các word đảo ngược. Stream cũng đảo (mới nhất đầu): kiểm tra prefix của reversed-stream match Trie.

Implementation: giữ buffer các ký tự đã query. Mỗi query, walk Trie với buffer từ cuối lên đầu (= word đảo).

Code Python 3

from typing import List

class StreamChecker:
    def __init__(self, words: List[str]):
        self.root: dict = {}
        for w in words:
            node = self.root
            for ch in reversed(w):
                node = node.setdefault(ch, {})
            node['$'] = True
        self.buf: list[str] = []

    def query(self, letter: str) -> bool:
        self.buf.append(letter)
        node = self.root
        for ch in reversed(self.buf):
            if '$' in node:
                return True
            if ch not in node:
                return False
            node = node[ch]
        return '$' in node

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Trie design tradeoff

Tiêu chí Dict children {ch: TrieNode} Array [26]
Bộ nhớ Nhỏ khi sparse Lớn nhưng đều
Truy cập O(1) hash O(1) index
Hỗ trợ Unicode ❌ (chỉ 26 chữ)
Code Pythonic, ngắn Nhanh hơn ở C++/Java

Word Search II (LC 212) — pruning

Stream of Characters (LC 1032) — reverse trie

Replace Words (LC 648) — shortest root

Khi đi xuống trie, dừng ngay tại node terminal đầu tiên — đó là root ngắn nhất thay được. Đi tiếp chỉ ra root dài hơn (vô ích).


Chương 24 — Union Find (Disjoint Set Union)

Union Find (DSU) là cấu trúc dữ liệu cho 2 thao tác trên các tập rời: (i) find(x) — root của tập chứa x, (ii) union(x, y) — gộp 2 tập. Với path compression + union by rank/size, mỗi op gần như O(1) (chính xác O(α(n)) với α là hàm Ackermann ngược).

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Template code

class DSU:
    def __init__(self, n: int):
        self.parent = list(range(n))
        self.rank = [0] * n
        self.size = [1] * n
        self.components = n

    def find(self, x: int) -> int:
        while self.parent[x] != x:
            self.parent[x] = self.parent[self.parent[x]]   # path compression
            x = self.parent[x]
        return x

    def union(self, x: int, y: int) -> bool:
        rx, ry = self.find(x), self.find(y)
        if rx == ry:
            return False              # đã cùng tập
        # Union by rank — đính cây thấp dưới cây cao.
        if self.rank[rx] < self.rank[ry]:
            rx, ry = ry, rx
        self.parent[ry] = rx
        self.size[rx] += self.size[ry]
        if self.rank[rx] == self.rank[ry]:
            self.rank[rx] += 1
        self.components -= 1
        return True

    def connected(self, x: int, y: int) -> bool:
        return self.find(x) == self.find(y)

Bài tự luyện cuối chương


24.1 Number of Provinces (LC 547)

Đề bài

n thành phố và ma trận isConnected[i][j] = 1 nếu i, j nối trực tiếp. Province là tập thành phố liên thông. Đếm số province.

Ví dụ

Input:  isConnected=[[1,1,0],[1,1,0],[0,0,1]]
Output: 2

Ràng buộc

Clarifying questions

Hướng tiếp cận

DSU: union các cặp có cạnh, đếm components. Hoặc DFS/BFS — đơn giản hơn.

Code Python 3

from typing import List

class Solution:
    def findCircleNum(self, isConnected: List[List[int]]) -> int:
        n = len(isConnected)
        dsu = DSU(n)
        for i in range(n):
            for j in range(i + 1, n):
                if isConnected[i][j]:
                    dsu.union(i, j)
        return dsu.components

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


24.2 Redundant Connection (LC 684)

Đề bài

Input: edges: List[List[int]] — mảng cạnh [u, v] của graph vô hướng. Graph này được tạo từ 1 tree (n đỉnh, n-1 cạnh) sau đó thêm 1 cạnh thừa làm xuất hiện chu trình. Tìm cạnh thừa cuối cùng trong input có thể xoá để còn lại là tree.

Ví dụ

Input:  edges=[[1,2],[1,3],[2,3]]
Output: [2,3]

Ràng buộc

Clarifying questions

Hướng tiếp cận

DSU union từng cạnh. Cạnh đầu tiên mà find(u) == find(v) đã → đó là cạnh tạo chu trình → return.

Code Python 3

from typing import List

class Solution:
    def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
        n = len(edges)
        dsu = DSU(n + 1)
        for u, v in edges:
            if not dsu.union(u, v):
                return [u, v]
        return []

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


24.3 Accounts Merge (LC 721)

Đề bài

Cho mảng accounts, mỗi cái = [name, email1, email2, ...]. 2 account thuộc cùng người ↔︎ chia sẻ ít nhất 1 email. Gộp các account cùng người thành 1, output email sorted lex.

Ví dụ

Input:  accounts=[["John","a@x","b@x"],["John","b@x","c@x"],["Mary","d@x"]]
Output: [["John","a@x","b@x","c@x"],["Mary","d@x"]]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Code Python 3

from collections import defaultdict
from typing import List

class Solution:
    def accountsMerge(self, accounts: List[List[str]]) -> List[List[str]]:
        dsu_parent: dict[str, str] = {}
        email_to_name: dict[str, str] = {}

        def find(x: str) -> str:
            if dsu_parent.setdefault(x, x) != x:
                dsu_parent[x] = find(dsu_parent[x])
            return dsu_parent[x]

        def union(x: str, y: str) -> None:
            dsu_parent[find(x)] = find(y)

        for acc in accounts:
            name, emails = acc[0], acc[1:]
            for e in emails:
                email_to_name[e] = name
                union(emails[0], e)

        groups: dict[str, list[str]] = defaultdict(list)
        for e in email_to_name:
            groups[find(e)].append(e)

        return [[email_to_name[g[0]]] + sorted(g) for g in groups.values()]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


24.4 Number of Islands II (LC 305)

Đề bài

Cho grid m × n ban đầu toàn nước. Cho mảng positions[i] = [r, c] — biến mỗi ô thành đất theo thứ tự. Sau mỗi thêm, trả về số đảo hiện tại.

Ví dụ

Input:  m = 3, n = 3
        positions = [[0,0], [0,1], [1,2], [2,1]]

Output: [1, 1, 2, 3]

Trace từng bước:

  Sau [0,0]:  [1, 0, 0]     → 1 đảo
              [0, 0, 0]
              [0, 0, 0]

  Sau [0,1]:  [1, 1, 0]     → 1 đảo  (nối với (0,0))
              [0, 0, 0]
              [0, 0, 0]

  Sau [1,2]:  [1, 1, 0]     → 2 đảo
              [0, 0, 1]
              [0, 0, 0]

  Sau [2,1]:  [1, 1, 0]     → 3 đảo
              [0, 0, 1]
              [0, 1, 0]

Ràng buộc

Clarifying questions

Hướng tiếp cận

DSU online. Mỗi position: - Mark ô là đất, components += 1. - Với mỗi hàng xóm 4 hướng đã là đất, union → nếu union thành công, components -= 1. - Push components vào kết quả.

Code Python 3

from typing import List

class Solution:
    def numIslands2(self, m: int, n: int, positions: List[List[int]]) -> List[int]:
        parent = [-1] * (m * n)         # -1 = nước
        components = 0
        DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]
        result: list[int] = []

        def find(x: int) -> int:
            while parent[x] != x:
                parent[x] = parent[parent[x]]
                x = parent[x]
            return x

        def union(x: int, y: int) -> bool:
            rx, ry = find(x), find(y)
            if rx == ry: return False
            parent[rx] = ry
            return True

        for r, c in positions:
            idx = r * n + c
            if parent[idx] != -1:
                result.append(components)
                continue
            parent[idx] = idx
            components += 1
            for dr, dc in DIRS:
                nr, nc = r + dr, c + dc
                if 0 <= nr < m and 0 <= nc < n:
                    nidx = nr * n + nc
                    if parent[nidx] != -1 and union(idx, nidx):
                        components -= 1
            result.append(components)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


24.5 Satisfiability of Equality Equations (LC 990)

Đề bài

Cho mảng equations dạng "a==b" hoặc "a!=b". Trả về True nếu có thể gán giá trị cho biến thoả tất cả equations.

Ví dụ

Input:  ["a==b","b!=a"]
Output: False

Ràng buộc

Clarifying questions

Hướng tiếp cận

2 lượt: 1. Union các == equation. 2. Kiểm tra != equation: nếu 2 biến cùng root → mâu thuẫn.

Code Python 3

from typing import List

class Solution:
    def equationsPossible(self, equations: List[str]) -> bool:
        parent = list(range(26))

        def find(x):
            while parent[x] != x:
                parent[x] = parent[parent[x]]
                x = parent[x]
            return x

        for eq in equations:
            if eq[1] == '=':
                parent[find(ord(eq[0]) - 97)] = find(ord(eq[3]) - 97)
        for eq in equations:
            if eq[1] == '!':
                if find(ord(eq[0]) - 97) == find(ord(eq[3]) - 97):
                    return False
        return True

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


24.6 Swim in Rising Water (LC 778)

Đề bài

Ma trận grid[i][j] = “elevation” tại ô (i, j). Tại thời điểm t, ô có elevation ≤ t có nước phủ đủ để bơi vào. Bắt đầu (0, 0), mục tiêu (n-1, n-1). Tìm t nhỏ nhất.

Ví dụ

Input:  grid=[[0,1,2,3,4],[24,23,22,21,5],[12,13,14,15,16],[11,17,18,19,20],[10,9,8,7,6]]
Output: 16

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cách 1 — DSU offline. Sort các ô theo elevation tăng. Lần lượt “kích hoạt” ô và union với 4 hàng xóm đã kích hoạt. Khi (0,0)(n-1,n-1) cùng component → trả elevation của ô vừa thêm.

Cách 2 — Binary search trên answer + DFS (Chương 37). Cách 3 — Dijkstra (Chương 30).

Cách 1 là pattern DSU đặc trưng nhất.

Code Python 3

from typing import List

class Solution:
    def swimInWater(self, grid: List[List[int]]) -> int:
        n = len(grid)
        parent = list(range(n * n))
        def find(x):
            while parent[x] != x:
                parent[x] = parent[parent[x]]
                x = parent[x]
            return x

        # Liệt kê các ô theo elevation tăng.
        cells = sorted((grid[r][c], r, c) for r in range(n) for c in range(n))
        active = [[False] * n for _ in range(n)]
        DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]

        for t, r, c in cells:
            active[r][c] = True
            idx = r * n + c
            for dr, dc in DIRS:
                nr, nc = r + dr, c + dc
                if 0 <= nr < n and 0 <= nc < n and active[nr][nc]:
                    parent[find(idx)] = find(nr * n + nc)
            if find(0) == find(n * n - 1):
                return t
        return -1

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

DSU = “dynamic connectivity, không traversal”

DSU invariant — parent forest

nodes:  0  1  2  3  4  5
parent: 0  0  0  3  3  5   (sau union(1,0), union(2,1), union(4,3))

forest:
   0           3        5
  / \          |
 1   2         4

Path compression — before/after

Before find(4):           After find(4) with compression:
   0                            0
   |                          / | \
   1                         1  2  4
   |                         |
   2                         3  ← 4 trỏ thẳng lên 0 (qua compression)
   |
   3
   |
   4

Number of Islands II (LC 305) — online add

Swim in Rising Water (LC 778) — threshold connectivity

Cầu nối sang Chương 37 (BS + Graph)Chương 33 (MST): - Sort các ô theo độ cao tăng dần. - Union các ô liền kề đã “ngập” (cùng độ cao đến thời điểm hiện tại). - Khi (0, 0)(n−1, n−1) rơi vào cùng một component, độ cao hiện tại chính là đáp án.


Chương 25 — Advanced Binary Search (Search on Answer)

Chương 5 dạy binary search trên mảng đã sort. Chương này dạy kỹ thuật mạnh hơn: binary search trên đáp án (search on answer / parametric search). Khi không gian đáp án có predicate đơn điệu, ta binary search trên giá trị đáp án thay vì index.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Quy trình: 1. Xác định đáp án là biến gì (capacity, speed, distance, …). 2. Bound [lo, hi] của đáp án. 3. Viết check(mid) -> bool đơn điệu. 4. Binary search “first True” hoặc “last True”.

Template code

def search_on_answer(lo: int, hi: int, check) -> int:
    """Tìm giá trị nhỏ nhất trong [lo, hi] thoả check(x)=True."""
    while lo < hi:
        mid = (lo + hi) // 2
        if check(mid):
            hi = mid                # cố thu nhỏ về True
        else:
            lo = mid + 1
    return lo

Bài tự luyện cuối chương


25.1 Koko Eating Bananas (LC 875)

Đề bài

Koko có n đống chuối, đống ipiles[i] quả. Mỗi giờ Koko ăn 1 đống với tốc độ k quả/giờ (nếu đống < k, vẫn coi như 1 giờ). Tìm k nhỏ nhất để ăn hết trong h giờ.

Ví dụ

Input:  piles = [3,6,7,11], h = 8   → 4
Input:  piles = [30,11,23,4,20], h = 5 → 30
Input:  piles = [30,11,23,4,20], h = 6 → 23

Ràng buộc

Clarifying questions

Hướng tiếp cận

Đáp án k[1, max(piles)]. Predicate check(k) = “ăn hết trong ≤ h giờ?”.

time(k) = Σ ceil(piles[i] / k). Predicate đơn điệu: k lớn hơn → time nhỏ hơn. → Tìm k nhỏ nhất thoả time(k) ≤ h.

Code Python 3

from math import ceil
from typing import List

class Solution:
    def minEatingSpeed(self, piles: List[int], h: int) -> int:
        def can_finish(k: int) -> bool:
            return sum((p + k - 1) // k for p in piles) <= h

        lo, hi = 1, max(piles)
        while lo < hi:
            mid = (lo + hi) // 2
            if can_finish(mid):
                hi = mid
            else:
                lo = mid + 1
        return lo

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


25.2 Capacity To Ship Packages Within D Days (LC 1011)

Đề bài

Cho weights[] (theo thứ tự), số ngày days. Tìm capacity nhỏ nhất của ship để chuyển hết hàng trong days ngày (mỗi ngày 1 chuyến, ship phải chở package liên tục theo thứ tự).

Ví dụ

Input:  weights = [1,2,3,4,5,6,7,8,9,10], days = 5
Output: 15

Ràng buộc

Clarifying questions

Hướng tiếp cận

Đáp án cap[max(weights), sum(weights)]. (cap < max → không chở được package lớn nhất; cap = sum → 1 ngày xong.)

Predicate: “với cap này, chuyển hết trong ≤ days ngày?” → greedy đếm ngày.

Code Python 3

from typing import List

class Solution:
    def shipWithinDays(self, weights: List[int], days: int) -> int:
        def can_ship(cap: int) -> bool:
            d = 1
            cur = 0
            for w in weights:
                if cur + w > cap:
                    d += 1
                    cur = 0
                cur += w
            return d <= days

        lo, hi = max(weights), sum(weights)
        while lo < hi:
            mid = (lo + hi) // 2
            if can_ship(mid):
                hi = mid
            else:
                lo = mid + 1
        return lo

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


25.3 Split Array Largest Sum (LC 410)

Đề bài

Cho nums (non-negative), số k. Chia mảng thành k subarray liên tục không rỗng. Tìm cách chia sao cho max sum subarraynhỏ nhất.

Ví dụ

Input:  nums = [7,2,5,10,8], k = 2
Output: 18    (chia [7,2,5] và [10,8])

Ràng buộc

Clarifying questions

Hướng tiếp cận

Y hệt 25.2! Đáp án = max sum. check(cap) = “chia được thành ≤ k subarray với mỗi subarray sum ≤ cap?”.

Code Python 3

from typing import List

class Solution:
    def splitArray(self, nums: List[int], k: int) -> int:
        def can_split(cap: int) -> bool:
            groups = 1
            cur = 0
            for x in nums:
                if cur + x > cap:
                    groups += 1
                    cur = 0
                cur += x
            return groups <= k

        lo, hi = max(nums), sum(nums)
        while lo < hi:
            mid = (lo + hi) // 2
            if can_split(mid):
                hi = mid
            else:
                lo = mid + 1
        return lo

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


25.4 Find K-th Smallest Pair Distance (LC 719)

Đề bài

Cho nums. Tính tất cả khoảng cách |nums[i] - nums[j]| với i < j. Trả về khoảng cách nhỏ thứ k.

Ví dụ

Input:  nums=[1,3,1], k=1
Output: 0

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force: tạo tất cả C(n, 2) khoảng cách, sort, lấy thứ k. O(n² log n²). TLE với n = 10^4.

Search on answer + Sliding window đếm: - Sort nums. Đáp án ∈ [0, max - min]. - check(d) = “có ≥ k cặp có khoảng cách ≤ d?” - Đếm cặp bằng sliding window: với mỗi right, mở rộng left đến khi nums[right] - nums[left] <= d. Số cặp tạo thành = right - left.

Code Python 3

from typing import List

class Solution:
    def smallestDistancePair(self, nums: List[int], k: int) -> int:
        nums.sort()

        def count_le(d: int) -> int:
            cnt = 0
            left = 0
            for right in range(len(nums)):
                while nums[right] - nums[left] > d:
                    left += 1
                cnt += right - left
            return cnt

        lo, hi = 0, nums[-1] - nums[0]
        while lo < hi:
            mid = (lo + hi) // 2
            if count_le(mid) >= k:
                hi = mid
            else:
                lo = mid + 1
        return lo

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


25.5 Median of Two Sorted Arrays (LC 4)

Đề bài

Cho 2 mảng sort nums1, nums2. Tìm median của 2 mảng gộp lại. O(log(m+n)).

Ví dụ

Input:  nums1=[1,3], nums2=[2]
Output: 2.0
Giải thích: Median của [1,2,3] = 2

Ràng buộc

Clarifying questions

Hướng tiếp cận

Binary search trên partition. Tìm i (chia nums1 ở vị trí i) và j = (m + n + 1) // 2 - i (chia nums2) sao cho: - nums1[i-1] <= nums2[j]nums2[j-1] <= nums1[i].

Khi tìm được, median = max(left) (nếu tổng lẻ) hoặc (max(left) + min(right)) / 2 (chẵn).

Đảm bảo binary search trên mảng ngắn hơn để O(log min(m, n)).

Code Python 3

from typing import List

class Solution:
    def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
        # Đảm bảo nums1 ngắn hơn.
        if len(nums1) > len(nums2):
            nums1, nums2 = nums2, nums1
        m, n = len(nums1), len(nums2)
        half = (m + n + 1) // 2
        INF = float('inf')

        lo, hi = 0, m
        while lo <= hi:
            i = (lo + hi) // 2
            j = half - i
            left1 = nums1[i - 1] if i > 0 else -INF
            right1 = nums1[i] if i < m else INF
            left2 = nums2[j - 1] if j > 0 else -INF
            right2 = nums2[j] if j < n else INF
            if left1 <= right2 and left2 <= right1:
                if (m + n) % 2:
                    return max(left1, left2)
                return (max(left1, left2) + min(right1, right2)) / 2
            elif left1 > right2:
                hi = i - 1
            else:
                lo = i + 1
        return 0.0

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


25.6 Aggressive Cows (bài kinh điển)

Đề bài

Cho n chuồng tại vị trí pos[i] (đã sort) và c con bò. Đặt mỗi con vào 1 chuồng sao cho khoảng cách nhỏ nhất giữa 2 conlớn nhất. Trả về khoảng cách đó.

Ví dụ

Input:  pos = [1, 2, 4, 8, 9], c = 3
Output: 3
Giải thích: đặt 1, 4, 8 → min distance = 3.

Ràng buộc

Clarifying questions

Hướng tiếp cận

Đáp án d[0, max - min]. Predicate: “đặt được c con với khoảng cách ≥ d?” → greedy: đặt con đầu ở pos đầu tiên, kế tiếp ở pos ≥ pos_trước + d.

Code Python 3

from typing import List

class Solution:
    def aggressiveCows(self, pos: List[int], c: int) -> int:
        pos.sort()

        def can_place(d: int) -> bool:
            placed = 1
            last = pos[0]
            for p in pos[1:]:
                if p - last >= d:
                    placed += 1
                    last = p
                    if placed >= c:
                        return True
            return False

        lo, hi = 0, pos[-1] - pos[0]
        while lo < hi:
            mid = (lo + hi + 1) // 2     # search "last True" → ceil mid
            if can_place(mid):
                lo = mid
            else:
                hi = mid - 1
        return lo

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Search on Answer — universal table

Bài Đáp số tìm lo, hi Predicate can(x) Mục tiêu
Koko (LC 875) tốc độ k 1, max(piles) tổng giờ ≤ H min x sao cho can(x)
Ship (LC 1011) capacity max(w), sum(w) giao trong ≤ D ngày min
Split Array (LC 410) largest subarray sum max(arr), sum(arr) chia được ≤ m phần min
Aggressive Cows khoảng cách d 1, max - min đặt được ≥ C bò max x sao cho can(x)
Magnetic Force (LC 1552) force 1, max - min đặt được ≥ m max

Khung chung (first-true / last-true):

# first-true monotonic: F F F T T T → trả T đầu tiên
while lo < hi:
    mid = (lo + hi) // 2
    if can(mid): hi = mid
    else: lo = mid + 1
return lo

Predicate monotonic — chứng minh

Phải kiểm chứng can(x) đơn điệu theo x: - Koko: k lớn → ăn nhanh hơn → tổng giờ giảm ⇒ can đơn điệu tăng. - Aggressive Cows: d lớn → khó đặt hơn → số bò đặt được giảm ⇒ can đơn điệu giảm.

Median of Two Sorted Arrays — KHÔNG phải search on answer

Đây là binary partition: tìm i, j sao cho A[..i] ∪ B[..j] là nửa nhỏ. Pattern khác hẳn, đừng ép vào template chung.

Aggressive Cows — feasibility

positions sorted: [1, 2, 4, 8, 9]    cows c = 3, d = ?
d = 3 ⇒ pick 1, 4, 8 ✅ (3 con)
d = 4 ⇒ pick 1, 8     (chỉ 2 con) ❌
⇒ max d sao cho ≥ 3 con = 3

Chương 26 — Two Pointers (Fast & Slow Pointer)

Two pointers là vũ khí cơ bản đã xuất hiện ở Array (1.4, 1.5), String (2.2), Linked List (7.3-7.6). Chương này hệ thống hoá thành 3 mẫu: (i) 2 đầu (đối nghịch), (ii) slow & fast (1×/2×), (iii) same direction (sliding window). Mỗi mẫu giải được một họ bài.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

3 mẫu hay nhất:

Mẫu Ví dụ bài Đặc điểm
2 đầu 3Sum, Container Most Water, Trapping sort trước, hội tụ từ 2 phía
Slow & Fast Cycle, Middle of LL, Move Zeroes tốc độ khác nhau
Same direction Sliding window, Remove Dup window mở rộng / thu hẹp

Template code

# 1) 2 đầu (sorted)
l, r = 0, len(arr) - 1
while l < r:
    if check(arr[l], arr[r]):
        # ... dùng cặp ...
        l += 1
        r -= 1
    elif arr[l] + arr[r] < target:
        l += 1
    else:
        r -= 1


# 2) Slow & Fast (in-place)
slow = 0
for fast in range(len(arr)):
    if condition(arr[fast]):
        arr[slow] = arr[fast]
        slow += 1

Bài tự luyện cuối chương


26.1 3Sum (LC 15)

Đề bài

Cho nums. Tìm tất cả triplet [a, b, c] với a + b + c == 0, không trùng lặp.

Ví dụ

Input:  nums = [-1, 0, 1, 2, -1, -4]
Output: [[-1, -1, 2], [-1, 0, 1]]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force O(n³). Mọi triplet.

Sort + two pointers O(n²).

Sort. Với mỗi i, dùng two pointers l, r tìm cặp tổng -nums[i] trong nums[i+1..n-1].

Xử lý duplicate: skip ký tự trùng tại cả i, l, r.

Hình minh hoạ với nums = [-1, 0, 1, 2, -1, -4] sort → [-4, -1, -1, 0, 1, 2]:

i=0 (-4): target=4. l=1 (-1), r=5 (2). -1+2=1 < 4. l++. ... không tìm được.
i=1 (-1): target=1. l=2 (-1), r=5 (2). -1+2=1 ✓ → add [-1,-1,2].
                    l++, r--. l=3 (0), r=4 (1). 0+1=1 ✓ → add [-1,0,1].
i=2 (-1): skip (trùng nums[1]).
i=3 (0): target=0. l=4 (1), r=5 (2). 1+2=3 > 0. r--. → l=r → dừng.

Code Python 3

from typing import List

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        result: list[list[int]] = []
        n = len(nums)
        for i in range(n - 2):
            if nums[i] > 0:
                break
            if i > 0 and nums[i] == nums[i - 1]:
                continue
            l, r = i + 1, n - 1
            target = -nums[i]
            while l < r:
                s = nums[l] + nums[r]
                if s == target:
                    result.append([nums[i], nums[l], nums[r]])
                    l += 1; r -= 1
                    while l < r and nums[l] == nums[l - 1]: l += 1
                    while l < r and nums[r] == nums[r + 1]: r -= 1
                elif s < target:
                    l += 1
                else:
                    r -= 1
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


26.2 Trapping Rain Water (LC 42)

Đề bài

Cho mảng height[]. Mỗi cột rộng 1. Tính lượng nước đọng giữa các cột.

Ví dụ

Input:  height = [0,1,0,2,1,0,1,3,2,1,2,1]
        (mỗi phần tử là chiều cao 1 cột rộng 1 đơn vị)
Output: 6   (tổng lượng nước đọng giữa các cột)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cách 1 — Prefix max + Suffix max, O(n) time, O(n) space.

Mỗi ô i chứa được: min(max_left[i], max_right[i]) - height[i].

Cách 2 — Two pointers, O(n) time, O(1) space.

l = 0, r = n - 1, giữ lmax, rmax. Dịch con trỏ ở phía cột thấp hơn — chính bên đó quyết định nước đọng tại ô đang xét.

Code Python 3

from typing import List

class Solution:
    def trap(self, height: List[int]) -> int:
        l, r = 0, len(height) - 1
        lmax = rmax = 0
        water = 0
        while l < r:
            if height[l] < height[r]:
                if height[l] >= lmax:
                    lmax = height[l]
                else:
                    water += lmax - height[l]
                l += 1
            else:
                if height[r] >= rmax:
                    rmax = height[r]
                else:
                    water += rmax - height[r]
                r -= 1
        return water

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


26.3 Container With Most Water (LC 11) — recap

Đã giải đầy đủ ở Chương 1.5. Đây là two pointers pattern cốt lõi.

Liên hệ với chương này

Code Python 3 (recap)

from typing import List

class Solution:
    def maxArea(self, height: List[int]) -> int:
        l, r = 0, len(height) - 1
        best = 0
        while l < r:
            h = min(height[l], height[r])
            best = max(best, h * (r - l))
            if height[l] < height[r]:
                l += 1
            else:
                r -= 1
        return best

Phân tích độ phức tạp

Bài tự luyện liên quan


26.4 Sort Colors (LC 75) — recap

Đã giải đầy đủ ở Chương 4.1. Three pointers (Dutch flag).

Liên hệ với chương này

Code Python 3 (recap)

from typing import List

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        lo, mid, hi = 0, 0, len(nums) - 1
        while mid <= hi:
            if nums[mid] == 0:
                nums[lo], nums[mid] = nums[mid], nums[lo]
                lo += 1; mid += 1
            elif nums[mid] == 1:
                mid += 1
            else:
                nums[mid], nums[hi] = nums[hi], nums[mid]
                hi -= 1

Phân tích độ phức tạp

Bài tự luyện liên quan


26.5 Remove Duplicates from Sorted Array II (LC 80)

Đề bài

Cho mảng nums đã sort. Xoá duplicate sao cho mỗi phần tử xuất hiện tối đa 2 lần, trả về độ dài mới. In-place.

Ví dụ

Input:  nums = [1,1,1,2,2,3]
Output: 5, nums = [1,1,2,2,3,_]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Two pointers slow & fast. Vì sort, chỉ cần check nums[fast] != nums[slow - 2].

Code Python 3

from typing import List

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        slow = 0
        for x in nums:
            if slow < 2 or x != nums[slow - 2]:
                nums[slow] = x
                slow += 1
        return slow

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


26.6 4Sum (LC 18)

Đề bài

Cho numstarget. Tìm tất cả quadruplet [a, b, c, d] với tổng target, không trùng.

Ví dụ

Input:  nums=[1,0,-1,0,-2,2], target=0
Output: [[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Generalization của 3Sum. Sort + 2 vòng lặp ngoài (i, j) + two pointers cho (l, r). O(n³).

Skip duplicate ở cả 4 vị trí i, j, l, r.

Code Python 3

from typing import List

class Solution:
    def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
        nums.sort()
        n = len(nums)
        result: list[list[int]] = []
        for i in range(n - 3):
            if i > 0 and nums[i] == nums[i - 1]:
                continue
            for j in range(i + 1, n - 2):
                if j > i + 1 and nums[j] == nums[j - 1]:
                    continue
                l, r = j + 1, n - 1
                tgt = target - nums[i] - nums[j]
                while l < r:
                    s = nums[l] + nums[r]
                    if s == tgt:
                        result.append([nums[i], nums[j], nums[l], nums[r]])
                        l += 1; r -= 1
                        while l < r and nums[l] == nums[l - 1]: l += 1
                        while l < r and nums[r] == nums[r + 1]: r -= 1
                    elif s < tgt:
                        l += 1
                    else:
                        r -= 1
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Two-pointer family — chọn shape nào?

Shape Khi dùng Bài tiêu biểu
Two-end (l=0, r=n-1, đi vào giữa) Sorted, palindrome, container LC 11, 15, 167, 125
Same-direction (l, r đều đi tiến) Subarray với invariant (sliding window là dạng đặc biệt) LC 26, 283; Chương 27
Slow-fast (slow 1 bước, fast 2 bước) Cycle detect, middle node LC 141, 142, 876

3Sum / 4Sum — duplicate skip checklist

nums.sort()
for i in range(n):
    if i > 0 and nums[i] == nums[i-1]: continue          # skip anchor dup
    l, r = i+1, n-1
    while l < r:
        s = nums[i] + nums[l] + nums[r]
        if s == 0:
            ans.append([nums[i], nums[l], nums[r]])
            l += 1; r -= 1
            while l < r and nums[l] == nums[l-1]: l += 1  # skip inner left dup
            while l < r and nums[r] == nums[r+1]: r -= 1  # skip inner right dup
        elif s < 0: l += 1
        else: r -= 1

4 chỗ skip — quên chỗ nào cũng sinh duplicate.

Trapping Rain Water (LC 42) — 2 approaches

Prefix/suffix max array Two pointers
Time O(n) O(n)
Space O(n) O(1)
Code dài Trung bình Ngắn hơn
Hiểu Trực quan: nước tại i = min(L[i], R[i]) - h[i] Cần argue tại sao “thanh thấp hơn quyết định”

Recommend: nói trước prefix/suffix (dễ giải thích), rồi tối ưu two-pointer nếu interviewer hỏi space.

Recap Container / Sort Colors lens


Chương 27 — Sliding Window

Sliding Window = two pointers cùng chiều. Window [l, r] mở rộng r, co l khi vi phạm điều kiện. Pattern này giải gọn rất nhiều bài “longest / shortest / count substring/subarray thoả điều kiện” trong O(n).

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

2 mẫu chính:

Mẫu Pseudo
Fixed-size window duy trì r - l + 1 == k luôn
Variable-size window mở rộng r luôn, co l đến khi valid() thoả

Template code

from collections import defaultdict, Counter

# 1) Variable-size: longest with condition
l = 0
state = ...
best = 0
for r in range(len(s)):
    add(s[r], state)
    while not valid(state):
        remove(s[l], state)
        l += 1
    best = max(best, r - l + 1)


# 2) Count subarray "exactly K" = "at most K" - "at most K-1"
def at_most(k):
    l, total = 0, 0
    state = ...
    for r in range(len(arr)):
        add(arr[r], state)
        while violates(state):
            remove(arr[l], state)
            l += 1
        total += r - l + 1
    return total

result = at_most(k) - at_most(k - 1)

Bài tự luyện cuối chương


27.1 Longest Substring Without Repeating Characters (LC 3)

Đề bài

Cho chuỗi s. Tìm độ dài substring dài nhất không có ký tự lặp.

Ví dụ

Input:  s = "abcabcbb"      → Output: 3   (substring đẹp nhất: "abc")
Input:  s = "bbbbb"         → Output: 1   (substring "b")
Input:  s = "pwwkew"        → Output: 3   (substring "wke")

Ràng buộc

Clarifying questions

Hướng tiếp cận

Sliding window + set / dict.

Mở rộng r. Khi s[r] đã trong window, co l cho đến khi không còn.

Tối ưu hơn — dict last-seen index: l = max(l, last_seen[s[r]] + 1).

Code Python 3

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        last: dict[str, int] = {}
        l = 0
        best = 0
        for r, ch in enumerate(s):
            if ch in last and last[ch] >= l:
                l = last[ch] + 1
            last[ch] = r
            best = max(best, r - l + 1)
        return best

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


27.2 Minimum Window Substring (LC 76)

Đề bài

Cho s, t. Tìm substring nhỏ nhất của s chứa mọi ký tự của t (kể cả frequency).

Ví dụ

Input:  s = "ADOBECODEBANC", t = "ABC"   → "BANC"

Ràng buộc

Clarifying questions

Hướng tiếp cận

Sliding window + 2 Counter.

Optimization: track formed (số key thoả) thay vì so sánh dict.

Code Python 3

from collections import Counter

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        if not t or len(t) > len(s):
            return ""
        need = Counter(t)
        have: dict[str, int] = {}
        required = len(need)
        formed = 0
        l = 0
        best = (float('inf'), 0, 0)
        for r, ch in enumerate(s):
            have[ch] = have.get(ch, 0) + 1
            if ch in need and have[ch] == need[ch]:
                formed += 1
            while formed == required:
                if r - l + 1 < best[0]:
                    best = (r - l + 1, l, r)
                have[s[l]] -= 1
                if s[l] in need and have[s[l]] < need[s[l]]:
                    formed -= 1
                l += 1
        return "" if best[0] == float('inf') else s[best[1]:best[2] + 1]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


27.3 Longest Repeating Character Replacement (LC 424)

Đề bài

Cho sk. Bạn có thể đổi tối đa k ký tự thành ký tự bất kỳ. Tìm độ dài substring dài nhất có tất cả ký tự giống nhau sau khi đổi.

Ví dụ

Input:  s = "ABAB", k = 2   → 4
Input:  s = "AABABBA", k = 1 → 4 ("AABA" hoặc "ABBA")

Ràng buộc

Clarifying questions

Hướng tiếp cận

Insight: window valid ↔︎ window_len - max_freq <= k (số ký tự cần đổi ≤ k).

Mở rộng r, cập nhật max_freq. Nếu vi phạm → co l 1 bước.

Trick: max_freq không cần “decrease” khi co l — vì kết quả không giảm khi max_freq chỉ giữ giá trị cũ (window vẫn không lớn hơn).

Code Python 3

from collections import defaultdict

class Solution:
    def characterReplacement(self, s: str, k: int) -> int:
        count: dict[str, int] = defaultdict(int)
        l = 0
        max_freq = 0
        best = 0
        for r, ch in enumerate(s):
            count[ch] += 1
            max_freq = max(max_freq, count[ch])
            while (r - l + 1) - max_freq > k:
                count[s[l]] -= 1
                l += 1
            best = max(best, r - l + 1)
        return best

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


27.4 Permutation in String (LC 567)

Đề bài

Cho s1, s2. Kiểm tra s2 có chứa permutation của s1 không (= có substring nào trong s2 mà Counter == Counter(s1)).

Ví dụ

Input:  s1="ab", s2="eidbaooo"
Output: True
Giải thích: s2 chứa "ba" — permutation của "ab"

Ràng buộc

Clarifying questions

Hướng tiếp cận

Fixed-size sliding window với độ dài len(s1). So sánh 2 array đếm 26 phần tử mỗi bước.

Code Python 3

class Solution:
    def checkInclusion(self, s1: str, s2: str) -> bool:
        if len(s1) > len(s2):
            return False
        need = [0] * 26
        have = [0] * 26
        for ch in s1:
            need[ord(ch) - 97] += 1
        for i, ch in enumerate(s2):
            have[ord(ch) - 97] += 1
            if i >= len(s1):
                have[ord(s2[i - len(s1)]) - 97] -= 1
            if have == need:
                return True
        return False

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


27.5 Subarrays with K Different Integers (LC 992)

Đề bài

Cho numsk. Đếm số subarray có đúng k ký tự phân biệt.

Ví dụ

Input:  nums = [1,2,1,2,3], k = 2   → 7
Input:  nums = [1,2,1,3,4], k = 3   → 3

Ràng buộc

Clarifying questions

Hướng tiếp cận

Trick: “exactly K” = “at most K” - “at most K - 1”.

at_most(k): sliding window đếm số subarray với ≤ k distinct integer.

Code Python 3

from collections import defaultdict
from typing import List

class Solution:
    def subarraysWithKDistinct(self, nums: List[int], k: int) -> int:

        def at_most(k: int) -> int:
            count: dict[int, int] = defaultdict(int)
            distinct = 0
            l = 0
            total = 0
            for r in range(len(nums)):
                if count[nums[r]] == 0:
                    distinct += 1
                count[nums[r]] += 1
                while distinct > k:
                    count[nums[l]] -= 1
                    if count[nums[l]] == 0:
                        distinct -= 1
                    l += 1
                total += r - l + 1
            return total

        return at_most(k) - at_most(k - 1)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


27.6 Sliding Window Maximum (LC 239) — recap

Đã giải đầy đủ ở Chương 18.4 (Monotonic Queue).

Code Python 3

from collections import deque
from typing import List


class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        dq: deque[int] = deque()  # lưu **index**, value tại dq giảm dần
        result: List[int] = []
        for i, x in enumerate(nums):
            while dq and nums[dq[-1]] <= x:
                dq.pop()
            dq.append(i)
            if dq[0] <= i - k:
                dq.popleft()
            if i >= k - 1:
                result.append(nums[dq[0]])
        return result

Liên hệ

Phân tích độ phức tạp

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Sliding window applicability

Minimum Window Substring (LC 76) — need / have trace

S = "ADOBECODEBANC", T = "ABC"
need = {A:1, B:1, C:1}; need_unique = 3
have_unique = số ký tự đã match đủ.

window mở rộng tới khi have_unique == need_unique
→ co trái cho đến khi mất 1 ký tự cần thiết → cập nhật min.

Bug điển hình: nhầm “match đủ” với “tổng count đủ”. Chỉ tăng have_unique khi cnt[c] == need[c] (vừa khớp), giảm khi cnt[c] < need[c] (vừa thiếu).

Exactly K = atMost(K) - atMost(K-1)

Fixed vs Variable vs atMost

Loại Pattern Bài
Fixed k Window size k, slide 1 bước LC 643, 567
Variable shrink khi vi phạm Mở r, co l đến khi hợp lệ LC 3, 76, 209
atMost K Mở r, co l đến khi < K đặc tính LC 992, 1248

Chương 28 — Backtracking

Backtracking = DFS đi kèm với undo state khi quay lui. Đây là pattern để liệt kê mọi cấu hình hợp lệ (permutation, subset, combination, Sudoku, N-Queens). Bí mật của backtracking hiệu quả là pruning — cắt nhánh sớm khi biết chắc không dẫn đến nghiệm.

Chương 12 bài — đầy đủ template + 12 bài kinh điển từ Medium đến Hard.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

3 thành phần của backtracking: 1. Choice: tại mỗi bước có lựa chọn nào? 2. Constraint: lựa chọn nào hợp lệ? 3. Goal: khi nào dừng và ghi kết quả?

Template code

def backtrack(path, choices):
    if is_goal(path):
        result.append(path.copy())
        return
    for c in choices:
        if not valid(c, path):
            continue
        path.append(c)
        backtrack(path, next_choices(choices, c))
        path.pop()              # UNDO — đặc trưng backtracking

Bài tự luyện cuối chương


28.1 Combinations (LC 77)

Đề bài

Cho nk. Trả về tất cả combination chọn k số từ 1..n.

Ví dụ

Input:  n = 4, k = 2
Output: [[1,2], [1,3], [1,4], [2,3], [2,4], [3,4]]
        (thứ tự các combination trong output có thể tuỳ ý)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Backtracking với start: tại mỗi bước chọn 1 số ≥ start để tránh trùng.

def backtrack(start):
    if len(path) == k:
        result.append(path.copy()); return
    for i in range(start, n + 1):
        path.append(i)
        backtrack(i + 1)
        path.pop()

Code Python 3

from typing import List

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        result: list[list[int]] = []
        path: list[int] = []

        def backtrack(start: int) -> None:
            if len(path) == k:
                result.append(path.copy())
                return
            # Pruning: cần thêm (k - len(path)) số, max start = n - (k - len(path)) + 1.
            for i in range(start, n - (k - len(path)) + 2):
                path.append(i)
                backtrack(i + 1)
                path.pop()

        backtrack(1)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


28.2 Subsets II (LC 90)

Đề bài

Cho nums (có thể duplicate). Trả về tất cả tập con không trùng.

Ví dụ

Input:  nums = [1, 2, 2]   (có duplicate)
Output: [[], [1], [1,2], [1,2,2], [2], [2,2]]
        (mọi subset duy nhất, mảng kết quả có thể theo thứ tự bất kỳ)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Sort nums. Tại mỗi level, skip duplicate với if i > start and nums[i] == nums[i-1]: continue.

Code Python 3

from typing import List

class Solution:
    def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        result: list[list[int]] = []
        path: list[int] = []

        def backtrack(start: int) -> None:
            result.append(path.copy())
            for i in range(start, len(nums)):
                if i > start and nums[i] == nums[i - 1]:
                    continue
                path.append(nums[i])
                backtrack(i + 1)
                path.pop()

        backtrack(0)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


28.3 Permutations II (LC 47)

Đề bài

Cho nums có duplicate. Trả về tất cả hoán vị phân biệt.

Ví dụ

Input:  nums = [1, 1, 2]   (có duplicate)
Output: [[1,1,2], [1,2,1], [2,1,1]]
        (mọi permutation duy nhất)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Sort + skip duplicate. Mảng used. Trick: nếu nums[i] == nums[i-1] nums[i-1] chưa dùng → skip (đảm bảo dùng theo thứ tự index xuất hiện).

Code Python 3

from typing import List

class Solution:
    def permuteUnique(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        result: list[list[int]] = []
        path: list[int] = []
        used = [False] * len(nums)

        def backtrack() -> None:
            if len(path) == len(nums):
                result.append(path.copy())
                return
            for i in range(len(nums)):
                if used[i]:
                    continue
                if i > 0 and nums[i] == nums[i - 1] and not used[i - 1]:
                    continue
                used[i] = True
                path.append(nums[i])
                backtrack()
                path.pop()
                used[i] = False

        backtrack()
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


28.4 Letter Combinations of a Phone Number (LC 17)

Đề bài

Cho chuỗi số 2..9. Trả về tất cả tổ hợp ký tự từ bàn phím điện thoại.

Ví dụ

Input:  digits = "23"
Output: ["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]
        ('2' → "abc"; '3' → "def"; thứ tự có thể tuỳ ý)

Input:  digits = ""
Output: []

Ràng buộc

Clarifying questions

Hướng tiếp cận

Mapping {'2':'abc', ..., '9':'wxyz'}. Backtrack chọn 1 chữ cho mỗi digit.

Code Python 3

from typing import List

class Solution:
    MAP = {'2':'abc','3':'def','4':'ghi','5':'jkl','6':'mno','7':'pqrs','8':'tuv','9':'wxyz'}

    def letterCombinations(self, digits: str) -> List[str]:
        if not digits:
            return []
        result: list[str] = []
        path: list[str] = []

        def backtrack(i: int) -> None:
            if i == len(digits):
                result.append(''.join(path))
                return
            for ch in self.MAP[digits[i]]:
                path.append(ch)
                backtrack(i + 1)
                path.pop()

        backtrack(0)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


28.5 Combination Sum (LC 39)

Đề bài

Cho candidates (phân biệt) và target. Tìm tất cả combination tổng target. Mỗi số dùng được nhiều lần.

Ví dụ

Input:  candidates=[2,3,6,7], target=7
Output: [[2,2,3],[7]]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Backtrack với start. Cho phép dùng lại → đệ quy backtrack(i) thay vì i+1.

Code Python 3

from typing import List

class Solution:
    def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
        candidates.sort()
        result: list[list[int]] = []
        path: list[int] = []

        def backtrack(start: int, remain: int) -> None:
            if remain == 0:
                result.append(path.copy())
                return
            for i in range(start, len(candidates)):
                if candidates[i] > remain:
                    break               # sorted → mọi sau đều lớn
                path.append(candidates[i])
                backtrack(i, remain - candidates[i])
                path.pop()

        backtrack(0, target)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


28.6 Palindrome Partitioning (LC 131)

Đề bài

Cho s. Trả về tất cả cách phân chia s sao cho mỗi phần là palindrome.

Ví dụ

Input:  s = "aab"
Output: [["a","a","b"], ["aa","b"]]
        (mọi cách phân chia mà mỗi phần là palindrome; thứ tự có thể tuỳ ý)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Backtrack: tại mỗi start, thử mọi end sao cho s[start..end] palindrome, đệ quy start = end + 1.

Code Python 3

from typing import List

class Solution:
    def partition(self, s: str) -> List[List[str]]:
        result: list[list[str]] = []
        path: list[str] = []

        def is_pal(l: int, r: int) -> bool:
            while l < r:
                if s[l] != s[r]:
                    return False
                l += 1; r -= 1
            return True

        def backtrack(start: int) -> None:
            if start == len(s):
                result.append(path.copy())
                return
            for end in range(start, len(s)):
                if is_pal(start, end):
                    path.append(s[start:end + 1])
                    backtrack(end + 1)
                    path.pop()

        backtrack(0)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


28.7 N-Queens (LC 51)

Đề bài

Đặt n hậu trên bàn n × n sao cho không có 2 hậu nào tấn công nhau. Trả về tất cả cấu hình (mỗi cấu hình là list các string).

Ví dụ

Input:  n=4
Output: [[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Backtrack theo hàng. Tại hàng r, thử mỗi cột c. Đảm bảo c chưa dùng, đường chéo (r - c) chưa dùng, đường chéo (r + c) chưa dùng.

Code Python 3

from typing import List

class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        cols = set()
        diag1 = set()       # r - c
        diag2 = set()       # r + c
        board = [['.'] * n for _ in range(n)]
        result: List[List[str]] = []

        def backtrack(r: int) -> None:
            if r == n:
                result.append([''.join(row) for row in board])
                return
            for c in range(n):
                if c in cols or (r - c) in diag1 or (r + c) in diag2:
                    continue
                cols.add(c); diag1.add(r - c); diag2.add(r + c)
                board[r][c] = 'Q'
                backtrack(r + 1)
                board[r][c] = '.'
                cols.discard(c); diag1.discard(r - c); diag2.discard(r + c)

        backtrack(0)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


28.8 Sudoku Solver (LC 37)

Đề bài

Giải Sudoku 9×9 in-place. Ô chưa điền là '.'.

Ví dụ

Input:  board = (ma trận 9×9, ký tự '.' cho ô trống, '1'..'9' cho số đã có)
  [['5','3','.','.','7','.','.','.','.'],
   ['6','.','.','1','9','5','.','.','.'],
   ['.','9','8','.','.','.','.','6','.'],
   ['8','.','.','.','6','.','.','.','3'],
   ['4','.','.','8','.','3','.','.','1'],
   ['7','.','.','.','2','.','.','.','6'],
   ['.','6','.','.','.','.','2','8','.'],
   ['.','.','.','4','1','9','.','.','5'],
   ['.','.','.','.','8','.','.','7','9']]

Output: board được mutate in-place thành lời giải hợp lệ
  [['5','3','4','6','7','8','9','1','2'],
   ['6','7','2','1','9','5','3','4','8'],
   ['1','9','8','3','4','2','5','6','7'],
   ['8','5','9','7','6','1','4','2','3'],
   ['4','2','6','8','5','3','7','9','1'],
   ['7','1','3','9','2','4','8','5','6'],
   ['9','6','1','5','3','7','2','8','4'],
   ['2','8','7','4','1','9','6','3','5'],
   ['3','4','5','2','8','6','1','7','9']]

(Hàm không return giá trị; chỉ mutate `board`. Mỗi hàng, mỗi cột,
 và mỗi sub-grid 3×3 đều chứa '1'..'9' đúng 1 lần.)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Backtrack mỗi ô trống. Track 3 set: row, col, 3×3 box.

Code Python 3

from typing import List

class Solution:
    def solveSudoku(self, board: List[List[str]]) -> None:
        rows = [set() for _ in range(9)]
        cols = [set() for _ in range(9)]
        boxes = [set() for _ in range(9)]
        empty: list[tuple[int, int]] = []
        for r in range(9):
            for c in range(9):
                if board[r][c] != '.':
                    d = board[r][c]
                    rows[r].add(d); cols[c].add(d); boxes[(r//3)*3 + c//3].add(d)
                else:
                    empty.append((r, c))

        def backtrack(i: int) -> bool:
            if i == len(empty):
                return True
            r, c = empty[i]
            b = (r // 3) * 3 + c // 3
            for d in '123456789':
                if d in rows[r] or d in cols[c] or d in boxes[b]:
                    continue
                rows[r].add(d); cols[c].add(d); boxes[b].add(d)
                board[r][c] = d
                if backtrack(i + 1):
                    return True
                rows[r].discard(d); cols[c].discard(d); boxes[b].discard(d)
                board[r][c] = '.'
            return False

        backtrack(0)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


28.9 Word Search (LC 79)

Đề bài

Cho boardword. Kiểm tra word có thể được build bằng đường đi 4 hướng không qua 1 ô 2 lần.

Ví dụ

Input:  board=[["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word="ABCCED"
Output: True

Ràng buộc

Clarifying questions

Hướng tiếp cận

DFS từ mỗi ô có board[r][c] == word[0]. Backtrack: mark # rồi restore.

Code Python 3

from typing import List

class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        rows, cols = len(board), len(board[0])
        DIRS = [(-1,0),(1,0),(0,-1),(0,1)]

        def dfs(r: int, c: int, i: int) -> bool:
            if i == len(word):
                return True
            if not (0 <= r < rows and 0 <= c < cols) or board[r][c] != word[i]:
                return False
            board[r][c] = '#'
            found = any(dfs(r+dr, c+dc, i+1) for dr, dc in DIRS)
            board[r][c] = word[i]
            return found

        return any(dfs(r, c, 0) for r in range(rows) for c in range(cols))

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


28.10 Word Break II (LC 140)

Đề bài

Cho swordDict. Trả về tất cả cách chia s thành các từ trong dict (cách nhau dấu cách).

Ví dụ

Input:  s="catsanddog", wordDict=["cat","cats","and","sand","dog"]
Output: ["cats and dog","cat sand dog"]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Backtrack với memoization (cache theo start).

Code Python 3

from functools import cache
from typing import List

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> List[str]:
        words = set(wordDict)

        @cache
        def helper(start: int) -> List[str]:
            if start == len(s):
                return [""]
            result = []
            for end in range(start + 1, len(s) + 1):
                if s[start:end] in words:
                    for rest in helper(end):
                        result.append(s[start:end] + ("" if not rest else " " + rest))
            return result

        return helper(0)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


28.11 Restore IP Addresses (LC 93)

Đề bài

Cho chuỗi số. Liệt kê tất cả IP hợp lệ có thể tạo (4 số 0..255, mỗi số không leading zero trừ khi là “0”).

Ví dụ

Input:  s = "25525511135"   (chuỗi chỉ chứa chữ số)
Output: ["255.255.11.135", "255.255.111.35"]
        (mảng mọi IPv4 hợp lệ tạo từ s bằng cách chèn 3 dấu chấm; không leading zero)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Backtrack chia thành 4 phần. Mỗi phần độ dài 1, 2, hoặc 3 và thoả ràng buộc.

Code Python 3

from typing import List

class Solution:
    def restoreIpAddresses(self, s: str) -> List[str]:
        result: list[str] = []
        path: list[str] = []

        def is_valid(p: str) -> bool:
            if len(p) > 3 or not p:
                return False
            if p[0] == '0' and len(p) > 1:
                return False
            return int(p) <= 255

        def backtrack(start: int) -> None:
            if len(path) == 4:
                if start == len(s):
                    result.append('.'.join(path))
                return
            for length in (1, 2, 3):
                if start + length > len(s):
                    break
                p = s[start:start + length]
                if is_valid(p):
                    path.append(p)
                    backtrack(start + length)
                    path.pop()

        backtrack(0)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


28.12 Expression Add Operators (LC 282)

Đề bài

Cho chuỗi số numtarget. Thêm +, -, * giữa các digit sao cho biểu thức = target. Trả về tất cả biểu thức.

Ví dụ

Input:  num="123", target=6
Output: ["1+2+3","1*2*3"]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Backtrack với 2 state phụ trợ: - cur = giá trị biểu thức cho đến giờ. - prev = giá trị của term cuối (cần để xử lý * đúng precedence).

Khi gặp +: cur += x, prev = x. Khi gặp -: cur -= x, prev = -x. Khi gặp *: cur = cur - prev + prev * x, prev = prev * x.

Code Python 3

from typing import List

class Solution:
    def addOperators(self, num: str, target: int) -> List[str]:
        result: list[str] = []

        def backtrack(i: int, expr: str, cur: int, prev: int) -> None:
            if i == len(num):
                if cur == target:
                    result.append(expr)
                return
            for j in range(i + 1, len(num) + 1):
                if j > i + 1 and num[i] == '0':
                    break               # leading zero
                x = int(num[i:j])
                if i == 0:
                    backtrack(j, str(x), x, x)
                else:
                    backtrack(j, expr + '+' + str(x), cur + x, x)
                    backtrack(j, expr + '-' + str(x), cur - x, -x)
                    backtrack(j, expr + '*' + str(x), cur - prev + prev * x, prev * x)

        backtrack(0, "", 0, 0)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Backtracking schema

Thành phần Câu hỏi cần trả lời trước khi code
Path (trạng thái hiện tại) List / string / mask?
Choice list Từ n phần tử, hay vị trí, hay chữ số 1..9?
Goal test Khi nào ghi nhận / return?
Pruning / constraints Có thể loại sớm không? Sort trước để skip dup?
Undo Pop list, xoá set, đổi lại mask?

Decision tree (Permutations of [1,2,3])

            []
       /   |    \
      1    2     3
    / |    | \   | \
   2  3    1  3  1  2
   |  |    |  |  |  |
   3  2    3  1  2  1

Mỗi đường root→leaf = 1 permutation. Backtrack = DFS trên cây ảo này.

Duplicate handling (Subsets II / Permutations II)

Subsets II (sort + skip dup tại same level):

nums.sort()
for i in range(start, n):
    if i > start and nums[i] == nums[i-1]: continue   # cùng level, skip
    path.append(nums[i])
    dfs(i + 1)
    path.pop()

Permutations II (used array):

nums.sort()
for i in range(n):
    if used[i]: continue
    if i > 0 and nums[i] == nums[i-1] and not used[i-1]: continue  # giữ thứ tự

N-Queens / Sudoku constraint sets

Expression Add Operators (LC 282) — prev_operand

* ưu tiên hơn +/-, khi nhân, mình phải rút lại prev rồi nhân:

cur_total - prev_operand + prev_operand * num

Chương 29 — Dynamic Programming

Dynamic Programming (DP) = đệ quy + memoization (hoặc bottom-up). Cần 2 tính chất: optimal substructure (lời giải tối ưu của bài lớn xây từ lời giải tối ưu của bài con) + overlapping subproblems (bài con bị lặp). Chương lớn nhất sách — 18 bài chia 3 nhóm:

Mục tiêu chương

Sau chương này, bạn sẽ:

Taxonomy 4 nhóm DP trong chương này

Chương dài nhất sách. Để không lạc, mỗi bài rơi vào 1 trong 4 nhóm:

Nhóm Bài (LC) Đặc điểm chung
DP I — Sequence DP (2D) 29.1 LCS, 29.2 LIS, 29.3 Edit Distance State = (i, j) đại diện 2 prefix
DP I — Knapsack 29.4 0/1 Knapsack, 29.5 Partition Equal Sum, 29.6 Russian Doll State = (i, capacity)
DP II — Linear DP 29.7 Coin Change, 29.8 Coin Change II, 29.9 Stock Cooldown, 29.10 Stock IV, 29.11 House Robber II, 29.12 Max Product State = dp[i] hoặc dp[i][k]
DP III — Interval / Partition DP 29.13 Palindrome Partition II, 29.14 Burst Balloons, 29.15 MCM, 29.16 Cut Stick, 29.17 Stone Game VII, 29.18 Strange Printer State = dp[l][r], split tại k

Bảng state/transition cho mỗi bài

Khi gặp 1 bài DP, 4 câu hỏi cốt lõi: (1) State là gì? (2) Transition thế nào? (3) Base case? (4) Đáp án ở đâu?

Bài State Transition Base Answer
29.1 LCS dp[i][j] = LCS của s1[..i], s2[..j] Match: dp[i-1][j-1]+1; Else: max(dp[i-1][j], dp[i][j-1]) dp[0][j] = dp[i][0] = 0 dp[m][n]
29.2 LIS tails[k] = giá trị nhỏ nhất kết LIS dài k+1 bisect_left + replace/append tails = [] len(tails)
29.3 Edit Distance dp[i][j] = min edit s1[..i] → s2[..j] Match: dp[i-1][j-1]; Else: 1 + min(3 chiều) dp[i][0]=i, dp[0][j]=j dp[m][n]
29.4 0/1 Knapsack dp[w] = max value với capacity w dp[w] = max(dp[w], dp[w-wi]+vi) (duyệt ngược) dp[0..W] = 0 dp[W]
29.5 Partition Equal Sum dp[w] = có subset sum = w? dp[w] = dp[w] or dp[w-x] dp[0] = True dp[sum/2]
29.6 Russian Doll Sort 2D + LIS trên h Như LIS (29.2) len(tails)
29.7 Coin Change dp[a] = min coin tổng a dp[a] = min(dp[a-c]+1) dp[0] = 0 dp[amount]
29.8 Coin Change II dp[a] = số cách dp[a] += dp[a-c] (outer coin) dp[0] = 1 dp[amount]
29.9 Stock Cooldown hold[i], sold[i], rest[i] 3 transitions giữa state hold[0] = -prices[0] max(sold[-1], rest[-1])
29.10 Stock IV dp[t][i] = max profit ≤ t txn đến ngày i max(dp[t][i-1], price[i]+max_diff) dp[0][i] = 0 dp[k][n-1]
29.11 House Robber II Tách 2 case (rob 0 / không) Như Robber I max(case_1, case_2)
29.12 Max Product cur_max, cur_min rolling Swap khi x < 0 Start = nums[0] max(best)
29.13 Palindrome Partition II dp[i] = min cut cho s[..i] dp[i] = min(dp[j-1]+1) nếu s[j..i] palindrome dp[i]=0 nếu s[0..i] palindrome dp[n-1]
29.14 Burst Balloons dp[i][j] = max điểm nổ (i, j) exclusive max(arr[i]*arr[k]*arr[j] + dp[i][k] + dp[k][j]) dp[i][i+1] = 0 dp[0][n-1]
29.15 MCM dp[i][j] = min phép nhân Ai..Aj min(dp[i][k]+dp[k+1][j]+p[i-1]*p[k]*p[j]) dp[i][i]=0 dp[1][n]
29.16 Cut Stick dp[i][j] = min cost cuts (cuts[i], cuts[j]) min(dp[i][k]+dp[k][j]) + cuts[j]-cuts[i] dp[i][i+1]=0 dp[0][m-1]
29.17 Stone Game VII dp[l][r] = max diff người hiện tại max(sum-stones[l] - dp[l+1][r], sum-stones[r] - dp[l][r-1]) dp[i][i]=0 dp[0][n-1]
29.18 Strange Printer dp[i][j] = min turn in s[i..j] min(dp[i][j-1]+1, dp[i][k] + dp[k+1][j-1] khi s[k]==s[j]) dp[i][i]=1 dp[0][n-1]

Roadmap đọc 18 bài

Nếu chỉ có 1 ngày, đọc theo thứ tự ưu tiên:

  1. Foundations (must): 29.1 LCS, 29.2 LIS, 29.7 Coin Change — gateway cho mọi DP khác.
  2. Stock family (rất hay hỏi): 29.9 Stock Cooldown, 29.10 Stock IV.
  3. Knapsack (LC tag siêu phổ biến): 29.4 0/1, 29.5 Partition.
  4. Interval DP (thường Hard): 29.14 Burst Balloons → 29.13 Palindrome Partition II.
  5. Còn lại: skip nếu thiếu thời gian.

Khi nào dùng pattern này?

3 bước xây DP: 1. Định nghĩa state: dp[i][j] = ? 2. Transition: dp[i][j] = f(dp[i-1][j], dp[i][j-1], ...) 3. Base case + thứ tự duyệt: từ nhỏ đến lớn.

Top-down (memo) vs Bottom-up (tabulation): - Top-down: viết đệ quy + @cache. Dễ thấy “transition”. - Bottom-up: vòng for tăng dần. Tiết kiệm stack, dễ tối ưu space.

Template code

from functools import cache

# 1) Top-down
@cache
def dp(*state):
    if base_condition(*state):
        return base_value
    return combine([dp(*sub_state) for sub_state in transitions(*state)])


# 2) Bottom-up 2D
dp = [[0] * cols for _ in range(rows)]
for i in range(rows):
    for j in range(cols):
        if base(i, j):
            dp[i][j] = base_value
        else:
            dp[i][j] = f(dp[i-1][j], dp[i][j-1], ...)
return dp[-1][-1]


# 3) Space-optimized 1D (khi transition chỉ dùng dòng trước)
prev = [0] * cols
for i in range(rows):
    cur = [0] * cols
    for j in range(cols):
        cur[j] = f(prev[j], cur[j-1], ...)
    prev = cur

Bài tự luyện cuối chương


DP I — DP 2D kinh điển (LCS, LIS, Knapsack)

29.1 Longest Common Subsequence (LC 1143)

Đề bài

Cho 2 chuỗi s1, s2. Tìm độ dài LCS (longest common subsequence).

Ví dụ

Input:  text1 = "abcde", text2 = "ace"
Output: 3
        (LCS dài nhất là "ace")

Input:  text1 = "abc", text2 = "def"
Output: 0
        (không có ký tự chung)

Ràng buộc

Clarifying questions

Hướng tiếp cận

dp[i][j] = LCS của s1[0..i-1]s2[0..j-1].

Code Python 3

class Solution:
    def longestCommonSubsequence(self, s1: str, s2: str) -> int:
        m, n = len(s1), len(s2)
        dp = [[0] * (n + 1) for _ in range(m + 1)]
        for i in range(1, m + 1):
            for j in range(1, n + 1):
                if s1[i - 1] == s2[j - 1]:
                    dp[i][j] = dp[i - 1][j - 1] + 1
                else:
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
        return dp[m][n]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


29.2 Longest Increasing Subsequence (LC 300) — O(n log n)

Đề bài

Tìm độ dài LIS (strictly increasing) của nums.

Ví dụ

Input:  nums = [10, 9, 2, 5, 3, 7, 101, 18]
Output: 4
        (1 LIS dài nhất: [2, 3, 7, 18]; có thể có nhiều LIS khác)

Input:  nums = [0, 1, 0, 3, 2, 3]
Output: 4
        (1 LIS dài nhất: [0, 1, 2, 3])

Ràng buộc

Clarifying questions

Hướng tiếp cận

DP O(n²)dp[i] = max(dp[j]) + 1 với j < i, nums[j] < nums[i].

O(n log n)Patience Sort: - tails[i] = giá trị nhỏ nhất kết thúc LIS độ dài i + 1. - Duyệt nums, dùng binary search (bisect_left) để tìm vị trí thay/extend.

Code Python 3

from bisect import bisect_left
from typing import List

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        tails: list[int] = []
        for x in nums:
            i = bisect_left(tails, x)
            if i == len(tails):
                tails.append(x)
            else:
                tails[i] = x
        return len(tails)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


29.3 Edit Distance (LC 72)

Đề bài

Cho word1, word2. Số phép biến đổi tối thiểu (insert, delete, replace) để word1 → word2.

Ví dụ

Input:  word1="horse", word2="ros"
Output: 3
Giải thích: horse → rorse → rose → ros (3 thao tác)

Ràng buộc

Clarifying questions

Hướng tiếp cận

dp[i][j] = edit distance giữa prefix i của word1 và prefix j của word2.

Code Python 3

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        m, n = len(word1), len(word2)
        dp = [[0] * (n + 1) for _ in range(m + 1)]
        for i in range(m + 1): dp[i][0] = i
        for j in range(n + 1): dp[0][j] = j
        for i in range(1, m + 1):
            for j in range(1, n + 1):
                if word1[i - 1] == word2[j - 1]:
                    dp[i][j] = dp[i - 1][j - 1]
                else:
                    dp[i][j] = 1 + min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
        return dp[m][n]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


29.4 0/1 Knapsack (bài kinh điển)

Đề bài

Cho n đồ vật, mỗi cái có weight[i]value[i]. Túi có sức chứa W. Chọn các đồ vật để max tổng value, mỗi cái dùng ≤ 1 lần.

Ví dụ

Input:  weights=[1,3,4,5], values=[1,4,5,7], W=7
Output: 9

Ràng buộc

Clarifying questions

Hướng tiếp cận

dp[i][w] = max value khi xét i đồ đầu tiên với capacity w.

dp[i][w] = max(dp[i-1][w], dp[i-1][w - weight[i-1]] + value[i-1]) (nếu fit).

Code Python 3

from typing import List

def knapsack_01(weights: List[int], values: List[int], W: int) -> int:
    n = len(weights)
    dp = [0] * (W + 1)
    for i in range(n):
        # Duyệt ngược để mỗi item dùng ≤ 1 lần.
        for w in range(W, weights[i] - 1, -1):
            dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
    return dp[W]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


29.5 Partition Equal Subset Sum (LC 416)

Đề bài

Cho nums (positive). Kiểm tra có thể chia làm 2 tập có tổng bằng nhau không.

Ví dụ

Input:  nums = [1, 5, 11, 5]
Output: True
(chia được thành [1,5,5] và [11], 2 nhóm tổng = 11)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Đặt S = sum(nums). Nếu S lẻ → False. Tìm subset có tổng S / 2knapsack boolean DP.

dp[w] = True/False (có subset tổng = w không).

Code Python 3

from typing import List

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        total = sum(nums)
        if total % 2: return False
        target = total // 2
        dp = [False] * (target + 1)
        dp[0] = True
        for x in nums:
            for w in range(target, x - 1, -1):
                dp[w] = dp[w] or dp[w - x]
        return dp[target]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


29.6 Russian Doll Envelopes (LC 354)

Đề bài

Cho envelopes[i] = [w, h]. Envelope A fit trong B nếu A.w < B.wA.h < B.h. Tìm độ dài chuỗi fit tối đa.

Ví dụ

Input:  envelopes = [[5,4], [6,4], [6,7], [2,3]]
        (mỗi phần tử [width, height])
Output: 3   (chuỗi [2,3] ⊂ [5,4] ⊂ [6,7])

Ràng buộc

Clarifying questions

Hướng tiếp cận

Trick “sort 2D”: sort theo w tăng, h giảm với cùng w (để tránh 2 envelope cùng w fit lẫn nhau). Sau đó tìm LIS trên dãy h.

Code Python 3

from bisect import bisect_left
from typing import List

class Solution:
    def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
        envelopes.sort(key=lambda e: (e[0], -e[1]))
        tails: list[int] = []
        for _, h in envelopes:
            i = bisect_left(tails, h)
            if i == len(tails):
                tails.append(h)
            else:
                tails[i] = h
        return len(tails)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


DP II — Coin Change & Stock Trading

29.7 Coin Change (LC 322)

Đề bài

Cho coin denominations và amount. Số coin tối thiểu để gộp thành amount. -1 nếu không thể.

Ví dụ

Input:  coins=[1,2,5], amount=11
Output: 3
Giải thích: 11 = 5+5+1

Ràng buộc

Clarifying questions

Hướng tiếp cận

dp[a] = min coin để gộp thành a. dp[0] = 0. dp[a] = min(dp[a - c] + 1 for c in coins if c <= a).

Code Python 3

from typing import List

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        INF = amount + 1
        dp = [0] + [INF] * amount
        for a in range(1, amount + 1):
            for c in coins:
                if c <= a:
                    dp[a] = min(dp[a], dp[a - c] + 1)
        return -1 if dp[amount] > amount else dp[amount]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


29.8 Coin Change II (LC 518)

Đề bài

Đếm số cách gộp amount từ coins (unbounded).

Ví dụ

Input:  amount=5, coins=[1,2,5]
Output: 4

Ràng buộc

Clarifying questions

Hướng tiếp cận

dp[a] = số cách gộp thành a.

Thứ tự duyệt quan trọng: outer loop coin, inner loop amount → tránh đếm trùng (mỗi combination đếm 1 lần).

Code Python 3

from typing import List

class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        dp = [0] * (amount + 1)
        dp[0] = 1
        for c in coins:
            for a in range(c, amount + 1):
                dp[a] += dp[a - c]
        return dp[amount]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


29.9 Best Time to Buy and Sell Stock with Cooldown (LC 309)

Đề bài

Mua/bán nhiều lần, sau khi bán phải cooldown 1 ngày mới mua lại được.

Ví dụ

Input:  prices = [1, 2, 3, 0, 2]   (prices[i] = giá ngày i)
Output: 3   (giao dịch: buy@1 sell@2; cooldown; buy@0 sell@2)

Ràng buộc

Clarifying questions

Hướng tiếp cận

3 state mỗi ngày: hold (đang giữ), sold (vừa bán hôm nay), rest (nghỉ).

Đáp án = max(sold[-1], rest[-1]).

Code Python 3

from typing import List

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        if not prices: return 0
        hold = -prices[0]
        sold = 0
        rest = 0
        for p in prices[1:]:
            prev_sold = sold
            sold = hold + p
            hold = max(hold, rest - p)
            rest = max(rest, prev_sold)
        return max(sold, rest)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


29.10 Best Time to Buy and Sell Stock IV (LC 188)

Đề bài

Tối đa k giao dịch. Max profit.

Ví dụ

Input:  k=2, prices=[3,2,6,5,0,3]
Output: 7

Ràng buộc

Clarifying questions

Hướng tiếp cận

dp[t][i] = max profit dùng ≤ t giao dịch tới ngày i.

dp[t][i] = max(dp[t][i-1], max(price[i] - price[j] + dp[t-1][j-1]) for j < i).

Tối ưu: theo dõi max_diff = max(dp[t-1][j-1] - price[j]) while iterating.

Code Python 3

from typing import List

class Solution:
    def maxProfit(self, k: int, prices: List[int]) -> int:
        n = len(prices)
        if n == 0 or k == 0: return 0
        if k >= n // 2:
            # Unlimited transactions.
            return sum(max(prices[i] - prices[i - 1], 0) for i in range(1, n))
        dp = [[0] * n for _ in range(k + 1)]
        for t in range(1, k + 1):
            max_diff = -prices[0]
            for i in range(1, n):
                dp[t][i] = max(dp[t][i - 1], prices[i] + max_diff)
                max_diff = max(max_diff, dp[t - 1][i] - prices[i])
        return dp[k][n - 1]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


29.11 House Robber II (LC 213)

Đề bài

Tên trộm không thể trộm 2 nhà kề nhau. Nhà xếp vòng tròn (nums[0]nums[n-1] kề nhau).

Ví dụ

Input:  nums = [2, 3, 2]   (nums[i] = tiền nhà thứ i, các nhà xếp vòng tròn)
Output: 3   (chỉ trộm được 1 trong 2 nhà đầu/cuối, ở đây trộm 1 nhà giá 3)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Tách 2 case: trộm hoặc không trộm nhà đầu. - Case 1: trộm nhà 0 → không trộm nhà n-1 → House Robber trên nums[0..n-2]. - Case 2: không trộm nhà 0 → House Robber trên nums[1..n-1].

Đáp án = max của 2.

Code Python 3

from typing import List

class Solution:
    def rob(self, nums: List[int]) -> int:
        def rob_linear(arr: List[int]) -> int:
            prev = curr = 0
            for x in arr:
                prev, curr = curr, max(curr, prev + x)
            return curr

        if len(nums) == 1: return nums[0]
        return max(rob_linear(nums[:-1]), rob_linear(nums[1:]))

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


29.12 Maximum Product Subarray (LC 152)

Đề bài

Cho nums. Tìm subarray liên tục có tích max.

Ví dụ

Input:  nums = [2, 3, -2, 4]
Output: 6   (subarray [2, 3] có tích 6 là tích lớn nhất của 1 subarray liên tiếp)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Track cả max và min tại mỗi i (vì 2 số âm × nhau ra dương): - cur_max = max(x, x * prev_max, x * prev_min) - cur_min = min(x, x * prev_max, x * prev_min)

Code Python 3

from typing import List

class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        cur_max = cur_min = best = nums[0]
        for x in nums[1:]:
            if x < 0:
                cur_max, cur_min = cur_min, cur_max
            cur_max = max(x, cur_max * x)
            cur_min = min(x, cur_min * x)
            best = max(best, cur_max)
        return best

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


DP III — Partition / Interval DP

29.13 Palindrome Partitioning II (LC 132)

Đề bài

Cho s. Min cuts để chia s thành toàn palindrome.

Ví dụ

Input:  s = "aab"
Output: 1
(cắt 1 lần thành ["aa", "b"], 2 mảnh đều là palindrome)
Giải thích: Cut thành ["aa", "b"]

Ràng buộc

Clarifying questions

Hướng tiếp cận

dp[i] = min cuts cho s[0..i]. Pre-compute is_pal[i][j]. Transition: - Nếu s[0..i] đã palindrome → dp[i] = 0. - Ngược lại: dp[i] = min(dp[j-1] + 1 for j <= i if s[j..i] is palindrome).

Code Python 3

class Solution:
    def minCut(self, s: str) -> int:
        n = len(s)
        is_pal = [[False] * n for _ in range(n)]
        for i in range(n):
            for j in range(i + 1):
                if s[j] == s[i] and (i - j < 2 or is_pal[j + 1][i - 1]):
                    is_pal[j][i] = True

        dp = [0] * n
        for i in range(n):
            if is_pal[0][i]:
                dp[i] = 0
            else:
                dp[i] = min(dp[j - 1] + 1 for j in range(1, i + 1) if is_pal[j][i])
        return dp[n - 1]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


29.14 Burst Balloons (LC 312)

Đề bài

Cho nums đại diện balloon. Nổ mỗi balloon i được điểm nums[i-1] * nums[i] * nums[i+1] (boundary là 1). Max điểm tổng.

Ví dụ

Input:  nums = [3, 1, 5, 8]   (balloons từ trái sang phải)
Output: 167   (max coin khi nổ tất cả; mỗi lần nổ i được nums[L]*nums[i]*nums[R])

Ràng buộc

Clarifying questions

Hướng tiếp cận

Interval DP đảo ngược: thay vì nghĩ “nổ trước cái nào”, nghĩ “nổ cuối cùng cái nào trong interval [i, j]”.

dp[i][j] = max điểm khi nổ tất cả balloon trong (i, j) (exclusive).

dp[i][j] = max(nums[i] * nums[k] * nums[j] + dp[i][k] + dp[k][j]) với k ∈ (i, j).

Pad nums với 1 ở 2 đầu.

Code Python 3

from typing import List

class Solution:
    def maxCoins(self, nums: List[int]) -> int:
        arr = [1] + nums + [1]
        n = len(arr)
        dp = [[0] * n for _ in range(n)]
        for length in range(2, n):
            for i in range(n - length):
                j = i + length
                for k in range(i + 1, j):
                    dp[i][j] = max(dp[i][j],
                                   arr[i] * arr[k] * arr[j] + dp[i][k] + dp[k][j])
        return dp[0][n - 1]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


29.15 Matrix Chain Multiplication (bài kinh điển)

Đề bài

Cho n ma trận A1 · A2 · ... · An với chiều p[i-1] × p[i]. Tìm cách đặt ngoặc để min số phép nhân.

Ví dụ

Input:  p=[10,20,30,40,30]
Output: 30000

Ràng buộc

Clarifying questions

Hướng tiếp cận

dp[i][j] = min cost nhân A_i · A_{i+1} · ... · A_j. Split tại k: dp[i][j] = min(dp[i][k] + dp[k+1][j] + p[i-1]*p[k]*p[j]).

Code Python 3

from typing import List

def matrix_chain(p: List[int]) -> int:
    n = len(p) - 1
    dp = [[0] * (n + 1) for _ in range(n + 1)]
    for length in range(2, n + 1):
        for i in range(1, n - length + 2):
            j = i + length - 1
            dp[i][j] = float('inf')
            for k in range(i, j):
                dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + p[i-1]*p[k]*p[j])
    return dp[1][n]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


29.16 Minimum Cost to Cut a Stick (LC 1547)

Đề bài

Stick độ dài n. Mảng cuts là vị trí cần cắt. Cost mỗi lần cắt = chiều dài stick hiện tại. Tìm cost min.

Ví dụ

Input:  n=7, cuts=[1,3,4,5]
Output: 16

Ràng buộc

Clarifying questions

Hướng tiếp cận

Sort cuts cộng thêm boundary [0, n]. dp[i][j] = min cost cắt stick từ cuts[i] đến cuts[j]. Try mỗi cut k ∈ (i, j): dp[i][j] = min(dp[i][k] + dp[k][j] + cuts[j] - cuts[i]).

Code Python 3

from typing import List

class Solution:
    def minCost(self, n: int, cuts: List[int]) -> int:
        cuts = sorted([0] + cuts + [n])
        m = len(cuts)
        dp = [[0] * m for _ in range(m)]
        for length in range(2, m):
            for i in range(m - length):
                j = i + length
                dp[i][j] = min(dp[i][k] + dp[k][j] for k in range(i + 1, j)) + cuts[j] - cuts[i]
        return dp[0][m - 1]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


29.17 Stone Game VII (LC 1690)

Đề bài

Alice và Bob lần lượt lấy hòn đá ở 2 đầu array. Người lấy được điểm = sum các đá còn lại. Cả 2 chơi optimal. Tìm chênh lệch Alice - Bob.

Ví dụ

Input:  stones = [5, 3, 1, 4, 2]   (giá trị từng stone, 2 đầu mới được lấy)
Output: 6   (Alice - Bob với cả hai đều chơi tối ưu)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Game theory + Interval DP. dp[i][j] = chênh lệch tối ưu khi xét stones [i..j]. Người hiện tại pick lấy điểm sum[i+1..j] (bỏ stones[i]) hoặc sum[i..j-1] (bỏ stones[j]) trừ đi dp[next][next].

Code Python 3

from functools import cache
from typing import List

class Solution:
    def stoneGameVII(self, stones: List[int]) -> int:
        n = len(stones)
        prefix = [0] * (n + 1)
        for i, s in enumerate(stones):
            prefix[i + 1] = prefix[i] + s

        @cache
        def dp(i: int, j: int) -> int:
            if i >= j: return 0
            # Remove stones[i]: gain = sum của [i+1..j]
            gain_l = prefix[j + 1] - prefix[i + 1]
            # Remove stones[j]: gain = sum của [i..j-1]
            gain_r = prefix[j] - prefix[i]
            return max(gain_l - dp(i + 1, j), gain_r - dp(i, j - 1))

        return dp(0, n - 1)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


29.18 Strange Printer (LC 664)

Đề bài

Cho s. Máy in mỗi turn in 1 chuỗi gồm cùng 1 ký tự, có thể đè lên cái trước. Tìm số turn tối thiểu để in được s.

Ví dụ

Input:  s = "aaabbb"
Output: 2   (máy in chỉ in được 1 ký tự liên tiếp 1 lần; tối thiểu 2 lượt)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Interval DP. dp[i][j] = min turn in s[i..j].

Code Python 3

class Solution:
    def strangePrinter(self, s: str) -> int:
        n = len(s)
        dp = [[0] * n for _ in range(n)]
        for i in range(n):
            dp[i][i] = 1
        for length in range(2, n + 1):
            for i in range(n - length + 1):
                j = i + length - 1
                dp[i][j] = dp[i][j - 1] + 1
                for k in range(i, j):
                    if s[k] == s[j]:
                        cost = dp[i][k] + (dp[k + 1][j - 1] if k + 1 <= j - 1 else 0)
                        dp[i][j] = min(dp[i][j], cost)
        return dp[0][n - 1]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Reading roadmap (chọn 8 bài nếu thiếu thời gian)

  1. House Robber (LC 198) — DP sequence 1D cơ bản.
  2. Coin Change (LC 322) — unbounded knapsack.
  3. Longest Increasing Subsequence (LC 300) — patience.
  4. LCS (LC 1143) — DP 2D trên 2 chuỗi.
  5. Edit Distance (LC 72) — kinh điển.
  6. Best Time IV (LC 188) — stock DP với k giao dịch.
  7. Burst Balloons (LC 312) — interval DP “chọn cuối”.
  8. Stone Game (LC 877) — game DP minimax.

Interval DP — “choose last operation” framing

Burst Balloons / Strange Printer / Matrix Chain: - dp[i][j] = đáp số tối ưu cho range [i..j]. - Hỏi: operation nào thực hiện CUỐI CÙNG trong range này? Lựa chọn k ∈ [i..j] chia thành [i..k-1][k+1..j] đã giải xong. - Khác với “chọn đầu tiên” — đa số trường hợp “chọn cuối” cho recurrence sạch hơn.

State table mẫu — LCS (LC 1143)

"" a b c d e
"" 0 0 0 0 0 0
a 0 1 1 1 1 1
c 0 1 1 2 2 2
e 0 1 1 2 2 3

Edit Distance state table — "horse" → "ros"

"" r o s
"" 0 1 2 3
h 1 1 2 3
o 2 2 1 2
r 3 2 2 2
s 4 3 3 2
e 5 4 4 3

3 phép min(insert, delete, replace) + 1; khớp ký tự → kế thừa chéo.


Chương 30 — Dijkstra

Dijkstra tìm đường đi ngắn nhất từ 1 source đến mọi đỉnh trong graph trọng số không âm. Khi cạnh có thể âm → Bellman-Ford. Khi cạnh đồng nhất (= 1) → BFS đủ rồi. Dijkstra với heap chạy O((V + E) log V).

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Template code

import heapq
from collections import defaultdict

def dijkstra(graph: dict, source: int, n: int) -> list[float]:
    INF = float('inf')
    dist = [INF] * n
    dist[source] = 0
    heap = [(0, source)]
    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:                # outdated entry
            continue
        for v, w in graph[u]:
            nd = d + w
            if nd < dist[v]:
                dist[v] = nd
                heapq.heappush(heap, (nd, v))
    return dist

Bài tự luyện cuối chương


30.1 Network Delay Time (LC 743)

Đề bài

Cho times[i] = [u, v, w] (directed weighted), n đỉnh, source k. Tìm thời gian để tín hiệu đến tất cả đỉnh, hoặc -1.

Ví dụ

Input:  times=[[2,1,1],[2,3,1],[3,4,1]], n=4, k=2
Output: 2

Ràng buộc

Clarifying questions

Hướng tiếp cận

Dijkstra từ k. Đáp án = max của dist[].

Code Python 3

import heapq
from collections import defaultdict
from typing import List

class Solution:
    def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int:
        graph = defaultdict(list)
        for u, v, w in times:
            graph[u].append((v, w))
        INF = float('inf')
        dist = {i: INF for i in range(1, n + 1)}
        dist[k] = 0
        heap = [(0, k)]
        while heap:
            d, u = heapq.heappop(heap)
            if d > dist[u]: continue
            for v, w in graph[u]:
                if d + w < dist[v]:
                    dist[v] = d + w
                    heapq.heappush(heap, (d + w, v))
        ans = max(dist.values())
        return ans if ans < INF else -1

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


30.2 Path With Minimum Effort (LC 1631)

Đề bài

Grid heights. Đi từ (0,0) đến (m-1,n-1) (4 hướng). Effort = max |h(u) - h(v)| qua các cạnh trên path. Tìm min effort.

Ví dụ

Input:  heights = [[1,2,2],
                   [3,8,2],
                   [5,3,5]]   (grid m×n, đi 4 hướng từ (0,0) → (m-1,n-1))
Output: 2   (effort min = max |h[u] - h[v]| trên path tốt nhất)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Dijkstra với “trọng số path” = max edge weight thay vì sum. nd = max(d, |h(u) - h(v)|).

Code Python 3

import heapq
from typing import List

class Solution:
    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        rows, cols = len(heights), len(heights[0])
        INF = float('inf')
        effort = [[INF] * cols for _ in range(rows)]
        effort[0][0] = 0
        heap = [(0, 0, 0)]      # (effort, r, c)
        DIRS = [(-1,0),(1,0),(0,-1),(0,1)]
        while heap:
            e, r, c = heapq.heappop(heap)
            if (r, c) == (rows - 1, cols - 1):
                return e
            if e > effort[r][c]: continue
            for dr, dc in DIRS:
                nr, nc = r + dr, c + dc
                if 0 <= nr < rows and 0 <= nc < cols:
                    ne = max(e, abs(heights[nr][nc] - heights[r][c]))
                    if ne < effort[nr][nc]:
                        effort[nr][nc] = ne
                        heapq.heappush(heap, (ne, nr, nc))
        return 0

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


30.3 Cheapest Flights Within K Stops (LC 787)

Đề bài

Cho flights[i] = [u, v, price], n đỉnh, source src, dest dst, k stops. Tìm cheapest flight ≤ k stops.

Ví dụ

Input:  n=3, flights=[[0,1,100],[1,2,100],[0,2,500]], src=0, dst=2, k=1
Output: 200

Ràng buộc

Clarifying questions

Hướng tiếp cận

Bellman-Ford với k + 1 lần lặp là cách gọn nhất.

Dijkstra cũng giải được, nhưng state phải mở rộng thành (cost, node, stops) — phức tạp hơn vì state có 2 chiều.

Code Python 3

from typing import List

class Solution:
    def findCheapestPrice(self, n: int, flights: List[List[int]], src: int, dst: int, k: int) -> int:
        INF = float('inf')
        dist = [INF] * n
        dist[src] = 0
        for _ in range(k + 1):
            new_dist = dist.copy()
            for u, v, p in flights:
                if dist[u] + p < new_dist[v]:
                    new_dist[v] = dist[u] + p
            dist = new_dist
        return dist[dst] if dist[dst] < INF else -1

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


30.4 Swim in Rising Water (LC 778) — recap

Đã giải đầy đủ ở Chương 24.6 (DSU offline). Cũng giải được bằng Dijkstra: trọng số path = max elevation gặp phải.

Code Python 3

import heapq
class Solution:
    def swimInWater(self, grid):
        n = len(grid)
        dist = [[float('inf')] * n for _ in range(n)]
        dist[0][0] = grid[0][0]
        heap = [(grid[0][0], 0, 0)]
        DIRS = [(-1,0),(1,0),(0,-1),(0,1)]
        while heap:
            d, r, c = heapq.heappop(heap)
            if (r, c) == (n-1, n-1): return d
            for dr, dc in DIRS:
                nr, nc = r+dr, c+dc
                if 0 <= nr < n and 0 <= nc < n:
                    nd = max(d, grid[nr][nc])
                    if nd < dist[nr][nc]:
                        dist[nr][nc] = nd
                        heapq.heappush(heap, (nd, nr, nc))
        return -1

Phân tích độ phức tạp

Bài tự luyện liên quan


30.5 Shortest Path in a Grid with Obstacles Elimination (LC 1293)

Đề bài

Input: grid: List[List[int]] (m × n, 0 = ô trống đi được, 1 = vật cản) và số nguyên k. Đi 4 hướng từ (0,0)(m-1, n-1), được phá tối đa k vật cản. Tìm shortest path từ (0,0) đến (m-1,n-1).

Ví dụ

Input:  grid=[[0,0,0],[1,1,0],[0,0,0],[0,1,1],[0,0,0]], k=1
Output: 6

Ràng buộc

Clarifying questions

Hướng tiếp cận

BFS với state (r, c, eliminations_left) (vì cạnh trọng số 1). Dijkstra cũng được nhưng overkill.

Code Python 3

from collections import deque
from typing import List

class Solution:
    def shortestPath(self, grid: List[List[int]], k: int) -> int:
        rows, cols = len(grid), len(grid[0])
        if rows == 1 and cols == 1: return 0
        # Optimization: k đủ lớn → BFS thuần.
        if k >= rows + cols - 2:
            return rows + cols - 2

        visited = set([(0, 0, k)])
        queue = deque([(0, 0, k, 0)])    # (r, c, eliminations, steps)
        DIRS = [(-1,0),(1,0),(0,-1),(0,1)]
        while queue:
            r, c, e, steps = queue.popleft()
            for dr, dc in DIRS:
                nr, nc = r + dr, c + dc
                if not (0 <= nr < rows and 0 <= nc < cols): continue
                ne = e - grid[nr][nc]
                if ne < 0: continue
                if (nr, nc) == (rows - 1, cols - 1): return steps + 1
                if (nr, nc, ne) not in visited:
                    visited.add((nr, nc, ne))
                    queue.append((nr, nc, ne, steps + 1))
        return -1

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


30.6 Minimum Cost to Make at Least One Valid Path in a Grid (LC 1368)

Đề bài

Grid với mũi tên ở mỗi ô (4 hướng). Đi theo mũi tên free; đổi mũi tên cost 1. Tìm cost min để có path từ (0,0) đến (m-1,n-1).

Ví dụ

Input:  grid = [[1,1,1,1],
                [2,2,2,2],
                [1,1,1,1],
                [2,2,2,2]]
        (1=right, 2=left, 3=down, 4=up; theo mũi tên free, đổi cost 1)
Output: 3   (cần đổi 3 ô để có path hợp lệ từ (0,0) tới (m-1,n-1))

Ràng buộc

Clarifying questions

Hướng tiếp cận

0-1 BFS (Dijkstra với deque): - Cạnh “đi theo mũi tên” = 0 → đẩy vào đầu deque. - Cạnh “đổi mũi tên” = 1 → đẩy vào cuối.

O(V + E).

Code Python 3

from collections import deque
from typing import List

class Solution:
    def minCost(self, grid: List[List[int]]) -> int:
        rows, cols = len(grid), len(grid[0])
        INF = float('inf')
        dist = [[INF] * cols for _ in range(rows)]
        dist[0][0] = 0
        # 1: right, 2: left, 3: down, 4: up
        DIRS = {1: (0,1), 2: (0,-1), 3: (1,0), 4: (-1,0)}
        dq = deque([(0, 0, 0)])      # (cost, r, c)
        while dq:
            cost, r, c = dq.popleft()
            if cost > dist[r][c]: continue
            for d, (dr, dc) in DIRS.items():
                nr, nc = r + dr, c + dc
                if not (0 <= nr < rows and 0 <= nc < cols): continue
                nc_cost = cost + (0 if grid[r][c] == d else 1)
                if nc_cost < dist[nr][nc]:
                    dist[nr][nc] = nc_cost
                    if grid[r][c] == d:
                        dq.appendleft((nc_cost, nr, nc))
                    else:
                        dq.append((nc_cost, nr, nc))
        return dist[rows - 1][cols - 1]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Shortest path algorithm chooser

Tính chất đồ thị Thuật toán Time
Unweighted (mọi cạnh = 1) BFS O(V + E)
Edge weight ∈ {0, 1} 0-1 BFS (deque, push_front cho 0, push_back cho 1) O(V + E)
Edge weight ≥ 0 Dijkstra (heap) O((V + E) log V)
Có cạnh âm, không vòng âm Bellman-Ford O(V · E)
Có cạnh âm, cần detect vòng âm Bellman-Ford + check vòng V O(V · E)
All-pairs, V ≤ 500 Floyd-Warshall O(V³)
DAG Topo + relax O(V + E)

Cheapest Flights with K Stops (LC 787)

Minimum Cost Valid Path (LC 1368) — 0-1 BFS


Chương 31 — Game Theory

Game theory trong phỏng vấn coding là bài 2 người chơi luân phiên, mỗi người chơi optimally. Pattern chung: DP minimax — tại mỗi state, người đi tìm cách max điểm mình hoặc min điểm đối thủ. Bài có chu trình → dùng BFS multi-source từ “terminal states”.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Template code

from functools import cache

@cache
def dp(state, is_alice_turn):
    if terminal(state):
        return score(state)
    if is_alice_turn:
        return max(dp(next_state(s), False) - delta for s in moves(state))
    else:
        return min(dp(next_state(s), True) + delta for s in moves(state))

Bài tự luyện cuối chương


31.1 Nim Game (LC 292)

Đề bài

n đá. Mỗi turn lấy 1, 2, hoặc 3 đá. Người lấy đá cuối thắng. Alice đi trước. Hỏi Alice có thắng không.

Ví dụ

Input:  n=4
Output: False

Ràng buộc

Clarifying questions

Hướng tiếp cận

Quan sát (induction): - n ∈ {1,2,3}: Alice lấy hết → thắng. - n == 4: bất kể Alice lấy 1/2/3, Bob luôn ở trạng thái {1,2,3} → Bob thắng. - Mở rộng: Alice thắng ↔︎ n % 4 != 0.

Code Python 3

class Solution:
    def canWinNim(self, n: int) -> bool:
        return n % 4 != 0

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


31.2 Stone Game (LC 877)

Đề bài

Mảng piles (chẵn cái, sum lẻ). Alice và Bob lần lượt lấy đống ở 2 đầu. Alice đi trước. Cả 2 chơi optimal. Alice thắng?

Ví dụ

Input:  piles = [5, 3, 4, 5]   (số pile chẵn, tổng stones lẻ; 2 đầu mới lấy được)
Output: True   (Alice luôn thắng khi cả 2 chơi tối ưu)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Trick math: Vì n chẵn, Alice có thể chiến lược luôn lấy chỉ số chẵn hoặc luôn lẻ — chọn tổng lớn hơn. Vì sum lẻ, 1 trong 2 phải > sum/2.

→ Alice luôn thắng.

Code Python 3

from typing import List

class Solution:
    def stoneGame(self, piles: List[int]) -> bool:
        return True

class SolutionDP:
    """DP version — pattern chuẩn cho biến thể."""

    def stoneGame(self, piles: List[int]) -> bool:
        from functools import cache
        n = len(piles)

        @cache
        def diff(l: int, r: int) -> int:
            if l > r: return 0
            return max(piles[l] - diff(l + 1, r), piles[r] - diff(l, r - 1))

        return diff(0, n - 1) > 0

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


31.3 Predict the Winner (LC 486)

Đề bài

Như Stone Game, nhưng n bất kỳ (có thể lẻ), sum bất kỳ. Alice thắng nếu score(Alice) ≥ score(Bob).

Ví dụ

Input:  nums = [1, 5, 2]   (2 player luân phiên lấy 1 đầu mảng)
Output: False   (Player 1 không thể thắng khi cả 2 chơi tối ưu)

Ràng buộc

Clarifying questions

Hướng tiếp cận

DP diff(l, r) y hệt 31.2.

Code Python 3

from functools import cache
from typing import List

class Solution:
    def predictTheWinner(self, nums: List[int]) -> bool:

        @cache
        def diff(l: int, r: int) -> int:
            if l == r: return nums[l]
            return max(nums[l] - diff(l + 1, r), nums[r] - diff(l, r - 1))

        return diff(0, len(nums) - 1) >= 0

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


31.4 Stone Game II (LC 1140)

Đề bài

Piles. Alice trước. Mỗi turn lấy X pile đầu tiên với 1 <= X <= 2*M (M ban đầu 1, sau mỗi turn M = max(M, X)). Tối đa số đá Alice lấy được.

Ví dụ

Input:  piles = [2, 7, 9, 4, 4]   (M ban đầu = 1; người chơi lấy X piles với 1 ≤ X ≤ 2M, sau đó M = max(M, X))
Output: 10   (số stones tối đa Alice lấy được)

Ràng buộc

Clarifying questions

Hướng tiếp cận

DP dfs(i, M) = số đá người hiện tại có thể lấy được từ piles[i:] với M hiện tại. Maximize của các X.

Code Python 3

from functools import cache
from typing import List

class Solution:
    def stoneGameII(self, piles: List[int]) -> int:
        n = len(piles)
        suffix = [0] * (n + 1)
        for i in range(n - 1, -1, -1):
            suffix[i] = suffix[i + 1] + piles[i]

        @cache
        def dfs(i: int, M: int) -> int:
            if i + 2 * M >= n:
                return suffix[i]
            return suffix[i] - min(dfs(i + x, max(M, x)) for x in range(1, 2 * M + 1))

        return dfs(0, 1)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


31.5 Cat and Mouse (LC 913)

Đề bài

Input: graph: List[List[int]]adjacency list của một graph vô hướng, graph[i] là danh sách các node kề node i. Đỉnh đánh số 0..n-1.

Luật chơi: - Mouse bắt đầu ở node 1, Cat bắt đầu ở node 2, hole là node 0. - Lượt 1: chuột đi, lượt 2: mèo đi (mỗi lượt mỗi con buộc phải đi sang 1 neighbor). - Mèo không được vào hole (node 0). - Mouse thắng khi đến hole; Cat thắng khi cùng ô với Mouse; hoà nếu lặp state.

Trả về 1 trong 3 giá trị: 1 = MOUSE_WIN, 2 = CAT_WIN, 0 = DRAW.

Ví dụ

Input:  graph = [[2,5], [3], [0,4,5], [1,4,5], [2,3], [0,2,3]]
        (adjacency list của graph vô hướng; graph[i] = các node kề node i)
        Graph trên có 6 node 0..5; node 0 là hole.
Output: 0
        (0 = DRAW, 1 = MOUSE_WIN, 2 = CAT_WIN)

Ràng buộc

Clarifying questions

Hướng tiếp cận

BFS từ terminal states (retrograde analysis). Pattern này khác DP vì state graph có chu trình (mèo & chuột có thể đi tới đi lui) ⇒ memoization top-down không terminate.

State = (mouse, cat, turn) với turn ∈ {0=mouse, 1=cat}. Tổng O(n²) states.

Terminal: - mouse == 0 (chuột vào hole) ⇒ MOUSE_WIN. - mouse == cat (cùng ô, mèo bắt) ⇒ CAT_WIN.

Outcome propagation (retrograde): với mỗi state đã biết outcome, đi ngược (xét các parent state có thể dẫn đến state này) và suy: - Nếu đến lượt người chiến thắng ở parent (vd parent là lượt chuột và state hiện tại là MOUSE_WIN) ⇒ parent cũng WIN (chỉ cần 1 nước thắng). - Nếu đến lượt người thua ở parent ⇒ chỉ ghi parent thua khi tất cả nước đi của họ đều dẫn về thua (đếm bằng degree).

State/outcome diagram (rút gọn):

Terminal layer:
  (0, *, *)        ─────────────► MOUSE_WIN
  (k, k, *)  k>0   ─────────────► CAT_WIN

Retrograde propagation (BFS):

  state S đã biết outcome = X
       │
       ▼ for each parent P of S (đảo lượt):
       │
       ├── lượt(P) là "người thắng X" ?  YES → P = X (push)
       │
       └── lượt(P) là "người thua X" ?
              degree[P] -= 1
              if degree[P] == 0:
                  P = X (push)     ← mọi nước đều thua

  state còn lại sau khi BFS kết thúc → DRAW.

Đây là bài Hard có độ khó cao trên LeetCode; chương này tập trung giới thiệu pattern (retrograde BFS), phần cài đặt đầy đủ ngay bên dưới.

Code Python 3

from collections import deque
from typing import List

MOUSE_WIN, CAT_WIN, DRAW = 1, 2, 0

class Solution:
    def catMouseGame(self, graph: List[List[int]]) -> int:
        n = len(graph)
        # state: (mouse, cat, turn) — turn 0=mouse, 1=cat
        color = {}
        degree = {}
        for m in range(n):
            for c in range(n):
                degree[(m, c, 0)] = len(graph[m])
                degree[(m, c, 1)] = len(graph[c]) - (0 in graph[c])

        q = deque()
        for c in range(n):
            for t in range(2):
                color[(0, c, t)] = MOUSE_WIN
                q.append((0, c, t, MOUSE_WIN))
            color[(c, c, 0)] = color[(c, c, 1)] = CAT_WIN if c != 0 else MOUSE_WIN
            for t in range(2):
                q.append((c, c, t, color[(c, c, t)]))

        def parents(m: int, c: int, t: int):
            prev_turn = 1 - t
            if prev_turn == 0:           # mouse just moved
                for prev_m in graph[m]:
                    yield (prev_m, c, prev_turn)
            else:
                for prev_c in graph[c]:
                    if prev_c == 0: continue
                    yield (m, prev_c, prev_turn)

        while q:
            m, c, t, col = q.popleft()
            for pm, pc, pt in parents(m, c, t):
                if (pm, pc, pt) in color: continue
                if (pt == 0 and col == MOUSE_WIN) or (pt == 1 and col == CAT_WIN):
                    color[(pm, pc, pt)] = col
                    q.append((pm, pc, pt, col))
                else:
                    degree[(pm, pc, pt)] -= 1
                    if degree[(pm, pc, pt)] == 0:
                        color[(pm, pc, pt)] = col
                        q.append((pm, pc, pt, col))
        return color.get((1, 2, 0), DRAW)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


31.6 Can I Win (LC 464)

Đề bài

1..maxChoosable. Mỗi turn người chơi chọn 1 số chưa chọn. Người đầu tiên làm tổng các số đã chọn ≥ desiredTotal thắng. Alice trước. Alice thắng?

Ví dụ

Input:  maxChoosable=10, desiredTotal=11
Output: False

Ràng buộc

Clarifying questions

Hướng tiếp cận

Bitmask DP — state = set các số đã chọn (bitmask) + current sum. @cache memoize.

Code Python 3

from functools import cache

class Solution:
    def canIWin(self, maxChoosable: int, desiredTotal: int) -> bool:
        if (1 + maxChoosable) * maxChoosable // 2 < desiredTotal:
            return False

        @cache
        def dfs(mask: int, remaining: int) -> bool:
            for i in range(1, maxChoosable + 1):
                bit = 1 << i
                if mask & bit:
                    continue
                if i >= remaining:
                    return True
                if not dfs(mask | bit, remaining - i):
                    return True             # đối thủ thua → ta thắng
            return False

        return dfs(0, desiredTotal)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Game taxonomy

Loại Đặc trưng Bài
Math trick Phân tích parity / sum / “always wins” LC 877 (Stone Game I), LC 292 Nim
Minimax DP dp[state] = giá trị tối ưu cho người đến lượt LC 486, 1140
Memoized state graph State rời rạc, transitions phức tạp LC 464 (Can I Win)
Retrograde BFS Truyền outcome từ trạng thái terminal lùi LC 913 (Cat and Mouse)

Stone Game (LC 877) — vì sao Alice luôn thắng

Can I Win (LC 464) — bitmask state

Cat and Mouse (LC 913) — retrograde


Chương 32 — String Parser (Stack, State Machine, Regex)

Bài “string parser” hỏi bạn implement 1 mini-language: calculator, atom formula, lisp, … Pattern chung: (i) Stack cho cấu trúc lồng, (ii) State machine cho parsing token, (iii) Recursive descent cho grammar có nested rule. Bài 8.6 (Decode String) và 2.4 (atoi) đã teaser.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

3 mẫu thường dùng:

Mẫu Bài ví dụ
Stack với operator Basic Calculator I/II
FSM tokens atoi, Valid Number
Recursive descent Parse Lisp, Add Operators

Bài tự luyện cuối chương


32.1 Basic Calculator II (LC 227)

Đề bài

Cho chuỗi s chứa số, +, -, *, /, dấu cách. Tính giá trị biểu thức. Chia lấy phần nguyên hướng về 0. Không có ngoặc.

Ví dụ

Input:  s = " 3+5 / 2 "
Output: 5
        (3 + (5/2) = 3 + 2 = 5; chia lấy phần nguyên)

Input:  s = " 14-3/2 "
Output: 13
        (14 - (3/2) = 14 - 1 = 13)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Stack giữ “term” đang được build. Khi gặp operator mới: - +x → push x. - -x → push -x. - *x → top = top * x. - /x → top = int(top / x).

Cuối: sum stack.

Code Python 3

class Solution:
    def calculate(self, s: str) -> int:
        s += '+'                  # sentinel
        stack: list[int] = []
        num = 0
        op = '+'
        for ch in s:
            if ch.isdigit():
                num = num * 10 + int(ch)
            elif ch in '+-*/':
                if op == '+': stack.append(num)
                elif op == '-': stack.append(-num)
                elif op == '*': stack[-1] *= num
                else: stack[-1] = int(stack[-1] / num)    # truncate to 0
                op = ch
                num = 0
        return sum(stack)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


32.2 Decode String (LC 394) — recap

Đã giải đầy đủ ở Chương 8.6. Pattern stack lồng với (prev_str, k).

Liên hệ với chương này

Đây là gateway cho String Parser — nested structure → stack. Toàn chương 32 mở rộng pattern này: Calculator (operator stack), Number of Atoms (dict stack), Regex (DP table), Valid Number (FSM).

Code Python 3 (recap)

class Solution:
    def decodeString(self, s: str) -> str:
        stack: list[tuple[str, int]] = []
        cur, k = "", 0
        for ch in s:
            if ch.isdigit():
                k = k * 10 + int(ch)
            elif ch == '[':
                stack.append((cur, k))
                cur, k = "", 0
            elif ch == ']':
                prev_str, prev_k = stack.pop()
                cur = prev_str + cur * prev_k
            else:
                cur += ch
        return cur

Phân tích độ phức tạp

Bài tự luyện liên quan


32.3 Number of Atoms (LC 726)

Đề bài

Cho công thức hoá học (vd "K4(ON(SO3)2)2"). Trả về dạng chuẩn K4N2O14S4 — alphabetic atomic names + counts.

Ví dụ

Input:  formula = "H2O"
Output: "H2O"

Input:  formula = "Mg(OH)2"
Output: "H2MgO2"
        (Mg×1, O×2, H×2; sort alphabetic; số 1 lược bỏ)

Input:  formula = "K4(ON(SO3)2)2"
Output: "K4N2O14S4"
        (K×4, N×2, O×14, S×4; sort alphabetic)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Stack of dicts. Mỗi ( mở 1 dict mới. Mỗi ) + multiplier → multiply dict và merge vào parent. Mỗi atom name + count → cộng vào dict top.

Code Python 3

from collections import Counter, defaultdict

class Solution:
    def countOfAtoms(self, formula: str) -> str:
        stack: list[dict] = [defaultdict(int)]
        i, n = 0, len(formula)
        while i < n:
            ch = formula[i]
            if ch == '(':
                stack.append(defaultdict(int))
                i += 1
            elif ch == ')':
                i += 1
                j = i
                while j < n and formula[j].isdigit():
                    j += 1
                mult = int(formula[i:j]) if j > i else 1
                i = j
                top = stack.pop()
                for atom, cnt in top.items():
                    stack[-1][atom] += cnt * mult
            else:
                j = i + 1
                while j < n and formula[j].islower():
                    j += 1
                atom = formula[i:j]
                i = j
                k = i
                while k < n and formula[k].isdigit():
                    k += 1
                count = int(formula[i:k]) if k > i else 1
                i = k
                stack[-1][atom] += count
        result = stack[-1]
        return ''.join(
            atom + (str(cnt) if cnt > 1 else '')
            for atom, cnt in sorted(result.items())
        )

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


32.4 Regular Expression Matching (LC 10)

Đề bài

Kiểm tra string s có match pattern p không. p chứa . (match 1 char) và * (match 0+ ký tự trước đó).

Ví dụ

Input:  s = "aa", p = "a*"
Output: True
        (`a*` match 0+ ký tự 'a' → match "aa")

Input:  s = "mississippi", p = "mis*is*p*."
Output: False

Input:  s = "ab", p = ".*"
Output: True
        (`.*` match 0+ ký tự bất kỳ → match "ab")

Ràng buộc

Clarifying questions

Hướng tiếp cận

DP 2D. dp[i][j] = s[:i] match p[:j] không.

Code Python 3

class Solution:
    def isMatch(self, s: str, p: str) -> bool:
        m, n = len(s), len(p)
        dp = [[False] * (n + 1) for _ in range(m + 1)]
        dp[0][0] = True
        for j in range(2, n + 1):
            if p[j - 1] == '*':
                dp[0][j] = dp[0][j - 2]
        for i in range(1, m + 1):
            for j in range(1, n + 1):
                if p[j - 1] == '*':
                    dp[i][j] = dp[i][j - 2]
                    if p[j - 2] == '.' or p[j - 2] == s[i - 1]:
                        dp[i][j] = dp[i][j] or dp[i - 1][j]
                else:
                    if p[j - 1] == '.' or p[j - 1] == s[i - 1]:
                        dp[i][j] = dp[i - 1][j - 1]
        return dp[m][n]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


32.5 Valid Number (LC 65)

Đề bài

Kiểm tra một chuỗi s có phải là số hợp lệ hay không. Loại số hợp lệ bao gồm: số nguyên, số thập phân, và ký pháp khoa học (scientific notation), ví dụ "2", "-2.5", "3e+10", "-.5".

Ví dụ

Input:  s = "0"             → Output: True
Input:  s = "e"             → Output: False  (chỉ chữ e, không có digit)
Input:  s = "."             → Output: False  (chỉ dấu chấm, không có digit)
Input:  s = "+6e-1"         → Output: True   (số khoa học hợp lệ)

Ràng buộc

Clarifying questions

Hướng tiếp cận

FSM 9 trạng thái hoặc dùng regex. FSM gọn và dễ giải thích hơn cho phỏng vấn.

Code Python 3 (dùng regex)

import re

class Solution:
    def isNumber(self, s: str) -> bool:
        pattern = r'^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$'
        return bool(re.match(pattern, s.strip()))

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


32.6 Integer to English Words (LC 273)

Đề bài

Convert số num (≤ 2³¹-1) sang text tiếng Anh.

Ví dụ

Input:  num = 123
Output: "One Hundred Twenty Three"

Input:  num = 12345
Output: "Twelve Thousand Three Hundred Forty Five"

Input:  num = 0
Output: "Zero"

Ràng buộc

Clarifying questions

Hướng tiếp cận

Chia thành nhóm 3 chữ số (units, thousands, millions, billions). Mỗi nhóm 3 có pattern: hundreds + tens-ones.

Code Python 3

class Solution:
    UNDER_20 = ["", "One","Two","Three","Four","Five","Six","Seven","Eight","Nine",
                "Ten","Eleven","Twelve","Thirteen","Fourteen","Fifteen","Sixteen",
                "Seventeen","Eighteen","Nineteen"]
    TENS = ["", "", "Twenty","Thirty","Forty","Fifty","Sixty","Seventy","Eighty","Ninety"]
    THOUSANDS = ["", "Thousand", "Million", "Billion"]

    def numberToWords(self, num: int) -> str:
        if num == 0: return "Zero"

        def under_thousand(n: int) -> str:
            if n == 0: return ""
            if n < 20: return self.UNDER_20[n] + " "
            if n < 100: return self.TENS[n // 10] + " " + under_thousand(n % 10)
            return self.UNDER_20[n // 100] + " Hundred " + under_thousand(n % 100)

        parts = []
        i = 0
        while num > 0:
            if num % 1000 != 0:
                parts.append((under_thousand(num % 1000) + self.THOUSANDS[i]).strip())
            num //= 1000
            i += 1
        return ' '.join(reversed(parts)).strip()

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Parser taxonomy

Loại Đặc trưng Bài
Stack-based Nested structures (paren, brackets) LC 224, 394, 726
FSM / state machine Token tuyến tính, các state rõ ràng LC 8, 65
Recursive descent Grammar có cấu trúc đệ quy LC 736, 770
DP on string Match với pattern (., *) LC 10, 44

Valid Number (LC 65) — FSM table

States: 0 start, 1 sign, 2 int, 3 dot_after_int, 4 dot_before_int (require frac), 5 frac, 6 e/E, 7 exp_sign, 8 exp_int, 9 end (whitespace/error). | Trạng thái | digit | +/- | . | e/E | |—|—|—|—|—| | 0 (start) | 2 | 1 | 4 | – | | 1 (sign) | 2 | – | 4 | – | | 2 (int) | 2 | – | 3 | 6 | | 3 (dot after int) | 5 | – | – | 6 | | 4 (dot only) | 5 | – | – | – | | 5 (frac) | 5 | – | – | 6 | | 6 (e) | 8 | 7 | – | – | | 7 (exp sign) | 8 | – | – | – | | 8 (exp int) | 8 | – | – | – |

Accept states: {2, 3, 5, 8}.

Regex Matching (LC 10) — vì sao DP?

Number of Atoms (LC 726) — nested stack trace

"K4(ON(SO3)2)2":

stack = [Counter()]
'K' '4'  → top {K:4}
'('      → push new {}
'O' 'N'  → top {O:1, N:1}
'('      → push
'S' 'O' '3' → top {S:1, O:3}
')' '2'  → pop, multiply by 2: {S:2, O:6} → merge into below {O:1,N:1,S:2,O:7} ⇒ {O:7, N:1, S:2}
')' '2'  → pop, mul 2, merge into base {K:4} → {K:4, O:14, N:2, S:4}

Chương 33 — Minimum Spanning Tree (MST)

MST = subset cạnh kết nối tất cả đỉnh với tổng trọng số min, không có chu trình. 2 thuật toán: Kruskal (sort edge + DSU) và Prim (heap grow từ 1 đỉnh). Cả 2 đều O(E log E).

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Template code

# Kruskal: sort edges + DSU
def kruskal(n, edges):
    edges.sort(key=lambda e: e[2])
    dsu = DSU(n)
    total = 0
    for u, v, w in edges:
        if dsu.union(u, v):
            total += w
    return total


# Prim: heap-based grow
import heapq
def prim(n, graph):
    visited = [False] * n
    heap = [(0, 0)]                # (weight, node)
    total = 0
    while heap:
        w, u = heapq.heappop(heap)
        if visited[u]: continue
        visited[u] = True
        total += w
        for v, weight in graph[u]:
            if not visited[v]:
                heapq.heappush(heap, (weight, v))
    return total

Bài tự luyện cuối chương


33.1 Min Cost to Connect All Points (LC 1584)

Đề bài

Cho points 2D. Cost nối 2 điểm = Manhattan distance. Tìm cost min để nối tất cả.

Ví dụ

Input:  points = [[0,0], [2,2], [3,10], [5,2], [7,0]]
        (mỗi điểm [x, y]; cost giữa 2 điểm = |x1-x2| + |y1-y2| Manhattan)
Output: 20   (tổng cost MST nối tất cả điểm)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Kruskal: tạo tất cả O(n²) cạnh, sort, DSU. Prim: hợp lý hơn với dense graph (mỗi cặp đều có cạnh) — O(n²).

Code Python 3 (Prim)

import heapq
from typing import List

class Solution:
    def minCostConnectPoints(self, points: List[List[int]]) -> int:
        n = len(points)
        visited = [False] * n
        heap = [(0, 0)]
        total = 0
        count = 0
        while count < n:
            w, u = heapq.heappop(heap)
            if visited[u]: continue
            visited[u] = True
            total += w
            count += 1
            for v in range(n):
                if not visited[v]:
                    dist = abs(points[u][0]-points[v][0]) + abs(points[u][1]-points[v][1])
                    heapq.heappush(heap, (dist, v))
        return total

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


33.2 Connecting Cities With Minimum Cost (LC 1135)

Đề bài

Cho cities 1..n và list connections[i] = [a, b, cost]. Min cost kết nối tất cả, hoặc -1 nếu không thể.

Ví dụ

Input:  n=3, connections=[[1,2,5],[1,3,6],[2,3,1]]
Output: 6

Ràng buộc

Clarifying questions

Hướng tiếp cận

Kruskal cơ bản. Sort tất cả cạnh theo trọng số tăng dần, dùng DSU để chỉ nhận cạnh nối 2 component khác nhau; sau khi duyệt hết, nếu DSU còn đúng 1 component thì trả về tổng trọng số.

Code Python 3

from typing import List

class Solution:
    def minimumCost(self, n: int, connections: List[List[int]]) -> int:
        connections.sort(key=lambda c: c[2])
        parent = list(range(n + 1))
        def find(x):
            while parent[x] != x:
                parent[x] = parent[parent[x]]
                x = parent[x]
            return x

        total = 0
        used = 0
        for a, b, c in connections:
            ra, rb = find(a), find(b)
            if ra != rb:
                parent[ra] = rb
                total += c
                used += 1
                if used == n - 1:
                    return total
        return -1

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


33.3 Optimize Water Distribution in a Village (LC 1168)

Đề bài

n nhà. Mỗi nhà có thể: (a) đào giếng riêng cost wells[i], hoặc (b) nối ống với nhà khác pipes[j] = [u, v, c]. Min total cost cấp nước cho tất cả.

Ví dụ

Input:  n=3, wells=[1,2,2], pipes=[[1,2,1],[2,3,1]]
Output: 3

Ràng buộc

Clarifying questions

Hướng tiếp cận

Trick virtual node 0: thêm node 0 và cạnh (0, i, wells[i]) cho mỗi nhà. Sau đó MST trên n+1 đỉnh.

Code Python 3

from typing import List

class Solution:
    def minCostToSupplyWater(self, n: int, wells: List[int], pipes: List[List[int]]) -> int:
        edges = pipes[:]
        for i, w in enumerate(wells):
            edges.append([0, i + 1, w])
        edges.sort(key=lambda e: e[2])
        parent = list(range(n + 1))
        def find(x):
            while parent[x] != x:
                parent[x] = parent[parent[x]]
                x = parent[x]
            return x
        total = 0
        for u, v, c in edges:
            ru, rv = find(u), find(v)
            if ru != rv:
                parent[ru] = rv
                total += c
        return total

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


33.4 Critical and Pseudo-Critical Edges in MST (LC 1489)

Đề bài

Input: n (số đỉnh) và edges: List[List[int]] — mỗi cạnh [u, v, w] là cạnh vô hướng có trọng số w. Tìm tất cả critical edges (xoá → MST cost tăng) và pseudo-critical (có thể xuất hiện trong some MST).

Ví dụ

Input:  n=5, edges=[[0,1,1],[1,2,1],[2,3,2],[0,3,2],[0,4,3],[3,4,3],[1,4,6]]
Output: [[0,1],[2,3,4,5]]

Ràng buộc

Clarifying questions

Hướng tiếp cận

  1. Tính mst_cost ban đầu.
  2. Với mỗi edge e:

Code Python 3

from typing import List

class Solution:
    def findCriticalAndPseudoCriticalEdges(self, n: int, edges: List[List[int]]) -> List[List[int]]:
        # Đánh nhãn index để track sau khi sort.
        indexed = [e + [i] for i, e in enumerate(edges)]
        indexed.sort(key=lambda e: e[2])

        def kruskal(skip=-1, force=-1) -> int:
            parent = list(range(n))
            def find(x):
                while parent[x] != x:
                    parent[x] = parent[parent[x]]
                    x = parent[x]
                return x
            total = used = 0
            if force != -1:
                u, v, w, _ = indexed[force]
                parent[find(u)] = find(v)
                total = w
                used = 1
            for i, (u, v, w, _) in enumerate(indexed):
                if i == skip or i == force: continue
                ru, rv = find(u), find(v)
                if ru != rv:
                    parent[ru] = rv
                    total += w
                    used += 1
            return total if used == n - 1 else float('inf')

        base = kruskal()
        critical, pseudo = [], []
        for i, (u, v, w, orig) in enumerate(indexed):
            if kruskal(skip=i) > base:
                critical.append(orig)
            elif kruskal(force=i) == base:
                pseudo.append(orig)
        return [critical, pseudo]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


33.5 Checking Existence of Edge Length Limited Paths (LC 1697)

Đề bài

Cho graph vô hướng với edgeList[i] = [u, v, dist] và mảng queries[j] = [p, q, limit]. Với mỗi query, trả về True nếu tồn tại path giữa p, q mà mọi cạnh trên path có dist < limit.

Ví dụ

Input:  n=3, edges=[[0,1,2],[1,2,4],[2,0,8],[1,0,16]]
        queries=[[0,1,2],[0,2,5]]
Output: [False, True]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force: BFS/DFS mỗi query với điều kiện dist < limit. O(Q · E). TLE.

Tối ưu — Offline Kruskal-style — O((E + Q) log).

Sort cả edgeList (theo dist) và queries (theo limit). Duyệt queries tăng: với mỗi query, union tất cả edge có dist < limit vào DSU. Sau đó check find(p) == find(q).

Code Python 3

from typing import List

class Solution:
    def distanceLimitedPathsExist(self, n: int, edgeList: List[List[int]],
                                   queries: List[List[int]]) -> List[bool]:
        parent = list(range(n))
        def find(x: int) -> int:
            while parent[x] != x:
                parent[x] = parent[parent[x]]
                x = parent[x]
            return x
        def union(x: int, y: int) -> None:
            rx, ry = find(x), find(y)
            if rx != ry:
                parent[rx] = ry

        edgeList.sort(key=lambda e: e[2])
        # Thêm index gốc của query để output đúng thứ tự.
        indexed_q = sorted(enumerate(queries), key=lambda kv: kv[1][2])

        result = [False] * len(queries)
        i = 0
        for q_idx, (p, q, limit) in indexed_q:
            while i < len(edgeList) and edgeList[i][2] < limit:
                u, v, _ = edgeList[i]
                union(u, v)
                i += 1
            result[q_idx] = (find(p) == find(q))
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


33.6 Path With Maximum Minimum Value (LC 1102)

Đề bài

Grid m × n. Tìm path từ (0,0) đến (m-1,n-1) (4 hướng) sao cho giá trị min trên pathmax có thể. Trả về giá trị đó.

Ví dụ

Input:  grid = [[5, 4, 5],
                [1, 2, 6],
                [7, 4, 6]]   (grid m × n, đi 4 hướng từ (0,0) → (m-1, n-1))
Output: 4   (giá trị MIN gặp phải trên path tốt nhất đạt cực đại = 4)   (path 5→4→5→6→6 có min = 4)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Pattern “Kruskal ngược” — kích hoạt ô theo giá trị giảm dần. Khi (0,0)(m-1,n-1) cùng component → giá trị ô vừa kích hoạt là đáp án (vì nó là min trên path tìm được).

Bài này cùng pattern với Swim in Rising Water (Ch 24.6) nhưng đảo chiều: thay vì “elevation thấp nhất activate trước” → “value cao nhất activate trước”.

Code Python 3

from typing import List

class Solution:
    def maximumMinimumPath(self, grid: List[List[int]]) -> int:
        m, n = len(grid), len(grid[0])
        parent = list(range(m * n))
        def find(x: int) -> int:
            while parent[x] != x:
                parent[x] = parent[parent[x]]
                x = parent[x]
            return x
        def union(x: int, y: int) -> None:
            rx, ry = find(x), find(y)
            if rx != ry:
                parent[rx] = ry

        # Sort cells theo value GIẢM dần.
        cells = sorted(
            ((grid[r][c], r, c) for r in range(m) for c in range(n)),
            reverse=True,
        )
        active = [[False] * n for _ in range(m)]
        DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)]

        for val, r, c in cells:
            active[r][c] = True
            idx = r * n + c
            for dr, dc in DIRS:
                nr, nc = r + dr, c + dc
                if 0 <= nr < m and 0 <= nc < n and active[nr][nc]:
                    union(idx, nr * n + nc)
            if find(0) == find(m * n - 1):
                return val
        return -1

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

MST proof principles

Kruskal vs Prim

Kruskal Prim
Cấu trúc dữ liệu DSU + sort edges Heap + visited
Time O(E log E) O(E log V)
Đồ thị thưa ✅ Tốt OK
Đồ thị dày OK ✅ Tốt hơn
Cần edge list Cần adjacency list
Streaming edges ✅ (xử lý tăng dần)

Kruskal mental model

“Sort cạnh theo trọng số → mỗi cạnh, nếu nối 2 component khác nhau, lấy. DSU theo dõi connectivity dần dần.”

LC 1697 — Offline connectivity threshold (KHÔNG phải MST nhưng cùng họ)

Path Maximum Minimum / Bottleneck path


Chương 34 — Rolling Hash

Rolling Hash = hash chuỗi mà ta có thể “trượt window” với O(1) cập nhật. Pattern này biến O(n · m) thành O(n + m) cho bài substring matching, duplicate detection, …

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Pattern (Rabin-Karp):

hash(s[l..r]) = (s[l]·base^(r-l) + s[l+1]·base^(r-l-1) + ... + s[r]) mod M

Khi trượt: hash_new = (hash_old · base + s[r+1] - s[l] · base^(r-l+1)) mod M

Bẫy va chạm (collision). Modulo nguyên tố 10^9 + 7 thường an toàn cho bài LC; nếu có adversary cố tình tấn công, dùng 2 hash khác nhau (double hashing).

Template code

def rolling_hash(s: str, length: int) -> set[int]:
    BASE = 26
    MOD = (1 << 61) - 1
    n = len(s)
    base_pow = pow(BASE, length, MOD)
    h = 0
    seen = set()
    for i in range(n):
        h = (h * BASE + ord(s[i])) % MOD
        if i >= length:
            h = (h - ord(s[i - length]) * base_pow) % MOD
        if i >= length - 1:
            seen.add(h)
    return seen

Bài tự luyện cuối chương


34.1 Repeated DNA Sequences (LC 187)

Đề bài

Cho chuỗi DNA s (chứa A, C, G, T). Trả về substring độ dài 10 xuất hiện ≥ 2 lần.

Ví dụ

Input:  s = "AAAAACCCCCAAAAACCCCCCAAAAAGGGTTT"   (chuỗi chỉ chứa A/C/G/T)
Output: ["AAAAACCCCC", "CCCCCAAAAA"]
        (mọi substring độ dài 10 xuất hiện ≥ 2 lần; thứ tự không quan trọng)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force: set substring 10-char. O(n · 10) time, O(n · 10) space.

Rolling hash: O(n) time, O(n) space (memory smaller because hash là int).

Code Python 3

from typing import List

class Solution:
    def findRepeatedDnaSequences(self, s: str) -> List[str]:
        seen: dict[str, int] = {}
        result: list[str] = []
        for i in range(len(s) - 9):
            sub = s[i:i + 10]
            seen[sub] = seen.get(sub, 0) + 1
            if seen[sub] == 2:
                result.append(sub)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


34.2 Longest Duplicate Substring (LC 1044)

Đề bài

Cho s. Tìm substring dài nhất xuất hiện ít nhất 2 lần (có thể overlap). Nếu nhiều, trả bất kỳ.

Ví dụ

Input:  s = "banana"
Output: "ana"   (substring lặp lại dài nhất; nếu nhiều, trả về bất kỳ)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Binary search trên length + rolling hash check.

check(L): rolling hash + set; nếu collision → có duplicate.

Code Python 3

class Solution:
    def longestDupSubstring(self, s: str) -> str:
        BASE = 26
        MOD = (1 << 61) - 1
        n = len(s)
        nums = [ord(c) - ord('a') for c in s]

        def search(L: int) -> int:
            base_pow = pow(BASE, L, MOD)
            h = 0
            for i in range(L):
                h = (h * BASE + nums[i]) % MOD
            seen = {h: 0}
            for i in range(1, n - L + 1):
                h = (h * BASE - nums[i - 1] * base_pow + nums[i + L - 1]) % MOD
                if h in seen:
                    return i
                seen[h] = i
            return -1

        lo, hi = 1, n - 1
        start = 0
        best_len = 0
        while lo <= hi:
            mid = (lo + hi) // 2
            idx = search(mid)
            if idx != -1:
                if mid > best_len:
                    best_len = mid
                    start = idx
                lo = mid + 1
            else:
                hi = mid - 1
        return s[start:start + best_len]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


34.3 Distinct Echo Substrings (LC 1316)

Đề bài

Đếm số substring phân biệt có dạng a + a (concat của chuỗi với chính nó).

Ví dụ

Input:  text = "abcabcabc"
Output: 3
(số echo substring khác nhau; echo = substring dạng a+a;
 ở đây có "abcabc", "bcabca", "cabcab")

Ràng buộc

Clarifying questions

Hướng tiếp cận

Với mỗi L (độ dài a), check mọi vị trí có s[i..i+L-1] == s[i+L..i+2L-1]. Rolling hash giúp so sánh O(1). Collect các hash đã thấy.

Code Python 3

class Solution:
    def distinctEchoSubstrings(self, s: str) -> int:
        n = len(s)
        BASE = 26
        MOD = (1 << 61) - 1
        nums = [ord(c) - ord('a') for c in s]

        # Precompute prefix hash + power.
        h = [0] * (n + 1)
        p = [1] * (n + 1)
        for i in range(n):
            h[i + 1] = (h[i] * BASE + nums[i]) % MOD
            p[i + 1] = (p[i] * BASE) % MOD

        def get_hash(l: int, r: int) -> int:
            return (h[r + 1] - h[l] * p[r - l + 1]) % MOD

        seen: set[int] = set()
        for L in range(1, n // 2 + 1):
            for i in range(n - 2 * L + 1):
                h1 = get_hash(i, i + L - 1)
                h2 = get_hash(i + L, i + 2 * L - 1)
                if h1 == h2:
                    seen.add(h1)
        return len(seen)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


34.4 Shortest Palindrome (LC 214) — Rolling Hash version

Đề bài

Cho s. Thêm ký tự vào đầu sao cho s thành palindrome, đầu càng ít càng tốt.

Ví dụ

Input:  s = "aacecaaa"
Output: "aaacecaaa"   (thêm ký tự ÍT NHẤT vào ĐẦU s để thành palindrome)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Tìm prefix dài nhất của s là palindrome. Phần còn lại reverse và thêm vào đầu.

Rolling hash approach: compute hash của sreverse(s). Tìm L lớn nhất mà hash(s[:L]) == hash(rev_s[n-L:]) (= s[:L] palindrome).

Code Python 3

class Solution:
    def shortestPalindrome(self, s: str) -> str:
        n = len(s)
        if n <= 1: return s
        BASE = 131
        MOD = 10**9 + 7
        h1 = h2 = 0
        power = 1
        best = 0
        for i, ch in enumerate(s):
            h1 = (h1 * BASE + ord(ch)) % MOD
            h2 = (h2 + ord(ch) * power) % MOD
            power = power * BASE % MOD
            if h1 == h2:
                best = i + 1
        return s[best:][::-1] + s

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


34.5 Strings Differ by One Character (LC 1638)

Đề bài

Cho dict các chuỗi cùng độ dài. Trả về True nếu tồn tại 2 chuỗi chỉ khác đúng 1 ký tự.

Ví dụ

Input:  dict = ["abcd", "acbd", "aacd"]
Output: True   (tồn tại 2 chuỗi cùng độ dài khác đúng 1 vị trí; ví dụ "abcd" vs "aacd")

Ràng buộc

Clarifying questions

Hướng tiếp cận

Cho mỗi chuỗi và mỗi vị trí i, “mask” ký tự thứ i → hash phần còn lại. Nếu trùng → có cặp khác 1 vị trí.

Code Python 3

from typing import List

class Solution:
    def differByOne(self, dict: List[str]) -> bool:
        n_words = len(dict)
        L = len(dict[0])
        seen: set[tuple[int, str, int]] = set()
        for w in dict:
            for i in range(L):
                key = (i, w[:i] + w[i+1:])
                if key in seen:
                    return True
                seen.add(key)
        return False

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


34.6 Sum of Scores of Built Strings (LC 2223)

Đề bài

Build s bằng cách prepend từng ký tự. Tại bước i, “score” = độ dài prefix dài nhất của s_i đồng thời là suffix của s_i. Tổng tất cả scores.

Ví dụ

Input:  s = "babab"
Output: 9   (score[i] = LCP(s, s[i:]); tổng score qua mọi i)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Z function ở Chương 36 là pattern chính. Rolling hash cũng giải được: với mỗi i tìm prefix-suffix length bằng binary search trên hash.

Code Python 3 (Z function — preview)

class Solution:
    def sumScores(self, s: str) -> int:
        # Z function — chi tiết ở Chương 36.
        n = len(s)
        z = [0] * n
        z[0] = n
        l = r = 0
        for i in range(1, n):
            if i < r:
                z[i] = min(r - i, z[i - l])
            while i + z[i] < n and s[z[i]] == s[i + z[i]]:
                z[i] += 1
            if i + z[i] > r:
                l, r = i, i + z[i]
        return sum(z)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Rolling hash correctness — 2 cấp độ

Mục tiêu Đủ
LC accepted 1 hash với prime modulus lớn (10⁹+7 hoặc (1<<61) - 1)
Production / robust Double hash (2 cặp (base, mod)) hoặc kiểm chứng lại substring khi va chạm
Tuyệt đối không sai Lưu substring thay hash → tốn bộ nhớ

Prefix-hash convention [l, r)

hash[i] = (hash[i-1] * base + ord(s[i-1])) mod M
substring s[l:r] hash = (hash[r] - hash[l] * pow_base[r - l]) mod M

Lưu ý Python: (a - b) % M luôn đúng (negative-safe). Java/C++ cần ((... % M) + M) % M.

Distinct Echo Substrings (LC 1316) — collision

Rolling hash vs KMP/Z

Rolling hash KMP / Z
Tìm pattern P trong text T O(|T|) avg, có thể va chạm O(|T| + |P|) đảm bảo
Đa truy vấn (search nhiều pattern) Cần index hash của T Cần build per-pattern
Substring so sánh tổng quát ✅ Hash any range KMP/Z không hỗ trợ trực tiếp
Risk va chạm Không

Chương 35 — KMP (Knuth–Morris–Pratt)

KMP giải bài substring matching trong O(n + m), cốt lõi là mảng LPS (Longest Prefix Suffix) — với mỗi vị trí i trong pattern, lps[i] = độ dài prefix dài nhất của pattern[0..i] đồng thời là suffix của nó (không phải chính nó).

LPS — visualize bằng ví dụ ababaca

LPS là khái niệm khó nhất KMP. Trace tay với pattern = "ababaca":

index  :   0    1    2    3    4    5    6
char   :   a    b    a    b    a    c    a
LPS    :   0    0    1    2    3    0    1

Giải thích từng ô:
  LPS[0]=0  (1 ký tự, không có proper prefix nào)
  LPS[1]=0  "ab" — prefix "a" ≠ suffix "b"
  LPS[2]=1  "aba" — prefix "a" = suffix "a" → 1
  LPS[3]=2  "abab" — prefix "ab" = suffix "ab" → 2
  LPS[4]=3  "ababa" — prefix "aba" = suffix "aba" → 3
  LPS[5]=0  "ababac" — không có prefix "_" = suffix "_c" → 0
  LPS[6]=1  "ababaca" — prefix "a" = suffix "a" → 1

Trực giác: LPS[i] cho biết khi mismatch tại pattern[i+1], ta không cần restart từ đầu — có thể nhảy về pattern[LPS[i]] vì các ký tự pattern[0..LPS[i]-1] chắc chắn đã match (do nó cũng là suffix của những gì đã match).

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Template code

def build_lps(p: str) -> list[int]:
    n = len(p)
    lps = [0] * n
    length = 0
    i = 1
    while i < n:
        if p[i] == p[length]:
            length += 1
            lps[i] = length
            i += 1
        elif length > 0:
            length = lps[length - 1]
        else:
            lps[i] = 0
            i += 1
    return lps


def kmp_search(text: str, p: str) -> list[int]:
    lps = build_lps(p)
    result = []
    i = j = 0
    while i < len(text):
        if text[i] == p[j]:
            i += 1; j += 1
            if j == len(p):
                result.append(i - j)
                j = lps[j - 1]
        elif j > 0:
            j = lps[j - 1]
        else:
            i += 1
    return result

Bài tự luyện cuối chương


35.1 Implement strStr() (LC 28)

Đề bài

Tìm index đầu tiên của needle trong haystack, hoặc -1.

Ví dụ

Input:  haystack="sadbutsad", needle="sad"
Output: 0

Ràng buộc

Clarifying questions

Hướng tiếp cận

KMP O(n + m) time, O(m) space cho lps.

Code Python 3

class Solution:
    def strStr(self, haystack: str, needle: str) -> int:
        n, m = len(haystack), len(needle)
        if m == 0: return 0
        lps = [0] * m
        length = 0
        i = 1
        while i < m:
            if needle[i] == needle[length]:
                length += 1
                lps[i] = length
                i += 1
            elif length > 0:
                length = lps[length - 1]
            else:
                i += 1

        i = j = 0
        while i < n:
            if haystack[i] == needle[j]:
                i += 1; j += 1
                if j == m:
                    return i - j
            elif j > 0:
                j = lps[j - 1]
            else:
                i += 1
        return -1

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


35.2 Shortest Palindrome (LC 214) — KMP version

Đề bài

Như Chương 34.4 nhưng dùng KMP.

Ví dụ

Input:  s = "aacecaaa"
Output: "aaacecaaa"   (thêm ký tự ÍT NHẤT vào ĐẦU s để thành palindrome)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Tạo combined = s + '#' + reverse(s). Tính lps của combined; giá trị lps[-1] chính là độ dài prefix dài nhất của s đồng thời là suffix của reverse(s) = prefix palindrome dài nhất.

Code Python 3

class Solution:
    def shortestPalindrome(self, s: str) -> str:
        combined = s + '#' + s[::-1]
        lps = [0] * len(combined)
        length = 0
        for i in range(1, len(combined)):
            while length > 0 and combined[i] != combined[length]:
                length = lps[length - 1]
            if combined[i] == combined[length]:
                length += 1
            lps[i] = length
        return s[lps[-1]:][::-1] + s

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


35.3 Repeated Substring Pattern (LC 459)

Đề bài

Kiểm tra s có thể tạo bằng concat nhiều lần 1 substring không.

Ví dụ

Input:  s = "abab"
Output: True
("abab" = "ab" lặp 2 lần ⇒ True)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Trick KMP: nếu s = pattern * k (k ≥ 2), thì lps[-1] > 0n - lps[-1] chia hết n.

Cụ thể: len(pattern) = n - lps[-1].

Code Python 3

class Solution:
    def repeatedSubstringPattern(self, s: str) -> bool:
        n = len(s)
        lps = [0] * n
        length = 0
        for i in range(1, n):
            while length > 0 and s[i] != s[length]:
                length = lps[length - 1]
            if s[i] == s[length]:
                length += 1
            lps[i] = length
        return lps[-1] > 0 and n % (n - lps[-1]) == 0

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


35.4 Find All Anagrams in a String (LC 438)

Đề bài

Tìm tất cả start index trong s mà substring length-|p| là anagram của p.

Ví dụ

Input:  s="cbaebabacd", p="abc"
Output: [0, 6]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Sliding window + Counter. KMP không trực tiếp giải bài này — Anagram không phải substring match. Nhưng vẫn được liệt kê vì nằm trong family bài “string pattern”.

Code Python 3

from collections import Counter
from typing import List

class Solution:
    def findAnagrams(self, s: str, p: str) -> List[int]:
        if len(s) < len(p): return []
        need = Counter(p)
        have = Counter(s[:len(p)])
        result = []
        if have == need:
            result.append(0)
        for i in range(len(p), len(s)):
            have[s[i]] += 1
            have[s[i - len(p)]] -= 1
            if have[s[i - len(p)]] == 0:
                del have[s[i - len(p)]]
            if have == need:
                result.append(i - len(p) + 1)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


35.5 Maximum Number of Occurrences of a Substring (LC 1297)

Đề bài

Cho s, maxLetters, minSize, maxSize. Tìm substring có maxOccurrence mà length ∈ [minSize, maxSize] và số ký tự phân biệt ≤ maxLetters.

Ví dụ

Input:  s="aababcaab", maxLetters=2, minSize=3, maxSize=4
Output: 2

Ràng buộc

Clarifying questions

Hướng tiếp cận

Trick: chỉ cần xét length = minSize (substring lớn hơn không thể xuất hiện nhiều hơn).

Sliding window + Counter.

Code Python 3

from collections import Counter, defaultdict

class Solution:
    def maxFreq(self, s: str, maxLetters: int, minSize: int, maxSize: int) -> int:
        freq: dict[str, int] = defaultdict(int)
        for i in range(len(s) - minSize + 1):
            sub = s[i:i + minSize]
            if len(set(sub)) <= maxLetters:
                freq[sub] += 1
        return max(freq.values(), default=0)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


35.6 Longest Happy Prefix (LC 1392)

Đề bài

“Happy prefix” = prefix non-trivial đồng thời là suffix. Trả về cái dài nhất.

Ví dụ

Input:  s = "level"
Output: "l"   (prefix DÀI NHẤT của s cũng là suffix của s, KHÔNG bằng chính s;
              empty string nếu không tồn tại)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Trực tiếp lps[-1] của s chính là đáp án.

Code Python 3

class Solution:
    def longestPrefix(self, s: str) -> str:
        n = len(s)
        lps = [0] * n
        length = 0
        for i in range(1, n):
            while length > 0 and s[i] != s[length]:
                length = lps[length - 1]
            if s[i] == s[length]:
                length += 1
            lps[i] = length
        return s[:lps[-1]]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

LPS trace — ababaca

i char j (prev LPS) lps[i]
0 a 0
1 b lps[0]=0; s[0]!=s[1] 0
2 a j=0; s[0]==s[2]j=1 1
3 b j=1; s[1]==s[3]j=2 2
4 a j=2; s[2]==s[4]j=3 3
5 c j=3; s[3]!=s[5]; fallback j=lps[2]=1; s[1]!=s[5]; fallback j=lps[0]=0; s[0]!=s[5] 0
6 a j=0; s[0]==s[6]j=1 1

lps = [0, 0, 1, 2, 3, 0, 1].

Sentinel safety

Khi concat P + '#' + T (LC tham khảo), # phải không nằm trong charset cho phép. Nếu input có thể chứa mọi ký tự Unicode, dùng cặp 2 sentinel hoặc Z-function (Chương 36) trực tiếp.

Find All Anagrams (LC 438) ≠ KMP

KMP vs Z (Chương 36)

KMP Z
Preprocess LPS array của P Z array của P + '#' + T
Tư duy “Failure → quay lui khôn ngoan” “Khớp tiền tố tại mọi vị trí”
Cài đặt Failure function, 2 con trỏ 1 box [l, r), dễ off-by-one
Ứng dụng đặc thù Period of string, find pattern LCP, distinct substring, Z-array properties

Chương 36 — Z Function

Z function của chuỗi s: z[i] = độ dài đoạn dài nhất bắt đầu tại s[i] đồng thời là prefix của s. z[0] được set bằng n theo quy ước. Tính được trong O(n).

Z-box invariant

Z function dùng “Z-box” [l, r) = đoạn matched gần nhất với prefix của s. Đây là chìa khoá để hiểu vì sao tính được trong O(n).

              [l ........... r)
s :  ┌───┐    ┌─────────────┐
     │ A │ ...│  B = prefix  │  ...
     └───┘    └─────────────┘
     prefix    matched với prefix
       │              │
       └──────────────┴── đoạn này = prefix của s, độ dài r - l

Khi xét z[i] với i ∈ [l, r):
  • Đã biết z[i - l] (vì s[l..r) = s[0..r-l) tương ứng).
  • Lấy lower bound: z[i] = min(r - i, z[i - l]).
  • Sau đó mở rộng bằng so sánh ký tự.

Khi i ≥ r:
  • Không có thông tin → compare từ s[0].

Update Z-box khi tìm được đoạn mới dài hơn (i + z[i] > r).

Trace với s = "aabxaabxaab":

i  l  r  z[i] tính từ
0   0  0   11     (z[0] = n)
1   0  0   ?      i ≥ r, compare từ đầu
              s[1]=a, s[0]=a → match. z[1]=1 (vì s[2]=b ≠ s[1]=a)
              Update Z-box: l=1, r=2.
2   1  2   ?      i = r, không có info → compare từ đầu.
              s[2]=b ≠ s[0]=a → z[2]=0.
3   1  2   0      Tương tự, z[3]=0.
4   1  2   ?      i ≥ r, compare: s[4..]=aabxaab, prefix=aabxaab
              Match cả 7 → z[4]=7. Update Z-box: l=4, r=11.
5   4 11   ?      i ∈ [4, 11). z[5] = min(11-5, z[5-4]) = min(6, z[1]) = min(6, 1) = 1.
              Compare thêm: s[6]=b ≠ s[1]=a → dừng. z[5]=1.
6   4 11   ?      z[6] = min(5, z[2]) = min(5, 0) = 0.
7   4 11   ?      z[7] = min(4, z[3]) = min(4, 0) = 0.
8   4 11   ?      z[8] = min(3, z[4]) = min(3, 7) = 3.
              Compare thêm: i+z[i]=11=r → có thể vượt? s[11] out of bound. z[8]=3.
9   4 11   ?      z[9] = min(2, z[5]) = min(2, 1) = 1.
10  4 11   ?      z[10] = min(1, z[6]) = min(1, 0) = 0.

Final z: [11, 1, 0, 0, 7, 1, 0, 0, 3, 1, 0]

Amortize chứng minh O(n): mỗi lần “compare extend” trong while loop làm r tăng. r đơn điệu không giảm và ≤ n → tổng số extend ≤ n. Cộng O(1) cho mỗi i không extend → tổng O(n).

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Template code

def z_function(s: str) -> list[int]:
    n = len(s)
    z = [0] * n
    z[0] = n
    l = r = 0
    for i in range(1, n):
        if i < r:
            z[i] = min(r - i, z[i - l])
        while i + z[i] < n and s[z[i]] == s[i + z[i]]:
            z[i] += 1
        if i + z[i] > r:
            l, r = i, i + z[i]
    return z

Bài tự luyện cuối chương


36.1 Z-function — implementation & visualization

Đề bài

Tính mảng z[] cho chuỗi s. Đây là cài đặt cốt lõi để mọi bài Z function khác dùng tới.

Ví dụ

Input:  s = "aabxaabxaab"
Output: [11, 1, 0, 0, 7, 1, 0, 0, 3, 1, 0]

Giải thích vài giá trị tiêu biểu (xem trace đầy đủ ở mục "Z-box invariant"):
  z[0] = 11   (quy ước: bằng độ dài chuỗi)
  z[4] = 7    (s[4..10] = "aabxaab" trùng prefix "aabxaab")
  z[8] = 3    (s[8..10] = "aab" trùng prefix "aab")

Ràng buộc

Clarifying questions

Hướng tiếp cận

Maintain [l, r] = đoạn matched gần nhất với prefix. Tại mỗi i: - Nếu i < r: dùng giá trị z[i - l] đã biết để skip. - Mở rộng z[i] bằng cách so sánh s[z[i]] với s[i + z[i]]. - Update [l, r] nếu vùng matched mới lớn hơn.

Amortize: mỗi ký tự được “extend” tối đa 1 lần → O(n).

Code Python 3

def z_function(s: str) -> list[int]:
    n = len(s)
    z = [0] * n
    z[0] = n
    l = r = 0
    for i in range(1, n):
        if i < r:
            z[i] = min(r - i, z[i - l])
        while i + z[i] < n and s[z[i]] == s[i + z[i]]:
            z[i] += 1
        if i + z[i] > r:
            l, r = i, i + z[i]
    return z

Trực giác Z function với s = "aabcaabxaaaz":

i :  0  1  2  3  4  5  6  7  8  9 10 11
s :  a  a  b  c  a  a  b  x  a  a  a  z
z : 12  1  0  0  3  1  0  0  2  2  1  0

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


36.2 Find the Index of the First Occurrence (LC 28) — Z version

Đề bài

Tìm index đầu tiên của needle trong haystack, hoặc -1. Bài này dùng Z function thay vì KMP (Chương 35.1).

Ví dụ

Input:  haystack="sadbutsad", needle="sad"
Output: 0

Ràng buộc

Clarifying questions

Hướng tiếp cận

combined = pattern + '#' + text. Tính z. Tìm i đầu tiên có z[i] == len(pattern), trả i - len(pattern) - 1 (vị trí trong text).

Code Python 3

class Solution:
    def strStr(self, haystack: str, needle: str) -> int:
        if not needle: return 0
        combined = needle + '#' + haystack
        z = self.z_function(combined)
        target = len(needle)
        for i, v in enumerate(z):
            if v == target:
                return i - target - 1
        return -1

    @staticmethod
    def z_function(s: str) -> list[int]:
        n = len(s)
        z = [0] * n
        z[0] = n
        l = r = 0
        for i in range(1, n):
            if i < r:
                z[i] = min(r - i, z[i - l])
            while i + z[i] < n and s[z[i]] == s[i + z[i]]:
                z[i] += 1
            if i + z[i] > r:
                l, r = i, i + z[i]
        return z

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


36.3 Sum of Scores of Built Strings (LC 2223) — recap

Đã giải đầy đủ ở Chương 34.6 (dùng Z function).

Insight

score(s_i) = z[n - len(s_i)] (số ký tự khớp prefix khi build dần). Cộng dồn chính là sum(z[i] for i in range(1, n)) + n (cho cả z[0] = n).

Code Python 3 (recap)

class Solution:
    def sumScores(self, s: str) -> int:
        n = len(s)
        z = [0] * n
        z[0] = n
        l = r = 0
        for i in range(1, n):
            if i < r:
                z[i] = min(r - i, z[i - l])
            while i + z[i] < n and s[z[i]] == s[i + z[i]]:
                z[i] += 1
            if i + z[i] > r:
                l, r = i, i + z[i]
        return sum(z)

Phân tích độ phức tạp

Bài tự luyện liên quan


36.4 Maximum Deletions on a String (LC 2430)

Đề bài

Cho chuỗi s. Mỗi lượt thao tác, bạn được chọn một trong hai cách:

  1. Chọn chỉ số i với 1 ≤ i ≤ len(s) / 2 sao cho s[0..i-1] == s[i..2i-1] (tiền tố độ dài i trùng với đoạn ngay sau nó). Xoá tiền tố độ dài i; phần còn lại của ss[i:].
  2. Nếu không tồn tại i thoả mãn, xoá toàn bộ s (kết thúc).

Trả về số lượt thao tác lớn nhất có thể thực hiện cho đến khi s rỗng.

Ví dụ

Input:  s = "abcabcdabc"
Output: 2   (số phép xoá tối đa; mỗi phép xoá tiền tố P bằng tiền tố tiếp theo)

Ràng buộc

Clarifying questions

Hướng tiếp cận

DP từ phải sang trái. dp[i] = max turn từ s[i..].

Transition: dp[i] = max(1, max(dp[i + j] + 1 for j với s[i..i+j-1] == s[i+j..i+2j-1])).

So sánh substring nhanh bằng Z function hoặc precomputed LCP.

Code Python 3

class Solution:
    def deleteString(self, s: str) -> int:
        n = len(s)
        # lcp[i][j] = LCP của s[i:] và s[j:].
        lcp = [[0] * (n + 1) for _ in range(n + 1)]
        for i in range(n - 1, -1, -1):
            for j in range(n - 1, -1, -1):
                if s[i] == s[j]:
                    lcp[i][j] = lcp[i + 1][j + 1] + 1
        dp = [1] * (n + 1)
        dp[n] = 0
        for i in range(n - 1, -1, -1):
            for j in range(1, (n - i) // 2 + 1):
                if lcp[i][i + j] >= j:
                    dp[i] = max(dp[i], dp[i + j] + 1)
        return dp[0]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


36.5 Match Substring After Replacement (LC 2301)

Đề bài

Cho s, sub, và mappings[i] = [old, new] (mỗi ký tự old có thể đổi thành new ≤ 1 lần khi match). Kiểm tra sub (sau khi áp dụng tuỳ ý mappings) có là substring của s không.

Ví dụ

Input:  s="fool3e7bar", sub="leet", mappings=[["e","3"],["t","7"],["t","8"]]
Output: True

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force O(n · m) với check thông minh (mỗi ký tự match nếu == hoặc ∈ mapping).

Z function hoặc KMP có thể adapt — phức tạp hơn.

Code Python 3

from collections import defaultdict
from typing import List

class Solution:
    def matchReplacement(self, s: str, sub: str, mappings: List[List[str]]) -> bool:
        m: dict[str, set[str]] = defaultdict(set)
        for a, b in mappings:
            m[a].add(b)
        def matches(a: str, b: str) -> bool:
            return a == b or b in m[a]
        n, ns = len(s), len(sub)
        for i in range(n - ns + 1):
            if all(matches(sub[j], s[i + j]) for j in range(ns)):
                return True
        return False

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


36.6 Distinct Echo Substrings (LC 1316) — recap

Cùng bài 34.3, nhưng dùng Z function thay rolling hash để tránh collision.

Đề bài

Đếm số substring phân biệt có dạng a + a (chuỗi nối với chính nó).

Ví dụ

Input:  text = "aaaa"
Output: 1   (echo substring duy nhất là "aa"; "aaaa" có dạng a+a nhưng đếm theo distinct)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Với mỗi i, tính z trên s[i:]. Tìm các j với z[j] >= j (= echo length j tại vị trí i). Gom các echo substring distinct dùng set lưu (start_i, len).

Code Python 3

class Solution:
    def distinctEchoSubstrings(self, s: str) -> int:
        def z_function(t: str) -> list[int]:
            n = len(t)
            z = [0] * n
            z[0] = n
            l = r = 0
            for i in range(1, n):
                if i < r:
                    z[i] = min(r - i, z[i - l])
                while i + z[i] < n and t[z[i]] == t[i + z[i]]:
                    z[i] += 1
                if i + z[i] > r:
                    l, r = i, i + z[i]
            return z

        seen: set[tuple[int, int]] = set()
        n = len(s)
        for i in range(n):
            z = z_function(s[i:])
            for j in range(1, len(z)):
                if z[j] >= j:
                    seen.add((i, j))   # echo "aa" với mỗi a = s[i..i+j-1]
        # Lọc distinct theo nội dung substring.
        return len({s[i:i + 2 * j] for i, j in seen})

O(n²) worst case.

Phân tích độ phức tạp

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Z-box visualization — aabcaabxaaaz

i:    0 1 2 3 4 5 6 7 8 9 10 11
s:    a a b c a a b x a a  a  z
Z:    – 1 0 0 3 1 0 0 2 1  1  0

Box [l, r): vùng đang khớp tiền tố hiện tại. Tại mỗi i ∈ [l, r): - Nếu i + Z[i-l] < r: copy Z[i] = Z[i-l]. - Ngược lại: brute extend từ Z[i] = r - i.

Cập nhật box khi mở rộng:

i = 4: chưa có box, brute: s[4..]=aabxaaaz vs s[0..]=aabcaab... ⇒ khớp 3 ⇒ Z[4]=3, [l,r)=[4,7)
i = 5: i-l=1, Z[1]=1, i+Z[1]=6 < 7 ⇒ Z[5] = 1 (copy)
i = 6: i-l=2, Z[2]=0, 6+0=6 < 7 ⇒ Z[6] = 0
i = 7: ngoài box, brute: Z[7]=0
i = 8: brute, khớp aaz vs aab → 2 ⇒ Z[8]=2, [l,r)=[8,10)

Maximum Deletions (LC 2430) — LCP DP, không thuần Z

Match Replacement contrast

KMP/Z giả định so sánh bằng. Nếu match relation không đối xứng (vd wildcards có ràng buộc), failure function không xài được — phải DP.

Distinct Echo recap

Bài đầy đủ ở Chương 34 (rolling hash). Recap ở đây để so sánh: Z-function có thể đếm echo s[i:i+L] == s[i+L:i+2L] bằng cách kiểm tra Z[i+L] ≥ L.


Chương 37 — Binary Search kết hợp Graph Traversal

Khi bài có dạng “min của max” hoặc “max của min” trên graph/grid, ta có thể binary search trên đáp án + kiểm tra tính khả thi bằng BFS/DFS. Pattern này xuất hiện khi ta muốn lời giải O((V + E) · log range) đơn giản, thay cho Dijkstra hoặc MST tinh vi hơn.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Template code

def search_on_answer_graph(lo, hi, can_reach):
    while lo < hi:
        mid = (lo + hi) // 2
        if can_reach(mid):
            hi = mid
        else:
            lo = mid + 1
    return lo

Bài tự luyện cuối chương


37.1 Swim in Rising Water (LC 778) — recap

Đã giải 2 cách (DSU offline, Dijkstra) ở Chương 24.6 và 30.4. Đây là cách thứ 3: binary search trên t + BFS check.

Đề bài

Grid n × n elevation. Tại thời điểm t, ô có elevation ≤ t có nước phủ. Tìm t nhỏ nhất bơi được từ (0,0) đến (n-1,n-1).

Hướng tiếp cận

Binary search trên t. Với mỗi t, BFS check có path qua các ô elevation ≤ t không. Predicate đơn điệu: t lớn hơn → tới được nhiều ô hơn → dễ path hơn.

Code Python 3

from collections import deque
from typing import List

class Solution:
    def swimInWater(self, grid: List[List[int]]) -> int:
        n = len(grid)
        DIRS = [(-1,0),(1,0),(0,-1),(0,1)]

        def can_reach(t: int) -> bool:
            if grid[0][0] > t: return False
            visited = {(0, 0)}
            queue = deque([(0, 0)])
            while queue:
                r, c = queue.popleft()
                if (r, c) == (n - 1, n - 1): return True
                for dr, dc in DIRS:
                    nr, nc = r + dr, c + dc
                    if 0 <= nr < n and 0 <= nc < n and grid[nr][nc] <= t and (nr, nc) not in visited:
                        visited.add((nr, nc))
                        queue.append((nr, nc))
            return False

        lo, hi = 0, n * n - 1
        while lo < hi:
            mid = (lo + hi) // 2
            if can_reach(mid): hi = mid
            else: lo = mid + 1
        return lo

Phân tích độ phức tạp

Bài tự luyện liên quan


37.2 Path With Minimum Effort (LC 1631) — recap

Đã giải bằng Dijkstra ở Chương 30.2. BS version: binary search trên effort, BFS check.

Code Python 3

from collections import deque
from typing import List

class Solution:
    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        rows, cols = len(heights), len(heights[0])
        DIRS = [(-1,0),(1,0),(0,-1),(0,1)]

        def can_reach(e: int) -> bool:
            visited = {(0, 0)}
            queue = deque([(0, 0)])
            while queue:
                r, c = queue.popleft()
                if (r, c) == (rows - 1, cols - 1): return True
                for dr, dc in DIRS:
                    nr, nc = r + dr, c + dc
                    if 0 <= nr < rows and 0 <= nc < cols and (nr, nc) not in visited:
                        if abs(heights[nr][nc] - heights[r][c]) <= e:
                            visited.add((nr, nc))
                            queue.append((nr, nc))
            return False

        lo, hi = 0, 10**6
        while lo < hi:
            mid = (lo + hi) // 2
            if can_reach(mid): hi = mid
            else: lo = mid + 1
        return lo

Phân tích độ phức tạp

Bài tự luyện liên quan


37.3 Last Day Where You Can Still Cross (LC 1970)

Đề bài

Lưới row × col. Mỗi ngày 1 ô nước (cells[i]). Tìm ngày cuối cùng còn đường từ row 0 đến row n-1 (qua các ô đất).

Ví dụ

Input:  row=2, col=2, cells=[[1,1],[2,1],[1,2],[2,2]]
Output: 2

Ràng buộc

Clarifying questions

Hướng tiếp cận

Binary search trên day d + BFS/DFS check: với d ngày, lưới có đường đi không?

Code Python 3

from collections import deque
from typing import List

class Solution:
    def latestDayToCross(self, row: int, col: int, cells: List[List[int]]) -> int:

        def can_cross(d: int) -> bool:
            grid = [[0] * col for _ in range(row)]
            for i in range(d):
                r, c = cells[i]
                grid[r - 1][c - 1] = 1   # nước
            queue = deque()
            visited = set()
            for c in range(col):
                if grid[0][c] == 0:
                    queue.append((0, c))
                    visited.add((0, c))
            DIRS = [(-1,0),(1,0),(0,-1),(0,1)]
            while queue:
                r, c = queue.popleft()
                if r == row - 1: return True
                for dr, dc in DIRS:
                    nr, nc = r + dr, c + dc
                    if 0 <= nr < row and 0 <= nc < col and grid[nr][nc] == 0 and (nr, nc) not in visited:
                        visited.add((nr, nc))
                        queue.append((nr, nc))
            return False

        lo, hi = 0, len(cells)
        while lo < hi:
            mid = (lo + hi + 1) // 2
            if can_cross(mid): lo = mid
            else: hi = mid - 1
        return lo

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


37.4 Trapping Rain Water II (LC 407)

Đề bài

Trapping Rain Water mở rộng lên 2D. Tính tổng nước đọng trên grid heights.

Ví dụ

Input:  heightMap = [[1,4,3,1,3,2],
                     [3,2,1,3,2,4],
                     [2,3,3,2,3,1]]   (grid m × n, mỗi ô là chiều cao cột)
Output: 4   (tổng lượng nước đọng sau mưa)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Dijkstra/BFS từ biên với min-heap: thay vì binary search, dùng heap pop ô thấp nhất từ biên. Mỗi lần pop, “nâng” mực nước cho hàng xóm.

Code Python 3

import heapq
from typing import List

class Solution:
    def trapRainWater(self, heights: List[List[int]]) -> int:
        rows, cols = len(heights), len(heights[0])
        visited = [[False] * cols for _ in range(rows)]
        heap = []
        for r in range(rows):
            for c in range(cols):
                if r == 0 or r == rows - 1 or c == 0 or c == cols - 1:
                    heapq.heappush(heap, (heights[r][c], r, c))
                    visited[r][c] = True
        water = 0
        DIRS = [(-1,0),(1,0),(0,-1),(0,1)]
        while heap:
            h, r, c = heapq.heappop(heap)
            for dr, dc in DIRS:
                nr, nc = r + dr, c + dc
                if 0 <= nr < rows and 0 <= nc < cols and not visited[nr][nc]:
                    visited[nr][nc] = True
                    water += max(0, h - heights[nr][nc])
                    heapq.heappush(heap, (max(h, heights[nr][nc]), nr, nc))
        return water

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


37.5 Minimize the Maximum of Two Arrays (LC 2513)

Đề bài

Input: 4 số nguyên divisor1, divisor2, uniqueCnt1, uniqueCnt2.

Cần tạo 2 mảng arr1 (độ dài uniqueCnt1) và arr2 (độ dài uniqueCnt2) chứa các số nguyên dương sao cho: - Mọi phần tử trong arr1 không chia hết cho divisor1. - Mọi phần tử trong arr2 không chia hết cho divisor2. - arr1 ∪ arr2 toàn bộ là các giá trị phân biệt (không trùng nhau giữa hai mảng).

Trả về giá trị lớn nhất trong arr1 ∪ arr2nhỏ nhất có thể.

Ví dụ

Input:  divisor1 = 2, divisor2 = 7, uniqueCnt1 = 1, uniqueCnt2 = 3
Output: 4

Giải thích:
  arr1 = [1]         (1 không chia hết cho 2)
  arr2 = [2, 3, 4]   (đều không chia hết cho 7)
  max(arr1 ∪ arr2) = 4, đã tối thiểu.

Ràng buộc

Clarifying questions

Hướng tiếp cận

Binary search trên đáp án X: - Số trong [1, X] không chia hết d1: X - X // d1. - Tương tự với d2. - Chỉ chia hết lcm(d1, d2): là phần share — quyết định ai nhận.

Inclusion-exclusion + binary search.

Code Python 3

from math import lcm

class Solution:
    def minimizeSet(self, divisor1: int, divisor2: int, uniqueCnt1: int, uniqueCnt2: int) -> int:
        L = lcm(divisor1, divisor2)

        def enough(x: int) -> bool:
            # Số ≤ x chia cho d1: x // d1; chia cho d2: x // d2; chia cho L: x // L
            available1 = x - x // divisor1
            available2 = x - x // divisor2
            available_both = x - x // L
            # arr1 cần available1 cho mình, có thể share với arr2.
            need1 = max(0, uniqueCnt1 - (available1 - (x // divisor2 - x // L)))
            # ... brain teaser, easier with formula.
            shared = available_both
            return available1 >= uniqueCnt1 and available2 >= uniqueCnt2 and \
                   shared >= max(0, uniqueCnt1 - (x - x // divisor1 - (x // divisor2 - x // L))) + \
                              max(0, uniqueCnt2 - (x - x // divisor2 - (x // divisor1 - x // L)))

        lo, hi = 1, 10**11
        while lo < hi:
            mid = (lo + hi) // 2
            if enough(mid):
                hi = mid
            else:
                lo = mid + 1
        return lo

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


37.6 Find a Peak Element II (LC 1901)

Đề bài

Ma trận. Peak = ô lớn hơn 4 hàng xóm. Trả về [r, c] của 1 peak. O(m log n).

Ví dụ

Input:  mat = [[1, 4],
               [3, 2]]   (mọi ô khác nhau; biên grid xem như -∞)
Output: [0, 1]   (1 vị trí peak: lớn hơn 4 hàng xóm; có nhiều, trả bất kỳ)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Binary search trên cột. Chọn cột giữa, tìm max trong cột → coi như có “đỉnh trong row đó”. Compare với 2 cột kề: nếu max < cột bên trái → đỉnh ở nửa trái; tương tự phải.

Code Python 3

from typing import List

class Solution:
    def findPeakGrid(self, mat: List[List[int]]) -> List[int]:
        rows, cols = len(mat), len(mat[0])
        lo, hi = 0, cols - 1
        while lo <= hi:
            mid = (lo + hi) // 2
            max_row = max(range(rows), key=lambda r: mat[r][mid])
            left = mat[max_row][mid - 1] if mid > 0 else -1
            right = mat[max_row][mid + 1] if mid < cols - 1 else -1
            if mat[max_row][mid] > left and mat[max_row][mid] > right:
                return [max_row, mid]
            if left > mat[max_row][mid]:
                hi = mid - 1
            else:
                lo = mid + 1
        return [-1, -1]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Template BS + Graph

def solve():
    lo, hi = min_possible, max_possible
    while lo < hi:
        mid = (lo + hi) // 2
        if can_reach(mid):     # reachability dưới ngưỡng / capacity mid
            hi = mid
        else:
            lo = mid + 1
    return lo

Predicate table (chương này)

Bài Answer can(x) Monotonic
Path with Min Effort (LC 1631) max edge diff BFS chỉ qua cạnh ≤ x True khi x ↗
Swim in Rising Water (LC 778) min ngưỡng BFS qua cell height ≤ x True khi x ↗
Min Bridges (LC 1102) max path min DFS qua cell ≥ x True khi x ↘
Aggressive Cows / Magnetic Force distance Greedy đặt True khi x ↘

So với Dijkstra / DSU

Trapping Rain Water II (LC 407) — vì sao đặt ở đây?

Bản chất bài này là Dijkstra-like với priority queue (chọn cell biên thấp nhất tiếp theo). Đặt trong chương 37 để so sánh “BS + Graph” với “Min-heap traversal” — cả hai đều thuộc gia đình “answer = ngưỡng”.


Chương 38 — Binary Search kết hợp Dynamic Programming

Khi DP transition cần “tìm nhanh giá trị tối ưu trong subset đã xét”, binary search vào prefix optimal có thể giảm O(n²) xuống O(n log n). Pattern này gặp ở LIS, Russian Doll Envelopes, Constrained Subsequence Sum.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Template code

from bisect import bisect_left, bisect_right

# DP với binary search lookup
tails = []
for x in arr:
    idx = bisect_left(tails, x)
    if idx == len(tails):
        tails.append(x)
    else:
        tails[idx] = x

Bài tự luyện cuối chương


38.1 Russian Doll Envelopes (LC 354) — recap

Đã giải đầy đủ ở Chương 29.6. Sort 2D + LIS với binary search.

Liên hệ với chương này

Đây là gateway cho pattern BS + DP: sau sort 2D, transition của LIS (tails[bisect_left(tails, x)] = x) chính là binary search trên cấu trúc DP.

Code Python 3 (recap)

from bisect import bisect_left
from typing import List

class Solution:
    def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
        envelopes.sort(key=lambda e: (e[0], -e[1]))
        tails: list[int] = []
        for _, h in envelopes:
            i = bisect_left(tails, h)
            if i == len(tails):
                tails.append(h)
            else:
                tails[i] = h
        return len(tails)

Phân tích độ phức tạp

Bài tự luyện liên quan


38.2 Longest Increasing Subsequence II (LC 2407)

Đề bài

LIS với constraint: nums[i+1] - nums[i] <= k. Tìm độ dài max.

Ví dụ

Input:  nums=[4,2,1,4,3,4,5,8,15], k=3
Output: 5

Ràng buộc

Clarifying questions

Hướng tiếp cận

Segment Tree với max query (range max trên dp values).

dp[v] = max LIS kết thúc với value v. Transition: dp[v] = max(dp[v-k..v-1]) + 1.

Segment tree cho phép range max query O(log V) per step. Total O(n log V).

Code Python 3

from typing import List

class Solution:
    def lengthOfLIS(self, nums: List[int], k: int) -> int:
        n = max(nums)
        size = 1
        while size < n + 1: size <<= 1
        tree = [0] * (2 * size)

        def update(i: int, val: int):
            i += size
            tree[i] = max(tree[i], val)
            while i > 1:
                i //= 2
                tree[i] = max(tree[2*i], tree[2*i+1])

        def query(l: int, r: int) -> int:
            res = 0
            l += size; r += size + 1
            while l < r:
                if l & 1: res = max(res, tree[l]); l += 1
                if r & 1: r -= 1; res = max(res, tree[r])
                l //= 2; r //= 2
            return res

        best = 0
        for x in nums:
            lo = max(1, x - k)
            cur = query(lo, x - 1) + 1
            update(x, cur)
            best = max(best, cur)
        return best

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


38.3 Number of Longest Increasing Subsequence (LC 673)

Đề bài

Đếm số LIS có độ dài max của nums.

Ví dụ

Input:  nums = [1, 3, 5, 4, 7]
Output: 2   (số LIS độ dài tối đa: [1,3,5,7] và [1,3,4,7])

Ràng buộc

Clarifying questions

Hướng tiếp cận

DP O(n²): dp[i] = (length, count) — length của LIS kết thúc tại i, count của số LIS đó.

Có version O(n log n) với segment tree.

Code Python 3

from typing import List

class Solution:
    def findNumberOfLIS(self, nums: List[int]) -> int:
        n = len(nums)
        length = [1] * n
        count = [1] * n
        for i in range(n):
            for j in range(i):
                if nums[j] < nums[i]:
                    if length[j] + 1 > length[i]:
                        length[i] = length[j] + 1
                        count[i] = count[j]
                    elif length[j] + 1 == length[i]:
                        count[i] += count[j]
        max_len = max(length)
        return sum(c for l, c in zip(length, count) if l == max_len)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


38.4 Max Sum of Rectangle No Larger Than K (LC 363)

Đề bài

Ma trận. Tìm submatrix có tổng max ≤ k.

Ví dụ

Input:  matrix=[[1,0,1],[0,-2,3]], k=2
Output: 2

Ràng buộc

Clarifying questions

Hướng tiếp cận

Fix 2 cột (c1, c2). Tổng mỗi row trong khoảng → 1D array. Tìm subarray tổng gần k nhất ≤ k: prefix sum + SortedList + binary search.

Code Python 3

from sortedcontainers import SortedList
from typing import List

class Solution:
    def maxSumSubmatrix(self, matrix: List[List[int]], k: int) -> int:
        rows, cols = len(matrix), len(matrix[0])
        result = -float('inf')
        for c1 in range(cols):
            row_sums = [0] * rows
            for c2 in range(c1, cols):
                for r in range(rows):
                    row_sums[r] += matrix[r][c2]
                # Tìm subarray sum lớn nhất ≤ k.
                sl = SortedList([0])
                prefix = 0
                for v in row_sums:
                    prefix += v
                    # Tìm prefix' ≥ prefix - k → subarray sum = prefix - prefix' ≤ k.
                    idx = sl.bisect_left(prefix - k)
                    if idx < len(sl):
                        result = max(result, prefix - sl[idx])
                    sl.add(prefix)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


38.5 Constrained Subsequence Sum (LC 1425)

Đề bài

Cho numsk. Tìm max sum của subsequence sao cho mọi cặp index liên tiếp trong subsequence cách nhau ≤ k.

Ví dụ

Input:  nums=[10,2,-10,5,20], k=2
Output: 37

Ràng buộc

Clarifying questions

Hướng tiếp cận

DP dp[i] = nums[i] + max(0, max(dp[i-k..i-1])).

max(dp[i-k..i-1]) qua sliding window max (Chương 18.4) → O(n).

Code Python 3

from collections import deque
from typing import List

class Solution:
    def constrainedSubsetSum(self, nums: List[int], k: int) -> int:
        n = len(nums)
        dp = [0] * n
        dq: deque[int] = deque()
        best = -float('inf')
        for i in range(n):
            window_max = dp[dq[0]] if dq else 0
            dp[i] = nums[i] + max(0, window_max)
            best = max(best, dp[i])
            while dq and dp[dq[-1]] <= dp[i]:
                dq.pop()
            dq.append(i)
            if dq[0] == i - k:
                dq.popleft()
        return best

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


38.6 Allocate Mailboxes (LC 1478)

Đề bài

Cho houses (vị trí). Đặt k mailbox sao cho tổng distance(house → nearest mailbox) min.

Ví dụ

Input:  houses=[1,4,8,10,20], k=3
Output: 5

Ràng buộc

Clarifying questions

Hướng tiếp cận

DP: dp[i][j] = min cost dùng j mailbox cho houses[:i].

Transition: dp[i][j] = min(dp[p][j-1] + cost(houses[p:i])) với cost là tổng distance khi đặt 1 mailbox cho range — = sum |x - median|.

O(k · n²). Có thể tăng tốc bằng divide-conquer DP optimization → O(k · n log n).

Code Python 3

from typing import List

class Solution:
    def minDistance(self, houses: List[int], k: int) -> int:
        houses.sort()
        n = len(houses)
        # cost[l][r] = chi phí đặt 1 mailbox cho houses[l..r]
        cost = [[0] * n for _ in range(n)]
        for l in range(n):
            for r in range(l + 1, n):
                mid = houses[(l + r) // 2]
                cost[l][r] = sum(abs(houses[i] - mid) for i in range(l, r + 1))

        INF = float('inf')
        dp = [[INF] * (k + 1) for _ in range(n + 1)]
        dp[0][0] = 0
        for i in range(1, n + 1):
            for j in range(1, k + 1):
                for p in range(j - 1, i):
                    dp[i][j] = min(dp[i][j], dp[p][j - 1] + cost[p][i - 1])
        return dp[n][k]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

“BS as a lookup inside DP/Optimization”

Range max query — Segment Tree

LC 2407 (Longest Increasing Subsequence II): không thể dùng patience đơn giản vì có ràng buộc gap ≤ k ⇒ cần segment tree lookup dp[v - k .. v - 1].

Max Sum Rectangle ≤ K (LC 363)

Allocate Mailboxes (LC 1478) — median cost precompute


Chương 39 — Sorting kết hợp Dynamic Programming

Sort + DP là combo mạnh khi thứ tự items quan trọng và DP trên thứ tự đó. Pattern thường gặp: sort theo một thuộc tính, rồi LIS-like DP trên thuộc tính khác.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Bài tự luyện cuối chương


39.1 Largest Divisible Subset (LC 368)

Đề bài

Cho nums (phân biệt). Tìm subset lớn nhất sao cho mọi cặp (a, b) trong đó a % b == 0 hoặc b % a == 0.

Ví dụ

Input:  nums = [1, 2, 4, 8]
Output: [1, 2, 4, 8]   (subset lớn nhất mà mọi cặp (a, b) đều có a % b == 0 hoặc b % a == 0)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Sort tăng dần. dp[i] = subset lớn nhất kết thúc tại nums[i].

dp[i] = max(dp[j] + 1 for j < i if nums[i] % nums[j] == 0).

Track parent[] để reconstruct subset.

Code Python 3

from typing import List

class Solution:
    def largestDivisibleSubset(self, nums: List[int]) -> List[int]:
        nums.sort()
        n = len(nums)
        dp = [1] * n
        parent = [-1] * n
        best_idx = 0
        for i in range(n):
            for j in range(i):
                if nums[i] % nums[j] == 0 and dp[j] + 1 > dp[i]:
                    dp[i] = dp[j] + 1
                    parent[i] = j
            if dp[i] > dp[best_idx]:
                best_idx = i
        result = []
        while best_idx != -1:
            result.append(nums[best_idx])
            best_idx = parent[best_idx]
        return result[::-1]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


39.2 Russian Doll Envelopes (LC 354) — recap

Đã giải đầy đủ ở Chương 29.6. Sort 2D + LIS với binary search.

Liên hệ với chương này

Pattern “sort 2D + LIS theo chiều còn lại” tái xuất ở bài 39.3 (Stacking Cuboids, 3D), 39.5 (Visible People), 45.x (Sorting + DP nâng cao).

Code Python 3 (recap)

from bisect import bisect_left
from typing import List

class Solution:
    def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
        envelopes.sort(key=lambda e: (e[0], -e[1]))
        tails: list[int] = []
        for _, h in envelopes:
            i = bisect_left(tails, h)
            if i == len(tails):
                tails.append(h)
            else:
                tails[i] = h
        return len(tails)

Phân tích độ phức tạp

Bài tự luyện liên quan


39.3 Maximum Height by Stacking Cuboids (LC 1691)

Đề bài

Cho cuboids[i] = [w, l, h]. Có thể rotate mỗi cuboid (chọn 1 trong 3 chiều làm height). Stack i lên j nếu mọi chiều i ≤ chiều j. Tìm height max.

Ví dụ

Input:  cuboids = [[50,45,20], [95,37,53], [45,23,12]]
        (mỗi cuboid [w, l, h]; có thể xoay; stack được nếu w1≤w2, l1≤l2, h1≤h2)
Output: 190   (chiều cao stack lớn nhất)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Insight: sort 3 chiều mỗi cuboid → cuboid khác stack được lên nhau ↔︎ 3 chiều sorted của cái dưới ≥ 3 chiều sorted của cái trên (sau khi sort all cuboids tăng).

Sau khi sort, bài thành LIS 3D.

Code Python 3

from typing import List

class Solution:
    def maxHeight(self, cuboids: List[List[int]]) -> int:
        for c in cuboids:
            c.sort()
        cuboids.sort()
        n = len(cuboids)
        dp = [c[2] for c in cuboids]
        for i in range(n):
            for j in range(i):
                if all(cuboids[j][k] <= cuboids[i][k] for k in range(3)):
                    dp[i] = max(dp[i], dp[j] + cuboids[i][2])
        return max(dp)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


39.4 Maximum Profit in Job Scheduling (LC 1235)

Đề bài

Cho jobs = [(start, end, profit)]. Chọn các job không overlap, max tổng profit.

Ví dụ

Input:  startTime=[1,2,3,3], endTime=[3,4,5,6], profit=[50,10,40,70]
Output: 120

Ràng buộc

Clarifying questions

Hướng tiếp cận

Sort theo end. dp[i] = max profit dùng các job [0..i].

dp[i] = max(dp[i-1], dp[k] + jobs[i].profit) với k = job cuối có end <= jobs[i].start.

Binary search tìm kO(n log n).

Code Python 3

from bisect import bisect_right
from typing import List

class Solution:
    def jobScheduling(self, startTime: List[int], endTime: List[int], profit: List[int]) -> int:
        jobs = sorted(zip(endTime, startTime, profit))
        ends = [j[0] for j in jobs]
        dp = [0] * (len(jobs) + 1)
        for i, (end, start, p) in enumerate(jobs):
            k = bisect_right(ends, start)
            dp[i + 1] = max(dp[i], dp[k] + p)
        return dp[-1]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


39.5 Number of Visible People in a Queue (LC 1944)

Đề bài

Cho heights. Mỗi người thấy được người sau họ trong queue đến khi gặp người cao hơn họ hoặc cao hơn cả người vừa thấy. Đếm số người mỗi người thấy.

Ví dụ

Input:  heights = [10, 6, 8, 5, 11, 9]
        (heights[i] = chiều cao người thứ i trong queue, mọi heights[i] khác nhau)
Output: [3, 1, 2, 1, 1, 0]
        (answer[i] = số người ở bên phải mà người thứ i nhìn thấy)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Monotonic stack từ phải sang trái. Mỗi người: pop hết người thấp hơn từ stack (đếm vào kết quả), cộng 1 nếu stack còn (người cao hơn).

Code Python 3

from typing import List

class Solution:
    def canSeePersonsCount(self, heights: List[int]) -> List[int]:
        n = len(heights)
        result = [0] * n
        stack: list[int] = []
        for i in range(n - 1, -1, -1):
            while stack and heights[i] > stack[-1]:
                stack.pop()
                result[i] += 1
            if stack:
                result[i] += 1
            stack.append(heights[i])
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


39.6 Minimum Number of Taps to Open to Water a Garden (LC 1326)

Đề bài

Garden độ dài n. Tap i ở vị trí i tưới [i - r, i + r]. Min tap để tưới hết.

Ví dụ

Input:  n=5, ranges=[3,4,1,1,0,0]
Output: 1

Ràng buộc

Clarifying questions

Hướng tiếp cận

Chuyển sang Jump Game II (16.2): cho mỗi vị trí i, farthest[i] = vị trí xa nhất tới được nếu bắt đầu ở i. Greedy.

Code Python 3

from typing import List

class Solution:
    def minTaps(self, n: int, ranges: List[int]) -> int:
        farthest = [0] * (n + 1)
        for i, r in enumerate(ranges):
            left = max(0, i - r)
            right = min(n, i + r)
            farthest[left] = max(farthest[left], right)
        taps = 0
        current_end = 0
        next_end = 0
        for i in range(n + 1):
            if i > next_end:
                return -1
            if i > current_end:
                taps += 1
                current_end = next_end
            next_end = max(next_end, farthest[i])
        return taps

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

“Sort as preprocessing” — phạm vi rộng hơn DP

Chương này tập hợp các bài mà bước đầu tiên là sort, sau đó áp dụng: - DP (Russian Doll, Job Scheduling). - Monotonic stack (LC 1944 Visible People — đứng riêng nhưng phụ thuộc thứ tự). - Greedy interval (LC 1326 Minimum Taps).

Nhãn chương rộng hơn “Sort + DP” cứng nhắc; coi như “Sort preprocessing patterns”.

Russian Doll Envelopes (LC 354) — lens phân biệt

Job Scheduling (LC 1235) — timeline + previous-compatible

jobs sorted by endTime: J1=[1,3,50], J2=[2,4,10], J3=[3,5,40], J4=[3,6,70]
                                ↑           ↑           ↑
                              end=3        end=4       end=5,6

dp[i] = max(dp[i-1], jobs[i].profit + dp[prev(i)])
prev(i) = job cuối cùng có endTime ≤ jobs[i].startTime  ← binary search!

Visible People In Queue (LC 1944) — monotonic stack

Minimum Taps (LC 1326) — greedy interval


Chương 40 — Bitmask Dynamic Programming

Khi state là subset của tập nhỏ (≤ 20 phần tử), ta encode subset bằng bitmask int 32-bit và DP trên bitmask. State space O(2^n). Pattern cho các bài TSP-style, set cover, partition.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Template code

from functools import cache

@cache
def dp(mask, *extra_state):
    if mask == 0:        # base case (hoặc full = (1 << n) - 1)
        return base_value
    return best([dp(mask ^ (1 << i), ...) for i in iterate_bits(mask)])

Bài tự luyện cuối chương


40.1 Partition to K Equal Sum Subsets (LC 698)

Đề bài

Cho numsk. Chia thành k subset có tổng bằng nhau?

Ví dụ

Input:  nums=[4,3,2,3,5,2,1], k=4
Output: True

Ràng buộc

Clarifying questions

Hướng tiếp cận

dp[mask] = boolean “subset bitmask có thể partition thoả”.

Total = sum(nums). Mỗi subset tổng = total / k.

Duyệt: cho mỗi mask, nếu dp[mask] true và current_subset_sum % target == 0, thử thêm các element chưa dùng.

Code Python 3

from functools import cache
from typing import List

class Solution:
    def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
        total = sum(nums)
        if total % k: return False
        target = total // k
        nums.sort(reverse=True)
        if nums[0] > target: return False
        n = len(nums)

        @cache
        def dfs(mask: int, current_sum: int) -> bool:
            if mask == (1 << n) - 1:
                return True
            for i in range(n):
                if mask & (1 << i): continue
                new_sum = current_sum + nums[i]
                if new_sum > target: continue
                next_sum = new_sum if new_sum < target else 0
                if dfs(mask | (1 << i), next_sum):
                    return True
            return False

        return dfs(0, 0)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


40.2 Shortest Path Visiting All Nodes (LC 847)

Đề bài

Input: graph: List[List[int]] — adjacency list của 1 graph vô hướng liên thông n đỉnh (đỉnh đánh số 0..n-1). Tìm path ngắn nhất (đếm cạnh) visit tất cả đỉnh; được xuất phát/kết thúc tại đỉnh bất kỳ, được lặp lại đỉnh.

Ví dụ

Input:  graph = [[1,2,3], [0], [0], [0]]
        (adjacency list undirected; graph[i] = các node kề node i; được xuất phát/kết thúc tại node bất kỳ)
Output: 4   (số cạnh ngắn nhất để visit tất cả node; được lặp lại node)

Ràng buộc

Clarifying questions

Hướng tiếp cận

BFS với state (node, visited_mask). Đáp án = số bước đầu tiên đạt được state (_, full_mask).

Code Python 3

from collections import deque
from typing import List

class Solution:
    def shortestPathLength(self, graph: List[List[int]]) -> int:
        n = len(graph)
        full = (1 << n) - 1
        if n == 1: return 0
        queue = deque((i, 1 << i, 0) for i in range(n))   # (node, mask, dist)
        visited = {(i, 1 << i) for i in range(n)}
        while queue:
            node, mask, dist = queue.popleft()
            if mask == full: return dist
            for nb in graph[node]:
                new_mask = mask | (1 << nb)
                if (nb, new_mask) not in visited:
                    visited.add((nb, new_mask))
                    queue.append((nb, new_mask, dist + 1))
        return -1

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


40.3 Smallest Sufficient Team (LC 1125)

Đề bài

Cho req_skills và mảng people[i] = list of skill. Tìm subset người nhỏ nhất cover tất cả skill.

Ví dụ

Input:  req_skills=["java","nodejs","reactjs"], people=[["java"],["nodejs"],["nodejs","reactjs"]]
Output: [0,2]

Ràng buộc

Clarifying questions

Hướng tiếp cận

DP bitmask trên skills. dp[mask] = smallest team cover skills trong mask.

dp[mask | skills_of_p] = min(dp[mask | skills_of_p], dp[mask] + [p]).

Code Python 3

from typing import List

class Solution:
    def smallestSufficientTeam(self, req_skills: List[str], people: List[List[str]]) -> List[int]:
        skill_idx = {s: i for i, s in enumerate(req_skills)}
        n = len(req_skills)
        full = (1 << n) - 1
        people_mask = []
        for p in people:
            mask = 0
            for s in p:
                if s in skill_idx:
                    mask |= 1 << skill_idx[s]
            people_mask.append(mask)

        dp: dict[int, list[int]] = {0: []}
        for i, mask in enumerate(people_mask):
            for cur_mask, team in list(dp.items()):
                new_mask = cur_mask | mask
                if new_mask == cur_mask: continue
                if new_mask not in dp or len(team) + 1 < len(dp[new_mask]):
                    dp[new_mask] = team + [i]
        return dp[full]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


40.4 Find the Shortest Superstring (LC 943)

Đề bài

Cho mảng strings. Tìm string ngắn nhất chứa tất cả các string trong mảng làm substring.

Ví dụ

Input:  words = ["alex", "loves", "leetcode"]
Output: "alexlovesleetcode"   (chuỗi ngắn nhất chứa mọi word làm substring;
                               có thể có nhiều đáp án, trả về 1 trong số đó)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Bitmask DP kết hợp TSP. State (mask, last_string_idx) = string ngắn nhất visit set mask và kết thúc tại string last_idx.

Pre-compute overlap[i][j] = max overlap khi nối string i + j.

Code Python 3

from typing import List

class Solution:
    def shortestSuperstring(self, words: List[str]) -> str:
        n = len(words)
        # overlap[i][j] = max k sao cho words[i] kết thúc bằng prefix length-k của words[j].
        overlap = [[0] * n for _ in range(n)]
        for i in range(n):
            for j in range(n):
                if i != j:
                    for k in range(min(len(words[i]), len(words[j])), 0, -1):
                        if words[i].endswith(words[j][:k]):
                            overlap[i][j] = k
                            break

        INF = float('inf')
        # dp[mask][i] = length của superstring ngắn nhất visit set mask, kết thúc tại i.
        dp = [[INF] * n for _ in range(1 << n)]
        parent = [[-1] * n for _ in range(1 << n)]
        for i in range(n):
            dp[1 << i][i] = len(words[i])

        for mask in range(1 << n):
            for i in range(n):
                if not (mask & (1 << i)) or dp[mask][i] == INF:
                    continue
                for j in range(n):
                    if mask & (1 << j):
                        continue
                    new_mask = mask | (1 << j)
                    new_len = dp[mask][i] + len(words[j]) - overlap[i][j]
                    if new_len < dp[new_mask][j]:
                        dp[new_mask][j] = new_len
                        parent[new_mask][j] = i

        # Trace path: tìm end-state có length min.
        full = (1 << n) - 1
        last = min(range(n), key=lambda i: dp[full][i])

        # Reconstruct order ngược.
        order: list[int] = []
        mask = full
        cur = last
        while cur != -1:
            order.append(cur)
            prev = parent[mask][cur]
            mask ^= 1 << cur
            cur = prev
        order.reverse()

        # Build superstring.
        result = words[order[0]]
        for idx in range(1, len(order)):
            prev_i, cur_i = order[idx - 1], order[idx]
            result += words[cur_i][overlap[prev_i][cur_i]:]
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


40.5 Maximum Students Taking Exam (LC 1349)

Đề bài

Phòng thi m × n. seats[i][j] = . (ok) hoặc # (hỏng). 2 học sinh không được kề bên trái/phải hoặc 2 góc chéo (vì copy được). Max số học sinh ngồi.

Ví dụ

Input:  seats = [["#",".","#","#",".","#"],
                 [".","#","#","#","#","."],
                 ["#",".","#","#",".","#"]]
        ("." = ghế OK, "#" = ghế hỏng; HS xem được bài 4 hướng chéo & cạnh)
Output: 4   (số HS tối đa xếp được mà không ai gian lận)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Row DP bitmask. dp[i][mask] = max ở hàng i với học sinh ngồi theo mask.

Mỗi mask phải valid (không 2 bit kề) và không đè ô hỏng.

Transition: dp[i][mask] = max(dp[i-1][prev]) + popcount(mask) với prev không xung đột chéo với mask.

Code Python 3

from typing import List

class Solution:
    def maxStudents(self, seats: List[List[str]]) -> int:
        m, n = len(seats), len(seats[0])
        # row_bad[i] = bitmask các ô '#' trong hàng i.
        row_bad = [0] * m
        for i in range(m):
            for j in range(n):
                if seats[i][j] == '#':
                    row_bad[i] |= 1 << j

        # Valid masks: không 2 bit kề nhau.
        valid = [m for m in range(1 << n) if (m & (m << 1)) == 0]

        dp = {0: 0}     # mask -> max students
        for i in range(m):
            new_dp = {}
            for mask in valid:
                if mask & row_bad[i]: continue
                cnt = bin(mask).count('1')
                best = 0
                for prev, prev_cnt in dp.items():
                    if (mask & (prev << 1)) or (mask & (prev >> 1)): continue
                    best = max(best, prev_cnt)
                new_dp[mask] = best + cnt
            dp = new_dp
        return max(dp.values(), default=0)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


40.6 Minimum XOR Sum of Two Arrays (LC 1879)

Đề bài

2 mảng độ dài n. Hoán vị nums2 để min Σ nums1[i] ^ nums2[π(i)].

Ví dụ

Input:  nums1=[1,2], nums2=[2,3]
Output: 2

Ràng buộc

Clarifying questions

Hướng tiếp cận

Bitmask DP O(n · 2^n). dp[mask] = min sum khi đã ghép cặp các index trong mask (cùng số bit = số i đã xét) với cùng số element của nums2.

Code Python 3

from typing import List

class Solution:
    def minimumXORSum(self, nums1: List[int], nums2: List[int]) -> int:
        n = len(nums1)
        INF = float('inf')
        dp = [INF] * (1 << n)
        dp[0] = 0
        for mask in range(1 << n):
            i = bin(mask).count('1')
            if i >= n: continue
            for j in range(n):
                if mask & (1 << j): continue
                new_mask = mask | (1 << j)
                dp[new_mask] = min(dp[new_mask], dp[mask] + (nums1[i] ^ nums2[j]))
        return dp[(1 << n) - 1]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Bitmask cookbook

Operation Code
Bit i set? (mask >> i) & 1
Set bit i mask | (1 << i)
Clear bit i mask & ~(1 << i)
Toggle bit i mask ^ (1 << i)
Lowest set bit mask & -mask
Pop count bin(mask).count('1') hoặc int.bit_count() (Py 3.10+)
Iterate submasks sub = mask; while sub: ...; sub = (sub - 1) & mask (cuối cùng còn sub=0)
Iterate set bits while mask: i = (mask & -mask).bit_length() - 1; mask &= mask - 1

Constraint feasibility table

n Bitmask 2^n Common DP shape Time
≤ 20 ≤ 10⁶ dp[mask] (TSP, Assignment) O(2^n · n)
≤ 16 ≤ 65k dp[mask][i] (TSP với endpoint) O(2^n · n²)
≤ 22-25 ≤ 33M Cần tối ưu hoặc submask enum O(3^n) cho subset-sum DP

Shortest Superstring (LC 943) — duplicate words

Maximum Students (LC 1349) — row mask conflict

Row mask  s  (bit i = 1 nếu có HS ở cột i):
- Valid trong row: s & (s << 1) == 0 (không 2 HS kế).
- Compatible với prev mask p: 
    (s & (p << 1)) == 0 AND (s & (p >> 1)) == 0
- Phải nằm trong cell allowed: s & broken_mask[row] == 0.

Chương 41 — Bitmask kết hợp Trie

Khi bài cần “max XOR” với số trong array, ta build Trie binary của các số (mỗi bit là 1 cấp). Query max XOR là DFS greedy đi theo bit “khác” với số hiện tại.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Template code

Lưu ý: mỗi ### Code Python 3 bên dưới đều standalone — copy nguyên block vào LeetCode chạy được. Nếu chỉ cần template tham khảo, dùng class này:

class BinaryTrie:
    """Binary Trie cho int 32-bit. Hỗ trợ insert / max_xor."""

    def __init__(self):
        self.children = [None, None]    # 0, 1

    def insert(self, x: int, bits: int = 31) -> None:
        node = self
        for b in range(bits, -1, -1):
            bit = (x >> b) & 1
            if not node.children[bit]:
                node.children[bit] = BinaryTrie()
            node = node.children[bit]

    def max_xor(self, x: int, bits: int = 31) -> int:
        node = self
        out = 0
        for b in range(bits, -1, -1):
            bit = (x >> b) & 1
            opposite = 1 - bit
            if node.children[opposite]:
                out |= 1 << b
                node = node.children[opposite]
            else:
                node = node.children[bit]
        return out

Bài tự luyện cuối chương


41.1 Maximum XOR of Two Numbers (LC 421) — Trie version

Đã giải bằng “greedy bit + set” ở Chương 21.6. Trie cách thanh lịch hơn.

Đề bài

Cho nums. Trả về max XOR của 2 phần tử khác nhau.

Ví dụ

Input:  nums = [3, 10, 5, 25, 2, 8]
Output: 28   (max XOR giữa 2 phần tử bất kỳ: 5 XOR 25 = 28)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Build Trie binary của các số. Với mỗi x, query max_xor(x) (DFS theo bit ngược lại). Lấy max qua tất cả x.

Code Python 3

from typing import List

class _Trie:
    __slots__ = ("children",)
    def __init__(self):
        self.children = [None, None]


class Solution:
    BITS = 31

    def findMaximumXOR(self, nums: List[int]) -> int:
        root = _Trie()
        for x in nums:
            node = root
            for b in range(self.BITS, -1, -1):
                bit = (x >> b) & 1
                if not node.children[bit]:
                    node.children[bit] = _Trie()
                node = node.children[bit]

        best = 0
        for x in nums:
            node = root
            cur = 0
            for b in range(self.BITS, -1, -1):
                bit = (x >> b) & 1
                opp = 1 - bit
                if node.children[opp]:
                    cur |= 1 << b
                    node = node.children[opp]
                else:
                    node = node.children[bit]
            best = max(best, cur)
        return best

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


41.2 Maximum XOR With an Element From Array (LC 1707)

Đề bài

Cho numsqueries[i] = [x, m]. Với mỗi query, trả về max XOR giữa x và một số y ∈ nums với y ≤ m. Không có y → -1.

Ví dụ

Input:  nums = [0,1,2,3,4], queries = [[3,1],[1,3],[5,6]]
Output: [3, 3, 7]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Offline + sort + Trie.

Code Python 3

from typing import List

class _Trie:
    __slots__ = ("children",)
    def __init__(self):
        self.children = [None, None]


class Solution:
    BITS = 30  # nums, x ≤ 10^9 < 2^30

    def maximizeXor(self, nums: List[int], queries: List[List[int]]) -> List[int]:
        def insert(root, x):
            node = root
            for b in range(self.BITS, -1, -1):
                bit = (x >> b) & 1
                if not node.children[bit]:
                    node.children[bit] = _Trie()
                node = node.children[bit]

        def max_xor(root, x):
            node = root
            out = 0
            for b in range(self.BITS, -1, -1):
                bit = (x >> b) & 1
                opp = 1 - bit
                if node.children[opp]:
                    out |= 1 << b
                    node = node.children[opp]
                else:
                    node = node.children[bit]
            return out

        nums.sort()
        indexed = sorted(enumerate(queries), key=lambda kv: kv[1][1])
        result = [-1] * len(queries)
        root = _Trie()
        i = 0
        for q_idx, (x, m) in indexed:
            while i < len(nums) and nums[i] <= m:
                insert(root, nums[i])
                i += 1
            if i == 0:
                result[q_idx] = -1
            else:
                result[q_idx] = max_xor(root, x)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


41.3 Count Pairs With XOR in a Range (LC 1803)

Đề bài

Cho nums[low, high]. Đếm số cặp (i, j) với i < jlow <= nums[i] ^ nums[j] <= high.

Ví dụ

Input:  nums = [1, 4, 2, 7], low = 2, high = 6
Output: 6

Giải thích 6 cặp (i, j) thoả 2 ≤ nums[i] ^ nums[j] ≤ 6:
  (0,1) 1 ^ 4 = 5      ✓
  (0,2) 1 ^ 2 = 3      ✓
  (0,3) 1 ^ 7 = 6      ✓
  (1,2) 4 ^ 2 = 6      ✓
  (1,3) 4 ^ 7 = 3      ✓
  (2,3) 2 ^ 7 = 5      ✓

Ràng buộc

Clarifying questions

Hướng tiếp cận

count_xor_less(x) = số cặp (num, y) (y đã insert trước) có num ^ y < x. Đáp án = count_xor_less(high + 1) - count_xor_less(low).

Trie có thêm countmỗi node = số y đã đi qua node đó. Với mỗi num, đi từ bit cao xuống thấp; tại bit hiện hành gọi bit_n = bit của num, bit_x = bit của x:

Nói gọn: “same bit ⇒ XOR = 0” và “opposite bit ⇒ XOR = 1”. Code dưới đếm subtree same (XOR = 0) khi bit_x = 1, rồi chuyển con trỏ sang opposite để tiếp tục.

Code Python 3

from typing import List

class _TrieCnt:
    __slots__ = ("children", "count")
    def __init__(self):
        self.children = [None, None]
        self.count = 0


class Solution:
    BITS = 15  # nums[i] ≤ 2·10^4 < 2^15

    def countPairs(self, nums: List[int], low: int, high: int) -> int:

        def count_less(target: int) -> int:
            root = _TrieCnt()
            total = 0
            for num in nums:
                node = root
                for b in range(self.BITS, -1, -1):
                    bit_n = (num >> b) & 1
                    bit_x = (target >> b) & 1
                    if bit_x == 1:
                        # XOR bit = 0 (cùng bit) → subtree cùng nhánh đếm hết.
                        if node.children[bit_n]:
                            total += node.children[bit_n].count
                        # Đi xuống nhánh XOR bit = 1 (= opposite).
                        if node.children[1 - bit_n]:
                            node = node.children[1 - bit_n]
                        else:
                            node = None
                            break
                    else:
                        # Bắt buộc XOR bit = 0 → đi xuống cùng nhánh.
                        if node.children[bit_n]:
                            node = node.children[bit_n]
                        else:
                            node = None
                            break
                # Insert num vào Trie sau khi đếm.
                node = root
                for b in range(self.BITS, -1, -1):
                    bit_n = (num >> b) & 1
                    if not node.children[bit_n]:
                        node.children[bit_n] = _TrieCnt()
                    node = node.children[bit_n]
                    node.count += 1
            return total

        return count_less(high + 1) - count_less(low)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


41.4 Maximum Genetic Difference Query (LC 1938)

Đề bài

Tree với parents[i] (cha của i, -1 cho root). Mỗi node trong LC này không có giá trị riêng — “giá trị” của node = chính chỉ số node. Mỗi query (node, val): tìm max val ^ x với x là chỉ số của một ancestor (kể cả node chính nó). Trả mảng max theo thứ tự query.

Ví dụ

Input:  parents = [-1, 0, 1, 1]
        queries = [[0, 2], [3, 2], [2, 5]]

        Cây thực tế (parents[0] = -1 nên 0 là root):
              0
              |
              1
             / \
            2   3

Output: [2, 3, 7]

Giải thích:
  query (0, 2): ancestor của 0 là {0};      max(2^0) = 2.
  query (3, 2): ancestor của 3 là {3,1,0};  max(2^3, 2^1, 2^0) = max(1,3,2) = 3.
  query (2, 5): ancestor của 2 là {2,1,0};  max(5^2, 5^1, 5^0) = max(7,4,5) = 7.

Ràng buộc

Clarifying questions

Hướng tiếp cận

Offline DFS trên tree. Maintain Trie chứa chỉ số các ancestors của node hiện tại trên DFS path (bao gồm node chính nó).

Trie cần hỗ trợ remove — track count mỗi node để biết khi nào prune nhánh khi count về 0.

Code Python 3

import sys
from collections import defaultdict
from typing import List

class _TrieX:
    __slots__ = ("children", "count")
    def __init__(self):
        self.children = [None, None]
        self.count = 0


class Solution:
    BITS = 17  # node indices ≤ n - 1 ≤ 10^5 - 1 < 2^17

    def maxGeneticDifference(self, parents: List[int], queries: List[List[int]]) -> List[int]:
        sys.setrecursionlimit(10**6)
        n = len(parents)
        children = defaultdict(list)
        root_node = -1
        for v, p in enumerate(parents):
            if p == -1:
                root_node = v
            else:
                children[p].append(v)

        # Group queries by node.
        node_queries = defaultdict(list)
        for q_idx, (node, val) in enumerate(queries):
            node_queries[node].append((q_idx, val))

        root = _TrieX()
        result = [0] * len(queries)

        def insert(x: int) -> None:
            node = root
            for b in range(self.BITS, -1, -1):
                bit = (x >> b) & 1
                if not node.children[bit]:
                    node.children[bit] = _TrieX()
                node = node.children[bit]
                node.count += 1

        def remove(x: int) -> None:
            node = root
            path = []
            for b in range(self.BITS, -1, -1):
                bit = (x >> b) & 1
                path.append((node, bit))
                node = node.children[bit]
                node.count -= 1
            # Prune branches with count = 0.
            for parent, bit in reversed(path):
                if parent.children[bit] and parent.children[bit].count == 0:
                    parent.children[bit] = None

        def query_max(val: int) -> int:
            node = root
            out = 0
            for b in range(self.BITS, -1, -1):
                bit = (val >> b) & 1
                opp = 1 - bit
                if node.children[opp] and node.children[opp].count > 0:
                    out |= 1 << b
                    node = node.children[opp]
                else:
                    node = node.children[bit]
            return out

        def dfs(u: int) -> None:
            insert(u)
            for q_idx, val in node_queries[u]:
                result[q_idx] = query_max(val)
            for v in children[u]:
                dfs(v)
            remove(u)

        dfs(root_node)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


41.5 Maximum Strong Pair XOR II (LC 2935)

Đề bài

Cặp (x, y) “strong” ↔︎ |x - y| <= min(x, y). Cho nums. Tìm max XOR trong các cặp strong.

Ví dụ

Input:  nums = [1, 2, 3, 4, 5]
Output: 7   (cặp "strong" là (x, y) với |x - y| ≤ min(x, y); max XOR = 3 XOR 4 = 7)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Quan sát: |x - y| ≤ min(x, y) ↔︎ max(x, y) ≤ 2 · min(x, y) (giả sử both ≥ 0). Sort nums, sliding window: với mỗi r, mở rộng l đến khi nums[r] > 2 · nums[l].

Trong window [l, r], max XOR = query Trie. Sliding Trie: khi l tiến, remove nums[l]. Khi r mới, insert nums[r].

Code Python 3

from typing import List

class _TrieRem:
    __slots__ = ("children", "count")
    def __init__(self):
        self.children = [None, None]
        self.count = 0


class Solution:
    BITS = 20  # nums ≤ 2·10^5 < 2^20

    def maximumStrongPairXor(self, nums: List[int]) -> int:
        nums.sort()
        root = _TrieRem()

        def insert(x: int) -> None:
            node = root
            for b in range(self.BITS, -1, -1):
                bit = (x >> b) & 1
                if not node.children[bit]:
                    node.children[bit] = _TrieRem()
                node = node.children[bit]
                node.count += 1

        def remove(x: int) -> None:
            node = root
            for b in range(self.BITS, -1, -1):
                bit = (x >> b) & 1
                node = node.children[bit]
                node.count -= 1

        def query_max(x: int) -> int:
            node = root
            out = 0
            for b in range(self.BITS, -1, -1):
                bit = (x >> b) & 1
                opp = 1 - bit
                if node.children[opp] and node.children[opp].count > 0:
                    out |= 1 << b
                    node = node.children[opp]
                else:
                    node = node.children[bit]
            return out

        l = 0
        best = 0
        for r in range(len(nums)):
            insert(nums[r])
            while nums[r] > 2 * nums[l]:
                remove(nums[l])
                l += 1
            best = max(best, query_max(nums[r]))
        return best

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


41.6 Maximum XOR After Operations (LC 2317)

Đề bài

Cho nums. Op: chọn nums[i] và bất kỳ x ≥ 0, đặt nums[i] = nums[i] AND (nums[i] XOR x). Tìm max XOR all elements sau ops bất kỳ.

Ví dụ

Input:  nums = [3, 2, 4, 6]
Output: 7   (mỗi phép: chọn i, x; thay nums[i] = nums[i] AND (nums[i] XOR x);
             max XOR toàn mảng đạt được = OR mọi nums[i] = 3|2|4|6 = 7)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Insight: Op cho phép tắt bit bất kỳ của nums[i] (giữ bit có sẵn). Vì XOR all = OR all khi mỗi bit có thể “toggle tự do” từ ít nhất 1 số → max = OR all.

Code Python 3

from typing import List

class Solution:
    def maximumXOR(self, nums: List[int]) -> int:
        result = 0
        for x in nums:
            result |= x
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Tên chương — “Binary Trie for XOR”

Tên đầy đủ hơn: Binary Trie để tối ưu XOR/operation theo từng bit. “Bitmask kết hợp Trie” trong index folder chỉ là viết tắt.

Count Pairs With XOR in Range (LC 1803) — bit-level

Genetic Difference (LC 1938) — node value = node index

LC này dùng parents[i] để build cây; value của node i đơn giản là i. Trie chứa giá trị i dọc đường từ root → query.

Strong Pair (LC 2941) — derivation


Chương 42 — Tree Dynamic Programming

Tree DP = DFS bottom-up trên cây. Mỗi node tính giá trị từ con. Pattern đặc biệt: re-rooting — tính kết quả cho từng node làm root (giảm O(n²) xuống O(n)).

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Template code

# 1) Tree DP bottom-up
def dfs(node, parent):
    state = base
    for child in graph[node]:
        if child == parent: continue
        sub = dfs(child, node)
        state = combine(state, sub)
    return state

# 2) Re-rooting (xem 42.5 cho implementation đầy đủ)
def re_root(graph, n):
    # 1st DFS: compute "down" values (subtree rooted at i).
    # 2nd DFS: propagate "up" values from parent to children.
    pass

Bài tự luyện cuối chương


42.1 House Robber III (LC 337) — recap

Đã giải đầy đủ ở Chương 11.5. Tree DP với 2 state mỗi node: rob_this, skip_this.

Liên hệ với chương này

Đây là gateway cho Tree DP — pattern “trả tuple 2 giá trị từ con”. Toàn chương 42 mở rộng pattern này: Binary Tree Cameras (3 state), Diameter (track best across subtrees), Sum of Distances (re-rooting).

Code Python 3 (recap)

from typing import Optional, Tuple

class Solution:
    def rob(self, root) -> int:
        def dfs(node) -> Tuple[int, int]:
            """Trả (rob_this, skip_this)."""
            if not node:
                return 0, 0
            l_rob, l_skip = dfs(node.left)
            r_rob, r_skip = dfs(node.right)
            rob_this = node.val + l_skip + r_skip
            skip_this = max(l_rob, l_skip) + max(r_rob, r_skip)
            return rob_this, skip_this
        return max(dfs(root))

Phân tích độ phức tạp

Bài tự luyện liên quan


42.2 Binary Tree Cameras (LC 968)

Đề bài

Đặt camera trên cây sao cho mọi node “phủ” (camera tại node hoặc kề camera). Min số camera.

Ví dụ

Input:  root = [0, 0, null, 0, 0]
        (LC level-order serialize; 'null' = node không tồn tại;
         giá trị node ở đây không liên quan, chỉ cấu trúc cây.)
Output: 1   (số camera tối thiểu phủ tất cả node)

Ràng buộc

Clarifying questions

Hướng tiếp cận

3 state mỗi node: - 0: chưa được phủ. - 1: được phủ (không có camera tại đây nhưng con có camera). - 2: có camera tại đây.

Greedy bottom-up: nếu child là 0 → đặt camera ở node hiện tại.

Code Python 3

class Solution:
    def minCameraCover(self, root) -> int:
        self.cnt = 0

        def dfs(node) -> int:
            if not node: return 1   # null coi như "covered"
            l = dfs(node.left)
            r = dfs(node.right)
            if l == 0 or r == 0:
                self.cnt += 1
                return 2
            if l == 2 or r == 2:
                return 1
            return 0

        if dfs(root) == 0:
            self.cnt += 1
        return self.cnt

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


42.3 Diameter of Binary Tree (LC 543)

Đề bài

Đường đi dài nhất giữa 2 node bất kỳ trong cây (cạnh đếm).

Ví dụ

Input:  root = [1, 2, 3, 4, 5]   (LC level-order, đọc trái → phải, tầng trên → tầng dưới)
Output: 3   (đường đi 4 → 2 → 1 → 3 hoặc 5 → 2 → 1 → 3 có 3 cạnh)

Ràng buộc

Clarifying questions

Hướng tiếp cận

DFS bottom-up. Mỗi node trả về depth từ nó xuống. diameter qua node = left_depth + right_depth.

Code Python 3

class Solution:
    def diameterOfBinaryTree(self, root) -> int:
        self.best = 0

        def depth(node) -> int:
            if not node: return 0
            l = depth(node.left)
            r = depth(node.right)
            self.best = max(self.best, l + r)
            return 1 + max(l, r)

        depth(root)
        return self.best

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


42.4 Longest Path With Different Adjacent Characters (LC 2246)

Đề bài

Input: parent: List[int] (parent array, parent[0] = -1 là root) và s: str — node i có ký tự s[i]. Tree gồm n node đánh số 0..n-1. Tìm longest path (đếm node) mà các node kề trên path có char khác nhau.

Ví dụ

Input:  parent=[-1,0,0,1,1,2], s="abacbe"
Output: 3

Ràng buộc

Clarifying questions

Hướng tiếp cận

Tree DP. Mỗi node u: longest chain xuống = max(1, 1 + max chain of children with different char). Update best với top 2 chains.

Code Python 3

from collections import defaultdict
from typing import List

class Solution:
    def longestPath(self, parent: List[int], s: str) -> int:
        n = len(parent)
        children = defaultdict(list)
        for i in range(1, n):
            children[parent[i]].append(i)
        self.best = 1

        def dfs(u: int) -> int:
            chains = [0]
            for v in children[u]:
                sub = dfs(v)
                if s[v] != s[u]:
                    chains.append(sub)
            chains.sort(reverse=True)
            top1 = chains[0] if chains else 0
            top2 = chains[1] if len(chains) > 1 else 0
            self.best = max(self.best, top1 + top2 + 1)
            return top1 + 1

        dfs(0)
        return self.best

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


42.5 Sum of Distances in Tree (LC 834) — Re-rooting

Đề bài

Input: n (số node) và edges: List[List[int]] — danh sách n-1 cạnh [u, v] của tree vô hướng. Trả về result: List[int] trong đó result[i] = tổng distance từ node i đến mọi node khác.

Ví dụ

Input:  n=6, edges=[[0,1],[0,2],[2,3],[2,4],[2,5]]
Output: [8,12,6,10,10,10]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Re-rooting kinh điển.

  1. DFS thứ 1: tính count[u] = số node trong subtree u, result[0] = tổng distance từ root 0.
  2. DFS thứ 2: cập nhật result[v] từ result[u] (parent của v): result[v] = result[u] - count[v] + (n - count[v]).

Code Python 3

from collections import defaultdict
from typing import List

class Solution:
    def sumOfDistancesInTree(self, n: int, edges: List[List[int]]) -> List[int]:
        graph = defaultdict(list)
        for u, v in edges:
            graph[u].append(v)
            graph[v].append(u)

        count = [1] * n
        answer = [0] * n

        def dfs1(u: int, parent: int) -> None:
            for v in graph[u]:
                if v != parent:
                    dfs1(v, u)
                    count[u] += count[v]
                    answer[u] += answer[v] + count[v]

        def dfs2(u: int, parent: int) -> None:
            for v in graph[u]:
                if v != parent:
                    answer[v] = answer[u] - count[v] + (n - count[v])
                    dfs2(v, u)

        dfs1(0, -1)
        dfs2(0, -1)
        return answer

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


42.6 Minimum Edge Reversals So Every Node Is Reachable (LC 2858)

Đề bài

Input: nedges: List[List[int]] — mỗi cạnh [u, v]có hướng u → v. Graph khi bỏ hướng là 1 tree (n-1 cạnh, vô hướng liên thông). Với mỗi node, đếm số cạnh phải đảo chiều để node đó reach mọi node khác.

Ví dụ

Input:  n=4, edges=[[2,0],[2,1],[1,3]]
Output: [1,1,0,2]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Re-rooting với “cost flip”.

  1. DFS thứ 1 từ node 0: cho mỗi cạnh, nếu hướng ngược với DFS → cost += 1. result[0] = tổng cost.
  2. DFS thứ 2: với mỗi cạnh u → v trong tree (undirected), kiểm tra direction gốc: nếu original là u → vresult[v] = result[u] + 1; nếu v → uresult[v] = result[u] - 1.

Code Python 3

from collections import defaultdict
from typing import List

class Solution:
    def minEdgeReversals(self, n: int, edges: List[List[int]]) -> List[int]:
        # graph[u] = list of (v, cost) where cost = 0 if directed u→v, 1 nếu phải đảo.
        graph = defaultdict(list)
        for u, v in edges:
            graph[u].append((v, 0))    # u → v: đi xuôi
            graph[v].append((u, 1))    # v → u: nếu đi từ v ra u thì phải đảo

        result = [0] * n
        def dfs1(u: int, parent: int) -> int:
            cost = 0
            for v, c in graph[u]:
                if v != parent:
                    cost += c + dfs1(v, u)
            return cost
        result[0] = dfs1(0, -1)

        def dfs2(u: int, parent: int) -> None:
            for v, c in graph[u]:
                if v != parent:
                    # Khi root từ u → v: cạnh u-v có cost c bị flip.
                    # Cost ban đầu là c → sau khi root v: 1 - c.
                    result[v] = result[u] + (1 - 2 * c)
                    dfs2(v, u)
        dfs2(0, -1)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Rerooting generic template

Pass 1 (post-order): tính down[v] = đáp số subtree của v khi root tại v. Pass 2 (pre-order từ root): chuyển root từ parent p sang child c:

ans[c] = ans[p] - contribution_of_c_to_p_subtree + contribution_of_rest_to_c_subtree

Cụ thể với “sum of distances”:

ans[c] = ans[p] - size[c] + (n - size[c])

Binary Tree Cameras (LC 968) — state 0/1/2

Sum of Distances (LC 834) — trace

Tree:

   0
  /|\
 1 2 3
 |
 4

Minimum Edge Reversals (LC 2858)


Chương 43 — Topological Sort kết hợp Dynamic Programming

DP trên DAG = topo sort + DP linear theo thứ tự topo. State của node phụ thuộc các node “trước” nó trong topo order.

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Bài tự luyện cuối chương


43.1 Longest Increasing Path in a Matrix (LC 329)

Đề bài

Ma trận. Tìm longest path tăng dần (4 hướng).

Ví dụ

Input:  matrix = [[9, 9, 4],
                  [6, 6, 8],
                  [2, 1, 1]]   (grid m × n, đi 4 hướng, value phải TĂNG nghiêm ngặt)
Output: 4   (path tăng dài nhất: 1 → 2 → 6 → 9)

Ràng buộc

Clarifying questions

Hướng tiếp cận

DAG ngầm: mỗi ô là 1 node, cạnh u → v nếu mat[v] > mat[u]. DFS với memo.

Code Python 3

from functools import cache
from typing import List

class Solution:
    def longestIncreasingPath(self, matrix: List[List[int]]) -> int:
        rows, cols = len(matrix), len(matrix[0])
        DIRS = [(-1,0),(1,0),(0,-1),(0,1)]

        @cache
        def dfs(r: int, c: int) -> int:
            best = 1
            for dr, dc in DIRS:
                nr, nc = r + dr, c + dc
                if 0 <= nr < rows and 0 <= nc < cols and matrix[nr][nc] > matrix[r][c]:
                    best = max(best, 1 + dfs(nr, nc))
            return best

        return max(dfs(r, c) for r in range(rows) for c in range(cols))

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


43.2 Parallel Courses III (LC 2050)

Đề bài

Input: n (số khoá, đánh số 1..n), relations: List[List[int]] — mỗi [a, b] nghĩa phải xong khoá a trước b (cạnh DAG a → b), và time: List[int] với time[i] = thời gian học khoá i+1. Có thể học đồng thời nếu prerequisite đã xong. Min thời gian tổng.

Ví dụ

Input:  n=3, relations=[[1,3],[2,3]], time=[3,2,5]
Output: 8

Ràng buộc

Clarifying questions

Hướng tiếp cận

Topo sort + DP. dp[u] = time[u] + max(dp[v] for v là prerequisite).

Code Python 3

from collections import defaultdict, deque
from typing import List

class Solution:
    def minimumTime(self, n: int, relations: List[List[int]], time: List[int]) -> int:
        graph = defaultdict(list)
        indeg = [0] * (n + 1)
        for u, v in relations:
            graph[u].append(v)
            indeg[v] += 1

        dp = [0] * (n + 1)
        queue = deque()
        for i in range(1, n + 1):
            if indeg[i] == 0:
                dp[i] = time[i - 1]
                queue.append(i)
        while queue:
            u = queue.popleft()
            for v in graph[u]:
                dp[v] = max(dp[v], dp[u] + time[v - 1])
                indeg[v] -= 1
                if indeg[v] == 0:
                    queue.append(v)
        return max(dp)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


43.3 Largest Color Value in a Directed Graph (LC 1857)

Đề bài

Input: colors: str (colors[i] = ký tự màu của node i, 'a'..'z') và edges: List[List[int]] — cạnh có hướng [u, v]. Tìm path mà max số node cùng 1 color trên path là lớn nhất.

Ví dụ

Input:  colors="abaca", edges=[[0,1],[0,2],[2,3],[3,4]]
Output: 3

Ràng buộc

Clarifying questions

Hướng tiếp cận

Topo + DP dp[u][c] = số node màu c lớn nhất đi qua u (kết thúc tại u).

dp[v][c] = max(dp[v][c], dp[u][c] + (1 if color[v] == c else 0)).

Code Python 3

from collections import defaultdict, deque
from typing import List

class Solution:
    def largestPathValue(self, colors: str, edges: List[List[int]]) -> int:
        n = len(colors)
        graph = defaultdict(list)
        indeg = [0] * n
        for u, v in edges:
            graph[u].append(v)
            indeg[v] += 1
        dp = [[0] * 26 for _ in range(n)]
        queue = deque()
        for i in range(n):
            if indeg[i] == 0:
                queue.append(i)
                dp[i][ord(colors[i]) - 97] = 1
        visited = 0
        best = 0
        while queue:
            u = queue.popleft()
            visited += 1
            best = max(best, max(dp[u]))
            for v in graph[u]:
                for c in range(26):
                    add = 1 if (ord(colors[v]) - 97) == c else 0
                    if dp[u][c] + add > dp[v][c]:
                        dp[v][c] = dp[u][c] + add
                indeg[v] -= 1
                if indeg[v] == 0:
                    queue.append(v)
        return best if visited == n else -1

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


43.4 Build a Matrix With Conditions (LC 2392)

Đề bài

Cho k (size) và row/col conditions (a phải đứng trên/trước b). Trả về ma trận k×k với mỗi số 1..k xuất hiện đúng 1 lần thoả conditions.

Ví dụ

Input:  k=3, rowConditions=[[1,2],[3,2]], colConditions=[[2,1],[3,2]]
Output: [[3,0,0],[0,0,1],[0,2,0]]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Topo sort 2 lần: row conditions → row position, col conditions → col position. Đặt mỗi số vào ô (row_pos, col_pos).

Code Python 3

from collections import defaultdict, deque
from typing import List

class Solution:
    def buildMatrix(self, k: int, rowConditions: List[List[int]], colConditions: List[List[int]]) -> List[List[int]]:
        def topo(conditions):
            graph = defaultdict(list)
            indeg = [0] * (k + 1)
            for u, v in conditions:
                graph[u].append(v)
                indeg[v] += 1
            queue = deque(i for i in range(1, k + 1) if indeg[i] == 0)
            order = []
            while queue:
                u = queue.popleft()
                order.append(u)
                for v in graph[u]:
                    indeg[v] -= 1
                    if indeg[v] == 0:
                        queue.append(v)
            return order if len(order) == k else []

        rows = topo(rowConditions)
        cols = topo(colConditions)
        if not rows or not cols: return []
        row_pos = {x: i for i, x in enumerate(rows)}
        col_pos = {x: i for i, x in enumerate(cols)}
        result = [[0] * k for _ in range(k)]
        for x in range(1, k + 1):
            result[row_pos[x]][col_pos[x]] = x
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


43.5 Course Schedule IV (LC 1462)

Đề bài

Cho numCoursesprerequisites[i] = [a, b] với ý nghĩa “phải hoàn thành môn a trước môn b (tức a là prerequisite của b, cạnh DAG là a → b). Cho mảng queries[i] = [u, v]; với mỗi query, trả về True nếu u là prerequisite (trực tiếp hoặc gián tiếp) của v, tức tồn tại đường đi u → ... → v trong DAG.

Quy ước hướng cạnh (xem Chương 13): “u trước v” ⇔ cạnh u → v. Đừng đảo dấu mũi tên — đây là bug điển hình ở Course Schedule family.

Ví dụ

Input:  numCourses = 3, prerequisites = [[1,2],[1,0],[2,0]], queries = [[1,0],[1,2]]
Output: [True, True]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Brute force: với mỗi query, BFS/DFS từ u → check v. O(Q · (V + E)).

Tối ưu — Topo + DP closure: reach[u] = set các đỉnh vu → ... → v. Duyệt theo topo order ngược, gán reach[u] = {u} ∪ union(reach[v] for v in adj[u]).

Sau preprocessing, mỗi query O(1).

Code Python 3

from collections import defaultdict, deque
from typing import List

class Solution:
    def checkIfPrerequisite(self, numCourses: int,
                            prerequisites: List[List[int]],
                            queries: List[List[int]]) -> List[bool]:
        graph = defaultdict(list)
        indeg = [0] * numCourses
        for a, b in prerequisites:
            graph[a].append(b)
            indeg[b] += 1

        # Topo order.
        queue = deque(i for i in range(numCourses) if indeg[i] == 0)
        topo = []
        while queue:
            u = queue.popleft()
            topo.append(u)
            for v in graph[u]:
                indeg[v] -= 1
                if indeg[v] == 0:
                    queue.append(v)

        # reach[u] = set các đỉnh u có thể đến.
        reach = [set() for _ in range(numCourses)]
        # Process theo topo order ngược: con đã có reach trước cha.
        for u in reversed(topo):
            for v in graph[u]:
                reach[u].add(v)
                reach[u] |= reach[v]

        return [v in reach[u] for u, v in queries]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


43.6 Find All Possible Recipes from Given Supplies (LC 2115)

Đề bài

Cho recipes (mỗi cái cần ingredients), supplies (ingredients sẵn có). Trả về recipes có thể làm.

Ví dụ

Input:  recipes=["bread"], ingredients=[["yeast","flour"]], supplies=["yeast","flour","corn"]
Output: ["bread"]

Ràng buộc

Clarifying questions

Hướng tiếp cận

Topo sort: cạnh ingredient → recipe needs ingredient. BFS từ supplies, mỗi khi tất cả ingredients của 1 recipe đã có → recipe đó “có sẵn”.

Code Python 3

from collections import defaultdict, deque
from typing import List

class Solution:
    def findAllRecipes(self, recipes: List[str], ingredients: List[List[str]], supplies: List[str]) -> List[str]:
        graph = defaultdict(list)
        indeg = {}
        for recipe, ings in zip(recipes, ingredients):
            indeg[recipe] = len(ings)
            for ing in ings:
                graph[ing].append(recipe)

        queue = deque(supplies)
        available = set(supplies)
        result = []
        recipe_set = set(recipes)
        while queue:
            ing = queue.popleft()
            for r in graph[ing]:
                indeg[r] -= 1
                if indeg[r] == 0:
                    result.append(r)
                    available.add(r)
                    queue.append(r)
        return result

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Topo + DP taxonomy

Loại Bài tiêu biểu
Longest path on DAG LC 329 Longest Increasing Path
Counting paths LC 1976 Number of Ways to Arrive
Reachability closure Transitive closure / “có thể đạt từ A đến B”
Dependency scheduling LC 1136 Parallel Courses, LC 2050
Aggregation along DAG LC 1857 Largest Color Value

Largest Color Value (LC 1857)

LC 1462 — Course Schedule IV

Recipes (LC 2115)

Direction consistency (cross-reference Chương 13)

LC 1462: prerequisites[i] = [u, v] nghĩa “phải học u trước v” ⇒ cạnh u → v (giống Chương 13). Đừng đảo!


Chương 44 — Combinatorics kết hợp Dynamic Programming

Chương cuối — DP đếm tổ hợp. Pattern: state là cấu trúc đếm + transition qua phép cộng. Mod 10^9 + 7 xuất hiện ở mọi bài (vì kết quả lớn).

Mục tiêu chương

Sau chương này, bạn sẽ:

Khi nào dùng pattern này?

Template code

MOD = 10**9 + 7

def dp_count(states):
    table = [0] * len(states)
    table[0] = base
    for i in range(1, len(states)):
        table[i] = (sum(table[j] for j in transitions(i)) % MOD)
    return table[-1]

Bài tự luyện cuối chương


44.1 Unique Paths (LC 62)

Đề bài

Lưới m × n. Robot ở (0,0) đi tới (m-1,n-1), chỉ đi phải hoặc xuống. Đếm số path.

Ví dụ

Input:  m=3, n=7
Output: 28

Ràng buộc

Clarifying questions

Hướng tiếp cận

DP dp[i][j] = dp[i-1][j] + dp[i][j-1]. Hoặc combinatorics: C(m+n-2, m-1).

Code Python 3

from math import comb

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        return comb(m + n - 2, m - 1)

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


44.2 Unique Paths II (LC 63)

Đề bài

Như 44.1, có vật cản. 1 = block.

Ví dụ

Input:  obstacleGrid = [[0, 0, 0],
                        [0, 1, 0],
                        [0, 0, 0]]   (0 = ô trống, 1 = vật cản)
Output: 2   (số đường đi từ (0,0) tới (m-1, n-1), chỉ đi xuống/sang phải)

Ràng buộc

Clarifying questions

Hướng tiếp cận

DP. Nếu ô block → dp[i][j] = 0.

Code Python 3

from typing import List

class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
        m, n = len(obstacleGrid), len(obstacleGrid[0])
        if obstacleGrid[0][0] == 1: return 0
        dp = [[0] * n for _ in range(m)]
        dp[0][0] = 1
        for i in range(m):
            for j in range(n):
                if obstacleGrid[i][j] == 1:
                    dp[i][j] = 0
                    continue
                if i > 0: dp[i][j] += dp[i - 1][j]
                if j > 0: dp[i][j] += dp[i][j - 1]
        return dp[-1][-1]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


44.3 Knight Dialer (LC 935)

Đề bài

Trên bàn phím số (3×4), knight chess đi n - 1 bước, mỗi bước đi 1 nước knight. Đếm số chuỗi tạo được. Mod 10^9 + 7.

Ví dụ

Input:  n=2
Output: 20

Ràng buộc

Clarifying questions

Hướng tiếp cận

Neighbor map cho bàn phím. DP dp[i][digit] = số chuỗi length i kết thúc tại digit.

Code Python 3

class Solution:
    MOD = 10**9 + 7
    NEIGHBORS = {
        0: [4, 6], 1: [6, 8], 2: [7, 9], 3: [4, 8], 4: [0, 3, 9],
        5: [], 6: [0, 1, 7], 7: [2, 6], 8: [1, 3], 9: [2, 4]
    }

    def knightDialer(self, n: int) -> int:
        dp = [1] * 10
        for _ in range(n - 1):
            new_dp = [0] * 10
            for d in range(10):
                for nb in self.NEIGHBORS[d]:
                    new_dp[nb] = (new_dp[nb] + dp[d]) % self.MOD
            dp = new_dp
        return sum(dp) % self.MOD

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


44.4 Count Vowels Permutation (LC 1220)

Đề bài

Đếm số chuỗi nguyên âm độ dài n với rules: - ‘a’ chỉ sau ‘e’. - ‘e’ chỉ sau ‘a’ hoặc ‘i’. - ‘i’ không sau ‘a’ hoặc ‘i’. - ‘o’ chỉ sau ‘i’ hoặc ‘u’. - ‘u’ chỉ sau ‘i’.

Ví dụ

Input:  n=1
Output: 5

Ràng buộc

Clarifying questions

Hướng tiếp cận

DP dp[i][v] = số chuỗi length i kết thúc tại nguyên âm v.

Code Python 3

class Solution:
    MOD = 10**9 + 7

    def countVowelPermutation(self, n: int) -> int:
        # Index: 0=a, 1=e, 2=i, 3=o, 4=u
        # Transitions backwards: ai có thể đứng trước cái này.
        prev_can = {
            0: [1, 2, 4],     # a sau e, i, u
            1: [0, 2],         # e sau a, i
            2: [1, 3],         # i sau e, o
            3: [2],            # o sau i
            4: [2, 3],         # u sau i, o
        }
        dp = [1] * 5
        for _ in range(n - 1):
            new_dp = [0] * 5
            for v in range(5):
                for prev in prev_can[v]:
                    new_dp[v] = (new_dp[v] + dp[prev]) % self.MOD
            dp = new_dp
        return sum(dp) % self.MOD

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


44.5 Number of Ways to Wear Different Hats to Each Other (LC 1434)

Đề bài

40 nón, n người (n ≤ 10), mỗi người có list nón mình thích. Đếm số cách phân nón sao cho mỗi người 1 nón khác nhau.

Ví dụ

Input:  hats = [[3, 4], [4, 5], [5]]
        (hats[i] = danh sách mũ mà người i thích; nhãn mũ ∈ [1, 40])
Output: 1   (số cách gán cho mỗi người đúng 1 mũ, các mũ phải khác nhau; mod 10^9 + 7)

Ràng buộc

Clarifying questions

Hướng tiếp cận

Bitmask DP trên người, iterate qua nón (vì người ≤ 10, nón ≤ 40):

dp[hat][mask] = số cách dùng nón <= hat để cover các người trong mask.

Code Python 3

from collections import defaultdict
from typing import List

class Solution:
    MOD = 10**9 + 7

    def numberWays(self, hats: List[List[int]]) -> int:
        n = len(hats)
        hat_to_people = defaultdict(list)
        for p, hs in enumerate(hats):
            for h in hs:
                hat_to_people[h].append(p)

        full = (1 << n) - 1
        dp = [0] * (1 << n)
        dp[0] = 1
        for h in range(1, 41):
            new_dp = dp[:]
            for mask in range(1 << n):
                for p in hat_to_people[h]:
                    if mask & (1 << p): continue
                    new_dp[mask | (1 << p)] = (new_dp[mask | (1 << p)] + dp[mask]) % self.MOD
            dp = new_dp
        return dp[full]

Phân tích độ phức tạp

Bình luận

Bài tự luyện liên quan


44.6 Number of Music Playlists (LC 920)

Đề bài

n bài hát, listen goal bài. Mỗi bài nghe ít nhất 1 lần. Khoảng cách giữa 2 lần nghe cùng bài ≥ k. Đếm số playlist.

Ví dụ

Input:  n=3, goal=3, k=1
Output: 6

Ràng buộc

Clarifying questions

Hướng tiếp cận

DP dp[i][j] = số playlist length i dùng j bài phân biệt.

Code Python 3

class Solution:
    MOD = 10**9 + 7

    def numMusicPlaylists(self, n: int, goal: int, k: int) -> int:
        dp = [[0] * (n + 1) for _ in range(goal + 1)]
        dp[0][0] = 1
        for i in range(1, goal + 1):
            for j in range(1, n + 1):
                dp[i][j] = dp[i - 1][j - 1] * (n - j + 1) % self.MOD
                if j > k:
                    dp[i][j] = (dp[i][j] + dp[i - 1][j] * (j - k)) % self.MOD
        return dp[goal][n]

Phân tích độ phức tạp

Bình luận


🎉 Hết phần Level 3!

Bạn vừa hoàn thành cuốn sách 288 câu hỏi coding DSA interview từ Easy đến Hard, cover 44 pattern. Chúc bạn pass mọi vòng phỏng vấn Big Tech!

“The best way to learn is to teach.” — Hãy thử giảng lại 1 chương cho bạn bè. Nếu bạn explain được dễ hiểu, bạn đã thuộc bài.

Bài tự luyện liên quan

Tóm tắt chương & Quyết định

Counting DP framework

Unique Paths (LC 62) — 2 cách

Cách Time Space Khi nào
DP grid dp[i][j] = dp[i-1][j] + dp[i][j-1] O(mn) O(mn) hoặc O(min(m,n)) Khi có obstacle (LC 63) hoặc đề mở rộng
Công thức C(m+n-2, m-1) O(min(m,n)) O(1) Khi không obstacle

Number of Music Playlists (LC 920) — state

Find All Good Strings (LC 1397) — digit DP + KMP

Count Vowels Permutation (LC 1220) — transition graph

a → e
e → a, i
i → a, e, o, u
o → i, u
u → a

DP dp[i][v] = số chuỗi độ dài i kết thúc bằng nguyên âm v.


Phụ lục

Phụ lục A — Bảng pattern → loại bài

Sử dụng bảng này để tra ngược: “đề bài giống thế này thì chọn pattern gì?”

Dấu hiệu đề bài Pattern (Chương)
“Find pair / triplet with sum” Two Pointers (26), Hash (6)
“Longest / shortest substring with condition” Sliding Window (27)
“Find element in sorted array” Binary Search (5)
“Min/max X such that …” Search on Answer (25)
“Number of subarray …” Prefix Sum + Hash (19, 6)
“Connected components / cycle in undirected” Union Find (24), DFS/BFS
“Shortest path unweighted” BFS (10)
“Shortest path weighted ≥ 0” Dijkstra (30)
“Shortest path with negative” Bellman-Ford
“Topological order / cycle in directed” Topo Sort (13)
“Min cost to connect all” MST (33)
“Next greater / smaller element” Monotonic Stack (18, 8.5)
“Sliding window max/min” Monotonic Deque (18.4)
“Substring matching” KMP (35), Rolling Hash (34), Z (36)
“Prefix matching, dictionary lookup” Trie (23)
“Max XOR pair” Trie binary (41)
“All permutations / subsets / combinations” Backtracking (3, 28)
“Min steps / number of ways” DP (29), Combinatorics DP (44)
“Game with 2 players optimal” Game Theory DP (31)
“Path in tree / depth/diameter/LCA” Tree DFS (11), Tree DP (42)
“Range sum / update” Prefix Sum (19), Fenwick/Segment (22)
“Median / kth largest stream” Heap (15)
“n ≤ 20 with subset” Bitmask DP (40)

Bảng phân biệt pattern dễ nhầm

BFS vs DFS vs Union Find (cho connectivity / components):

Pattern Khi dùng Time Space
BFS Shortest path unweighted, level-order O(V + E) O(V)
DFS Connectivity, cycle detection, topo sort, đường đi bất kỳ O(V + E) O(V) recursion stack
Union Find Online add edge, query “connected?”, offline edge sorting O((V + E) · α) O(V)

Binary Search thường vs Search on Answer:

Pattern Search trên gì Predicate
Binary Search thường (Ch 5) Index trong mảng đã sort nums[mid] vs target
Search on Answer (Ch 25) Giá trị đáp án check(mid) đơn điệu T/F

Sliding Window vs Two Pointers:

Pattern Khi dùng Window
Two Pointers 2-đầu (Ch 26) Sort + tìm cặp (sum, distance) Hội tụ từ 2 phía
Sliding Window same-direction (Ch 27) Longest/shortest subarray với điều kiện monotonic Mở rộng r, co l

KMP vs Z vs Rolling Hash:

Pattern Preprocess Khi tốt Khi xấu
KMP (Ch 35) LPS array O(m) Deterministic, in-place LPS construction khó hiểu
Z (Ch 36) Z array O(n) Dễ hình dung hơn LPS Tương đương KMP perf
Rolling Hash (Ch 34) Powers + prefix hash Multiple pattern, distinct count Collision risk

Dijkstra vs BFS+BS vs DSU offline (cho min/max path):

Pattern Khi dùng Complexity
Dijkstra (Ch 30) Weighted ≥ 0, online query O((V+E) log V)
BS + BFS (Ch 37) Predicate monotonic trên threshold O(log · (V+E))
DSU offline (Ch 24, 33) Process query theo thứ tự + sort cạnh O((V+E) · α)

Recursion vs Backtracking vs DFS vs Top-down DP:

Pattern Đặc trưng
Recursion (Ch 3) Đệ quy thuần, không track choice/state
Backtracking (Ch 28) choose → explore → unchoose, liệt kê tất cả
DFS (Ch 11, 12) Duyệt graph/tree, mark visited
Top-down DP (Ch 29) Recursion + @cache, overlapping subproblems

Phụ lục B — Python 3 cookbook (35 snippet hay dùng)

# 1. Counter
from collections import Counter
Counter("anagram").most_common(2)        # [('a',3), ('n',1)]

# 2. defaultdict
from collections import defaultdict
groups = defaultdict(list)
groups[key].append(val)

# 3. deque
from collections import deque
dq = deque([1,2,3]); dq.appendleft(0); dq.pop()

# 4. heapq (min-heap; max-heap = negate)
import heapq
h = []; heapq.heappush(h, (priority, value))
heapq.nsmallest(3, lst, key=lambda x: x.score)
# Max-heap idiom:
heapq.heappush(h, -value)
neg = heapq.heappop(h); val = -neg

# 5. bisect (binary search)
from bisect import bisect_left, insort
i = bisect_left(sorted_lst, x)
insort(sorted_lst, x)        # insert maintain sorted

# 6. functools.cache (memoization)
from functools import cache
@cache
def dp(state): ...
# Cẩn thận: state phải hashable. List/dict → tuple/frozenset.

# 7. itertools
from itertools import (
    combinations, permutations, product, accumulate, pairwise
)
list(combinations([1,2,3], 2))      # [(1,2),(1,3),(2,3)]
list(accumulate([1,2,3,4]))         # [1, 3, 6, 10]
list(pairwise([1,2,3,4]))           # [(1,2),(2,3),(3,4)]

# 8. cmp_to_key cho sort custom
from functools import cmp_to_key
arr.sort(key=cmp_to_key(lambda a, b: a-b))

# 9. math
from math import gcd, lcm, comb, factorial, inf, log2, ceil, floor
gcd(12, 18)         # 6
lcm(4, 6)           # 12 (Python 3.9+)
comb(5, 2)          # 10
log2(8)             # 3.0

# 10. SortedList (cần pip install sortedcontainers)
from sortedcontainers import SortedList
sl = SortedList()
sl.add(x); sl.remove(x)            # O(log n)
sl.bisect_left(x)

# 11. Increase recursion limit
import sys
sys.setrecursionlimit(10**6)

# 12. Bit operations
bin(12)             # '0b1100'
(12).bit_count()    # 2 (Python 3.10+)
(12 & -12)          # 4 (lowest set bit)
(12 & (12 - 1))     # 8 (clear lowest set bit)

# 13. String / character
chr(97); ord('a')   # 97; 'a'
'abc'.zfill(5)      # '00abc'
str(123).rjust(5)   # '  123'

# 14. List unpacking
a, *rest, b = [1, 2, 3, 4, 5]   # a=1, rest=[2,3,4], b=5

# 15. Walrus operator (Python 3.8+)
if (n := len(lst)) > 10:
    print(f"too long: {n}")

(Snippet 16-35 có thể tham khảo trong code mỗi chương.)


Phụ lục C — Template code 20 patterns

Các template code đã có sẵn ở đầu mỗi chương (mục “Template code”). Đây là index nhanh:

# Pattern Template ở chương
1 Two Pointers Ch. 26
2 Sliding Window Ch. 27
3 Binary Search Ch. 5
4 Search on Answer Ch. 25
5 Prefix Sum Ch. 19
6 Monotonic Stack/Deque Ch. 18
7 BFS Ch. 10
8 DFS Ch. 11
9 Backtracking Ch. 28
10 Union Find Ch. 24
11 Topological Sort Ch. 13
12 Dijkstra Ch. 30
13 MST (Kruskal+Prim) Ch. 33
14 Trie Ch. 23
15 Fenwick / Segment Ch. 22
16 LCS / LIS Ch. 29
17 Knapsack Ch. 29
18 Tree DP Ch. 42
19 Bitmask DP Ch. 40
20 KMP / Z Ch. 35, 36

Phụ lục D — 50 bài must-do trước phỏng vấn 1 tuần

Tinh giản, dedupe theo chương. Mỗi bài LC chỉ xuất hiện 1 lần.

Array & String (8): LC 1, 121, 238, 11, 53, 49, 76, 3.

Linked List (5): LC 206, 21, 141, 25, 146.

Tree (5): LC 104, 102, 98, 236, 124.

Graph & BFS/DFS (7): LC 200, 207, 269, 994, 127, 133, 743.

Heap & Sort (3): LC 215, 23, 56.

Hash & Sliding Window (4): LC 49 (đã ở Array, skip), LC 76 (đã ở Array, skip), LC 424, 560.

Binary Search (3): LC 33, 34, 875.

DP (8): LC 70, 198, 322, 300, 72, 152, 416, 309.

Backtracking (3): LC 46, 78, 22.

Bit / String parser (4): LC 136, 421, 224, 8.

Note: LC 121 (Stock I) ưu tiên hơn LC 207 (Course Schedule) cho ngày đầu — Stock dễ làm tâm lý nhẹ trước khi vào graph. LC 49, 76 xuất hiện trong cả Array và Sliding Window family — chỉ tính 1 lần ở Array (tier xuất hiện sớm hơn).


Phụ lục E — Behavioral interview kèm STAR framework

STAR framework

S | Situation | Mô tả bối cảnh (1-2 câu) |
T | Task | Vấn đề / yêu cầu của bạn |
A | Action | Bạn đã làm gì cụ thể (focus ở đây!) |
R | Result | Kết quả + bài học |

10 câu thường gặp

  1. “Tell me about a challenging bug you solved.”
  2. “Describe a conflict with a coworker.”
  3. “What’s your biggest weakness?”
  4. “Why do you want to work at [company]?”
  5. “Tell me about a project you’re proud of.”
  6. “Describe a time you missed deadline.”
  7. “How do you prioritize tasks?”
  8. “Tell me about a time you took initiative.”
  9. “Describe a time you received critical feedback.”
  10. “Why are you leaving your current job?”

Mẹo


Phụ lục F — System design coding hybrid

Một số vòng phỏng vấn yêu cầu cả design + implement. Ví dụ:

Bài Chương liên quan
LRU Cache (LC 146) Ch. 6, 7
LFU Cache (LC 460) Ch. 7
Snake Game (LC 353) Ch. 7 (deque)
Rate Limiter Ch. 14 (interval)
Twitter Feed (LC 355) Ch. 15 (heap)
Search Autocomplete (LC 642) Ch. 23 (Trie)
Tic-Tac-Toe (LC 348) Ch. 6 (hash)
File System (LC 588) Ch. 23
Stack with Pop Middle Ch. 22 (DLL)

Quy trình: làm rõ scope → thiết kế class/interface → cài đặt các method → test.


Phụ lục G — Tài liệu tham khảo & follow-up

Sách kinh điển

Online platforms

Vietnamese resources

Newsletters & blogs

YouTube channels (English)

Lời khuyên cuối


Phụ lục H — Glossary thuật ngữ Việt-Anh

Tiếng Anh Tiếng Việt Định nghĩa ngắn
Invariant Bất biến Thuộc tính luôn đúng tại mọi điểm trong vòng lặp / đệ quy
State Trạng thái Đại lượng đủ để mô tả bài con (DP, game theory)
Transition Bước chuyển dp[next] = f(dp[curr]) — quan hệ giữa state
Subproblem Bài con Bài nhỏ hơn dùng để xây bài lớn (DP, D&C)
Optimal substructure Cấu trúc con tối ưu Lời giải tối ưu được build từ lời giải con tối ưu
Overlapping subproblems Bài con trùng lặp Subproblem xuất hiện nhiều lần → cache được
Monotonic Đơn điệu Tăng / giảm theo 1 chiều (sort, stack, predicate BS)
Amortized Khấu hao Trung bình mỗi op O(1) dù worst-case có thể O(n)
Greedy Tham lam Mỗi bước chọn cái tốt nhất tại chỗ
Heuristic Heuristic Quy tắc giúp lựa chọn không bảo đảm tối ưu
Trie Trie (prefix tree) Cây mà mỗi node = 1 ký tự, đường đi = 1 prefix
Adjacency list Danh sách kề graph[u] = [v1, v2, ...]
In-place In-place Sửa trực tiếp input, không tạo cấu trúc phụ
Stable sort Sort ổn định Giữ thứ tự gốc của các phần tử bằng nhau (Python’s sorted là stable)
Sentinel Sentinel Phần tử biên ảo để tránh special-case (dummy head, [-1, n])
Pivot Pivot Phần tử chuẩn để partition (quicksort, quickselect)
Backtracking Backtracking Đệ quy + undo state khi quay lui
Memoization Memoization Cache kết quả subproblem (top-down DP)
Tabulation Tabulation Build DP table bottom-up
Bitmask Bitmask Encode subset bằng int (bit i = phần tử i có/không)
LIS LIS Longest Increasing Subsequence
LCS LCS Longest Common Subsequence
LPS LPS Longest Prefix Suffix (KMP)
MST MST Minimum Spanning Tree
DSU DSU Disjoint Set Union (Union Find)
DAG DAG Directed Acyclic Graph (đồ thị có hướng không chu trình)
BST BST Binary Search Tree
Big-O Big-O Notation độ phức tạp tiệm cận
α(n) Alpha(n) Hàm Ackermann ngược (≈ 4 với mọi n thực tế)

Phụ lục I — Index theo LC number

Tra theo số LeetCode → bài trong sách. Hỗ trợ tra ngược nhanh.

LC # Bài Chương Mục
1 Two Sum 1 1.1
2 Add Two Numbers 7 7.7
3 Longest Substring Without Repeating 27 27.1
4 Median of Two Sorted Arrays 25 25.5
8 String to Integer (atoi) 2 2.4
10 Regular Expression Matching 32 32.4
11 Container With Most Water 1, 26 1.5, 26.3
14 Longest Common Prefix 2 2.3
15 3Sum 26 26.1
17 Letter Combinations Phone 28 28.4
18 4Sum 26 26.6
19 Remove Nth From End 7 7.5
20 Valid Parentheses 8 8.1
21 Merge Two Sorted Lists 7 7.2
22 Generate Parentheses 3 3.4
23 Merge k Sorted Lists 15 15.5
25 Reverse Nodes in k-Group 7 7.9
28 strStr() 35, 36 35.1, 36.2
33 Search Rotated Sorted Array 5 5.5
34 Find First/Last Position 5 5.4
35 Search Insert Position 5 5.2
37 Sudoku Solver 28 28.8
38-… (xem chương tương ứng)
42 Trapping Rain Water 26 26.2
45 Jump Game II 16 16.2
46 Permutations 3 3.5
47 Permutations II 28 28.3
49 Group Anagrams 2 2.5
50 Pow(x, n) 3, 17 3.2, 17.4
51 N-Queens 28 28.7
53 Maximum Subarray 17 17.1
55 Jump Game 16 16.1
56 Merge Intervals 4, 14 4.2, 14.1
57 Insert Interval 14 14.2
62 Unique Paths 44 44.1
63 Unique Paths II 44 44.2
65 Valid Number 32 32.5
69 Sqrt(x) 5 5.6
72 Edit Distance 29 29.3
75 Sort Colors 4, 26 4.1, 26.4
76 Minimum Window Substring 27 27.2
77 Combinations 28 28.1
78 Subsets 3 3.6
79 Word Search 28 28.9
80 Remove Duplicates Sorted II 26 26.5
84 Largest Rectangle Histogram 18 18.3
90 Subsets II 28 28.2
93 Restore IP Addresses 28 28.11
98 Validate BST 11, 22 11.4, 22.1
99 Recover BST 22 22.2
102 Level Order Traversal 10 10.1
104 Max Depth Binary Tree 11 11.1
113 Path Sum II 11 11.2
121 Best Time to Buy/Sell 1 1.2
124 Max Path Sum 22 22.4
125 Valid Palindrome 2 2.2
127 Word Ladder 10 10.3
128 Longest Consecutive 6 6.2
131 Palindrome Partitioning 28 28.6
132 Palindrome Partitioning II 29 29.13
133 Clone Graph 9 9.2
134 Gas Station 16 16.3
135 Candy 16 16.6
136 Single Number 21 21.1
138 Copy List Random 7 7.8
140 Word Break II 28 28.10
141 Linked List Cycle 7 7.3
143 Reorder List 7 7.12
146 LRU Cache 6, 7 6.6, 7.11
148 Sort List 7 7.10
150 Eval RPN 8 8.4
151 Reverse Words 2 2.6
152 Max Product Subarray 29 29.12
155 Min Stack 8 8.2
179 Largest Number 4 4.3
187 Repeated DNA 34 34.1
188 Stock IV 29 29.10
189 Rotate Array 1 1.6
191 Number of 1 Bits 21 21.2
198 House Robber I (tự luyện Ch 29)
200 Number of Islands 12 12.1
201 Bitwise AND Range 21 21.5
204 Count Primes 20 20.1
205 Isomorphic Strings 6 6.5
206 Reverse Linked List 3, 7 3.3, 7.1
207 Course Schedule 9 9.4
208 Implement Trie 23 23.1
210 Course Schedule II 13 13.1
211 Add and Search Word 23 23.2
212 Word Search II 23 23.3
213 House Robber II 29 29.11
215 Kth Largest 15, 17 15.1, 17.3
217 Contains Duplicate 6 6.1
224 Basic Calculator (xem Ch 32)
227 Basic Calculator II 32 32.1
232 Implement Queue Stacks 8 8.3
234 Palindrome Linked List 7 7.6
236 LCA Binary Tree 11 11.6
238 Product Except Self 1, 19 1.3, 19.5
239 Sliding Window Maximum 18, 27 18.4, 27.6
241 Different Ways Parentheses 17 17.5
242 Valid Anagram 2 2.1
253 Meeting Rooms II 4, 14 4.4, 14.4
264 Ugly Number II 20 20.2
269 Alien Dictionary 13 13.2
273 Integer to English Words 32 32.6
278 First Bad Version 5 5.3
280 Wiggle Sort 4 4.6
282 Expression Add Operators 28 28.12
283 Move Zeroes 1 1.4
286 Walls and Gates 12 12.5
292 Nim Game 31 31.1
295 Find Median Stream 15 15.3
297 Serialize Tree 22 22.3
300 LIS 29 29.2
303 Range Sum Immutable 19 19.1
304 Range Sum 2D 19 19.4
305 Number Islands II 24 24.4
307 Range Sum Mutable 22 22.6
309 Stock Cooldown 29 29.9
310 Min Height Trees 13 13.3
312 Burst Balloons 29 29.14
315 Count Smaller After 22 22.5
322 Coin Change 29 29.7
323 Connected Components 9 9.3
329 Longest Increasing Path 43 43.1
332 Reconstruct Itinerary (Ch 11 follow)
337 House Robber III 11, 42 11.5, 42.1
338 Counting Bits 21 21.3
347 Top K Frequent 6 6.3
354 Russian Doll Envelopes 29, 38, 39 29.6, 38.1, 39.2
363 Max Sum Rectangle ≤ K 38 38.4
368 Largest Divisible Subset 39 39.1
371 Sum of Two Integers 21 21.4
394 Decode String 8, 32 8.6, 32.2
399 Evaluate Division 9 9.6
402 Remove K Digits 18 18.6
407 Trapping Rain Water II 37 37.4
410 Split Array Largest Sum 25 25.3
416 Partition Equal Subset Sum 29 29.5
417 Pacific Atlantic 12 12.4
421 Max XOR Pair 21, 41 21.6, 41.1
424 Char Replacement 27 27.3
435 Non-overlapping Intervals 14 14.3
438 Find All Anagrams 35 35.4
444 Sequence Reconstruction 13 13.5
452 Min Arrows Burst Balloons 14 14.5
455 Assign Cookies 16 16.4
459 Repeated Substring 35 35.3
464 Can I Win 31 31.6
486 Predict the Winner 31 31.3
503 Next Greater II 18 18.2
509 Fibonacci 3 3.1
518 Coin Change II 29 29.8
523 Continuous Subarray Sum 19 19.3
542 01 Matrix 12 12.6
543 Diameter Tree 42 42.3
547 Number of Provinces 24 24.1
560 Subarray Sum K 6, 19 6.4, 19.2
567 Permutation in String 27 27.4
591 Tag Validator (Ch 32)
621 Task Scheduler 15 15.6
648 Replace Words 23 23.5
664 Strange Printer 29 29.18
673 Number of LIS 38 38.3
684 Redundant Connection 24 24.2
692 Top K Frequent Words 15 15.2
695 Max Area Island 12 12.2
698 Partition K Equal Subsets 40 40.1
704 Binary Search 5 5.1
719 Find K-th Smallest Pair Dist 25 25.4
720 Longest Word 23 23.4
721 Accounts Merge 24 24.3
724 Find Pivot Index 19 19.6
726 Number of Atoms 32 32.3
736 Parse Lisp (Ch 32 ref)
739 Daily Temperatures 8, 18 8.5, 18.1
743 Network Delay Time 30 30.1
752 Open the Lock 10 10.4
759 Employee Free Time 14 14.6
763 Partition Labels 16 16.5
778 Swim Rising Water 24, 30, 37 24.6, 30.4, 37.1
785 Bipartite Graph 9 9.5
787 Cheapest Flights K Stops 30 30.3
791 Custom Sort String 4 4.5
797 All Paths Source Target 11 11.3
834 Sum of Distances Tree 42 42.5
841 Keys and Rooms 9 (tự luyện)
847 Shortest Path Visit All 40 40.2
876 Middle Linked List 7 7.4
877 Stone Game 31 31.2
907 Sum Subarray Mins 18 18.5
909 Snakes and Ladders 10 10.6
913 Cat and Mouse 31 31.5
920 Music Playlists 44 44.6
935 Knight Dialer 44 44.3
943 Shortest Superstring 40 40.4
947 Most Stones Removed 9 (tự luyện)
952 Largest Comp by Factor 20 20.5
968 Binary Tree Cameras 42 42.2
973 K Closest Points 15 15.4
990 Equation Satisfiability 24 24.5
992 Subarrays K Distinct 27 27.5
994 Rotting Oranges 10 10.2
1011 Capacity Ship Packages 25 25.2
1032 Stream of Characters 23 23.6
1044 Longest Duplicate Substring 34 34.2
1091 Shortest Path Binary Matrix 10 10.5
1102 Path Max Min Value 33 33.6
1125 Smallest Sufficient Team 40 40.3
1135 Connecting Cities 33 33.2
1136 Parallel Courses 13 13.6
1140 Stone Game II 31 31.4
1143 LCS 29 29.1
1168 Optimize Water 33 33.3
1175 Prime Arrangements 20 20.3
1203 Sort Items Groups 13 13.4
1220 Count Vowels Permutation 44 44.4
1235 Job Scheduling 39 39.4
1293 Shortest Path Obstacles 30 30.5
1297 Max Occurrences Substring 35 35.5
1316 Distinct Echo Substrings 34, 36 34.3, 36.6
1326 Min Taps Garden 39 39.6
1349 Max Students Exam 40 40.5
1368 Min Cost Valid Path 30 30.6
1392 Longest Happy Prefix 35 35.6
1425 Constrained Subseq Sum 38 38.5
1434 Hats Permutation 44 44.5
1462 Course Schedule IV 43 43.5
1478 Allocate Mailboxes 38 38.6
1489 Critical MST Edges 33 33.4
1547 Min Cost Cut Stick 29 29.16
1557 Min Vertices Reach 9 (tự luyện)
1584 Min Cost Connect Points 33 33.1
1631 Path Min Effort 30, 37 30.2, 37.2
1638 Strings Differ One Char 34 34.5
1690 Stone Game VII 29 29.17
1691 Stacking Cuboids 39 39.3
1697 Edge Length Limited Paths 33 33.5
1707 Max XOR With Element 41 41.2
1791 Center Star Graph 9 (tự luyện)
1803 Count Pairs XOR Range 41 41.3
1857 Largest Color Value 43 43.3
1879 Min XOR Sum 40 40.6
1901 Peak Element II 37 37.6
1938 Genetic Difference Query 41 41.4
1944 Visible People in Queue 39 39.5
1970 Last Day Cross 37 37.3
1971 Path Exists Graph 9 9.1
2050 Parallel Courses III 43 43.2
2115 Find Recipes 43 43.6
2223 Sum of Scores Built Strings 34, 36 34.6, 36.3
2246 Longest Path Diff Adj 42 42.4
2301 Match Substring Replace 36 36.5
2317 Max XOR After Ops 41 41.6
2392 Build Matrix Conditions 43 43.4
2407 LIS II 38 38.2
2430 Max Deletions String 36 36.4
2513 Min Max Two Arrays 37 37.5
2521 Distinct Prime Factors 20 20.6
2523 Closest Primes Range 20 20.4
2589 (đã thay bằng LC 1462)
2858 Min Edge Reversals 42 42.6
2935 Max Strong Pair XOR II 41 41.5

Phụ lục J — Bài xuất hiện nhiều chương (recap map)

Một số bài xuất hiện ở nhiều chương dưới những góc nhìn khác nhau. Đây là map để tránh nhầm lẫn:

LC Bản đầy đủ Recap ở Góc nhìn mới ở phần recap
LC 56 Merge Intervals Ch 4.2 (Sorting) Ch 14.1 (Interval) Sort vs interval pattern
LC 75 Sort Colors Ch 4.1 (Sorting Dutch flag) Ch 26.4 (Two Pointers) Sort vs 3-pointer
LC 11 Container Most Water Ch 1.5 (Array) Ch 26.3 (Two Pointers) Array trick vs converging pointers
LC 98 Validate BST Ch 11.4 (DFS) Ch 22.1 (Advanced Tree) DFS vs BST property
LC 146 LRU Cache Ch 6.6 (Hash Table) Ch 7.11 (Linked List) Hash + DLL vs DLL pattern
LC 206 Reverse LL Ch 7.1 (iterative) Ch 3.3 (recursive) 2 cách giải khác nhau
LC 50 Pow(x, n) Ch 3.2 (Recursion) Ch 17.4 (D&C) Recursion vs D&C framework
LC 215 Kth Largest Ch 15.1 (Heap) Ch 17.3 (D&C) Heap vs quickselect
LC 239 Sliding Window Max Ch 18.4 (Monotonic) Ch 27.6 (Sliding Window) Deque vs window
LC 253 Meeting Rooms II Ch 4.4 (Sorting) Ch 14.4 (Interval) Heap vs sweep line
LC 354 Russian Doll Ch 29.6 (DP) Ch 38.1, 39.2 3 góc nhìn: pure DP, BS + DP, sort + DP
LC 421 Max XOR Ch 21.6 (Bit) Ch 41.1 (Trie) Greedy bit vs binary trie
LC 560 Subarray Sum K Ch 6.4 (Hash) Ch 19.2 (Prefix Sum) Hash vs prefix sum
LC 778 Swim Water Ch 24.6 (DSU offline) Ch 30.4, 37.1 3 cách: DSU, Dijkstra, BS + BFS
LC 1316 Distinct Echo Ch 34.3 (Rolling Hash) Ch 36.6 (Z function) 2 thuật toán khác
LC 1631 Path Min Effort Ch 30.2 (Dijkstra) Ch 37.2 (BS + BFS) 2 cách
LC 2223 Sum of Scores Ch 34.6 (Rolling Hash) Ch 36.3 (Z) 2 thuật toán

Cách đọc recap: tập trung vào “góc nhìn mới” — pattern hiện tại đang nhìn bài cũ theo cách gì, chứ không phải đọc lại lời giải lần thứ hai.


Phụ lục K — Checklist trước phỏng vấn

24 giờ trước: - [ ] Ôn lại Frontmatter 0.2 (UMPIRE) — đọc lại 5 bước. - [ ] Đọc lại Phụ lục E (Behavioral) — chuẩn bị 3-4 câu chuyện STAR. - [ ] Ngủ đủ 7-8 tiếng. Đừng thức khuya code. - [ ] Kiểm tra thiết bị (micro, camera, chia sẻ màn hình, IDE / online editor).

1 tuần trước: - [ ] Hoàn thành 50 bài must-do (Phụ lục D). - [ ] 2-3 mock interview (Pramp / interviewing.io / bạn bè). - [ ] Đọc kỹ company-specific patterns (LeetCode tag theo company). - [ ] Review CV/resume — story behind mỗi project.

1 tháng trước: - [ ] Hoàn thành Level 1 + Level 2 (Chương 1-32). - [ ] Mock interview định kỳ 1-2 buổi / tuần. - [ ] Học behavioral framework + chuẩn bị 10 stories STAR. - [ ] Apply rộng (5-10 công ty) để có nhiều buổi practice.


Chúc bạn thành công trên hành trình phỏng vấn! Cảm ơn đã đọc cuốn sách này.