- Dùng cho các bài toán hỏi về đoạn con liên tiếp (subarray / substring) như: đoạn con độ dài
kcó tổng lớn nhất, đoạn con dài nhất/ngắn nhất thỏa mãn điều kiện. - Có 2 dạng chính: cửa sổ kích thước cố định
k(mỗi bước thêma[i]và bớta[i - k]) và cửa sổ kích thước co giãn (mở rộngr, thu hẹplbằng vòng lặpwhilekhi vi phạm điều kiện). - Dù dạng co giãn có vòng lặp
whilenằm trong vòng lặpfor, mỗi phần tử chỉ đi vào cửa sổ đúng 1 lần và rời khỏi cửa sổ tối đa 1 lần, nên độ phức tạp luôn là O(n).
Ý tưởng cửa sổ trượt qua hình ảnh khung kính trượt trên băng chuyền
Giả sử bạn có mảng a = [2, 1, 5, 1, 3, 2] và cần tìm tổng lớn nhất của 3 phần tử đứng liên tiếp nhau (k = 3).
Nếu làm theo cách ngây thơ, tại mỗi vị trí bắt đầu bạn lại cộng 3 phần tử từ đầu:
- Đoạn 1:
2 + 1 + 5 = 8 - Đoạn 2:
1 + 5 + 1 = 7 - Đoạn 3:
5 + 1 + 3 = 9 - Đoạn 4:
1 + 3 + 2 = 6
Hãy để ý chuyển động từ Đoạn 1 [2, 1, 5] sang Đoạn 2 [1, 5, 1]: hai số ở giữa là 1 và 5 vẫn nằm nguyên trong khung! Thay vì cộng lại cả k phần tử (rất chậm nếu k = 50.000), ta chỉ cần lấy tổng cũ trừ đi phần tử vừa bị đẩy ra ở bên trái (2) và cộng thêm phần tử mới vừa bước vào ở bên phải (1).
Dạng 1: Cửa sổ trượt có kích thước cố định K
Khi đề bài cho trước độ dài cố định k của đoạn con liên tiếp, thuật toán luôn gồm 2 bước rõ ràng:
- Khởi tạo cửa sổ đầu tiên: Tính tổng (hoặc số lượng phần tử thỏa điều kiện) của cửa sổ đầu tiên gồm đúng
kphần tử từ vị trí0đếnk - 1. Ghi nhận giá trị này làm kỷ lục ban đầubest = cur. - Trượt từng bước sang phải: Cho chỉ số đầu phải
itrượt từkđếnn - 1. Tại mỗi bước, phần tử mới bước vào cửa sổ ở biên phải làa[i], còn phần tử vừa tụt lại phía sau biên trái của cửa sổ làa[i - k]. Công thức cập nhật chỉ mất đúng hai phép toán số học:cur += a[i] - a[i - k]
Hãy đối chiếu chi tiết từng chỉ số khi k = 3 trên mảng a = [2, 1, 5, 1, 3, 2] để không bao giờ nhầm biên trái i - k:
| Bước | Chỉ số đầu phải i |
Các vị trí trong cửa sổ [i - k + 1 .. i] |
Phần tử vừa vào a[i] |
Phần tử vừa rời a[i - k] |
Tổng cửa sổ mới cur |
Kỷ lục best |
|---|---|---|---|---|---|---|
| Khởi tạo | 0 .. 2 |
[0, 1, 2] → [2, 1, 5] |
2, 1, 5 |
Chưa có | 8 |
8 |
| Trượt 1 | i = 3 |
[1, 2, 3] → [1, 5, 1] |
+ a[3] = +1 |
- a[0] = -2 |
8 + 1 - 2 = 7 |
8 |
| Trượt 2 | i = 4 |
[2, 3, 4] → [5, 1, 3] |
+ a[4] = +3 |
- a[1] = -1 |
7 + 3 - 1 = 9 |
9 |
| Trượt 3 | i = 5 |
[3, 4, 5] → [1, 3, 2] |
+ a[5] = +2 |
- a[2] = -5 |
9 + 2 - 5 = 6 |
9 |
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
// Tìm tổng lớn nhất của đoạn con liên tiếp có độ dài đúng k
long long tongDoanDaiK(const vector<int>& a, int k) {
long long cur = 0;
for (int i = 0; i < k; ++i) {
cur += a[i]; // tổng cửa sổ đầu tiên [0 .. k-1]
}
long long best = cur;
for (int i = k; i < (int)a.size(); ++i) {
cur += a[i] - a[i - k]; // thêm phần tử mới bên phải, bớt phần tử cũ bên trái
best = max(best, cur);
}
return best;
}
int main() {
vector<int> a = {2, 1, 5, 1, 3, 2};
cout << tongDoanDaiK(a, 3) << "\n"; // 9 (đoạn [5, 1, 3])
return 0;
}
def tong_doan_dai_k(a, k):
"""Tìm tổng lớn nhất của đoạn con liên tiếp có độ dài đúng k."""
cur = sum(a[:k])
best = cur
for i in range(k, len(a)):
cur += a[i] - a[i - k] # thêm phải, bớt trái
if cur > best:
best = cur
return best
a = [2, 1, 5, 1, 3, 2]
print(tong_doan_dai_k(a, 3)) # 9
Cách này chỉ cần một vòng lặp duy nhất đi qua mảng trong thời gian O(n) và bộ nhớ phụ O(1), bất kể k lớn tới đâu. Nếu n = 1.000.000 và k = 500.000, cách cộng lại từ đầu mất khoảng 2,5 × 10¹¹ phép tính (quá thời gian hàng trăm giây), trong khi cửa sổ trượt chỉ làm đúng 1.000.000 phép cộng trừ và chạy xong trong vài mili-giây.
Ngoài bài toán tính tổng đoạn dài k, mẫu cửa sổ cố định còn áp dụng trực tiếp cho rất nhiều câu hỏi thực tế khác:
- Trung bình cộng lớn nhất của đoạn dài
k: tìm tổng lớn nhất của cửa sổ dàikrồi chia chokở bước cuối cùng. - Đoạn dài
kchứa nhiều số chẵn (hoặc nguyên âm) nhất: thay vì cộng giá trịa[i], ta cộng1nếua[i]thỏa điều kiện và trừ1nếua[i - k]thỏa điều kiện. - Kiểm tra xem có đoạn con độ dài
knào có tổng đúng bằngShay không: chỉ cần kiểm traif (cur == S)ngay sau mỗi bước trượt.
Dạng 2: Cửa sổ trượt có kích thước thay đổi (Co giãn)
Trong nhiều bài toán thực tế, độ dài cửa sổ không cố định mà chính là thứ đề bài yêu cầu tìm. Ví dụ kinh điển: "Cho mảng các số nguyên dương a = [3, 1, 2, 7, 4, 2, 1, 1, 5] và số S = 8. Tìm độ dài của đoạn con liên tiếp dài nhất có tổng không vượt quá 8."
Lúc này ta kết hợp kỹ thuật hai con trỏ cùng chiều l (biên trái) và r (biên phải) như một con sâu đo co giãn trên mảng:
- Mở rộng biên phải
r: Vòng lặp ngoài chornhích từng bước từ0đếnn - 1, cộnga[r]vào trạng thái của cửa sổ hiện tạicur(kéo giãn đầu phải của cửa sổ thêm 1 ô). - Thu hẹp biên trái
lkhi vi phạm điều kiện: Ngay sau khi thêma[r], nếu tổngcur > S(cửa sổ bị quá tải), ta dùng vòng lặpwhile (cur > S)để trừ bớta[l]và tăngl++(rút ngắn đuôi trái lại) cho tới khicur <= Strở lại. - Cập nhật đáp án khi cửa sổ hợp lệ: Khi vòng
whilekết thúc, cửa sổ[l..r]chắc chắn là cửa sổ hợp lệ dài nhất kết thúc tại vị trír. Độ dài của nó làr - l + 1, và ta cập nhật kết quả tốt nhấtbest = max(best, r - l + 1).
Hãy xem bảng chạy từng bước với a = [3, 1, 2, 7, 4, 2, 1, 1, 5] và S = 8:
| Bước (r) | Phần tử thêm a[r] | Tổng trước khi co | Thu hẹp biên trái l (nếu tổng > 8) | Cửa sổ hợp lệ [l..r] | Độ dài r − l + 1 |
|---|---|---|---|---|---|
r = 0 |
3 | 3 | Không cần (l = 0) |
[3] |
1 |
r = 1 |
1 | 4 | Không cần (l = 0) |
[3, 1] |
2 |
r = 2 |
2 | 6 | Không cần (l = 0) |
[3, 1, 2] |
3 |
r = 3 |
7 | 13 | Bớt 3, bớt 1, bớt 2 → l = 3, tổng còn 7 |
[7] |
1 |
r = 4 |
4 | 11 | Bớt 7 → l = 4, tổng còn 4 |
[4] |
1 |
r = 5 |
2 | 6 | Không cần (l = 4) |
[4, 2] |
2 |
r = 6 |
1 | 7 | Không cần (l = 4) |
[4, 2, 1] |
3 |
r = 7 |
1 | 8 | Không cần (l = 4) |
[4, 2, 1, 1] |
4 (Max) |
r = 8 |
5 | 13 | Bớt 4, bớt 2 → l = 6, tổng còn 7 |
[1, 1, 5] |
3 |
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
// Tìm độ dài đoạn con liên tiếp dài nhất có tổng <= S (với mảng số nguyên dương)
int doanDaiNhatTongNhoHonS(const vector<int>& a, long long S) {
int l = 0, best = 0;
long long cur = 0;
for (int r = 0; r < (int)a.size(); ++r) {
cur += a[r]; // mở rộng biên phải của cửa sổ
while (cur > S) { // khi tổng vượt quá S thì thu hẹp biên trái
cur -= a[l];
l++;
}
best = max(best, r - l + 1); // cửa sổ [l..r] hiện tại hợp lệ
}
return best;
}
int main() {
vector<int> a = {3, 1, 2, 7, 4, 2, 1, 1, 5};
cout << doanDaiNhatTongNhoHonS(a, 8) << "\n"; // 4 (đoạn [4, 2, 1, 1])
return 0;
}
def doan_dai_nhat_tong_nho_hon_s(a, s):
"""Tìm độ dài đoạn con liên tiếp dài nhất có tổng <= s (mảng dương)."""
l = 0
cur = 0
best = 0
for r, val in enumerate(a):
cur += val # mở rộng biên phải
while cur > s: # thu hẹp biên trái khi tổng vượt quá s
cur -= a[l]
l += 1
if r - l + 1 > best:
best = r - l + 1
return best
a = [3, 1, 2, 7, 4, 2, 1, 1, 5]
print(doan_dai_nhat_tong_nho_hon_s(a, 8)) # 4
Chủ đề này nằm trong Lecture 04 - Two Pointers & Sliding Window của Khóa Blue 2.0. Xem đề cương
Ba biến thể cửa sổ co giãn hay gặp nhất trong đề thi
Cùng một khung for (r) kết hợp while (l), chỉ cần thay đổi vị trí cập nhật đáp án là bạn giải quyết được 3 bài toán hoàn toàn khác nhau:
1. Tìm đoạn con DÀI NHẤT thỏa mãn điều kiện (ví dụ: tổng ≤ S)
- Khi nào thu hẹp
l: Trong vòngwhile, chừng nào cửa sổ đang vi phạm điều kiện (cur > S) thì tăngl++. - Khi nào cập nhật
best: Đặt câu lệnhbest = max(best, r - l + 1)ở bên ngoài, ngay phía sau vòngwhile, vì lúc đó cửa sổ[l..r]đã quay trở lại trạng thái hợp lệ.
2. Tìm đoạn con NGẮN NHẤT đạt ngưỡng yêu cầu (ví dụ: tổng ≥ S)
- Khi nào thu hẹp
l: Trong vòngwhile, chừng nào cửa sổ đã đạt yêu cầu (cur >= S), ta thử ép nhỏ nó lại xem có ngắn hơn được nữa không. - Khi nào cập nhật
best: Đặt câu lệnhbest = min(best, r - l + 1)ngay bên trong vòngwhile, trước dòng trừa[l]và tăngl++. Lý do là chỉ khi đang ở bên trong vòngwhile (cur >= S)thì cửa sổ mới thỏa mãn điều kiện>= S; ngay khi thoát khỏiwhilethì tổng đã tụt xuống< Srồi!
3. Đếm số lượng đoạn con liên tiếp thỏa mãn điều kiện (ví dụ: đếm số đoạn có tổng ≤ S)
- Sau khi vòng
while (cur > S)kết thúc, nếu cả đoạn lớn[l..r]có tổng≤ S(với mảng toàn số dương), thì mọi đoạn con kết thúc tạirvà bắt đầu tại một vị trí bất kỳ từlđếnr([l..r],[l+1..r], ...,[r..r]) cũng đều có tổng≤ S. - Có đúng
r - l + 1đoạn con như vậy kết thúc tạir. Do đó ở mỗi bướcr, ta chỉ cần cộng dồncount += (r - l + 1)là đếm được toàn bộ số đoạn con hợp lệ trên mảng chỉ trong O(n)!
| Mục tiêu bài toán (trên mảng số dương) | Điều kiện vòng lặp while |
Vị trí và công thức cập nhật kết quả |
|---|---|---|
Đoạn dài nhất có tổng ≤ S |
while (cur > S) (co khi vi phạm) |
Ngoài while: best = max(best, r - l + 1) |
Đoạn ngắn nhất có tổng ≥ S |
while (cur >= S) (co khi đã đủ) |
Trong while: best = min(best, r - l + 1) rồi mới cur -= a[l]; l++ |
Đếm số đoạn con có tổng ≤ S |
while (cur > S) (co khi vi phạm) |
Ngoài while: ans += (r - l + 1) |
Cửa sổ trượt trên xâu ký tự và mảng tần suất
Không chỉ áp dụng cho tổng số học, cửa sổ trượt co giãn còn là công cụ số một để xử lý xâu ký tự (string) và các bài toán đếm số phần tử phân biệt. Thay vì biến cur lưu tổng, trạng thái của cửa sổ [l..r] lúc này là một mảng đếm tần suất cnt (kích thước 256 cho bảng mã ASCII hoặc bảng băm) cùng biến distinct đếm số loại ký tự khác nhau đang có mặt trong cửa sổ:
- Khi mở rộng biên phải
r(thêm ký tựs[r]): nếucnt[s[r]] == 0(ký tự này chưa từng xuất hiện trong cửa sổ hiện tại), ta tăngdistinct++. Sau đó tăngcnt[s[r]]++. - Khi thu hẹp biên trái
l(bớt ký tựs[l]): ta giảmcnt[s[l]]--. Nếu sau khi giảm màcnt[s[l]] == 0(ký tự này đã rút hết khỏi cửa sổ), ta giảmdistinct--. Sau đó tăngl++.
Với cách duy trì bảng tần suất này, mỗi bước mở rộng hay thu hẹp vẫn chỉ tốn O(1) phép gán. Bạn có thể giải ngay bài toán kinh điển "Tìm xâu con dài nhất không chứa ký tự lặp lại" (giữ điều kiện cnt[s[r]] <= 1) hoặc "Tìm đoạn con dài nhất chứa không quá K giá trị khác nhau" (giữ điều kiện distinct <= K) chỉ trong thời gian tuyến tính O(n).
Vì sao có vòng while nằm trong vòng for mà vẫn là O(n)?
Đây là điểm hay nhất của cửa sổ trượt co giãn. Thoạt nhìn, vòng while (cur > S) nằm bên trong vòng for (int r = 0; r < n; ++r) trông giống hệt hai vòng lặp lồng nhau O(n²).
Nhưng hãy nhìn vào biến l:
- Biến
lkhởi tạo bằng0ở ngoài cùng. - Mỗi lần thân vòng
whilechạy,llại tăng lên1(l++) và không bao giờ bị đặt lại về 0. - Do
lkhông thể vượt quán, nên tính gộp trên toàn bộ cả chương trình, thân vòngwhilechỉ có thể thực hiện tối đanlần!
Cộng n bước của vòng for với tối đa n bước của vòng while, tổng số phép tính chỉ là 2n, tức độ phức tạp thời gian là O(n) và bộ nhớ phụ là O(1).
Khi nào dùng được Cửa sổ trượt, khi nào không?
| Bài toán | Có dùng cửa sổ trượt được không? | Giải thích |
|---|---|---|
Tổng lớn nhất của đoạn con có độ dài cố định k (mảng có cả số âm lẫn số dương) |
Dùng được (Dạng cố định) | Kích thước cửa sổ luôn bằng k, công thức + a[i] - a[i - k] đúng với mọi số âm hay dương |
Đoạn con dài nhất có tổng ≤ S trên mảng toàn số không âm |
Dùng được (Dạng co giãn) | Vì mọi số ≥ 0, thêm phần tử luôn làm tổng tăng và bớt phần tử luôn làm tổng giảm (đơn điệu) |
Đoạn con dài nhất có tổng ≤ S hoặc bằng S khi mảng có cả số âm |
Không dùng được dạng co giãn | Khi có số âm, việc bớt một số âm ở đầu trái lại làm tổng của cửa sổ tăng lên, phá vỡ tính đơn điệu. Lúc này cần dùng mảng cộng dồn kết hợp bảng băm hoặc tìm kiếm nhị phân |
Đoạn con dài nhất có không quá k phần tử khác nhau (hoặc xâu con không lặp ký tự) |
Dùng được (Dạng co giãn) | Thêm ký tự chỉ có thể làm số loại ký tự tăng hoặc giữ nguyên; bớt ký tự chỉ làm giảm hoặc giữ nguyên |
Lỗi hay gặp
- Nhầm chỉ số phần tử rời khỏi cửa sổ cố định. Khi đầu phải của cửa sổ cố định độ dài
kđang ở chỉ sối, các phần tử trong cửa sổ lài - k + 1 .. i, còn phần tử vừa rơi ra ngoài ở ngay bước này làa[i - k](nhiều bạn viết nhầm thànha[i - k + 1]). - Tính sai độ dài của cửa sổ
[l..r]. Một đoạn đóng từ chỉ sốlđến chỉ sốrchứar - l + 1phần tử (không phảir - l). Ví dụ khil = 4, r = 7, đoạn gồm các vị trí4, 5, 6, 7nên có7 - 4 + 1 = 4phần tử. - Đặt biến kết quả
best = 0khi tìm tổng lớn nhất mà mảng có toàn số âm. Với bài tìm tổng lớn nhất của cửa sổ độ dàik, hãy khởi tạobestbằng chính tổng của cửa sổ đầu tiên (sum(a[0..k-1])), không khởi tạo bằng0.
Luyện ngay trên Big-O Coding
Thử sức ngay với các bài tập áp dụng trực tiếp kỹ thuật cửa sổ trượt trên hệ thống Big-O Coding:
Câu hỏi thường gặp
Cửa sổ trượt khác gì với Kỹ thuật hai con trỏ?
Cửa sổ trượt thực chất là một trường hợp con của kỹ thuật hai con trỏ khi cả hai con trỏ l và r cùng đi từ trái sang phải để đóng khung một đoạn con liên tiếp và ta quan tâm tới thuộc tính của cả đoạn nằm giữa [l..r] (tổng đoạn, số phần tử phân biệt trong đoạn), thay vì chỉ quan tâm tới hai phần tử ở hai đầu mút a[l] và a[r].
Muốn tìm đoạn con NGẮN NHẤT có tổng ≥ S (mảng dương) thì sửa vòng lặp thế nào?
Thay vì cập nhật kết quả ở ngoài vòng while, bạn đảo điều kiện thành while (cur >= S): chừng nào cửa sổ hiện tại [l..r] đã đủ lớn (cur >= S), bạn ghi nhận ngay độ dài best = min(best, r - l + 1) rồi thử thu hẹp biên trái (cur -= a[l]; l++) xem có rút ngắn thêm được nữa không.
Làm sao tìm giá trị lớn nhất trong mỗi cửa sổ trượt độ dài K trong O(n)?
Khác với phép cộng có thể trừ ngược a[i - k], phép lấy max không thể "bớt" phần tử cũ bằng một phép toán số học. Để tìm max trên mọi cửa sổ độ dài k trong O(n), bạn sẽ kết hợp cửa sổ trượt với một hàng đợi hai đầu đơn điệu (monotonic deque) — phần này nằm trong chủ đề Cấu trúc dữ liệu và giải thuật.
Luyện phỏng vấn Google, Meta, Amazon theo Interview Patterns: 16 buổi online live, lớp 7–15 người, C++/Python/Java. Học thử buổi 1 miễn phí.


