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

Two Pointers & Prefix Sum

Hai pattern xuất hiện ở khoảng 30% bài Easy/Medium đầu tiên trên LeetCode. Cả hai đều biến thuật toán O(n²) thành O(n) — đúng đắn cao và ít trick.

14 worked examples · 2 knowledge checks · 10 bài về nhà

Phần 1. Two Pointers

Khi nào dùng — hai biến thể — chín worked examples
1.1

1.1 Two Pointers là gì?

Định nghĩa:
Two Pointers là kỹ thuật dùng hai chỉ số (i, j) cùng duyệt một mảng/string, di chuyển theo điều kiện logic của bài. Mỗi pointer di chuyển ít nhất 1 lần và mỗi phần tử được "thăm" tối đa 1–2 lần → tổng thời gian O(n).

Tại sao mạnh?

Nhiều bài có lời giải brute force tự nhiên là duyệt cặp (i, j) → O(n²). Two Pointers khai thác một bất biến (invariant) nào đó của bài để loại trừ phần lớn cặp không cần xét, đưa về O(n).

Hai biến thể

  1. Opposite Ends — hai pointer khởi đầu ở hai đầu, di chuyển vào giữa.
  2. Same Direction (Fast & Slow) — hai pointer cùng đi từ trái, một nhanh một chậm.
1.2

1.2 Tín hiệu nhận biết Two Pointers

Khi đọc đề, các "mùi" sau gợi ý nên thử Two Pointers:

Tín hiệu trong đềBiến thể có khả năng
"Mảng đã sort" (sorted array)Opposite Ends
"Tìm cặp/bộ ba có tổng = X"Opposite Ends (sau sort)
"Palindrome", "đối xứng"Opposite Ends
"Đảo ngược tại chỗ"Opposite Ends
"Loại bỏ duplicate tại chỗ"Same Direction
"Phân vùng mảng" (partition, move zeros…)Same Direction
"Tìm chu kỳ trong linked list"Fast & Slow (Tuần 5)
"In-place, O(1) extra space"Mạnh khả năng là Two Pointers
Quy tắc:
Nếu đề yêu cầu xử lý tại chỗ với O(1) bộ nhớ phụ trên mảng/string, Two Pointers là lựa chọn đầu tiên cần thử.
1.3

1.3 Hai biến thể — trực quan

Biến thể 1: Opposite Ends 2 5 7 9 11 13 17 ↑ left ↑ right Hai bên tiến vào giữa cho đến khi gặp nhau Biến thể 2: Same Direction (Fast & Slow) a b c d e f ↑ slow ↑ fast

Trong cả hai biến thể, mỗi pointer chỉ đi một chiều và mỗi vị trí được thăm hữu hạn lần — đó là lý do tổng thời gian là O(n).

1.4

1.4 Worked Example 1 — Reverse String

Đề (LeetCode #344): Đảo ngược mảng ký tự s tại chỗ. Yêu cầu O(1) bộ nhớ phụ.

Brute force

Tạo mảng mới, copy ngược → O(n) time, O(n) space → vi phạm yêu cầu O(1).

Two Pointers (Opposite Ends)

def reverseString(s):
    left, right = 0, len(s) - 1
    while left < right:
        s[left], s[right] = s[right], s[left]   # swap
        left += 1
        right -= 1

Trace với s = ['h','e','l','l','o']

BướcleftrightMảng sau swap
004['o','e','l','l','h']
113['o','l','l','e','h']
222(left == right, dừng)

Time O(n) (n/2 swap), Space O(1).

1.5

1.5 Worked Example 2 — Two Sum II (Sorted)

Đề (LeetCode #167): Mảng numbers đã sort tăng dần. Tìm hai chỉ số (1-indexed) sao cho tổng = target. Yêu cầu O(1) bộ nhớ phụ.

Tại sao Tuần 1 (#1 Two Sum) phải dùng hash, mà bài này dùng Two Pointers?

Ở #1 mảng chưa sort, không khai thác được thứ tự. Bài này có tính sắp xếp — bất biến mạnh để giảm bộ nhớ.

def twoSum(numbers, target):
    left, right = 0, len(numbers) - 1
    while left < right:
        s = numbers[left] + numbers[right]
        if s == target:
            return [left + 1, right + 1]
        elif s < target:
            left += 1                  # cần tổng lớn hơn → dịch trái sang phải
        else:
            right -= 1                 # cần tổng nhỏ hơn → dịch phải sang trái
    return []

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

1.6

1.6 Two Sum II — Tại sao greedy này đúng?

Sinh viên thường viết được code nhưng không giải thích được. Đây là chứng minh tính đúng đắn:

Bất biến (loop invariant):
Tại mỗi bước, đáp án (nếu tồn tại) luôn nằm trong cửa sổ [left, right].

Chứng minh

Giả sử nums[left] + nums[right] < target. Ta tuyên bố: nums[left] không thể là phần tử của đáp án.

Vì sao? Mọi phần tử ở giữa hai pointer đều ≤ nums[right] (mảng sort). Nên nums[left] + nums[k] ≤ nums[left] + nums[right] < target với mọi k ∈ [left+1, right]. Tức không có k nào ghép với left ra target. Vậy loại bỏ left an toàn — di chuyển left += 1.

Lập luận đối xứng cho trường hợp tổng > target.

Kỹ năng phỏng vấn:
Khi giải xong, hãy nói câu này: "Bất biến của vòng lặp là... Khi điều kiện X xảy ra, ta loại bỏ pointer Y vì..." Đây là dấu hiệu của ứng viên hiểu sâu, không chỉ thuộc lòng template.
1.7

1.7 Worked Example 3 — Valid Palindrome

Đề (LeetCode #125): Cho string s, trả về True nếu nó là palindrome (sau khi bỏ ký tự không alphanumeric và đưa về chữ thường).

Ví dụ: "A man, a plan, a canal: Panama" → True (do "amanaplanacanalpanama").

Two Pointers với skip

def isPalindrome(s):
    left, right = 0, len(s) - 1
    while left < right:
        # bỏ qua ký tự không alphanumeric
        while left < right and not s[left].isalnum():
            left += 1
        while left < right and not s[right].isalnum():
            right -= 1
        if s[left].lower() != s[right].lower():
            return False
        left += 1
        right -= 1
    return True

Time O(n) (mỗi ký tự được thăm tối đa 1 lần), Space O(1).

Pattern phụ:
"Skip while" trong vòng while chính là cấu trúc xuất hiện ở rất nhiều bài Two Pointers + lọc dữ liệu (bỏ space, bỏ ký tự đặc biệt, bỏ duplicate, …).
1.8

1.8 Worked Example 4 — Container With Most Water

Đề (LeetCode #11): Mảng height[i] = chiều cao cột i. Chọn 2 cột tạo bình chứa nước. Diện tích = min(height[i], height[j]) × (j - i). Tìm diện tích lớn nhất.

Constraints: 2 ≤ n ≤ 10⁵ → cần O(n).

Brute force O(n²)

Duyệt mọi cặp (i, j), tính diện tích, lấy max. Với n = 10⁵ → 10¹⁰ → TLE.

Two Pointers Greedy

def maxArea(height):
    left, right = 0, len(height) - 1
    best = 0
    while left < right:
        h = min(height[left], height[right])
        best = max(best, h * (right - left))
        # luôn dịch pointer của cột THẤP HƠN
        if height[left] < height[right]:
            left += 1
        else:
            right -= 1
    return best
1.9

1.9 Container — Tại sao luôn dịch cột thấp hơn?

Đây là điểm quan trọng nhất của bài. Sinh viên phải hiểu, không chỉ nhớ.

Lập luận

Giả sử height[left] < height[right]. Diện tích hiện tại bị giới hạn bởi height[left].

Câu hỏi: Có lợi không nếu giữ left và dịch right sang trái?

→ Mọi cặp dùng left đều cho diện tích ≤ hiện tại. Loại bỏ left an toàn.

Bài học tổng quát:
Greedy two-pointers đúng đắn khi có thể chứng minh: "Pointer ở cột yếu hơn không thể tham gia vào đáp án tối ưu nữa." Đây cũng là cấu trúc lập luận của Trapping Rain Water (xem ở 3.3).
1.10

1.10 Biến thể 2 — Same Direction (Fast & Slow)

Hai pointer cùng đi từ trái. Pointer fast duyệt mảng nhanh, đọc dữ liệu. Pointer slow ghi dữ liệu hợp lệ vào.

Mô hình tổng quát:
slow = 0
for fast in range(len(nums)):
    if <điều kiện giữ>:
        nums[slow] = nums[fast]
        slow += 1
return slow   # số phần tử đã giữ

Pattern này hữu dụng cho:

Khoá học sẽ gặp lại pattern này ở Tuần 5 (Linked List — fast/slow tìm middle, detect cycle).

1.11

1.11 Worked Example 5 — Remove Duplicates from Sorted Array

Đề (LeetCode #26): Mảng nums đã sort. Loại duplicate tại chỗ, trả về số phần tử duy nhất k. Phần tử k đầu tiên của nums phải chứa các giá trị duy nhất.

def removeDuplicates(nums):
    if not nums: return 0
    slow = 1
    for fast in range(1, len(nums)):
        if nums[fast] != nums[fast - 1]:    # phần tử khác phần trước
            nums[slow] = nums[fast]
            slow += 1
    return slow

Trace với nums = [1,1,2,2,3]

fastnums[fast]khác trước?slownums sau bước
11không1[1,1,2,2,3]
222[1,2,2,2,3]
32không2[1,2,2,2,3]
433[1,2,3,2,3]

Trả về k = 3. Time O(n), Space O(1).

1.12

1.12 Worked Example 6 — Move Zeroes

Đề (LeetCode #283): Cho mảng nums. Đẩy mọi số 0 về cuối, giữ nguyên thứ tự tương đối của các số khác 0. Tại chỗ.

def moveZeroes(nums):
    slow = 0
    # Pha 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] = nums[fast], nums[slow]
            slow += 1

Trace với nums = [0, 1, 0, 3, 12]

fastnums[fast]swap?slownums sau
00không0[0,1,0,3,12]
11có (0,1)1[1,0,0,3,12]
20không1[1,0,0,3,12]
33có (1,3)2[1,3,0,0,12]
412có (2,4)3[1,3,12,0,0]

Time O(n), Space O(1). Số swap tối đa là số phần tử khác 0.

1.13

1.13 Worked Example 7 — 3Sum (Sort + Two Pointers)

Đề (LeetCode #15): Mảng nums. Tìm tất cả bộ ba phân biệt (a, b, c) sao cho a + b + c = 0. Không trùng bộ.

Ý tưởng kết hợp pattern

Sort mảng → cố định nums[i] là phần tử thứ nhất → tìm cặp (j, k) trong phần còn lại sao cho nums[j] + nums[k] = -nums[i] bằng Two Pointers (vì phần còn lại đã sort).

def threeSum(nums):
    nums.sort()
    res = []
    for i in range(len(nums) - 2):
        if i > 0 and nums[i] == nums[i-1]:    # bỏ trùng nums[i]
            continue
        left, right = i + 1, len(nums) - 1
        target = -nums[i]
        while left < right:
            s = nums[left] + nums[right]
            if s == target:
                res.append([nums[i], nums[left], nums[right]])
                left += 1; right -= 1
                while left < right and nums[left] == nums[left-1]: left += 1   # bỏ trùng
                while left < right and nums[right] == nums[right+1]: right -= 1
            elif s < target: left += 1
            else: right -= 1
    return res
1.14

1.14 3Sum — Phân tích complexity và bẫy

Time complexity

Bẫy 1: Bỏ trùng sai

Phải skip duplicate cả ba pointer (i, left, right). Nếu chỉ skip i, sẽ có bộ ba lặp lại do leftright trùng giá trị qua các vòng.

Bẫy 2: Khoảng vòng lặp ngoài

range(len(nums) - 2) chứ không phải range(len(nums)). Vì cần ít nhất 2 phần tử cho left và right.

Mở rộng:
Bài kSum tổng quát có thể giải đệ quy: 4Sum gọi 3Sum, 5Sum gọi 4Sum, … Time O(n^(k-1)). Sẽ gặp lại ở Tuần 11 khi học backtracking.
1.Q

Knowledge Check — Two Pointers

Q1: Trong bài Two Sum II (mảng đã sort), tại sao ta dịch left khi tổng nhỏ hơn target?
Mọi nums[k] với k ∈ [left+1, right] đều ≤ nums[right], nên nums[left] + nums[k] ≤ nums[left] + nums[right] < target. Loại left là an toàn.
Q2: Trong Container With Most Water, tại sao dịch cột thấp hơn?
Nếu giữ cột thấp, mọi cặp tiếp theo đều có chiều rộng nhỏ hơn và chiều cao bị chính cột thấp đó giới hạn → không thể lớn hơn diện tích hiện tại.
Q3: Pattern Same Direction (slow/fast) thường dùng cho?
Slow/fast là pattern "đọc bằng fast, ghi bằng slow" — phù hợp cho lọc, dồn, phân vùng. Tìm cặp tổng và palindrome dùng Opposite Ends.

Phần 2. Prefix Sum

Tiền xử lý O(n) để trả lời mọi truy vấn tổng đoạn trong O(1)
2.1

2.1 Vấn đề mở đầu — Range Sum Query

Bài toán: Cho mảng nums. Trả lời nhiều truy vấn dạng "tổng các phần tử từ index i đến j là bao nhiêu?"

Cách ngây thơ

# Mỗi truy vấn O(j - i + 1)
def rangeSum(nums, i, j):
    return sum(nums[i:j+1])

Với q truy vấn và mảng size n, tổng thời gian là O(q × n). Nếu q và n đều ~10⁵ → 10¹⁰ thao tác → TLE.

Câu hỏi quan trọng:
Liệu có thể trả lời mỗi truy vấn trong O(1) nếu được phép tiền xử lý mảng một lần?
2.2

2.2 Định nghĩa Prefix Sum

Định nghĩa:
Với mảng nums độ dài n, prefix sum P độ dài n+1 được xây dựng: $$ P[0] = 0, \quad P[i] = P[i-1] + \text{nums}[i-1] \quad \text{với } i \geq 1 $$ Tức P[i] = tổng nums[0..i-1].

Tính chất chính

$$ \text{sum}(\text{nums}[i..j]) = P[j+1] - P[i] $$

Ví dụ minh hoạ

nums = [3, 1, 4, 1, 5, 9, 2, 6]
P    = [0, 3, 4, 8, 9, 14, 23, 25, 31]

# Tổng nums[2..5] (= 4 + 1 + 5 + 9 = 19)
P[6] - P[2] = 23 - 4 = 19   ✓

Tiền xử lý: O(n). Mỗi truy vấn: O(1). Bộ nhớ phụ: O(n).

2.3

2.3 Cài đặt Prefix Sum

Phiên bản tiêu chuẩn (P có thêm vị trí 0)

def build_prefix(nums):
    n = len(nums)
    P = [0] * (n + 1)
    for i in range(n):
        P[i+1] = P[i] + nums[i]
    return P

def range_sum(P, i, j):
    return P[j+1] - P[i]    # tổng nums[i..j]

Phiên bản gọn (Pythonic)

from itertools import accumulate
P = [0] + list(accumulate(nums))
Quy ước "padding 0" cực quan trọng:
Thêm P[0] = 0 giúp công thức P[j+1] - P[i] đúng cho mọi i, j ≥ 0 (kể cả i = 0). Không có padding sẽ phải chia thành 2 trường hợp.
2.4

2.4 Worked Example 1 — Range Sum Query Immutable

Đề (LeetCode #303): Cài class NumArray với:

Sẽ có nhiều cuộc gọi sumRange → tiền xử lý quan trọng.

class NumArray:
    def __init__(self, nums):
        self.P = [0]
        for x in nums:
            self.P.append(self.P[-1] + x)

    def sumRange(self, i, j):
        return self.P[j+1] - self.P[i]

Constructor O(n); sumRange O(1); Space O(n).

Đây là ví dụ minh hoạ "tradeoff tiêu biểu":
Tốn O(n) bộ nhớ và preprocessing để đổi lại q truy vấn O(1) thay vì q × O(n). Lợi khi q lớn — gần như mọi bài range query đều thoả.
2.5

2.5 Worked Example 2 — Find Pivot Index

Đề (LeetCode #724): Tìm index i sao cho tổng bên trái = tổng bên phải. Trả về index nhỏ nhất; -1 nếu không có.

Quan sát

Tại mỗi i: left_sum = P[i], right_sum = P[n] - P[i+1]. Tìm i sao cho hai vế bằng nhau.

def pivotIndex(nums):
    total = sum(nums)
    left = 0
    for i, x in enumerate(nums):
        # right = total - left - x
        if left == total - left - x:
            return i
        left += x
    return -1

Time O(n), Space O(1) (không cần lưu cả mảng prefix — chỉ cần biến tổng chạy).

Tối ưu space:
Khi chỉ cần tổng đến vị trí i chứ không cần truy vấn ngẫu nhiên, có thể bỏ mảng P, chỉ giữ một biến left chạy. Đây là tinh thần "rolling prefix".
2.6

2.6 Combo Pattern — Prefix Sum + HashMap

Pattern mạnh nhất xuất hiện trong tuần này. Khi cần đếm/tìm subarray có tổng bằng K:

Quan sát then chốt:
Subarray nums[i..j] có tổng K \(\iff\) P[j+1] - P[i] = K \(\iff\) P[i] = P[j+1] - K.

Vậy: với mỗi vị trí j+1, ta cần đếm số i ≤ j+1 sao cho P[i] = P[j+1] - K. Đây là bài toán tìm phần tử trong hash map — O(1).

Đây chính là pattern "đổi space lấy time" đã gặp ở Tuần 1, kết hợp với prefix sum. Cộng hai pattern → O(n²) → O(n).

Tín hiệu nhận dạng:
Đề có chữ "subarray", "continuous", "tổng bằng K", "tổng chia hết K" → khả năng cao là Prefix Sum + HashMap.
2.7

2.7 Worked Example 3 — Subarray Sum Equals K

Đề (LeetCode #560): Mảng nums (có thể âm), số nguyên k. Đếm số subarray liên tiếp có tổng bằng k.

from collections import defaultdict

def subarraySum(nums, k):
    count = 0
    prefix = 0
    seen = defaultdict(int)
    seen[0] = 1                  # prefix rỗng = 0, gặp 1 lần (quan trọng!)
    for x in nums:
        prefix += x
        # cần i sao cho P[i] = prefix - k
        count += seen[prefix - k]
        seen[prefix] += 1
    return count

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

2.8

2.8 Subarray Sum K — Trace tay

nums = [1, 2, 3], k = 3. Kỳ vọng: 2 subarray ([1,2][3]).

xprefixcần tìm (prefix − k)seen[..] trướccountseen sau cập nhật
0{0:1}0{0:1}
11−2{0:1}0{0:1, 1:1}
230{0:1, 1:1}1{0:1, 1:1, 3:1}
363{0:1, 1:1, 3:1}2{0:1, 1:1, 3:1, 6:1}

Kết quả: 2.

Vì sao seen[0] = 1 ban đầu?
Để xử lý subarray bắt đầu từ index 0. Nếu prefix tại j+1 = k, cần "ghép" với prefix rỗng (P[0] = 0). Khởi tạo seen[0] = 1 đúng cho trường hợp này. Quên dòng này là lỗi phổ biến nhất của bài.
2.9

2.9 Worked Example 4 — Continuous Subarray Sum (Modular Prefix)

Đề (LeetCode #523): Trả True nếu có subarray độ dài ≥ 2 có tổng chia hết cho k.

Mẹo modular

Subarray nums[i..j] chia hết k \(\iff\) P[j+1] ≡ P[i] (mod k). Vậy lưu prefix sum theo modulo k trong hash map.

def checkSubarraySum(nums, k):
    seen = {0: -1}            # remainder → index sớm nhất
    prefix = 0
    for i, x in enumerate(nums):
        prefix = (prefix + x) % k
        if prefix in seen:
            if i - seen[prefix] >= 2:    # đủ độ dài
                return True
        else:
            seen[prefix] = i
    return False

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

Biến thể quan trọng:
Lưu index sớm nhất (không tăng count) khi cần kiểm tra khoảng cách hoặc tồn tại; lưu count khi cần đếm số subarray. Đây là khác biệt giữa #523 và #560.
2.10

2.10 Biến thể nâng cao — 2D Prefix Sum

Mở rộng cho ma trận. Truy vấn: tổng các phần tử trong submatrix (r1,c1) → (r2,c2).

# P[i+1][j+1] = tổng matrix[0..i][0..j]
def build2D(M):
    m, n = len(M), len(M[0])
    P = [[0] * (n+1) for _ in range(m+1)]
    for i in range(m):
        for j in range(n):
            P[i+1][j+1] = M[i][j] + P[i][j+1] + P[i+1][j] - P[i][j]
    return P

def query(P, r1, c1, r2, c2):
    return (P[r2+1][c2+1] - P[r1][c2+1]
            - P[r2+1][c1] + P[r1][c1])

Inclusion-Exclusion (Bao hàm-Loại trừ)

Công thức query áp dụng nguyên lý bao hàm-loại trừ trên 2D — trừ hai hàng/cột thừa, cộng lại phần bị trừ 2 lần. Đây là kỹ thuật xuất hiện ở LeetCode #304.

Phần này là tham khảo — chưa yêu cầu thuộc cho bài về nhà tuần này.

2.Q

Knowledge Check — Prefix Sum

Q1: Tại sao trong Subarray Sum K cần khởi tạo seen[0] = 1?
Khi prefix = k tại một vị trí, nghĩa là subarray từ đầu đến đó có tổng = k. Cần "ghép" với P[0] = 0. Nên seen[0] phải có giá trị 1 ngay từ đầu.
Q2: Biến thể nào của prefix sum dùng cho bài "tổng chia hết cho k"?
P[j+1] - P[i] ≡ 0 (mod k)P[j+1] ≡ P[i] (mod k). Lưu remainder thay vì giá trị thực giúp giảm số trạng thái xuống tối đa k.
Q3: Một mảng có q truy vấn range sum. Khi nào prefix sum lợi hơn duyệt thẳng?
Build O(n), q truy vấn × O(1) = O(n + q). So với O(qn) khi duyệt thẳng. Khi q ≥ 1 đã lợi nếu không cần update; nhưng "lợi rõ rệt" khi q lớn.

Phần 3. Tổng hợp và Decision Tree

Khi đứng trước một bài mới, chọn pattern nào?
3.1

3.1 Decision Tree — Mảng/String

Tín hiệu trong đềPattern phù hợp
Mảng đã sort, tìm cặp/bộ ba có tổng = XTwo Pointers (Opposite)
Mảng chưa sort, tìm cặp có tổngHashMap (Tuần 1) hoặc sort + Two Pointers
Palindrome, đối xứng, đảoTwo Pointers (Opposite)
In-place, O(1) extra spaceTwo Pointers (Same Direction)
Loại duplicate, partitionTwo Pointers (Same Direction)
Range sum query nhiều lầnPrefix Sum
Đếm subarray có tổng = KPrefix Sum + HashMap
Subarray có tổng chia hết KPrefix Sum modulo K
Submatrix sum query2D Prefix Sum
Window di động cố định/biến đổiSliding Window (Tuần 4)

Bảng này nên được sinh viên thuộc lòng đến hết khoá. Khi đề mới, đọc constraint + tín hiệu rồi tra bảng — đây là kỹ năng "pattern matching" thực sự.

3.2

3.2 Worked Example 8 — Product of Array Except Self

Đề (LeetCode #238): Mảng nums. Trả về mảng res sao cho res[i] = tích mọi phần tử trừ nums[i]. Không được dùng phép chia. Yêu cầu O(n) và O(1) bộ nhớ phụ (không tính output).

Ý tưởng — Prefix product + Suffix product

Pattern này là biến thể của prefix sum: thay tổng bằng tích, và cần cả prefix lẫn suffix.

def productExceptSelf(nums):
    n = len(nums)
    res = [1] * n
    # Pha 1: res[i] = tích các phần tử bên trái i
    left = 1
    for i in range(n):
        res[i] = left
        left *= nums[i]
    # Pha 2: nhân thêm tích các phần tử bên phải i
    right = 1
    for i in range(n - 1, -1, -1):
        res[i] *= right
        right *= nums[i]
    return res

Time O(n), Space O(1) (output không tính).

3.3

3.3 Worked Example 9 — Trapping Rain Water (preview Tuần 6)

Đề (LeetCode #42): Mảng height mô phỏng địa hình. Tính lượng nước giữ lại sau mưa.

Approach 1 — Prefix max + Suffix max (O(n) time, O(n) space)

Tại vị trí i, nước giữ = min(maxLeft[i], maxRight[i]) - height[i].

def trap(height):
    n = len(height)
    if n < 3: return 0
    L = [0]*n; R = [0]*n
    L[0] = height[0]
    for i in range(1, n): L[i] = max(L[i-1], height[i])
    R[-1] = height[-1]
    for i in range(n-2, -1, -1): R[i] = max(R[i+1], height[i])
    return sum(min(L[i], R[i]) - height[i] for i in range(n))

Approach 2 — Two Pointers (O(1) space) — Tuần 6 sẽ làm chi tiết

Dùng cùng tinh thần "luôn dịch pointer của cột thấp hơn" như Container With Most Water.

Bài học liên kết tuần:
Trapping Rain Water là cầu nối giữa Prefix array (Tuần 2) và Two Pointers nâng cao (Tuần 6). Sinh viên nên giải bằng prefix max ngay tuần này, để tuần 6 tối ưu xuống O(1) space.
4.0

Tổng kết Tuần 2

Đã học

Bài về nhà (10 bài)

  1. LeetCode #344 — Reverse String (đã chữa, code lại không nhìn)
  2. LeetCode #167 — Two Sum II — Input Array Is Sorted
  3. LeetCode #125 — Valid Palindrome
  4. LeetCode #11 — Container With Most Water
  5. LeetCode #15 — 3Sum
  6. LeetCode #26 — Remove Duplicates from Sorted Array
  7. LeetCode #303 — Range Sum Query Immutable
  8. LeetCode #560 — Subarray Sum Equals K
  9. LeetCode #238 — Product of Array Except Self
  10. LeetCode #42 — Trapping Rain Water (chỉ cần approach prefix max — O(n) space)

Mỗi bài: solution.py + 3 test tự nghĩ + analysis.md nêu pattern dùng và bất biến (nếu Two Pointers) hoặc công thức prefix.

Tuần 3 sẽ học gì?

Hashing — Frequency Counting và HashMap Patterns

Đào sâu vào pattern "đổi space lấy time". Học cách thiết kế key của hash, xử lý anagram groups, consecutive sequence, longest substring without repeating characters. Combo pattern với Sliding Window sẽ được giới thiệu cuối tuần để chuyển sang Tuần 4.

Hết Tuần 2 — Cảm ơn các em.

Mục lục

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