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

Dynamic Programming — 2D

Mở rộng từ 1D state lên 2D — hai chuỗi, knapsack 0/1, grid, interval. Bộ công cụ hoàn chỉnh cho mọi bài DP cấp Medium/Hard trên LeetCode.

10 worked examples · 3 knowledge checks · 12 bài về nhà

Phần 1. Nền tảng 2D DP

Khi 1D không đủ — cần thêm chiều thứ hai
1.1

1.1 Khi nào cần 2D state?

1D DP đủ khi state chỉ phụ thuộc vào "ta đang ở đâu" (vị trí i). 2D cần thiết khi state có hai chiều độc lập:

Loại bàiHai chiềuVí dụ
Hai chuỗiVị trí trên chuỗi 1, vị trí trên chuỗi 2LCS, Edit Distance
Knapsack 0/1Item đang xét, capacity còn lạiPartition Subset Sum
GridHàng, cộtUnique Paths, Min Path Sum
IntervalBiên trái, biên phảiBurst Balloons, Matrix Chain
Game / Decision với state phụVị trí, trạng thái thêmStock with Multiple Transactions

Quy tắc nhận biết

Khi state cần "ghi nhớ" cả vị trí + một chiều khác → 2D. Khi không thể tóm gọn vào 1 số → 2D.

1.2

1.2 State Design cho hai chuỗi

Mẫu chung:
Cho hai chuỗi s1, s2 có độ dài m, n. State dp[i][j] đại diện cho "câu trả lời cho bài con trên prefix s1[0..i-1] và prefix s2[0..j-1]".

Quy ước padding

Bảng dp có kích thước (m+1) × (n+1). Hàng/cột 0 đại diện cho "chuỗi rỗng" — đơn giản hoá base case.

Recurrence chung

if s1[i-1] == s2[j-1]:
    dp[i][j] = dp[i-1][j-1] + something      # ký tự khớp
else:
    dp[i][j] = combine(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])

Cụ thể "combine" tuỳ bài: max cho LCS, min cho Edit Distance, add cho Distinct Subsequences.

Tinh thần "i ↔ s1[i-1]":
Với padding +1, dp index i tương ứng với s1 character s1[i-1] (1-indexed view của bảng dp = 0-indexed view của chuỗi).
1.3

1.3 DP Table — Trực quan

Hình dung bảng dp giúp viết recurrence nhanh và đúng:

""abc
""0000
a0111
c0112
e0112

DP table cho LCS("ace", "abc") = 2

Ba mũi tên cốt lõi

Recurrence dùng ↖ khi ký tự khớp; dùng ↑/← khi không khớp. Nhìn vào bảng → dễ debug.

1.4

1.4 Tối ưu Space — Rolling Array

2D DP tốn O(m × n) space. Khi recurrence chỉ phụ thuộc vào hàng i-1 hoặc cell (i-1, j-1), có thể giảm xuống O(min(m, n)).

Hai biến thể tối ưu

# Two rows version cho LCS
def lcs(s1, s2):
    m, n = len(s1), len(s2)
    if m < n: s1, s2, m, n = s2, s1, n, m    # đảm bảo n nhỏ
    prev = [0] * (n + 1)
    cur = [0] * (n + 1)
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if s1[i-1] == s2[j-1]:
                cur[j] = prev[j-1] + 1
            else:
                cur[j] = max(prev[j], cur[j-1])
        prev, cur = cur, prev
    return prev[n]

Time vẫn O(m × n); Space O(min(m, n)).

Phần 2. Two-string DP

LCS, Edit Distance — pattern then chốt cho phỏng vấn senior
2.1

2.1 Pattern Two-string DP

State:
dp[i][j] = câu trả lời cho prefix s1[0..i-1] và prefix s2[0..j-1].

Logic chung khi gặp ký tự

if s1[i-1] == s2[j-1]:
    # KHỚP — kế thừa từ dp[i-1][j-1] (cả hai prefix giảm 1)
    dp[i][j] = dp[i-1][j-1] + (1 nếu cần đếm)
else:
    # KHÔNG KHỚP — phải "bỏ" 1 ký tự ở 1 trong 2 bên
    dp[i][j] = combine(dp[i-1][j], dp[i][j-1])
    # combine = max cho LCS, min cho Edit Distance, sum cho count

Bốn bài kinh điển

2.2

2.2 Worked Example 1 — Longest Common Subsequence

Đề (LeetCode #1143): Hai chuỗi text1, text2. Tìm độ dài chuỗi con chung dài nhất (subsequence, không cần liên tiếp).

Ví dụ: "abcde", "ace" → 3 ("ace").

def longestCommonSubsequence(text1, text2):
    m, n = len(text1), len(text2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if text1[i-1] == text2[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]

Time O(m × n), Space O(m × n) (hoặc O(min(m,n)) với rolling).

Recurrence trực giác

2.3

2.3 LCS — DP Table đầy đủ

text1 = "abcde", text2 = "ace". Kỳ vọng: 3.

""ace
""0000
a0111
b0111
c0122
d0122
e0123

Các cell bold là điểm "khớp" (text1[i-1] == text2[j-1])

Đáp án ở góc dưới phải = 3.

Truy hồi LCS thực sự:
Nếu cần biết LCS là gì (không chỉ độ dài), backtrack từ (m, n): nếu khớp → đi ↖ và prepend ký tự; ngược lại đi theo ↑ hoặc ← lớn hơn.
2.4

2.4 Worked Example 2 — Edit Distance (#72)

Đề: Hai chuỗi word1, word2. Tìm số phép biến đổi tối thiểu để biến word1 thành word2. Ba loại phép:

  1. Insert: chèn 1 ký tự.
  2. Delete: xoá 1 ký tự.
  3. Replace: thay 1 ký tự.

Ví dụ: "horse" → "ros" cần 3 phép (rorse → rose → ros).

State

dp[i][j] = số phép tối thiểu để biến word1[0..i-1] thành word2[0..j-1].

Base case

2.5

2.5 Edit Distance — Recurrence chi tiết

Khi word1[i-1] == word2[j-1]: ký tự khớp, không cần biến đổi → dp[i][j] = dp[i-1][j-1].

Khi không khớp, ba lựa chọn:

$$ dp[i][j] = 1 + \min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) $$
def minDistance(word1, word2):
    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-1], dp[i-1][j], dp[i][j-1])
    return dp[m][n]

Time O(m × n).

2.6

2.6 Edit Distance — DP Table

word1 = "horse", word2 = "ros". Kỳ vọng: 3.

""ros
""0123
h1123
o2212
r3222
s4332
e5443

Đáp án ở góc dưới phải = 3

Tinh thần phỏng vấn:
Khi giải bài này trong phỏng vấn, vẽ bảng dp ra giấy + giải thích "ý nghĩa của 3 mũi tên" (insert/delete/replace). Đó là cách thuyết phục interviewer rằng bạn hiểu sâu, không chỉ nhớ template.
2.7

2.7 Worked Example 3 — Distinct Subsequences (#115)

Đề (Hard): Hai chuỗi s, t. Đếm số cách t xuất hiện trong s dưới dạng subsequence.

Ví dụ: s = "rabbbit", t = "rabbit" → 3 (3 cách chọn các ký tự).

State

dp[i][j] = số cách t[0..j-1] xuất hiện trong s[0..i-1].

Recurrence

Khi s[i-1] == t[j-1]: hai lựa chọn — dùng s[i-1] hoặc bỏ qua nó:

$$ dp[i][j] = \underbrace{dp[i-1][j-1]}_{\text{dùng}} + \underbrace{dp[i-1][j]}_{\text{bỏ qua}} $$

Khi không khớp: phải bỏ qua s[i-1]dp[i][j] = dp[i-1][j].

def numDistinct(s, t):
    m, n = len(s), len(t)
    if n > m: return 0
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    for i in range(m + 1): dp[i][0] = 1     # 1 cách ghép chuỗi rỗng
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            dp[i][j] = dp[i-1][j]
            if s[i-1] == t[j-1]:
                dp[i][j] += dp[i-1][j-1]
    return dp[m][n]
2.8

2.8 Worked Example 4 — Longest Palindromic Subsequence (#516)

Đề: Cho string s. Tìm độ dài subsequence palindrome dài nhất.

Ví dụ: "bbbab" → 4 ("bbbb").

Mẹo: Quy về LCS

Subsequence palindrome của s = subsequence chung của sreverse(s).

def longestPalindromeSubseq(s):
    return longestCommonSubsequence(s, s[::-1])

Time O(n²).

Approach trực tiếp — Interval DP

State: dp[i][j] = độ dài LPS trong s[i..j]. Đây là Interval DP (xem Phần 5):

def longestPalindromeSubseq(s):
    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
            if s[i] == s[j]:
                dp[i][j] = (dp[i+1][j-1] if length > 2 else 0) + 2
            else:
                dp[i][j] = max(dp[i+1][j], dp[i][j-1])
    return dp[0][n-1]
2.Q

Knowledge Check — Two-string DP

Q1: Trong DP table cho 2 chuỗi, vì sao kích thước là (m+1) × (n+1) chứ không phải m × n?
Padding với "" giúp recurrence dp[i-1][j-1] không bị out-of-bounds khi i=1 hoặc j=1. Ngoài ra, dp[i][0] và dp[0][j] có ý nghĩa rõ (so sánh với chuỗi rỗng) → base case viết được trong 1 dòng.
Q2: Trong Edit Distance, recurrence khi không khớp ký tự là?
3 phép biến đổi tương ứng 3 hướng trên bảng dp: ↖ replace, ↑ delete, ← insert. Tất cả đều +1. Lấy min vì cần ít phép nhất.
Q3: Longest Palindromic Subsequence có thể quy về LCS như thế nào?
Subsequence palindrome đọc xuôi và ngược như nhau → là phần chung giữa s và đảo của s. Đây là kỹ thuật "reduce to known problem" — pattern phỏng vấn quan trọng.

Phần 3. Knapsack 0/1

Items với capacity — mỗi item dùng tối đa 1 lần
3.1

3.1 0/1 Knapsack — Pattern

Bài toán cổ điển:
n items, mỗi item có weight và value. Knapsack capacity W. Chọn subset items sao cho tổng weight ≤ W và tổng value max. Mỗi item chỉ được chọn 0 hoặc 1 lần.

State 2D

dp[i][w] = value max khi xét i items đầu tiên với capacity w.

Recurrence — choose or skip

$$ dp[i][w] = \max\left(\underbrace{dp[i-1][w]}_{\text{skip item } i}, \, \underbrace{dp[i-1][w-w_i] + v_i}_{\text{take item } i}\right) $$

Take chỉ khả dĩ khi w ≥ w_i. Khi đảo ngược (unbounded), thay dp[i-1][w-w_i] bằng dp[i][w-w_i] để cho phép dùng lại.

Tối ưu space — 1D

Có thể giảm xuống dp[w] bằng cách lặp w từ phải sang trái (đảm bảo dp[w-w_i] vẫn là giá trị "trước khi xét item i"):

for i in range(n):
    for w in range(W, weights[i] - 1, -1):    # ngược lại
        dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
3.2

3.2 Worked Example 5 — Partition Equal Subset Sum

Đề (LeetCode #416): Mảng số dương nums. Trả True nếu có thể chia thành 2 subset có tổng bằng nhau.

Quy về 0/1 Knapsack

Tổng = S. Nếu S lẻ → False. Nếu S chẵn → tìm subset có tổng = S/2.

State boolean: dp[w] = True nếu có subset tổng = w.

def canPartition(nums):
    s = sum(nums)
    if s % 2: return False
    target = s // 2
    dp = [False] * (target + 1)
    dp[0] = True
    for num in nums:
        for w in range(target, num - 1, -1):    # 0/1 → ngược
            dp[w] = dp[w] or dp[w - num]
    return dp[target]

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

Kỹ thuật quan trọng — loop ngược cho 0/1:
Khi loop xuôi, dp[w - num] đã được cập nhật ở vòng lặp này → dùng item nhiều lần → unbounded. Loop ngược giữ dp[w - num] là giá trị "trước item này" → 0/1 đúng.
3.3

3.3 Worked Example 6 — Target Sum

Đề (LeetCode #494): Mảng số dương nums, số target. Đặt + hoặc - trước mỗi số. Đếm số cách để tổng = target.

Quy về 0/1 Knapsack đếm

Đặt P = tập các số có dấu +, N = tập có dấu -. Khi đó:

Quy về: đếm số cách chọn subset có tổng = (S + target) / 2.

def findTargetSumWays(nums, target):
    s = sum(nums)
    if (s + target) % 2 or s < abs(target): return 0
    new_target = (s + target) // 2
    dp = [0] * (new_target + 1)
    dp[0] = 1
    for num in nums:
        for w in range(new_target, num - 1, -1):
            dp[w] += dp[w - num]
    return dp[new_target]

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

3.4

3.4 0/1 vs Unbounded — So sánh đầy đủ

Aspect0/1 KnapsackUnbounded Knapsack
Dùng item nhiều lần?Không (max 1)
Loop direction trong 1DNgược (W → 0)Xuôi (0 → W)
Ví dụ bàiPartition Subset Sum, Target Sum, Last Stone IICoin Change, Word Break
Recurrence 2Ddp[i][w] dùng dp[i-1][w-w_i]dp[i][w] dùng dp[i][w-w_i]

Quy tắc nhanh để debug

Nếu kết quả vượt kỳ vọng (đếm trùng) → bạn đang vô tình unbounded. Check loop direction.

Nếu kết quả thấp hơn kỳ vọng → bạn đang vô tình 0/1 nhưng bài cho phép unbounded. Đảo loop direction.

Trick nhớ:
"0/1 ngược, Unbounded xuôi" — vần điệu giúp nhớ lâu. Loop ngược "bảo vệ" giá trị cũ; loop xuôi "tận dụng" giá trị đã cập nhật.
3.Q

Knowledge Check — Knapsack 0/1

Q1: Trong 0/1 Knapsack 1D, vì sao loop capacity từ phải sang trái?
Khi loop xuôi từ trái, dp[w-num] được cập nhật trước → khi đến dp[w], dp[w-num] đã chứa "item này đã dùng". Thành unbounded. Loop ngược đảm bảo dp[w-num] vẫn là giá trị từ vòng for trước → 0/1.
Q2: Trong Target Sum, kỹ thuật "đặt P = (S + target) / 2" làm gì?
Bài gốc có dấu ±, không phải subset sum. Bằng đại số, ta thấy "chọn dấu +" tương đương "chọn subset có tổng P = (S+target)/2". Đây là kỹ thuật reduction quan trọng — biến bài lạ thành bài quen.

Phần 4. Grid DP

DP trên ma trận — Unique Paths, Min Path Sum
4.1

4.1 Grid DP — Pattern

Đặc trưng:
dp[r][c] đại diện cho "câu trả lời cho subproblem trên submatrix top-left là (r, c)" hoặc "đến ô (r, c) như thế nào".

Recurrence thường:

$$ dp[r][c] = f(dp[r-1][c], dp[r][c-1], dp[r-1][c-1]) $$

Tuỳ bài: tổng (Min Path Sum), đếm cách (Unique Paths), boolean (Dungeon Game).

Khi nào ưu tiên Grid DP?

4.2

4.2 Worked Example 7 — Unique Paths (#62)

Đề: Lưới m × n. Robot ở (0,0), cần đến (m-1, n-1). Mỗi bước chỉ phải hoặc xuống. Đếm số đường đi.

State

dp[r][c] = số đường từ (0,0) đến (r,c).

Recurrence: đến (r,c) chỉ có thể từ (r-1,c) hoặc (r,c-1):

$$ dp[r][c] = dp[r-1][c] + dp[r][c-1] $$
def uniquePaths(m, n):
    dp = [[1] * n for _ in range(m)]    # hàng/cột 0 đều có 1 cách
    for r in range(1, m):
        for c in range(1, n):
            dp[r][c] = dp[r-1][c] + dp[r][c-1]
    return dp[m-1][n-1]

Time O(m × n), Space O(m × n) hoặc O(n) với rolling.

Combinatorial alternative:
Mỗi đường có (m-1) bước xuống và (n-1) bước phải. Số đường = C(m+n-2, m-1). Time O(m+n) — nhưng DP rõ ràng hơn cho phỏng vấn.
4.3

4.3 Worked Example 8 — Minimum Path Sum (#64)

Đề: Lưới grid[i][j] > 0. Tìm đường từ top-left đến bottom-right có tổng nhỏ nhất. Mỗi bước phải hoặc xuống.

def minPathSum(grid):
    m, n = len(grid), len(grid[0])
    # in-place modification
    for r in range(m):
        for c in range(n):
            if r == 0 and c == 0: continue
            elif r == 0: grid[r][c] += grid[r][c-1]
            elif c == 0: grid[r][c] += grid[r-1][c]
            else: grid[r][c] += min(grid[r-1][c], grid[r][c-1])
    return grid[m-1][n-1]

Time O(m × n), Space O(1) (in-place).

Trace với grid = [[1,3,1],[1,5,1],[4,2,1]]

145
276
687

Đường tối ưu: 1→3→1→1→1 = 7.

Phần 5. Interval DP

DP trên khoảng [i, j] — Burst Balloons, Palindromic Substrings
5.1

5.1 Interval DP — Pattern

Đặc trưng:
dp[i][j] = câu trả lời cho subproblem trên khoảng [i..j]. Recurrence thường lặp qua điểm "split" k bên trong khoảng: $$ dp[i][j] = \min/\max_{i \leq k \leq j} \{ \text{combine}(dp[i][k], dp[k+1][j]) \} $$

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

Thứ tự tính

Tính theo chiều dài khoảng tăng dần: dp[i][i] (size 1), dp[i][i+1] (size 2), …

for length in range(1, n + 1):
    for i in range(n - length + 1):
        j = i + length - 1
        # tính dp[i][j] dựa trên dp con (length nhỏ hơn)

Time thường O(n³) (n² cells × n splits).

5.2

5.2 Worked Example 9 — Burst Balloons (#312, Hard)

Đề: Mảng nums (giá trị bóng bay). Mỗi lần nổ bóng i được nums[i-1] × nums[i] × nums[i+1] điểm (với biên giả lập = 1). Tìm tổng điểm tối đa khi nổ hết.

Tinh thần "ngược" — Bóng nào nổ CUỐI?

Quan trọng: nghĩ "nổ ngược" — chọn bóng nổ cuối cùng trong khoảng [i, j].

Khi nổ k cuối cùng trong [i, j], hai biên là nums[i-1] và nums[j+1] (đã được "bảo vệ" trong các bài con):

$$ dp[i][j] = \max_{i \leq k \leq j} \{ dp[i][k-1] + dp[k+1][j] + \text{nums}[i-1] \cdot \text{nums}[k] \cdot \text{nums}[j+1] \} $$
def maxCoins(nums):
    nums = [1] + nums + [1]                # padding biên
    n = len(nums)
    dp = [[0] * n for _ in range(n)]
    for length in range(1, n - 1):
        for i in range(1, n - length):
            j = i + length - 1
            for k in range(i, j + 1):
                dp[i][j] = max(dp[i][j],
                    dp[i][k-1] + dp[k+1][j] + nums[i-1]*nums[k]*nums[j+1])
    return dp[1][n-2]

Time O(n³).

5.3

5.3 Burst Balloons — Vì sao "nổ ngược"?

Cách tự nhiên — "nổ xuôi" — nghĩ bóng nào nổ trước. Nhưng có vấn đề:

Đảo lại — bóng nào nổ CUỐI

Khi k là bóng cuối trong [i, j], lúc đó hai bóng kế là nums[i-1] và nums[j+1] (vì mọi bóng trong [i,j] đã bị nổ).

Hai khoảng con [i, k-1] và [k+1, j] giải độc lập — không "ăn vào" nhau vì k còn nguyên.

Bài học design state:
Khi gặp bài interval DP mà recurrence "có vẻ" rối, thử đảo thứ tự thao tác. Nổ ngược, xếp ngược, ghép ngược — đôi khi cách "ngược" cho recurrence sạch.
5.4

5.4 Worked Example 10 — Palindromic Substrings (#647)

Đề: Cho string s. Đếm số substring là palindrome.

Approach Interval DP

dp[i][j] = True nếu s[i..j] là palindrome.

Recurrence: s[i..j] palindrome ⟺ s[i] == s[j] và s[i+1..j-1] palindrome (hoặc length ≤ 2):

def countSubstrings(s):
    n = len(s)
    dp = [[False] * n for _ in range(n)]
    count = 0
    for i in range(n):
        dp[i][i] = True
        count += 1                           # single char palindromes
    for length in range(2, n + 1):
        for i in range(n - length + 1):
            j = i + length - 1
            if s[i] == s[j]:
                if length == 2 or dp[i+1][j-1]:
                    dp[i][j] = True
                    count += 1
    return count

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

Approach thay thế — "Expand around center":
Với mỗi vị trí i, expand ra hai phía cho đến khi không còn palindrome. Time O(n²) nhưng O(1) space. Trong phỏng vấn, đôi khi đáng để biết cả hai.
5.Q

Knowledge Check — Grid & Interval DP

Q1: Trong Grid DP, khi nào không áp dụng được — phải dùng BFS/DFS?
DP yêu cầu thứ tự tính (topological order). Hướng phải/xuống là DAG nên DP OK. Hướng 4 phía có cycle (đi lên rồi đi xuống) → không có thứ tự → cần BFS/DFS với visited.
Q2: Tại sao Burst Balloons phải nghĩ "bóng nổ cuối cùng" thay vì đầu tiên?
Nổ trước: biên của subproblem thay đổi tuỳ bóng nổ trước → 2 subproblem dependent. Nổ cuối: bóng k còn nguyên cho đến cuối, nên 2 subproblem [i, k-1] và [k+1, j] giải độc lập với biên cố định nums[i-1] và nums[j+1].
Q3: Interval DP thường có time complexity gì?
Bảng dp có n² cells. Mỗi cell xét n điểm split khả dĩ → O(n³). Một số bài (Palindromic Substrings) chỉ cần O(n²) vì recurrence không cần split point. Nhưng pattern chung là O(n³).
6.0

6.0 Decision Tree — 2D DP

Tín hiệu trong đềPattern
"hai chuỗi": LCS, edit, transformTwo-string DP với dp[i][j]
"đếm số cách"Two-string DP cộng (Distinct Subsequences)
"palindrome"LCS với reverse, hoặc Interval DP
"chia subset bằng nhau", "target sum với ±"0/1 Knapsack với boolean/count dp
"unique paths / min path sum trên grid"Grid DP với hướng phải/xuống
"interval [i, j], split point"Interval DP O(n³)
"burst balloons / merge stones"Interval DP với "thao tác ngược"
"matrix chain multiplication"Interval DP cổ điển
"stock with K transactions"State machine 2D — dp[k][i]

Quy trình code 2D DP

  1. Định nghĩa dp[i][j] và padding chuẩn (m+1) × (n+1) cho two-string.
  2. Vẽ bảng dp với input nhỏ trên giấy.
  3. Viết recurrence dựa trên 3 mũi tên (↖, ↑, ←).
  4. Cài tabulation.
  5. Tối ưu space (rolling array) nếu cần.
6.1

Tổng kết Tuần 13

Đã học

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

  1. LeetCode #1143 — Longest Common Subsequence
  2. LeetCode #72 — Edit Distance (Hard)
  3. LeetCode #115 — Distinct Subsequences (Hard)
  4. LeetCode #516 — Longest Palindromic Subsequence
  5. LeetCode #416 — Partition Equal Subset Sum
  6. LeetCode #494 — Target Sum
  7. LeetCode #62 — Unique Paths
  8. LeetCode #64 — Minimum Path Sum
  9. LeetCode #312 — Burst Balloons (Hard)
  10. LeetCode #647 — Palindromic Substrings
  11. LeetCode #5 — Longest Palindromic Substring
  12. LeetCode #1035 — Uncrossed Lines (LCS variant)

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

Bit Manipulation, Trie và Mock Interview

Tuần cuối cùng. Học các kỹ thuật bitwise (XOR, bitmask DP), cấu trúc Trie cho bài về string nâng cao, và đặc biệt là quy trình mock interview chuẩn — phân tích đề, viết pseudocode, code, debug, complexity. Tổng hợp lại 13 tuần và chuẩn bị tâm thế phỏng vấn thực tế.

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

Mục lục

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