Khoá học C++ cơ bản
Bài học: Kỹ Thuật Cửa Sổ Trượt
🏆 +100 XP tiềm năng Thoát

Kỹ Thuật Cửa Sổ Trượt

2 khối nội dung 45 phút học tập MỞ BẢNG TRẮNG

Kỹ Thuật Cửa Sổ Trượt

Nội dung bài học

1. Khái niệm & bản chất của kỹ thuật cửa sổ trượt

Kỹ thuật Cửa sổ trượt (Sliding Window Technique) là phương pháp tối ưu hóa trên cấu trúc dữ liệu mảng hoặc chuỗi nhằm giải quyết các bài toán liên quan đến đoạn con liên tiếp (Contiguous Subarray / Substring).

Thay vì phải tính toán lại từ đầu hàm mục tiêu trên từng đoạn con $[i \dots j]$ với độ phức tạp $\mathcal{O}(K)$ hoặc $\mathcal{O}(N)$, kỹ thuật này duy trì một "khung cửa sổ" $[L \dots R]$ và cập nhật trạng thái mục tiêu trong $\mathcal{O}(1)$ thời gian bằng cách: $$\text{State}_{\text{mới}} = \text{State}_{\text{cũ}} + \text{Phần tử nạp vào } A_R - \text{Phần tử nhả ra } A_{L-1}$$

2. Cơ chế chuyển dịch & phân tích độ phức tạp $\mathcal{O}(N)$

2.1. Cơ chế hai con trỏ cùng chiều ($L \longrightarrow R$)

  • Con trỏ phải $R$ (Right / Lead pointer): Mở rộng biên phải để nạp thêm phần tử $A_R$ vào cửa sổ nhằm thỏa mãn điều kiện bài toán.
  • Con trỏ trái $L$ (Left / Trail pointer): Co hẹp biên trái để loại bỏ phần tử $A_L$ ra khỏi cửa sổ nhằm tối ưu hóa kích thước hoặc khôi phục tính hợp lệ của cửa sổ.

2.2. Phân tích chi phí khấu hao (amortized complexity analysis)

Mặc dù thuật toán thường được cài đặt dưới dạng một vòng lặp while lồng bên trong một vòng lặp for:

  • Con trỏ $R$ duyệt từ $0$ đến $N - 1$ (thực hiện đúng $N$ bước tăng).
  • Con trỏ $L$ duyệt từ $0$ đến $N$ (thực hiện tối đa $N$ bước tăng).
  • Mỗi phần tử trong mảng chỉ đi vào cửa sổ đúng 1 lần và ra khỏi cửa sổ tối đa 1 lần.

Do đó, tổng số thao tác thêm/bớt phần tử trong toàn bộ chương trình không bao giờ vượt quá $2N$. Độ phức tạp thời gian đạt $\mathcal{O}(N)$ tuyến tính tuyệt đối.

3. Phân loại hai dạng cửa sổ trượt chuẩn mực

3.1. Dạng 1: Cửa sổ cố định độ dài $K$ (fixed-size window)

Áp dụng cho các bài toán yêu cầu khảo sát mọi đoạn con liên tiếp có độ dài đúng bằng $K$.

  • Công thức trượt $\mathcal{O}(1)$:
  • Khởi tạo: $\text{Current\_Sum} = \sum_{i=0}^{K-1} A_i$.
  • Trượt từ vị trí $i = K$ đến $N - 1$: $$\text{Current\_Sum} \leftarrow \text{Current\_Sum} + A_i - A_{i-K}$$

  • Cập nhật giá trị cực trị: $\text{Ans} = \max(\text{Ans}, \text{Current\_Sum})$.

Ví dụ minh họa 1: Tìm tổng đoạn con $K = 3$ lớn nhất trên dãy $A = [2, 1, 5, 1, 3, 2]$

Phần tử $A[0]$ $A[1]$ $A[2]$ $A[3]$ $A[4]$ $A[5]$
Giá trị $\mathbf{2}$ $\mathbf{1}$ $\mathbf{5}$ $\mathbf{1}$ $\mathbf{3}$ $\mathbf{2}$

Bảng mô phỏng quá trình trượt cửa sổ:

Vị trí $i$ Đoạn cửa sổ Thao tác nạp / loại Tổng mới ($\text{Sum}$) $\text{Max\_Sum}$
Khởi tạo ($i=2$) $[2, 1, 5]$ (đoạn $0..2$) Tính tổng $3$ phần tử đầu $2 + 1 + 5 = \mathbf{8}$ $\mathbf{8}$
$i = 3$ $[1, 5, 1]$ (đoạn $1..3$) $+ A_3(1) - A_0(2)$ $8 + 1 - 2 = \mathbf{7}$ $8$
$i = 4$ $[5, 1, 3]$ (đoạn $2..4$) $+ A_4(3) - A_1(1)$ $7 + 3 - 1 = \mathbf{9}$ $\mathbf{9}$
$i = 5$ $[1, 3, 2]$ (đoạn $3..5$) $+ A_5(2) - A_2(5)$ $9 + 2 - 5 = \mathbf{6}$ $9$

$$\implies \text{Kết quả: Tổng lớn nhất của đoạn dài 3 là } \mathbf{9} \text{ (đoạn } [5, 1, 3]\text{), trượt trong đúng } \mathcal{O}(1) \text{ mỗi bước!}$$

3.2. Dạng 2: Cửa sổ biến thiên (variable-size window)

Áp dụng cho các bài toán tìm đoạn con liên tiếp dài nhất/ngắn nhất hoặc đếm số lượng đoạn con thỏa mãn điều kiện $f([L \dots R])$.

Dạng Bài Toán Chiến Lược Điều Khiển Con Trỏ Công Thức Cập Nhật Kết Quả
Đoạn con ngắn nhất có tổng $\ge S$ Mở $R$ cho đến khi $\text{Sum} \ge S$, sau đó co $L$ tối đa để tìm $\min(R - L + 1)$ $\text{Min\_Len} = \min(\text{Min\_Len}, R - L + 1)$
Đoạn con dài nhất có tổng $\le S$ Mở $R$, nếu $\text{Sum} > S$ thì co $L$ cho đến khi $\text{Sum} \le S$ $\text{Max\_Len} = \max(\text{Max\_Len}, R - L + 1)$
Đếm số lượng đoạn con có tổng $\le S$ Mở $R$, co $L$ cho đến khi $\text{Sum} \le S$. Mọi đoạn con kết thúc tại $R$ bắt đầu từ $[L \dots R]$ đều thỏa mãn $\text{Total} \leftarrow \text{Total} + (R - L + 1)$

Ví dụ minh họa 2: Tìm đoạn con ngắn nhất có tổng $\ge S = 7$ trên $A = [2, 3, 1, 2, 4, 3]$

Bước ($R$) Nạp $A_R$ Tổng cửa sổ Đánh giá $\ge 7$ Thao tác co $L$ & Độ dài tìm được $\text{Min\_Len}$
$R = 0$ $A_0 = 2$ $2$ Chưa đủ $\infty$
$R = 1$ $A_1 = 3$ $5$ Chưa đủ $\infty$
$R = 2$ $A_2 = 1$ $6$ Chưa đủ $\infty$
$R = 3$ $A_3 = 2$ $8$ $\ge 7$ (Thỏa mãn) Co $L=0 \to 1$ (bỏ $A_0=2$, tổng còn $6 < 7$) $\implies$ Đoạn $[3, 1, 2]$ dài $3$ $3$
$R = 4$ $A_4 = 4$ $10$ $\ge 7$ (Thỏa mãn) Co $L=1 \to 3$ (bỏ $A_1, A_2$, tổng còn $6 < 7$) $\implies$ Đoạn $[2, 4]$ dài $2$ $2$
$R = 5$ $A_5 = 3$ $9$ $\ge 7$ (Thỏa mãn) Co $L=3 \to 5$ (bỏ $A_3, A_4$, tổng còn $3 < 7$) $\implies$ Đoạn $[4, 3]$ dài $2$ $2$

$$\implies \text{Kết quả: Độ dài ngắn nhất là } \mathbf{2} \text{ (đoạn } [2, 4] \text{ hoặc } [4, 3]\text{)!}$$

4. Điều kiện áp dụng & giới hạn thất bại khi mảng có số âm

4.1. Điều kiện tiên quyết: Tính đơn điệu của hàm trạng thái

Cửa sổ trượt biến thiên bắt buộc yêu cầu hàm mục tiêu phải có tính đơn điệu:

  • Khi mở rộng $R$ ($R \to R + 1$): Trạng thái phải tăng dần (hoặc không giảm).
  • Khi co hẹp $L$ ($L \to L + 1$): Trạng thái phải giảm dần (hoặc không tăng).

Đối với bài toán tổng đoạn con, điều này tương đương với điều kiện: Tất cả các phần tử trong mảng phải là số không âm ($A_i \ge 0$).

4.2. Giới hạn: Vì sao Sliding Window thất bại khi có số âm?

Xét mảng $A = [2, -5, 10, -2, 8]$ với mục tiêu tìm đoạn con ngắn nhất có tổng $\ge 8$.

  • Khi $R$ nạp thêm số âm $-5$, tổng cửa sổ bị giảm.
  • Khi $L$ dịch qua số âm $-5$, tổng cửa sổ lại tăng lên.
  • Tính chất đơn điệu bị phá vỡ $\implies$ Con trỏ $L$ không thể đưa ra quyết định di chuyển một chiều chắc chắn $\implies$ Bỏ sót nghiệm tối ưu.
  • Giải pháp chuẩn: Chuyển sang sử dụng Mảng cộng dồn (Prefix Sum) kết hợp Hàng đợi hai đầu (Deque) / Cây chỉ số Fenwick / Segment Tree.

5. Mẫu cài đặt chuẩn thi đấu (competitive template)

Mẫu: Đoạn con liên tiếp ngắn nhất có tổng $\ge S$ ($A_i \ge 0$)

# include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    long long s;
    if (!(cin >> n >> s)) return 0;

    vector<long long> a(n);

    for (int i = 0; i < n; ++i) {
        cin >> a[i];

    }

    int l = 0;
    long long current_sum = 0;
    int min_len = n + 1; // Khởi tạo vô cực

    // Duyệt con trỏ R tuyến tính O(N)
    for (int r = 0; r < n; ++r) {
        current_sum += a[r]; // Nạp a[r] vào cửa sổ

        // Co hẹp con trỏ L khi cửa sổ đã thỏa mãn điều kiện
        while (current_sum >= s) {
            min_len = min(min_len, r - l + 1); // Cập nhật độ dài nhỏ nhất
            current_sum -= a[l];               // Nhả a[l] ra khỏi cửa sổ
            ++l;                               // Dịch chuyển biên trái
        }
    }

    if (min_len > n) {

        cout << 0 << "\n"; // Không tồn tại đoạn thỏa mãn
    } else {
        cout << min_len << "\n";
    }

    return 0;
}

6. Kỹ thuật cửa sổ trượt với bảng đếm ký tự / trạng thái

Khi xử lý bài toán chuỗi ký tự (như Đoạn con dài nhất chứa tối đa $K$ ký tự khác nhau):

  • Sử dụng mảng đếm tần suất int count[256] hoặc int count[26] và biến distinct_count lưu số ký tự khác nhau hiện có trong cửa sổ.
  • Khi nạp ký tự $S[R]$: nếu count[S[R]] == 0, tăng distinct_count. Tăng count[S[R]]++.
  • Khi distinct_count > K: co con trỏ $L$, giảm count[S[L]]--; nếu count[S[L]] == 0, giảm distinct_count. Tăng ++L.

7. Các bẫy lỗi thường gặp (bug traps)

  1. Bẫy tràn số nguyên khi tính tổng cửa sổ: Tổng đoạn con của mảng $N = 10^5$ phần tử với $A_i = 10^9$ có thể lên tới $10^{14}$. Khai báo biến current_sum kiểu long long.
  2. Bẫy điều kiện khởi tạo kết quả cực trị: Khi tìm $\min$, khởi tạo ans = n + 1 (hoặc $\infty$); khi không tìm thấy nghiệm phải in ra 0 hoặc -1 theo đúng quy cách đề bài.
  3. Bẫy chỉ số âm khi trượt cửa sổ cố định: Luôn đảm bảo chỉ thực hiện phép trừ a[i - k] khi chỉ số $i \ge K$.
1
A
B
C
D
2
A
B
C
D
3
A
B
C
D
4
A
B
C
D
5
A
B
C
D
6
A
B
C
D
7
A
B
C
D
8
A
B
C
D
9
A
B
C
D
10
A
B
C
D

Bài Tập Thực Hành & Rèn Luyện

14 Bài tập thực hành

Nhấn vào nút "Làm bài" bên dưới để mở giao diện làm bài trực tuyến (Online Judge) và thực hiện viết mã nguồn cho bài tập này.

Tổng Cửa Sổ Cố Định K
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Giá Trị Trung Bình Lớn Nhất Của Đoạn K
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Đoạn Con Ngắn Nhất Có Tổng Đạt S
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Đoạn Con Dài Nhất Có Tổng Không Quá S
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Đoạn Con Chứa Tối Đa K Số 0 (Lật Bit)
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Giám Sát Camera Giao Thông Thông Minh
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Tìm Min Trong Mọi Cửa Sổ Độ Dài K
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Đếm Số Lượng Đoạn Con Có Tổng Không Quá S
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Đếm Số Lượng Đoạn Con Có Tổng Đúng Bằng S
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Đoạn Con Dài Nhất Chứa Tối Đa K Ký Tự Khác Nhau
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Đoạn Con Ngắn Nhất Chứa Đủ Mọi Ký Tự Của Tập Hợp
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Phủ Sóng Trạm Phát Sóng Wifi Đô Thị
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Đoạn Con Có Độ Chênh Lệch Max - Min Không Quá K
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài
Tối Ưu Cửa Sổ Trượt Tuyến Tính Khi N = 2.10⁵
Độ khó: mediumĐiểm tối đa: 100.0 điểm
Làm bài