↔️ Deque – Hàng đợi hai đầu
Deque cho phép thêm và xóa ở cả hai đầu. Ngoài mô phỏng hai đầu, deque còn là nền tảng của Sliding Window Maximum/Minimum, Monotonic Deque, tối ưu DP và 0–1 BFS.
🎯 Mục tiêu bài học
Cần hiểu
- Khác nhau giữa stack, queue và deque.
- Các thao tác hai đầu và điều kiện kiểm tra rỗng.
- Vì sao Sliding Window Maximum cần lưu chỉ số.
- Cách duy trì deque đơn điệu.
- Vì sao 0–1 BFS dùng push_front/push_back.
Dấu hiệu nhận biết
- Chọn hoặc xóa ở đầu trái/phải.
- Mỗi đoạn liên tiếp độ dài k.
- Cần max/min cửa sổ trong O(n).
- Loại bỏ phần tử không còn hữu ích.
- Đồ thị chỉ có trọng số 0 hoặc 1.
1. Đặt vấn đề
Một dãy số cho phép thêm hoặc xóa ở cả đầu trái và đầu phải. Hãy xử lý q thao tác:
push_front x, push_back x, pop_front, pop_back.
1.1. Vì sao stack hoặc queue chưa đủ?
| Cấu trúc | Thêm | Xóa | Hạn chế |
|---|---|---|---|
| Stack | Một đầu | Cùng đầu đó | Chỉ LIFO. |
| Queue | Cuối | Đầu | Chỉ FIFO. |
| Deque | Cả hai đầu | Cả hai đầu | Linh hoạt nhất cho bài hai đầu. |
Cho mảng n phần tử và số k. Với mỗi đoạn liên tiếp có đúng k phần tử, hãy in giá trị lớn nhất.
Nếu mỗi cửa sổ đều duyệt lại k phần tử, tổng thời gian là O(nk). Với n lớn, cách này dễ TLE.
1.2. Quan sát dẫn tới Monotonic Deque
- Phần tử đã ra khỏi cửa sổ phải bị loại khỏi đầu deque.
- Nếu một phần tử mới lớn hơn phần tử ở cuối deque, phần tử cuối đó không thể trở thành max trong tương lai.
- Deque chỉ giữ các chỉ số còn hữu ích theo thứ tự giá trị giảm dần.
2. Deque cơ bản trong C++
Thao tác
push_front(x)push_back(x)pop_front()pop_back()front(),back()empty(),size(),clear()
Độ phức tạp
- Thêm/xóa ở hai đầu: O(1).
- Truy cập
dq[i]: O(1). - Chèn/xóa giữa: O(n).
- Không đảm bảo vùng nhớ liên tiếp như vector.
3. Mô phỏng Deque và highlight code
4. Sliding Window Maximum bằng Monotonic Deque
Mảng và cửa sổ hiện tại
Deque chỉ số
4.1. Ba bước tại vị trí i
- Xóa đầu deque nếu chỉ số đã ra khỏi cửa sổ:
dq.front() <= i-k. - Xóa cuối khi
a[dq.back()] <= a[i]. - Đưa i vào cuối. Khi cửa sổ đủ k phần tử, max là
a[dq.front()].
5. 0–1 BFS
Với đồ thị có trọng số cạnh chỉ 0 hoặc 1, khi relax:
Deque
6. Phân tích thuật toán
| Bài toán | Cách đơn giản | Dùng deque | Giải thích |
|---|---|---|---|
| Mô phỏng hai đầu | Vector có thể O(n) khi xóa đầu | O(1)/thao tác | Deque hỗ trợ trực tiếp hai đầu. |
| Max/min mỗi cửa sổ k | O(nk) | O(n) | Mỗi chỉ số vào và ra deque tối đa một lần. |
| 0–1 BFS | Dijkstra O((V+E)log V) | O(V+E) | Trọng số chỉ 0 hoặc 1. |
| DP có cửa sổ chuyển trạng thái | O(nk) | O(n) | Duy trì min/max của miền trạng thái hợp lệ. |
7. Code mẫu
#include <bits/stdc++.h>
using namespace std;
int main() {
deque<int> dq;
dq.push_front(3);
dq.push_back(5);
dq.push_front(2);
cout << dq.front() << ' ';
cout << dq.back() << '\n';
if (!dq.empty()) dq.pop_front();
if (!dq.empty()) dq.pop_back();
for (int x : dq) cout << x << ' ';
}#include <bits/stdc++.h>
using namespace std;
int main() {
int n, k;
cin >> n >> k;
vector<long long> a(n);
for (long long &x : a) cin >> x;
deque<int> dq;
for (int i = 0; i < n; i++) {
while (!dq.empty() && dq.front() <= i - k) {
dq.pop_front();
}
while (!dq.empty() && a[dq.back()] <= a[i]) {
dq.pop_back();
}
dq.push_back(i);
if (i >= k - 1) {
cout << a[dq.front()] << ' ';
}
}
}#include <bits/stdc++.h>
using namespace std;
const int INF = 1e9;
int main() {
int n, m, source;
cin >> n >> m >> source;
vector<vector<pair<int,int>>> adj(n + 1);
for (int i = 0; i < m; i++) {
int u, v, w;
cin >> u >> v >> w;
adj[u].push_back({v, w});
adj[v].push_back({u, w});
}
vector<int> dist(n + 1, INF);
deque<int> dq;
dist[source] = 0;
dq.push_front(source);
while (!dq.empty()) {
int u = dq.front();
dq.pop_front();
for (auto [v, w] : adj[u]) {
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
if (w == 0) dq.push_front(v);
else dq.push_back(v);
}
}
}
}from collections import deque
dq = deque()
dq.appendleft(3)
dq.append(5)
left = dq[0]
right = dq[-1]
dq.popleft()
dq.pop()8. Giải thích code
8.1. Vì sao phải kiểm tra empty?
Gọi front(), back(), pop_front() hoặc pop_back() khi deque rỗng gây hành vi không xác định.
8.2. Vì sao Sliding Window lưu chỉ số?
Giá trị không cho biết phần tử thuộc vị trí nào. Chỉ số giúp kiểm tra phần tử đã rời cửa sổ bằng dq.front() <= i-k.
8.3. Vì sao xóa các phần tử nhỏ hơn ở cuối?
Nếu a[j] <= a[i] và j đứng trước i, thì i vừa mới hơn vừa không nhỏ hơn j. Trong mọi cửa sổ tương lai chứa cả hai, j không bao giờ là max.
8.4. Vì sao Sliding Window là O(n)?
8.5. Vì sao 0–1 BFS đúng?
Deque giữ các đỉnh theo khoảng cách không giảm. Relax cạnh 0 giữ nguyên khoảng cách nên đẩy lên đầu; cạnh 1 tăng khoảng cách nên đẩy xuống cuối.
9. Lỗi thường gặp
- Không kiểm tra deque rỗng trước khi truy cập hoặc pop.
- Sliding Window lưu giá trị thay vì chỉ số.
- Xóa phần tử hết hạn bằng điều kiện sai: đúng là
dq.front() <= i-k. - Đặt bước loại phần tử hết hạn sau khi lấy kết quả.
- Dùng dấu so sánh sai khiến xử lý phần tử bằng nhau không nhất quán.
- Cho rằng deque luôn tốt hơn vector; deque không có vùng nhớ liên tiếp.
- Dùng 0–1 BFS khi trọng số có giá trị khác 0 và 1.
- Trong bài game hai đầu, áp dụng greedy lấy đầu lớn hơn mà chưa chứng minh.
10. Quiz và bài tập
📘 Cơ bản
- Mô phỏng q thao tác deque.
- Kiểm tra palindrome.
- Đảo dãy bằng push_front.
📗 Trung bình
- Sliding Window Max.
- Sliding Window Min.
- Max−min mỗi cửa sổ.
📙 Nâng cao
- DP tối ưu bằng monotonic deque.
- Shortest subarray có tổng ≥ K.
- Constrained subsequence sum.
🐉 HSG
- 0–1 BFS trên lưới.
- Dial's algorithm định hướng.
- Deque optimization trong DP.
💳 Quét mã ủng hộ tuỳ tâm nhé!