STL + Expression + Monotonic Stack

📚 Stack – Ngăn xếp LIFO

Stack xử lý phần tử theo nguyên tắc vào sau, ra trước. Từ thao tác push/pop cơ bản, Stack được dùng để kiểm tra dấu ngoặc, tính biểu thức, DFS không đệ quy và giải nhiều bài mảng bằng Monotonic Stack.

LIFOLast In, First Out
push/pop/topO(1)
BracketsKiểm tra cặp mở–đóng
MonotonicNext Greater O(n)

🎯 Mục tiêu bài học

Cần hiểu

  • Nguyên tắc LIFO.
  • Các thao tác push, top, pop, empty, size.
  • Vì sao phải kiểm tra rỗng trước top/pop.
  • Cách Stack xử lý ngoặc và biểu thức hậu tố.
  • Cách Monotonic Stack giảm O(n²) xuống O(n).

Dấu hiệu nhận biết

  • Cần đảo ngược thứ tự.
  • Cấu trúc mở–đóng lồng nhau.
  • Cần quay lại trạng thái gần nhất.
  • Tìm phần tử lớn/nhỏ gần nhất.
  • Mỗi phần tử được push/pop tối đa một lần.

1. Đặt vấn đề

Bài toán mở đầu 1 – cặp ngoặc.
Cho chuỗi gồm các ký tự ()[]{}. Hãy kiểm tra chuỗi có đúng cặp và đúng thứ tự lồng nhau hay không.

Ví dụ:

  • ({[]}): hợp lệ.
  • ([)]: không hợp lệ vì ngoặc đóng không khớp ngoặc mở gần nhất.

1.1. Cần nhớ ngoặc mở nào?

Khi gặp ngoặc đóng, ta phải so với ngoặc mở chưa được đóng gần nhất. Đây chính là phần tử được đưa vào sau cùng.

Ngoặc mở gần nhất phải được xử lý trước → LIFO → Stack
Bài toán mở đầu 2 – phần tử lớn hơn đầu tiên bên phải.
Với mỗi vị trí i, tìm phần tử đầu tiên bên phải lớn hơn a[i].

Duyệt trực tiếp từ i+1 cho từng i mất O(n²). Nếu lưu các chỉ số chưa tìm được đáp án trong Stack, mỗi chỉ số chỉ push và pop một lần, tổng O(n).

Bản chất: Stack hữu ích khi cần xử lý đối tượng gần nhất chưa hoàn tất hoặc cần quay lại trạng thái mới nhất.

2. Stack cơ bản trong C++

Thao tác

  • st.push(x): thêm vào đỉnh.
  • st.top(): xem đỉnh.
  • st.pop(): xóa đỉnh.
  • st.empty(): kiểm tra rỗng.
  • st.size(): số phần tử.

Độ phức tạp

  • push: O(1).
  • top: O(1).
  • pop: O(1).
  • Không truy cập ngẫu nhiên bằng chỉ số.
  • Muốn duyệt thường phải pop hoặc sao chép Stack.
Quan trọng: pop() không trả về phần tử. Mẫu chuẩn là x = st.top(); st.pop();.

3. Mô phỏng Stack và highlight code

stack st;
st.push(x);
if (!st.empty()) cout << st.top();
if (!st.empty()) st.pop();
cout << st.size();

4. Kiểm tra dấu ngoặc hợp lệ

Chuỗi

Stack ngoặc mở

Sai ngay nếu gặp ngoặc đóng khi Stack rỗng hoặc ngoặc đóng không khớp top(). Cuối chuỗi, Stack phải rỗng.

5. Tính biểu thức hậu tố

Trong hậu tố, toán tử đứng sau hai toán hạng. Ví dụ 5 1 2 + 4 * + 3 -.

Stack giá trị

Thứ tự toán hạng: pop lần đầu là b, pop lần hai là a, sau đó tính a op b. Điều này đặc biệt quan trọng với phép trừ và chia.

6. Monotonic Stack – Next Greater Element

Stack lưu chỉ số các phần tử chưa tìm được phần tử lớn hơn đầu tiên bên phải. Khi gặp a[i] lớn hơn a[st.top()], ta đã tìm được đáp án cho đỉnh Stack.

Stack chỉ số

Độ phức tạp O(n): mỗi chỉ số được push đúng một lần và pop tối đa một lần.

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

Bài toánThời gianBộ nhớGiải thích
q thao tác StackO(q)O(q)Mỗi thao tác O(1).
Kiểm tra ngoặcO(n)O(n)Mỗi ký tự xử lý một lần.
Tính postfixO(t)O(t)Mỗi token push/pop hữu hạn lần.
Next Greater ElementO(n)O(n)Mỗi chỉ số push/pop tối đa một lần.
DFS lặpO(V+E)O(V)Stack thay cho call stack.

8. Code mẫu

#include 
using namespace std;

int main() {
    stack st;

    st.push(10);
    st.push(20);
    st.push(30);

    cout << st.top() << '
'; if (!st.empty()) { int value = st.top(); st.pop(); cout << "Removed: " << value << '
'; } cout << st.size(); }
#include 
using namespace std;

bool isMatching(char open, char close) {
    return (open == '(' && close == ')')
        || (open == '[' && close == ']')
        || (open == '{' && close == '}');
}

int main() {
    string s;
    cin >> s;

    stack st;

    for (char c : s) {
        if (c == '(' || c == '[' || c == '{') {
            st.push(c);
        } else {
            if (st.empty() || !isMatching(st.top(), c)) {
                cout << "NO";
                return 0;
            }

            st.pop();
        }
    }

    cout << (st.empty() ? "YES" : "NO");
}
#include 
using namespace std;

bool isNumber(const string& token) {
    if (token.empty()) return false;

    int position = 0;

    if (token[0] == '-' && token.size() > 1) {
        position = 1;
    }

    for (; position < (int)token.size(); position++) {
        if (!isdigit(token[position])) return false;
    }

    return true;
}

int main() {
    string expression;
    getline(cin, expression);

    stringstream stream(expression);
    string token;
    stack st;

    while (stream >> token) {
        if (isNumber(token)) {
            st.push(stoll(token));
            continue;
        }

        long long right = st.top();
        st.pop();

        long long left = st.top();
        st.pop();

        if (token == "+") st.push(left + right);
        if (token == "-") st.push(left - right);
        if (token == "*") st.push(left * right);
        if (token == "/") st.push(left / right);
    }

    cout << st.top();
}
#include 
using namespace std;

int main() {
    int n;
    cin >> n;

    vector a(n);
    vector answer(n, -1);

    for (long long &x : a) cin >> x;

    stack st;

    for (int i = 0; i < n; i++) {
        while (!st.empty() && a[st.top()] < a[i]) {
            answer[st.top()] = a[i];
            st.pop();
        }

        st.push(i);
    }

    for (long long x : answer) {
        cout << x << ' ';
    }
}
def valid_brackets(text):
    stack = []
    matching = {
        ')': '(',
        ']': '[',
        '}': '{'
    }

    for character in text:
        if character in '([{':
            stack.append(character)
        else:
            if not stack or stack[-1] != matching[character]:
                return False

            stack.pop()

    return not stack

9. Giải thích code

9.1. Vì sao phải kiểm tra empty trước top/pop?

Gọi top() hoặc pop() khi Stack rỗng gây hành vi không xác định.

9.2. Vì sao pop không trả về giá trị?

Trong STL, pop() chỉ xóa. Muốn dùng phần tử, lấy top() trước rồi mới pop.

9.3. Vì sao ngoặc đóng phải so với top?

Do cấu trúc lồng nhau, ngoặc mở được thêm sau cùng phải được đóng trước tiên.

9.4. Vì sao postfix cần pop b trước a?

Hai toán hạng được push theo thứ tự a rồi b, nên b nằm ở đỉnh. Với phép trừ, phải tính a-b, không phải b-a.

9.5. Vì sao NGE lưu chỉ số thay vì giá trị?

Cần gán đáp án cho đúng vị trí. Lưu chỉ số cũng giúp truy cập cả giá trị và vị trí.

9.6. Vì sao vòng while trong NGE vẫn O(n)?

Mỗi chỉ số được push một lần và sau khi bị pop sẽ không quay lại Stack. Tổng số lần pop không vượt quá n.

10. Lỗi thường gặp

  • Gọi top() hoặc pop() khi Stack rỗng.
  • Tưởng pop() trả về phần tử.
  • Kiểm tra ngoặc nhưng quên điều kiện Stack phải rỗng ở cuối.
  • Postfix lấy sai thứ tự toán hạng khi trừ hoặc chia.
  • Monotonic Stack lưu giá trị nhưng cần trả lời theo chỉ số.
  • Dùng dấu < hay <= không đúng khi xử lý phần tử bằng nhau.
  • Cho rằng vòng while bên trong for luôn tạo O(n²).
  • Dùng DFS đệ quy quá sâu mà không cân nhắc Stack lặp.

11. Quiz và bài tập

Câu 1. Stack tuân theo nguyên tắc nào?
Câu 2. Trong postfix, pop đầu tiên nhận toán hạng nào?
Câu 3. Next Greater Element bằng Monotonic Stack có độ phức tạp nào?

📘 Cơ bản

  1. Đảo chuỗi bằng Stack.
  2. Mô phỏng q thao tác.
  3. Kiểm tra palindrome bằng Stack.

📗 Trung bình

  1. Kiểm tra dấu ngoặc.
  2. Tính postfix.
  3. DFS không đệ quy.

📙 Nâng cao

  1. Next Greater/Smaller Element.
  2. Daily Temperatures.
  3. Stock Span.

🐉 HSG

  1. Largest Rectangle in Histogram.
  2. Trapping Rain Water.
  3. Cartesian Tree định hướng.
Bài thực hành 1 – Kiểm tra ngoặc

Hỗ trợ bốn loại ngoặc ()[]{}<>.

Bài thực hành 2 – Next Greater

Trả về cả giá trị và khoảng cách đến phần tử lớn hơn đầu tiên.

Bài thực hành 3 – Postfix

Hỗ trợ số âm, số nhiều chữ số và kiểm tra biểu thức không hợp lệ.