STL + Sliding Window + Graph

↔️ 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.

Hai đầupush/pop front và back
O(1)Thao tác ở hai đầu
Sliding WindowMax/min mỗi cửa sổ O(n)
0–1 BFSĐường đi ngắn nhất O(V+E)

🎯 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 đề

Bài toán mở đầu 1 – mô phỏng hai đầu.
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úcThêmXóaHạn chế
StackMột đầuCùng đầu đóChỉ LIFO.
QueueCuốiĐầuChỉ FIFO.
DequeCả hai đầuCả hai đầuLinh hoạt nhất cho bài hai đầu.
Bài toán mở đầu 2 – cửa sổ trượt.
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.

Mục tiêu: duy trì phần tử lớn nhất của cửa sổ trong tổng thời gian O(n)

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.
Kết luận: deque vừa giải bài thao tác hai đầu, vừa là công cụ tối ưu giúp loại bỏ phần tử cũ và phần tử kém tối ưu.

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

deque<int> dq;
dq.push_front(x);
dq.push_back(x);
if (!dq.empty()) dq.pop_front();
if (!dq.empty()) dq.pop_back();
cout << dq.front() << dq.back();

4. Sliding Window Maximum bằng Monotonic Deque

Deque lưu chỉ số, không chỉ lưu giá trị. Ta cần biết phần tử nào đã ra khỏi cửa sổ bằng điều kiện chỉ số.

Mảng và cửa sổ hiện tại

Deque chỉ số

4.1. Ba bước tại vị trí i

  1. Xóa đầu deque nếu chỉ số đã ra khỏi cửa sổ: dq.front() <= i-k.
  2. Xóa cuối khi a[dq.back()] <= a[i].
  3. Đư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:

w = 0 → push_front(v),    w = 1 → push_back(v)

Deque

Trực giác: cạnh 0 tạo trạng thái ưu tiên ngang với đỉnh hiện tại nên đưa lên đầu; cạnh 1 có chi phí lớn hơn nên đưa xuống cuối.

6. Phân tích thuật toán

Bài toánCách đơn giảnDùng dequeGiải thích
Mô phỏng hai đầuVector có thể O(n) khi xóa đầuO(1)/thao tácDeque hỗ trợ trực tiếp hai đầu.
Max/min mỗi cửa sổ kO(nk)O(n)Mỗi chỉ số vào và ra deque tối đa một lần.
0–1 BFSDijkstra 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áiO(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)?

Mỗi chỉ số được push đúng một lần và bị pop nhiều nhất một lần ở mỗi đầu. Tổng số thao tác tuyến tính theo 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âu 1. Trong Sliding Window Maximum, deque nên lưu gì?
Câu 2. Điều kiện loại chỉ số hết hạn là gì?
Câu 3. Trong 0–1 BFS, cạnh trọng số 0 được xử lý thế nào?

📘 Cơ bản

  1. Mô phỏng q thao tác deque.
  2. Kiểm tra palindrome.
  3. Đảo dãy bằng push_front.

📗 Trung bình

  1. Sliding Window Max.
  2. Sliding Window Min.
  3. Max−min mỗi cửa sổ.

📙 Nâng cao

  1. DP tối ưu bằng monotonic deque.
  2. Shortest subarray có tổng ≥ K.
  3. Constrained subsequence sum.

🐉 HSG

  1. 0–1 BFS trên lưới.
  2. Dial's algorithm định hướng.
  3. Deque optimization trong DP.