Core Logic and Mathematical Principles
Monotonic stacks and queues are linear data structures that maintain the monotonicity of elements. Their core physical models are dynamic turning point elimination and lifespan elimination.
In a monotonic stack, the addition of a new element can disrupt the existing monotonicity. If we require the elements in the stack to be monotonically increasing, when a new element $x$ is less than the top element of the stack, the top element is completely obscured (or replaced) by $x$ in subsequent comparisons, losing its potential as an optimal solution, and must be popped immediately. Each element can be pushed and popped at most once, resulting in an amortized time complexity of $O(N)$.
In a monotonic queue, the concept of "lifespan" is introduced, which refers to the boundary constraints of the sliding window. The monotonic queue maintains a double-ended queue, and its core physical model is the dynamic evolution of range minimum queries (RMQ). For two elements $A[i]$ and $A[j]$, if $i < j$ and $A[i] ext{ ≤ } A[j]$, as the window slides to the right, $A[i]$ is dominated by $A[j]$ in both size and lifespan. Therefore, $A[i]$ becomes a useless redundancy and needs to be removed from the back of the queue. Meanwhile, if the front element of the queue exceeds the left boundary of the window $L = i - K + 1$, it must be popped from the front due to expiration.
State Design and Algorithm Derivation
1. Monotonic Stack: Next Greater Element
Given the input sequence $A$, the goal is to find the index $R[i]$ of the first element greater than $A[i]$ to the right of each position $i$.
- State Design: The stack stores the indices of elements. We traverse the sequence from left to right.
- Transition and Elimination Strategy: Currently, we are at $A[i]$. When the stack is not empty and $A[i] > A[ ext{stk.top()}]$, it indicates that $A[i]$ is the first number greater than the top element of the stack to its right. Trigger state update:
$$R[ ext{stk.top()}] = i$$
Perform the pop operation $ ext{stk.pop()}$, and repeat this process until the stack is empty or $A[i] ext{ ≤ } A[ ext{stk.top()}]$. Finally, push $i$ onto the stack.
2. Monotonic Queue: Sliding Window Maximum
Assuming the window size is $K$, we need to find the maximum value within the window ending at each position $i$.
- State Design: The double-ended queue
qstores the indices of elements, satisfying $A[ ext{q.front()}] ext{ ≥ } A[ ext{q.back()}]$. - Derivation Logic:
- Remove Expired from Front: Given the current index $i$, if $ ext{q.front()} < i - K + 1$, it indicates that the front minimum value has slid out of the window, and we perform
q.pop_front(). - Remove Redundant from Back: When the queue is not empty and $A[i] ext{ ≥ } A[ ext{q.back()}]$, the back element is removed as it is smaller and has a shorter lifespan.
- Enqueue and Decision: Push $i$ to the back of the queue. When $i ext{ ≥ } K - 1$, the current window maximum is given by $A[ ext{q.front()}]$.
C++ Standard Source Code
#include <iostream>
#include <vector>
#include <deque>
using namespace std;
// Simulating Luogu P5788 [Template] Monotonic Stack
void solveMonotoneStack() {
int n;
if (!(cin >> n)) return;
vector<int> a(n + 1);
vector<int> r(n + 1, 0);
vector<int> stk;
stk.reserve(n + 1); // Preallocate memory to avoid constant overhead from vector dynamic expansion
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
for (int i = 1; i <= n; ++i) {
// Key point: Trigger pop on strict greater, storing indices in the stack rather than values
while (!stk.empty() && a[i] > a[stk.back()]) {
r[stk.back()] = i;
stk.pop_back();
}
stk.push_back(i);
}
for (int i = 1; i <= n; ++i) {
cout << r[i] << (i == n ? "" : " ");
}
cout << "\n";
}
// Simulating Luogu P1886 Sliding Window / [Template] Monotonic Queue
void solveMonotoneQueue() {
int n, k;
if (!(cin >> n >> k)) return;
vector<int> a(n);
for (int i = 0; i < n; ++i) {
cin >> a[i];
}
deque<int> q; // Store indices
// 1. Find the minimum value in the range
for (int i = 0; i < n; ++i) {
// Check front: Remove expired indices exceeding the left boundary of the sliding window
if (!q.empty() && q.front() < i - k + 1) {
q.pop_front();
}
// Check back: New element is smaller, old elements are both larger and older, directly eliminate
while (!q.empty() && a[i] <= a[q.back()]) {
q.pop_back();
}
q.push_back(i);
if (i >= k - 1) {
cout << a[q.front()] << (i == n - 1 ? "" : " ");
}
}
cout << "\n";
}
int main() {
// Optimize standard input/output performance, disable synchronization with stdio to avoid constant slowdown
ios_base::sync_with_stdio(false);
cin.tie(NULL);
// Switch calls based on evaluation targets, this is just for demonstration
solveMonotoneStack();
return 0;
}
NOIP Practical Pitfall Guide
- Monotonic Queue Front Out-of-Bounds and Infinite Loops
When handling the movement of the sliding window, beginners often mistakenly write
if (q.front() == i - k). This exact match can lead to missed judgments when dealing with dynamic interval movements that do not step by 1 or when there are non-standard data jumps mid-way, causing the queue to retain expired illegal indices, leading to logical errors or even out-of-bounds array access. The correct approach is to use strict inequality checks: `if (q.front() < i - k + 1). Additionally, when usingwhileto pop from the back, it is essential to check!q.empty()first; otherwise, callingback()` on an empty queue will result in Undefined Behavior (segmentation fault or infinite loop). - Space Overhead and Index Misalignment
Monotonic stacks and queues must store indices rather than the actual values. If values are mistakenly stored, lifespan checks cannot be performed (it won't be possible to determine whether the element has slid out of the window), and the next greater element's position cannot be precisely located. The size of the monotonic stack array must strictly match the size of the original sequence $N$. In NOIP/Linux environments, if using standard
std::deque, its internal memory blocks are allocated in segments, which incurs a significant constant time overhead. In time-critical problems (such as processing $10^6$ data within 1.0s), it is advisable to use arrays to simulate double-ended queues (maintainingheadandtailpointers) to achieve extreme O2 compilation optimization performance.
Classic NOIP/Luogu Real Problems
1. Luogu P2866 [USACO06NOV] Bad Hair Day S
- Problem Description: $N$ cows are lined up, each looking to the right. Given each cow's height, a cow can see all the cows to its right that are strictly shorter until it is blocked by a cow that is taller or the same height. Calculate the total number of cows that can be seen by all cows.
- Core Problem Essence: Find the position of the first element greater than or equal to each element to its right.
- Core Solving Idea:
Traditional thinking calculates how many cows each cow can see, which requires maintaining a monotonic stack from right to left. The physical model can be inversely transformed to: How many cows can see one cow from the left.
Traverse the cows from left to right, maintaining a strictly monotonically decreasing stack from bottom to top. When a new cow $i$ is about to be pushed onto the stack, if the height of the top cow is less than or equal to the current cow $i$, it indicates that the top cow cannot see over the current tall cow to the right, and its lifespan ends, so we pop it. After popping, the remaining cow heights in the stack are all greater than the current cow $i$, indicating that they can all see the current cow $i$. The number of elements in the stack at this point is the count of how many times the current cow can be seen, which is directly added to the answer. Note that the final answer can reach a maximum of $rac{N(N-1)}{2}$, so
long longmust be used for storage.
2. Luogu P1419 Finding Paragraph
- Problem Description: Given a sequence $A$ of length $N$, find a continuous sub-segment of length between $[S, T]$ that maximizes the average of the elements within that sub-segment.
- Core Problem Essence: Fractional programming + Monotonic queue optimized DP (transformation of interval sums and prefix sums).
- Core Solving Idea: To maximize the average, binary search is typically employed. Let the current binary search average be $mid$; then, each element is reduced by $mid$, transforming the problem into whether there exists a sub-segment of length between $[S, T]$ whose sum is greater than or equal to 0. Let $B[i] = A[i] - mid$, and compute the prefix sum sequence $Sum$ of $B$. The sum of the sub-segment can be expressed as $Sum[r] - Sum[l-1]$, where the constraint is $S ext{ ≤ } r - l + 1 ext{ ≤ } T$, which can be rearranged to $r - T ext{ ≤ } l - 1 ext{ ≤ } r - S$. When fixing the right endpoint $r$, the goal is to maximize $Sum[r] - Sum[l-1]$, which means finding the left endpoint $l-1$ that minimizes $Sum[l-1]$. The feasible interval for $l-1$ is $[r - T, r - S]$. This is a typical sliding window maximum problem. Using a monotonic queue to maintain the minimum value of the prefix sum sequence $Sum$ within the sliding window $[r - T, r - S]$, as $r$ moves to the right, the window also shifts to the right. If at any moment it satisfies $Sum[r] - Sum[ ext{q.front()}] ext{ ≥ } 0$, it indicates that the current $mid$ is feasible, and we adjust the lower bound of the binary search; otherwise, we adjust the upper bound. The monotonic queue successfully reduces the complexity of each binary search check from $O(N^2)$ to $O(N)$.