Ngăn Xếp, Hàng Đợi & Deque
1. Khái niệm & bản chất của cấu trúc dữ liệu tuyến tính đơn điệu
Ngăn xếp (Stack - LIFO) và Hàng đợi (Queue - FIFO) là hai cấu trúc dữ liệu cơ sở có thời gian thêm và xóa ở đầu/cuối trong $\mathcal{O}(1)$.
Ở Level 2, ta nâng cấp lên Ngăn xếp đơn điệu (Monotonic Stack) và Hàng đợi hai đầu đơn điệu (Monotonic Deque) — hai công cụ tối ưu hóa cực mạnh giúp giải quyết các bài toán tìm kiếm phần tử lớn hơn/nhỏ hơn gần nhất và duy trì $\min/\max$ trên cửa sổ trượt trong thời gian tuyến tính $\mathcal{O}(N)$ (thay vì $\mathcal{O}(N^2)$ hoặc $\mathcal{O}(N \log K)$).
2. Ngăn xếp đơn điệu
2.1. Bài toán: Tìm phần tử lớn hơn gần nhất bên phải (Next Greater Element — NGE)
Cho mảng $A = [A_1, A_2, \dots, A_N]$. Với mỗi $i$, tìm chỉ số $j > i$ nhỏ nhất sao cho $A_j > A_i$.
-
Ý tưởng Monotonic Stack: Duyệt mảng từ phải sang trái (hoặc từ trái sang phải), duy trì một ngăn xếp chứa các phần tử giảm dần từ đáy lên đỉnh:
-
Khi xét phần tử $A_i$, loại bỏ tất cả các phần tử trên đỉnh ngăn xếp mà $\le A_i$ (vì chúng nhỏ hơn $A_i$ và nằm xa hơn, không bao giờ có thể là NGE cho các phần tử đứng trước $i$).
- Phần tử còn lại trên đỉnh ngăn xếp chính là NGE của $A_i$.
- Đẩy $A_i$ vào ngăn xếp.
vector<int> next_greater_element(const vector<int> &a) {
int n = a.size();
vector<int> res(n, -1);
stack<int> st; // Lưu chỉ số
for (int i = n - 1; i >= 0; --i) {
while (!st.empty() && a[st.top()] <= a[i]) {
st.pop();
}
if (!st.empty()) res[i] = st.top();
st.push(i);
}
return res;
}
Chứng minh độ phức tạp $\mathcal{O}(N)$: Mỗi phần tử chỉ được đẩy vào ngăn xếp đúng 1 lần và lấy ra khỏi ngăn xếp tối đa 1 lần $\implies$ Tổng số thao tác
push/poplà $2N$.
3. Hàng đợi hai đầu đơn điệu (Monotonic Deque)
3.1. Bài toán: Tìm giá trị nhỏ nhất trên mọi cửa sổ trượt độ dài $K$ (Sliding Window Minimum)
Cho mảng $A$ và kích thước cửa sổ $K$. Tìm $\min$ của mỗi cửa sổ con liên tiếp $K$ phần tử.
- Cơ chế Monotonic Deque:
Duy trì một
deque<int>lưu chỉ số sao cho giá trị tương ứng trong mảng luôn tăng dần từ đầu đến cuối:
- Loại bỏ phần tử ngoài cửa sổ: Nếu chỉ số ở đầu deque $\le i - K$, đẩy ra (
pop_front). - Duy trì tính đơn điệu: Trong khi đuôi deque có giá trị $\ge A_i$, đẩy ra (
pop_back) vì chúng vừa lớn hơn vừa già hơn $A_i$. - Thêm $i$ vào đuôi:
push_back(i). - Phần tử ở đầu deque
deque.front()luôn là $\min$ của cửa sổ hiện tại.
vector<int> sliding_window_min(const vector<int> &a, int k) {
int n = a.size();
vector<int> res;
deque<int> dq;
for (int i = 0; i < n; ++i) {
if (!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) res.push_back(a[dq.front()]);
}
return res;
}
4. Mẫu cài đặt chuẩn thi đấu: Diện tích hình chữ nhật lớn nhất trong biểu đồ cột (Histogram)
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
vector<long long> h(n);
for (int i = 0; i < n; ++i) cin >> h[i];
stack<int> st;
long long max_area = 0;
for (int i = 0; i <= n; ++i) {
long long cur_h = (i == n ? 0 : h[i]);
while (!st.empty() && cur_h < h[st.top()]) {
long long height = h[st.top()];
st.pop();
long long width = (st.empty() ? i : (i - st.top() - 1));
max_area = max(max_area, height * width);
}
st.push(i);
}
cout << max_area << "\n";
return 0;
}
5. Ranh giới áp dụng
| Kỹ Thuật | Mục Đích | Độ Phức Tạp Thời Gian | Độ Phức Tạp Không Gian |
|---|---|---|---|
| Monotonic Stack | Tìm phần tử lớn hơn/nhỏ hơn gần nhất (NGE/PLE), Diện tích Histogram | $\mathcal{O}(N)$ | $\mathcal{O}(N)$ |
| Monotonic Deque | Tìm $\min/\max$ trên cửa sổ trượt độ dài cố định $K$ | $\mathcal{O}(N)$ | $\mathcal{O}(K)$ |
| Multiset / Priority Queue | Duy trì $\min/\max$ khi cửa sổ co giãn tùy ý có xóa phần tử | $\mathcal{O}(N \log K)$ | $\mathcal{O}(K)$ |