Bài 15: Deque & Sliding Window¶
Tác giả: FPTOJ Team
Nội dung tham khảo từ: VNOI Wiki - Deque, Hàng đợi hai đầu
Bản chất vấn đề¶
Bài toán: Tìm max trong đoạn "trượt"¶
Cho dãy \(N\) số nguyên và cửa sổ kích thước \(K\) trượt từ trái sang phải. Với mỗi vị trí, tìm giá trị lớn nhất trong cửa sổ.
Ví dụ với dãy \(a = [1, 3, -1, -3, 5, 3, 6, 7]\) và \(K = 3\):
| Vị trí cửa sổ | Các phần tử | Giá trị max |
|---|---|---|
| \(0 \dots 2\) | \([1, 3, -1]\) | \(3\) |
| \(1 \dots 3\) | \([3, -1, -3]\) | \(3\) |
| \(2 \dots 4\) | \([-1, -3, 5]\) | \(5\) |
| \(3 \dots 5\) | \([-3, 5, 3]\) | \(5\) |
| \(4 \dots 6\) | \([5, 3, 6]\) | \(6\) |
| \(5 \dots 7\) | \([3, 6, 7]\) | \(7\) |
Cách "ngốc": Với mỗi vị trí, duyệt \(K\) phần tử \(\Rightarrow O(NK)\). Quá chậm!
Dùng Deque: \(O(N)\)! Mỗi phần tử chỉ push/pop tối đa 1 lần.
Deque là gì?¶
Deque = Double-Ended Queue = Hàng đợi hai đầu. Thêm/xóa được ở cả đầu lẫn cuối.
| Thao tác | Ý nghĩa | Độ phức tạp |
|---|---|---|
push_front(x) |
Thêm đầu | \(O(1)\) |
push_back(x) |
Thêm cuối | \(O(1)\) |
pop_front() |
Xóa đầu | \(O(1)\) |
pop_back() |
Xóa cuối | \(O(1)\) |
front() |
Xem đầu | \(O(1)\) |
back() |
Xem cuối | \(O(1)\) |
Tư duy cốt lõi¶
Hàng đợi đơn điệu (Monotonic Queue)¶
Bài toán sliding window yêu cầu tìm max/min trong mỗi cửa sổ. Nếu chỉ dùng mảng thường, mỗi lần phải duyệt toàn bộ cửa sổ \(\Rightarrow O(NK)\).
Hàng đợi đơn điệu (Monotonic Queue) là kỹ thuật giữ deque luôn sắp xếp theo một thứ tự (tăng dần hoặc giảm dần). Khi thêm phần tử mới, ta loại bỏ tất cả phần tử "vô dụng" ở cuối.
Tại sao phần tử bị loại bỏ là "vô dụng"?¶
Giả sử deque đang lưu các chỉ số có giá trị giảm dần: \([i_1, i_2, i_3]\) với \(a[i_1] > a[i_2] > a[i_3]\).
Khi thêm phần tử mới \(a[i]\): nếu \(a[i_3] \le a[i]\) thì \(a[i_3]\) không bao giờ là max nữa vì:
- \(a[i]\) lớn hơn \(a[i_3]\)
- \(a[i]\) vào cửa sổ sau \(a[i_3]\) nên sẽ tồn tại lâu hơn \(a[i_3]\)
\(\Rightarrow\) \(a[i_3]\) vô dụng, loại bỏ!
Đây chính là lý do deque luôn giảm dần: mọi phần tử trong deque đều có "cơ hội" trở thành max trong tương lai.
Ẩn dụ: Hàng người chờ tuyển chọn¶
Tưởng tượng bạn là tuyển thủ, đứng trong hàng theo thứ tự đến. Nếu có người phía sau mạnh hơn bạn, bạn sẽ không bao giờ được chọn (vì người đó đến sau, tồn tại lâu hơn). Bạn bị loại khỏi hàng!
Phân tích tính đúng đắn¶
Sliding Window Maximum¶
Ý tưởng: Duyệt mảng từ trái sang phải, giữ deque lưu chỉ số (không lưu giá trị) theo thứ tự giá trị giảm dần. Với mỗi phần tử \(a[i]\), thực hiện 3 bước:
- Loại bỏ phần tử "thoát" khỏi cửa sổ (ở đầu deque): nếu chỉ số đầu \(\le i - K\), pop front.
- Loại bỏ phần tử cuối \(\le a[i]\) (giữ deque giảm dần): vì \(a[i]\) lớn hơn sẽ "che" chúng.
- Thêm chỉ số \(i\) vào cuối deque. Nếu cửa sổ đã đầy (\(i \ge K - 1\)), ghi nhận \(a[\text{front}]\) là max.
Bước chạy chi tiết¶
Với \(a = [1, 3, -1, -3, 5, 3, 6, 7]\), \(K = 3\):
| \(i\) | \(a[i]\) | Thao tác | Deque (chỉ số) | Deque (giá trị) | Ghi nhận max |
|---|---|---|---|---|---|
| \(0\) | \(1\) | Thêm \(0\) | \([0]\) | \([1]\) | Chưa đầy |
| \(1\) | \(3\) | Loại \(0\) (\(1 \le 3\)), thêm \(1\) | \([1]\) | \([3]\) | Chưa đầy |
| \(2\) | \(-1\) | Thêm \(2\) | \([1, 2]\) | \([3, -1]\) | \(a[1] = 3\) |
| \(3\) | \(-3\) | Front \(1 > 0\), thêm \(3\) | \([1, 2, 3]\) | \([3, -1, -3]\) | \(a[1] = 3\) |
| \(4\) | \(5\) | Loại front \(1 \le 1\), loại \(3\) (\(-3 \le 5\)), loại \(2\) (\(-1 \le 5\)), thêm \(4\) | \([4]\) | \([5]\) | \(a[4] = 5\) |
| \(5\) | \(3\) | Thêm \(5\) | \([4, 5]\) | \([5, 3]\) | \(a[4] = 5\) |
| \(6\) | \(6\) | Loại \(5\) (\(3 \le 6\)), loại \(4\) (\(5 \le 6\)), thêm \(6\) | \([6]\) | \([6]\) | \(a[6] = 6\) |
| \(7\) | \(7\) | Loại \(6\) (\(6 \le 7\)), thêm \(7\) | \([7]\) | \([7]\) | \(a[7] = 7\) |
Kết quả: \([3, 3, 5, 5, 6, 7]\).
Sliding Window Minimum¶
Tìm giá trị nhỏ nhất trong mỗi cửa sổ \(\Rightarrow\) giữ deque tăng dần (thay vì giảm dần). Chỉ cần đảo dấu so sánh ở bước loại bỏ cuối.
Kết hợp cả min và max¶
Nhiều bài toán cần tìm cả min và max trong mỗi cửa sổ (ví dụ: chênh lệch max-min \(\le\) ngưỡng). Dùng hai deque cùng lúc!
Ứng dụng: Next Greater Element¶
Cho mảng \(A\), với mỗi phần tử \(A[i]\), tìm chỉ số \(j > i\) sao cho \(A[j] > A[i]\) đầu tiên. Nếu không tồn tại \(\Rightarrow -1\).
Tư duy: Duyệt từ phải sang trái, giữ deque có giá trị giảm dần. Với mỗi \(A[i]\): loại bỏ các phần tử đầu deque có giá trị \(\le A[i]\), phần tử đầu deque (nếu có) là next greater element.
Ví dụ: \(A = [4, 5, 2, 25]\), kết quả trả về chỉ số: \([1, 3, 3, -1]\).
- \(A[0] = 4\): next greater \(= A[1] = 5\)
- \(A[1] = 5\): next greater \(= A[3] = 25\)
- \(A[2] = 2\): next greater \(= A[3] = 25\)
- \(A[3] = 25\): không có \(\Rightarrow -1\)
Biến thể: Next Smaller Element¶
Tương tự, chỉ cần đảo dấu so sánh (\(\ge\) thay vì \(\le\)).
Ứng dụng: Largest Rectangle in Histogram¶
Đây là bài toán kinh điển kết hợp Next Smaller Element và Previous Smaller Element.
Đánh giá độ phức tạp¶
Độ phức tạp thời gian¶
- Sliding Window Maximum/Minimum: \(O(N)\) với \(N\) là độ dài mảng. Mỗi phần tử được push vào deque đúng 1 lần và pop ra tối đa 1 lần \(\Rightarrow\) tổng thao tác là \(O(2N) = O(N)\).
- Next Greater/Smaller Element: \(O(N)\), lý do tương tự.
- Largest Rectangle in Histogram: \(O(N)\), mỗi phần tử push/pop tối đa 1 lần.
Độ phức tạp không gian¶
- Sliding Window: \(O(K)\) cho deque (tối đa \(K\) phần tử trong cửa sổ).
- Next Greater/Smaller Element: \(O(N)\) cho deque trong trường hợp xấu nhất.
- Largest Rectangle: \(O(N)\) cho stack.
Lưu ý khi cài đặt¶
- Deque lưu chỉ số, không lưu giá trị (để kiểm tra cửa sổ có hợp lệ không).
- Monotonic Queue là kỹ thuật cốt lõi: deque luôn giảm dần (tìm max) hoặc tăng dần (tìm min).
- Với bài tìm min, chỉ cần đảo dấu so sánh ở bước loại bỏ phần tử cuối deque.
Bài tập luyện tập¶
| Bài | Nền tảng | Độ khó | Chủ đề |
|---|---|---|---|
| LeetCode - Sliding Window Maximum | LC | ⭐⭐⭐ | Deque cơ bản |
| LeetCode - Next Greater Element II | LC | ⭐⭐ | Stack/Deque |
| LeetCode - Largest Rectangle in Histogram | LC | ⭐⭐⭐ | Stack đơn điệu |
| VNOJ - NKSGAME | VNOJ | ⭐⭐ | Two pointers |
| VNOJ - VMQUABEO | VNOJ | ⭐⭐⭐ | Sliding window |
| CSES - Sliding Window Median | CSES | ⭐⭐⭐ | Sliding window nâng cao |
| CSES - Sliding Window Cost | CSES | ⭐⭐⭐ | Sliding window cost |
Bài viết liên quan¶
Tài liệu tham khảo¶
- GeeksforGeeks - Sliding Window Maximum
- USACO Guide - Sliding Window
- takeuforward - Sliding Window Maximum
- YouTube - Deque Sliding Window
- VNOI Wiki - Deque
Bài tiếp theo: Trie