“Bí quyết không nằm ở việc di chuyển con trỏ — mà ở việc KHÔNG BAO GIỜ lùi lại. Mỗi con trỏ chỉ tiến, tổng quãng đường là O(n). Đó là lý do một vòng lặp lồng nhau lại chạy nhanh như một vòng.”
Đây là quyển thứ sáu. Nếu Sliding Window là “con dao mổ trâu” biến vô số bài O(n²) thành O(n), thì quyển này sẽ dạy bạn vì sao con dao đó sắc — không chỉ thuộc template, mà hiểu phân tích khấu hao khiến nó nhanh và điều kiện đơn điệu khiến nó đúng.
Phần 0 — Tư tưởng cốt lõi: đừng quay lại
Two Pointers và Sliding Window là hai kỹ thuật dùng vài chỉ số (con trỏ) cùng quét qua một mảng/chuỗi, để tránh vòng lặp lồng nhau O(n²).
- Two Pointers (con trỏ đôi): hai chỉ số di chuyển qua dữ liệu — có thể từ hai đầu vào giữa, hoặc cùng chiều với tốc độ khác nhau.
- Sliding Window (cửa sổ trượt): giữ một đoạn liên tục
[left, right]và cho nó “trượt” — mở rộng bên phải, thu hẹp bên trái tùy điều kiện.
Bí mật chung: tính đơn điệu + không lùi
Điều khiến hai kỹ thuật này hoạt động là dữ liệu có tính đơn điệu: khi dịch con trỏ theo một hướng, “trạng thái” thay đổi theo một chiều dự đoán được. Nhờ vậy ta không bao giờ cần lùi con trỏ lại — mỗi con trỏ chỉ tiến về phía trước.
Sliding Window thực chất là một dạng đặc biệt của Two Pointers (hai con trỏ left, right cùng chiều, tạo thành một cửa sổ). Học chung một tư tưởng: quét một lần, không quay lại.
Phần 1 — Vì sao O(n)? Phân tích khấu hao
Đây là hiểu biết phân định “biết template” với “hiểu bản chất”. Nhìn code sliding window, ta thấy một vòng for lồng một vòng while — trông như O(n²). Nhưng nó thực sự là O(n). Vì sao?
for right in range(n): # vòng ngoài chạy n lần
...
while điều_kiện: # vòng trong — trông đáng sợ!
left += 1
...
Chìa khóa: con trỏ left chỉ TĂNG, không bao giờ giảm, và bị chặn bởi n. Nghĩa là qua toàn bộ thuật toán, left được tăng tối đa n lần tổng cộng — chứ không phải n lần mỗi vòng ngoài.
- Vòng ngoài (
right): chạynlần →O(n). - Vòng trong (
left): tổng số lần chạy qua tất cả các vòng ngoài cộng lại ≤n→O(n). - Tổng:
O(n) + O(n) = O(n). ✓
Đây gọi là phân tích khấu hao (amortized analysis): một thao tác lẻ có thể tốn nhiều, nhưng trung bình trên cả quá trình thì rẻ, vì tổng công việc bị chặn.
Đừng đếm vòng lặp lồng nhau — hãy đếm tổng số bước của mỗi con trỏ. Mỗi con trỏ đi tối đa n bước → tổng O(n), bất kể code trông có bao nhiêu vòng lặp lồng.
Phần A
Two Pointers
Con trỏ đôi — ba biến thể và khi nào dùng.
A.1 — Ba biến thể
| Biến thể | Hướng đi | Dùng cho |
|---|---|---|
| Đối đầu (converging) | Hai đầu → vào giữa | Mảng đã sắp, palindrome, two-sum, container |
| Nhanh-chậm (fast-slow) | Cùng chiều, tốc độ khác | Sửa mảng tại chỗ, phát hiện chu trình linked list |
| Hai mảng | Mỗi con trỏ một mảng | Trộn (merge), so khớp hai dãy đã sắp |
A.2 — Đối đầu: mảng sắp / palindrome / two-sum
Hai con trỏ left (từ đầu) và right (từ cuối) tiến về giữa. Điều kiện tiên quyết: dữ liệu thường phải ĐÃ SẮP XẾP (để việc dịch con trỏ có tác dụng đơn điệu lên “trạng thái”).
Two Sum trên mảng đã sắp:
def two_sum_sorted(nums, target):
left, right = 0, len(nums) - 1
while left < right:
s = nums[left] + nums[right]
if s == target:
return [left, right]
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 None
Vì sao đúng: mảng đã sắp nên nums[left]+nums[right] phản ứng đơn điệu với việc dịch con trỏ — dịch left chỉ làm tổng tăng, dịch right chỉ làm tổng giảm. Ta luôn biết dịch bên nào. O(n) thay vì O(n²) của hai vòng lồng.
Kiểm tra palindrome:
def is_palindrome(s):
left, right = 0, len(s) - 1
while left < right:
if s[left] != s[right]:
return False
left += 1
right -= 1
return True
Container With Most Water (mang hơi hướng greedy):
def max_area(height):
left, right = 0, len(height) - 1
best = 0
while left < right:
area = min(height[left], height[right]) * (right - left)
best = max(best, area)
if height[left] < height[right]: # ⭐ dịch cột THẤP hơn
left += 1
else:
right -= 1
return best
Tại sao luôn dịch cột thấp hơn? Vì chiều cao bị giới hạn bởi cột thấp; giữ cột thấp lại thì dù chiều rộng ra sao, diện tích cũng không thể tốt hơn. Đây là một lập luận greedy — bỏ đi lựa chọn không thể tối ưu.
3Sum (sắp xếp + đối đầu):
def three_sum(nums):
nums.sort() # ⭐ sắp trước để dùng two pointers
res, n = [], len(nums)
for i in range(n - 2):
if i > 0 and nums[i] == nums[i-1]:
continue # bỏ trùng cho phần tử neo
left, right = i + 1, n - 1
while left < right:
s = nums[i] + nums[left] + nums[right]
if s == 0:
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 < 0:
left += 1
else:
right -= 1
return res
Từ O(n³) (ba vòng lồng) xuống O(n²) (một vòng ngoài + two pointers).
A.3 — Nhanh-chậm: tại chỗ / chu trình linked list
Hai con trỏ cùng chiều, một nhanh một chậm. slow thường đánh dấu “ranh giới phần đã xử lý”, fast quét tới.
Xóa phần tử trùng tại chỗ (mảng đã sắp):
def remove_duplicates(nums):
if not nums: return 0
slow = 0
for fast in range(1, len(nums)):
if nums[fast] != nums[slow]:
slow += 1
nums[slow] = nums[fast] # slow = cuối phần kết quả không trùng
return slow + 1
Dồn số 0 về cuối:
def move_zeroes(nums):
slow = 0
for fast in range(len(nums)):
if nums[fast] != 0:
nums[slow], nums[fast] = nums[fast], nums[slow]
slow += 1 # slow = vị trí đặt số khác 0 tiếp theo
Phát hiện chu trình trong linked list (Floyd’s — rùa & thỏ):
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next # đi 1 bước
fast = fast.next.next # đi 2 bước
if slow == fast: # ⭐ gặp nhau → CÓ chu trình
return True
return False
🔗 Nối với Bí kíp Graph: đây chính là phát hiện chu trình bằng hai con trỏ! Nếu có vòng, con thỏ (nhanh) sẽ “đuổi kịp” con rùa (chậm) từ phía sau trong vòng. Floyd’s là phiên bản two-pointer của bài phát hiện chu trình, chỉ tốn O(1) bộ nhớ.
A.4 — Hai con trỏ trên hai mảng (merge)
Mỗi con trỏ quét một mảng đã sắp, so sánh và tiến con trỏ nhỏ hơn — chính là bước “trộn” của Merge Sort:
def merge_sorted(a, b):
i, j, result = 0, 0, []
while i < len(a) and j < len(b):
if a[i] <= b[j]:
result.append(a[i]); i += 1
else:
result.append(b[j]); j += 1
result.extend(a[i:]) # phần dư của mảng còn lại
result.extend(b[j:])
return result
Đại diện: Merge Two Sorted Lists, Intersection of Two Arrays, Merge Sorted Array. O(n+m).
A.5 — Khi nào Two Pointers hoạt động
- ✅ Dữ liệu đã sắp xếp (hoặc có thể sắp) — để dịch con trỏ có tác dụng đơn điệu (đối đầu).
- ✅ Bài về cặp / bộ ba thỏa điều kiện tổng (two-sum, 3sum, 4sum).
- ✅ Palindrome hoặc đối xứng (đối đầu).
- ✅ Sửa mảng tại chỗ (
O(1)bộ nhớ): xóa/dồn phần tử (nhanh-chậm). - ✅ Chu trình / điểm giữa linked list (nhanh-chậm).
- ✅ Trộn / giao / hợp hai dãy đã sắp (hai mảng).
⚠️ Điều kiện cốt tử: two pointers (đối đầu) cần tính đơn điệu — dịch con trỏ phải làm “trạng thái” thay đổi theo hướng dự đoán được. Đó là lý do mảng thường phải đã sắp. Không có tính đơn điệu → two pointers không áp dụng được.
Phần B
Sliding Window
Cửa sổ trượt — template vạn năng và các biến tấu.
B.1 — Hai loại: cố định vs biến đổi
Sliding Window giải các bài về đoạn/đoạn con liên tục (contiguous subarray/substring). Có hai loại:
| Loại | Đặc điểm | Câu hỏi điển hình |
|---|---|---|
| Cố định (fixed) | Cửa sổ luôn có kích thước k |
“Tổng/trung bình lớn nhất của đoạn dài k” |
| Biến đổi (variable) | Cửa sổ co giãn theo điều kiện | “Đoạn dài nhất/ngắn nhất thỏa điều kiện X” |
Chữ ký nhận diện sliding window: đề nói về “đoạn con LIÊN TỤC” (không phải dãy con rời rạc — đó là DP/LIS) + hỏi “dài nhất / ngắn nhất / đếm số đoạn thỏa điều kiện”.
B.2 — TEMPLATE VẠN NĂNG cho cửa sổ biến đổi
Đây là phần vàng nhất của Phần B. Gần như mọi bài sliding window biến đổi đều khớp khuôn ba bước sau. Học thuộc là xử được cả một lớp bài khổng lồ.
thêm s[right] → 2 · Thu hẹp
khi vi phạm → 3 · Cập nhật
đáp án
def sliding_window(s):
window = {} # trạng thái cửa sổ (đếm ký tự, tổng, số phần tử khác nhau...)
left = 0
result = 0 # hoặc float('inf') cho bài "ngắn nhất"
for right in range(len(s)):
# ── BƯỚC 1: MỞ RỘNG — thêm s[right] vào cửa sổ ──
thêm s[right] vào window
# ── BƯỚC 2: THU HẸP — khi cửa sổ VI PHẠM, co left ──
while cửa_sổ_vi_phạm_điều_kiện:
bỏ s[left] khỏi window
left += 1
# ── BƯỚC 3: CẬP NHẬT đáp án (cửa sổ [left, right] giờ hợp lệ) ──
result = max(result, right - left + 1)
return result
left right
│ │
┌─────┼────────────────────────┼─────┐
│ [ cửa sổ hợp lệ hiện tại ] │
└─────┼────────────────────────┼─────┘
│ │
co lại khi mở rộng mỗi bước
vi phạm (right luôn tiến)
Ví dụ — Chuỗi con dài nhất không lặp ký tự
def length_of_longest_substring(s):
window = set()
left = best = 0
for right in range(len(s)):
while s[right] in window: # VI PHẠM: có ký tự trùng
window.remove(s[left]) # co left cho tới khi hết trùng
left += 1
window.add(s[right]) # giờ thêm được an toàn
best = max(best, right - left + 1)
return best
Biến tấu “ngắn nhất” — Minimum Window Substring (khó hơn)
Khi hỏi đoạn ngắn nhất, cấu trúc đảo lại: mở rộng cho tới khi hợp lệ, rồi co lại để tối thiểu hóa:
from collections import Counter
def min_window(s, t):
need = Counter(t)
missing = len(t) # số ký tự còn thiếu
left = 0
best, best_len = "", float('inf')
for right, ch in enumerate(s):
if need[ch] > 0:
missing -= 1
need[ch] -= 1
while missing == 0: # ⭐ cửa sổ ĐÃ chứa đủ t → co để tối thiểu
if right - left + 1 < best_len:
best_len = right - left + 1
best = s[left:right+1]
need[s[left]] += 1
if need[s[left]] > 0: # bỏ đi thì lại thiếu
missing += 1
left += 1
return best
“Dài nhất”: mở rộng thoải mái, co khi VI PHẠM, cập nhật đáp án sau khi co (cửa sổ luôn hợp lệ).
“Ngắn nhất”: mở rộng đến khi HỢP LỆ, rồi co để tối thiểu, cập nhật đáp án trong lúc co.
B.3 — Cửa sổ cố định
Khi kích thước k cố định, không cần co giãn — chỉ “trượt”: thêm phần tử mới bên phải, bỏ phần tử cũ bên trái.
def max_sum_subarray(nums, k):
window_sum = sum(nums[:k]) # cửa sổ đầu tiên
best = window_sum
for right in range(k, len(nums)):
window_sum += nums[right] - nums[right - k] # ⭐ thêm mới, BỎ cũ
best = max(best, window_sum)
return best
Mấu chốt cửa sổ cố định: khi trượt, chỉ cần cập nhật phần thay đổi (+ phần tử vào, - phần tử ra), không tính lại cả cửa sổ. Đó là cách biến O(n·k) thành O(n).
B.4 — Đếm cửa sổ: mẹo “at most K”
Bài “đếm số đoạn con thỏa đúng K điều kiện” thường khó trực tiếp. Mẹo thần thánh:
Đúng K = (Tối đa K) − (Tối đa K−1)
Vì “tối đa K” dễ đếm bằng sliding window, còn phép trừ khử đi phần “tối đa K−1”, để lại đúng phần “đúng K”.
from collections import defaultdict
def at_most_k_distinct(nums, k): # số đoạn con có TỐI ĐA k phần tử khác nhau
count = defaultdict(int)
left = result = 0
for right in range(len(nums)):
count[nums[right]] += 1
while len(count) > k: # vi phạm: quá k loại
count[nums[left]] -= 1
if count[nums[left]] == 0:
del count[nums[left]]
left += 1
result += right - left + 1 # ⭐ số đoạn con hợp lệ KẾT THÚC tại right
return result
def exactly_k_distinct(nums, k):
return at_most_k_distinct(nums, k) - at_most_k_distinct(nums, k - 1)
1. Số đoạn con hợp lệ kết thúc tại right = right - left + 1 (cộng dồn mỗi bước).
2. Đúng K = tối đa K − tối đa (K−1) — biến bài khó thành hai bài dễ.
B.5 — Sliding Window Maximum (monotonic deque)
Bài kinh điển: tìm giá trị lớn nhất trong mỗi cửa sổ trượt kích thước k. Dùng deque đơn điệu (monotonic deque) lưu chỉ số theo giá trị giảm dần.
from collections import deque
def max_sliding_window(nums, k):
dq = deque() # lưu CHỈ SỐ, giá trị giảm dần từ đầu → cuối
result = []
for i in range(len(nums)):
if dq and dq[0] <= i - k: # bỏ chỉ số đã trượt khỏi cửa sổ (ở đầu)
dq.popleft()
while dq and nums[dq[-1]] < nums[i]: # bỏ các giá trị nhỏ hơn nums[i] (vô dụng)
dq.pop()
dq.append(i)
if i >= k - 1:
result.append(nums[dq[0]]) # ⭐ đầu deque luôn là MAX của cửa sổ
return result
Vì sao O(n): mỗi chỉ số vào deque đúng 1 lần và ra tối đa 1 lần → tổng thao tác O(n) (lại là phân tích khấu hao!).
🔗 Nối với Bí kíp DP: đây chính là kỹ thuật “tối ưu hàng đợi đơn điệu (monotonic queue)” — thứ biến nhiều công thức chuyển DPO(n²)thànhO(n). Sliding window maximum là dạng thuần túy của nó.
Phần C
Two Pointers vs Sliding Window vs Prefix Sum
Ba anh em “quét mảng”. Chọn đúng công cụ là nửa lời giải.
| Kỹ thuật | Dùng khi | Điều kiện |
|---|---|---|
| Two Pointers (đối đầu) | Cặp/bộ ba, palindrome, mảng đã sắp | Cần tính đơn điệu (thường đã sắp) |
| Sliding Window | Đoạn con liên tục, dài nhất/ngắn nhất/đếm | Cần đơn điệu theo cửa sổ (thường giá trị không âm) |
| Prefix Sum | Tổng đoạn, “tổng đoạn con = K” | Dùng được cả khi có số âm |
| Prefix Sum + HashMap | “Số đoạn con có tổng = K” (có số âm) | Khi sliding window thất bại vì số âm |
⚠️ RANH GIỚI QUAN TRỌNG — sliding window và số âm: Sliding window dựa vào giả định “thêm phần tử làm cửa sổ ‘tệ’ hơn theo một chiều”. Với số âm, giả định này sụp đổ — thêm một số âm có thể làm tổng giảm, phá vỡ tính đơn điệu. Ví dụ bài “đếm đoạn con có tổng = K” với số âm: sliding window SAI, phải dùng prefix sum + hashmap:
def subarray_sum_equals_k(nums, k):
from collections import defaultdict
prefix, count = 0, defaultdict(int)
count[0] = 1 # tổng tiền tố rỗng
result = 0
for x in nums:
prefix += x
result += count[prefix - k] # có bao nhiêu tiền tố trước cho ra tổng đoạn = k
count[prefix] += 1
return result
Đây là cạm bẫy cực phổ biến: thấy “đoạn con” liền phản xạ sliding window, nhưng nếu có số âm → phải prefix sum. Ghi nhớ ranh giới này!
Phần D
☠️ Cạm bẫy chết người
💀 Bẫy #1: Sliding Window với số âm
Kỹ thuật giả định tính đơn điệu, số âm phá vỡ nó. “Tổng đoạn con = K” có số âm → dùng prefix sum + hashmap, không phải sliding window.
💀 Bẫy #2: Lệch 1 ở kích thước cửa sổ
Kích thước cửa sổ [left, right] là right - left + 1 (có +1!). Quên +1 là lỗi kinh điển. Kiểm tra bằng ví dụ nhỏ: left=right → kích thước 1.
💀 Bẫy #3: Sai THỨ TỰ ba bước
Trong template: mở rộng → thu hẹp → cập nhật. Với bài “dài nhất”, cập nhật sau khi thu hẹp (cửa sổ mới hợp lệ). Cập nhật sai chỗ → tính cả cửa sổ vi phạm.
💀 Bẫy #4: Two pointers trên mảng CHƯA sắp
Two pointers đối đầu cần tính đơn điệu (thường là mảng đã sắp). Quên sắp trước khi chạy 3Sum/two-sum-sorted → kết quả sai.
💀 Bẫy #5: Quên xử lý phần tử trùng (3Sum, 4Sum)
Không bỏ trùng → sinh ra các bộ lặp lại. Nhớ các dòng while ... nums[left] == nums[left-1]: left += 1.
💀 Bẫy #6: Fast-slow trên linked list — quên kiểm tra null
fast.next.next sẽ lỗi nếu fast hoặc fast.next là None. Luôn kiểm tra while fast and fast.next trước.
💀 Bẫy #7: Nhầm “đoạn con liên tục” với “dãy con rời rạc”
Sliding window chỉ cho đoạn LIÊN TỤC. Nếu đề cho phép bỏ phần tử ở giữa (dãy con rời rạc, subsequence) → đó là DP (như LIS), không phải sliding window.
Phần E
🔗 Nối với bộ sách
- Two Pointers ~ tư duy Greedy. “Luôn dịch cột thấp hơn” (Container With Most Water) là một lập luận greedy — loại bỏ lựa chọn không thể tối ưu.
- Fast-slow ~ phát hiện chu trình. Floyd’s rùa-thỏ là phiên bản two-pointer của bài phát hiện chu trình trong đồ thị — chỉ tốn
O(1)bộ nhớ. - Sliding Window Maximum = monotonic queue. Chính là kỹ thuật tối ưu DP, thứ hạ
O(n²)xuốngO(n). - Prefix Sum — cũng là một tối ưu DP, và là “cứu tinh” khi sliding window gặp số âm.
- Tính đơn điệu ~ Binary Search. Điều kiện “đơn điệu” khiến two pointers/sliding window hoạt động cùng bản chất với điều kiện khiến binary search hoạt động — cả ba đều khai thác cấu trúc đơn điệu để bỏ qua công việc thừa.
Phần F
📋 Bảng tra cứu thần tốc
| Khái niệm | Ghi nhớ |
|---|---|
| Tư tưởng chung | Quét một lần, con trỏ chỉ tiến, không lùi → O(n) |
Vì sao O(n) |
Phân tích khấu hao: mỗi con trỏ đi tối đa n bước tổng cộng |
| Two pointers đối đầu | Hai đầu vào giữa; cần mảng đã sắp |
| Two pointers nhanh-chậm | Cùng chiều, tốc độ khác; sửa tại chỗ / chu trình |
| Floyd’s (rùa-thỏ) | Phát hiện chu trình linked list, O(1) bộ nhớ |
| Sliding window (biến đổi) | Template 3 bước: mở rộng → thu hẹp → cập nhật |
| Kích thước cửa sổ | right - left + 1 (nhớ +1!) |
| “Dài nhất” | Co khi vi phạm, cập nhật SAU khi co |
| “Ngắn nhất” | Mở đến khi hợp lệ, co để tối thiểu, cập nhật TRONG khi co |
| Đếm đoạn kết thúc tại right | += right - left + 1 |
| Đúng K | atMost(K) - atMost(K-1) |
| Sliding window max | Monotonic deque, đầu deque là max |
| ⚠️ Số âm | Sliding window hỏng → dùng prefix sum + hashmap |
| Liên tục vs rời rạc | Liên tục → sliding window; rời rạc → DP |
Bốn câu hỏi khi gặp bài mảng/chuỗi: (1) Đoạn con LIÊN TỤC hay rời rạc? (2) Có số âm không? (3) Dữ liệu đã sắp chưa? (4) Hỏi cặp/bộ (→two pointers) hay đoạn dài-ngắn (→sliding window)?
Phần G
🎯 Lộ trình luyện tập theo cấp độ
Tên bài theo phong cách LeetCode.
🟢 Cấp 1 — Nhập môn
- Two Sum II (Input Array Is Sorted) — two pointers đối đầu cơ bản nhất.
- Valid Palindrome — đối đầu, kiểm tra đối xứng.
- Move Zeroes / Remove Duplicates from Sorted Array — fast-slow tại chỗ.
- Maximum Average Subarray I — cửa sổ cố định.
🟡 Cấp 2 — Vận dụng
- Longest Substring Without Repeating Characters — template sliding window kinh điển, phải nắm.
- 3Sum — sắp xếp + two pointers, xử lý trùng.
- Container With Most Water — đối đầu mang tính greedy.
- Fruit Into Baskets — sliding window “tối đa 2 loại” (chính là at-most-K).
- Linked List Cycle — Floyd’s rùa-thỏ.
🔴 Cấp 3 — Nâng cao
- Minimum Window Substring — sliding window “ngắn nhất”, biến tấu khó.
- Longest Repeating Character Replacement — cửa sổ với điều kiện tinh tế.
- Subarrays with K Different Integers — mẹo đúng K = atMost(K) − atMost(K−1).
- Sliding Window Maximum — monotonic deque.
- Subarray Sum Equals K — ⚠️ bẫy số âm, phải dùng prefix sum + hashmap (không phải sliding window!).
⚫ Cấp trùm cuối
- Minimum Number of K Consecutive Bits Flips — sliding window + kỹ thuật lật khéo.
- Count Number of Nice Subarrays — atMost-K trên tính chẵn lẻ.
- Trapping Rain Water — two pointers đối đầu nâng cao (hoặc stack).
- Find All Anagrams in a String — cửa sổ cố định + đếm ký tự.
- Chứng minh bằng tay: vì sao sliding window là
O(n)(khấu hao); vì sao nó hỏng với số âm.
🏋️ Nguyên tắc luyện tập: với mỗi bài, trước khi code hãy trả lời bốn câu hỏi ở Phần F. Đặc biệt hai câu tối quan trọng: “Có số âm không?” (quyết định sliding window hay prefix sum) và “Liên tục hay rời rạc?” (quyết định sliding window hay DP). Chọn nhầm công cụ ngay từ đầu là sai từ gốc.
🎓 Lời kết — Tâm pháp Two Pointers & Sliding Window
Gói toàn bộ bí kíp vào năm câu tâm pháp:
- Con trỏ chỉ tiến, không bao giờ lùi. Đó là lý do một vòng lặp lồng nhau lại chạy
O(n)— hãy đếm tổng bước con trỏ, đừng đếm vòng lặp. - Sliding Window là Two Pointers cùng chiều. Học chung một tư tưởng “quét một lần”; template 3 bước xử được cả một lớp bài.
- Đơn điệu là điều kiện sống còn. Two pointers cần mảng đã sắp; sliding window cần “cửa sổ đơn điệu” (thường là giá trị không âm). Mất đơn điệu → mất kỹ thuật.
- Số âm là kẻ phá bĩnh. “Tổng đoạn con = K” có số âm → sliding window hỏng, chuyển sang prefix sum + hashmap. Đây là bẫy giết nhiều người nhất.
- Phân biệt liên tục vs rời rạc. Đoạn con liên tục → sliding window; dãy con rời rạc → DP. Nhìn nhầm là sai từ gốc.
Luyện đủ bốn cấp ở Phần G, mỗi bài đều tự hỏi bốn câu định hình — sau khoảng 30–40 bài, bạn sẽ “ngửi” ra ngay bài nào là two pointers, bài nào sliding window, và bài nào là cái bẫy prefix-sum trá hình. Đó là lúc bạn đã làm chủ nghệ thuật quét một lần. 🎯
Sợi chỉ đỏ xuyên suốt cả sáu quyển: khai thác cấu trúc để tránh công việc thừa — dù là tính đơn điệu (quyển này), cấu trúc con tối ưu (DP), hay tính chất greedy. Chúc bạn luyện công viên mãn, lục nghệ tinh thông! 🥋🎯
Bình luận 0
Chưa có bình luận nào. Hãy là người đầu tiên!