Monotonic Stack & Cartesian Tree
A monotonic stack is a special stack data structure that maintains monotonic order (increasing or decreasing) of elements. It's primarily used to solve problems like "next greater element" and "largest rectangle area". A Cartesian tree is a binary tree built from an array that satisfies both binary search tree properties (by position) and heap properties (by value), often used in conjunction with monotonic stacks.
Monotonic Stack Fundamentals
Monotonic stacks efficiently solve problems involving finding "greater/smaller elements" by maintaining element monotonicity.
Monotonic Stack Basics - Next Greater Element
#include <iostream>
#include <vector>
#include <stack>
#include <algorithm>
using namespace std;
class MonotonicStack {
public:
// Find the first greater element to the right of each element
vector<int> nextGreaterElement(vector<int>& nums) {
int n = nums.size();
vector<int> result(n, -1);
stack<int> stk; // Store indices
for (int i = 0; i < n; i++) {
// When current element is greater than stack top, stack top found its answer
while (!stk.empty() && nums[stk.top()] < nums[i]) {
result[stk.top()] = nums[i];
stk.pop();
}
stk.push(i);
}
return result;
}
// Find the first greater element to the left of each element
vector<int> previousGreaterElement(vector<int>& nums) {
int n = nums.size();
vector<int> result(n, -1);
stack<int> stk;
for (int i = 0; i < n; i++) {
// Maintain decreasing stack
while (!stk.empty() && nums[stk.top()] <= nums[i]) {
stk.pop();
}
if (!stk.empty()) {
result[i] = nums[stk.top()];
}
stk.push(i);
}
return result;
}
// Find the first smaller element to the right of each element
vector<int> nextSmallerElement(vector<int>& nums) {
int n = nums.size();
vector<int> result(n, -1);
stack<int> stk;
for (int i = 0; i < n; i++) {
while (!stk.empty() && nums[stk.top()] > nums[i]) {
result[stk.top()] = nums[i];
stk.pop();
}
stk.push(i);
}
return result;
}
// Find the first smaller element to the left of each element
vector<int> previousSmallerElement(vector<int>& nums) {
int n = nums.size();
vector<int> result(n, -1);
stack<int> stk;
for (int i = 0; i < n; i++) {
while (!stk.empty() && nums[stk.top()] >= nums[i]) {
stk.pop();
}
if (!stk.empty()) {
result[i] = nums[stk.top()];
}
stk.push(i);
}
return result;
}
};
// Classic application: Largest rectangle in histogram
class LargestRectangleInHistogram {
public:
int largestRectangleArea(vector<int>& heights) {
stack<int> stk;
int maxArea = 0;
// Add a bar with height 0 at the end to ensure all bars are processed
heights.push_back(0);
for (int i = 0; i < heights.size(); i++) {
while (!stk.empty() && heights[stk.top()] > heights[i]) {
int h = heights[stk.top()];
stk.pop();
int width = stk.empty() ? i : i - stk.top() - 1;
maxArea = max(maxArea, h * width);
}
stk.push(i);
}
heights.pop_back(); // Restore original array
return maxArea;
}
};
// Maximum rectangle problem (2D extension)
class MaximalRectangle {
public:
int maximalRectangle(vector<vector<char>>& matrix) {
if (matrix.empty() || matrix[0].empty()) return 0;
int m = matrix.size(), n = matrix[0].size();
vector<int> heights(n, 0);
int maxArea = 0;
for (int i = 0; i < m; i++) {
// Update height of each column
for (int j = 0; j < n; j++) {
if (matrix[i][j] == '1') {
heights[j]++;
} else {
heights[j] = 0;
}
}
// Calculate maximum rectangle area for current row
maxArea = max(maxArea, largestRectangleArea(heights));
}
return maxArea;
}
private:
int largestRectangleArea(vector<int>& heights) {
stack<int> stk;
int maxArea = 0;
heights.push_back(0);
for (int i = 0; i < heights.size(); i++) {
while (!stk.empty() && heights[stk.top()] > heights[i]) {
int h = heights[stk.top()];
stk.pop();
int width = stk.empty() ? i : i - stk.top() - 1;
maxArea = max(maxArea, h * width);
}
stk.push(i);
}
heights.pop_back();
return maxArea;
}
};
int main() {
// Test monotonic stack basic applications
vector<int> nums = {2, 1, 2, 4, 3, 1};
MonotonicStack ms;
cout << "Original array: ";
for (int x : nums) cout << x << " ";
cout << endl;
vector<int> nextGreater = ms.nextGreaterElement(nums);
cout << "Next greater elements: ";
for (int x : nextGreater) cout << x << " ";
cout << endl;
vector<int> prevGreater = ms.previousGreaterElement(nums);
cout << "Previous greater elements: ";
for (int x : prevGreater) cout << x << " ";
cout << endl;
// Test largest rectangle in histogram
cout << "\nLargest Rectangle Test:\n";
LargestRectangleInHistogram lrh;
vector<int> heights = {2, 1, 5, 6, 2, 3};
cout << "Heights: ";
for (int h : heights) cout << h << " ";
cout << "\nLargest rectangle area: " << lrh.largestRectangleArea(heights) << endl;
return 0;
}Monotonic stack maintains element monotonicity by popping elements that violate the order when inserting new elements. Each element is pushed and popped at most once, achieving O(n) time complexity.
Cartesian Tree Construction
A Cartesian tree can be efficiently constructed using a monotonic stack, providing a foundation for range minimum queries and divide-and-conquer optimizations.
Cartesian Tree with Monotonic Stack
#include <iostream>
#include <vector>
#include <stack>
using namespace std;
struct TreeNode {
int val;
int index;
TreeNode* left;
TreeNode* right;
TreeNode(int v, int idx) : val(v), index(idx), left(nullptr), right(nullptr) {}
};
class CartesianTree {
public:
// Build Cartesian tree using monotonic stack
TreeNode* buildTree(vector<int>& nums) {
if (nums.empty()) return nullptr;
stack<TreeNode*> stk;
for (int i = 0; i < nums.size(); i++) {
TreeNode* node = new TreeNode(nums[i], i);
TreeNode* lastPopped = nullptr;
// Maintain min-heap property (parent <= children)
while (!stk.empty() && stk.top()->val > nums[i]) {
lastPopped = stk.top();
stk.pop();
}
// Set relationships
if (!stk.empty()) {
stk.top()->right = node;
}
if (lastPopped) {
node->left = lastPopped;
}
stk.push(node);
}
// Root is at the bottom of stack
while (stk.size() > 1) {
stk.pop();
}
return stk.top();
}
// Range Minimum Query using Cartesian tree
int rangeMinQuery(TreeNode* root, int left, int right) {
if (!root || root->index < left || root->index > right) {
return INT_MAX;
}
if (root->index >= left && root->index <= right) {
int leftMin = rangeMinQuery(root->left, left, root->index - 1);
int rightMin = rangeMinQuery(root->right, root->index + 1, right);
return min({root->val, leftMin, rightMin});
}
return INT_MAX;
}
// In-order traversal to verify structure
void inorderTraversal(TreeNode* root, vector<int>& result) {
if (!root) return;
inorderTraversal(root->left, result);
result.push_back(root->val);
inorderTraversal(root->right, result);
}
// Print tree structure
void printTree(TreeNode* root, int depth = 0) {
if (!root) return;
printTree(root->right, depth + 1);
for (int i = 0; i < depth; i++) cout << " ";
cout << root->val << "(" << root->index << ")" << endl;
printTree(root->left, depth + 1);
}
};
int main() {
vector<int> nums = {3, 2, 6, 1, 9};
CartesianTree ct;
cout << "Original array: ";
for (int x : nums) cout << x << " ";
cout << endl;
TreeNode* root = ct.buildTree(nums);
cout << "\nCartesian Tree Structure:\n";
ct.printTree(root);
cout << "\nIn-order traversal: ";
vector<int> inorder;
ct.inorderTraversal(root, inorder);
for (int x : inorder) cout << x << " ";
cout << endl;
// Test range minimum queries
cout << "\nRange Minimum Queries:\n";
cout << "RMQ(0, 2): " << ct.rangeMinQuery(root, 0, 2) << endl;
cout << "RMQ(1, 4): " << ct.rangeMinQuery(root, 1, 4) << endl;
cout << "RMQ(2, 3): " << ct.rangeMinQuery(root, 2, 3) << endl;
return 0;
}Cartesian tree construction uses monotonic stack to maintain heap property. The resulting tree enables efficient range minimum queries and serves as a foundation for divide-and-conquer algorithms.
Advanced Applications
Monotonic stacks have numerous advanced applications in competitive programming, from stock span problems to expression evaluation.
Advanced Monotonic Stack Applications
#include <iostream>
#include <vector>
#include <stack>
#include <string>
#include <unordered_map>
using namespace std;
// Stock span problem
class StockSpanner {
private:
stack<pair<int, int>> stk; // (price, span)
public:
int next(int price) {
int span = 1;
// Accumulate days with prices <= current price
while (!stk.empty() && stk.top().first <= price) {
span += stk.top().second;
stk.pop();
}
stk.push({price, span});
return span;
}
};
// Remove duplicate letters maintaining lexicographical order
class RemoveDuplicateLetters {
public:
string removeDuplicateLetters(string s) {
unordered_map<char, int> count;
unordered_map<char, bool> inStack;
stack<char> stk;
// Count frequency of each character
for (char c : s) {
count[c]++;
}
for (char c : s) {
count[c]--;
if (inStack[c]) continue;
// Remove characters that are lexicographically larger and appear later in the string
while (!stk.empty() && stk.top() > c && count[stk.top()] > 0) {
inStack[stk.top()] = false;
stk.pop();
}
stk.push(c);
inStack[c] = true;
}
string result = "";
while (!stk.empty()) {
result = stk.top() + result;
stk.pop();
}
return result;
}
};
// Daily temperatures
class DailyTemperatures {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
int n = temperatures.size();
vector<int> result(n, 0);
stack<int> stk;
for (int i = 0; i < n; i++) {
while (!stk.empty() && temperatures[stk.top()] < temperatures[i]) {
int prevIndex = stk.top();
stk.pop();
result[prevIndex] = i - prevIndex;
}
stk.push(i);
}
return result;
}
};
// Sum of subarray minimums
class SumOfSubarrayMinimums {
public:
int sumSubarrayMins(vector<int>& arr) {
const int MOD = 1e9 + 7;
int n = arr.size();
// Find previous and next smaller elements
vector<int> prevSmaller(n, -1);
vector<int> nextSmaller(n, n);
stack<int> stk;
// Previous smaller
for (int i = 0; i < n; i++) {
while (!stk.empty() && arr[stk.top()] >= arr[i]) {
stk.pop();
}
if (!stk.empty()) {
prevSmaller[i] = stk.top();
}
stk.push(i);
}
// Clear stack for next smaller
while (!stk.empty()) stk.pop();
// Next smaller
for (int i = 0; i < n; i++) {
while (!stk.empty() && arr[stk.top()] > arr[i]) {
nextSmaller[stk.top()] = i;
stk.pop();
}
stk.push(i);
}
long long result = 0;
for (int i = 0; i < n; i++) {
long long left = i - prevSmaller[i];
long long right = nextSmaller[i] - i;
result = (result + arr[i] * left * right) % MOD;
}
return result;
}
};
// Trapping rain water
class TrappingRainWater {
public:
int trap(vector<int>& height) {
if (height.empty()) return 0;
stack<int> stk;
int water = 0;
for (int i = 0; i < height.size(); i++) {
while (!stk.empty() && height[stk.top()] < height[i]) {
int bottom = stk.top();
stk.pop();
if (stk.empty()) break;
int distance = i - stk.top() - 1;
int boundedHeight = min(height[i], height[stk.top()]) - height[bottom];
water += distance * boundedHeight;
}
stk.push(i);
}
return water;
}
};
int main() {
// Test stock price span
cout << "Stock Price Span Test:\n";
StockSpanner spanner;
vector<int> prices = {100, 80, 60, 70, 60, 75, 85};
cout << "Price sequence: ";
for (int price : prices) {
cout << price << "(" << spanner.next(price) << ") ";
}
cout << endl;
// Test remove duplicate letters
cout << "\nRemove Duplicate Letters Test:\n";
RemoveDuplicateLetters rdl;
string s = "bcabc";
cout << "Original string: " << s << endl;
cout << "After removing duplicates: " << rdl.removeDuplicateLetters(s) << endl;
// Test daily temperatures
cout << "\nDaily Temperatures Test:\n";
DailyTemperatures dt;
vector<int> temperatures = {73, 74, 75, 71, 69, 72, 76, 73};
vector<int> days = dt.dailyTemperatures(temperatures);
cout << "Temperatures: ";
for (int temp : temperatures) cout << temp << " ";
cout << "\nDays to wait: ";
for (int day : days) cout << day << " ";
cout << endl;
// Test sum of subarray minimums
cout << "\nSum of Subarray Minimums Test:\n";
SumOfSubarrayMinimums ssm;
vector<int> arr = {3, 1, 2, 4};
cout << "Array: ";
for (int x : arr) cout << x << " ";
cout << "\nSum of subarray minimums: " << ssm.sumSubarrayMins(arr) << endl;
// Test trapping rain water
cout << "\nTrapping Rain Water Test:\n";
TrappingRainWater trw;
vector<int> height = {0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1};
cout << "Height array: ";
for (int h : height) cout << h << " ";
cout << "\nTrapped water: " << trw.trap(height) << endl;
return 0;
}Advanced applications demonstrate the power of monotonic stacks in various complex problems. Stock span accumulates consecutive days with prices less than or equal to current price, duplicate removal maintains lexicographical order, rain water calculates trapped water in valleys.
Competitive Programming Techniques
Core Characteristics of Monotonic Stack:
- Maintains monotonicity of elements in stack (increasing or decreasing)
- Each element enters and exits stack at most once, achieving O(n) time complexity
- Suitable for "next greater/smaller element" type problems
- Pop operations determine the influence range of elements
Properties of Cartesian Tree:
- Satisfies both BST property (by index) and heap property (by value)
- In-order traversal yields the original array's index sequence
- Can be used for RMQ queries and divide-and-conquer optimizations
- Construction time complexity is O(n) using monotonic stack
Common Application Patterns:
- Range extremum problems: largest rectangle in histogram, trapping rain water
- Array range problems: sum of subarray minimums, temperature waiting
- String processing: duplicate removal with order, bracket matching
- Expression evaluation: operator precedence, reverse Polish notation
Implementation Techniques:
- Store indices instead of values: convenient for distance and range calculations
- Correct pop conditions: strict monotonic vs non-strict monotonic
- Boundary handling: add sentinel elements at array end
- Bidirectional scanning: separately calculate left and right boundaries
Debugging Points:
- Check monotonicity maintenance conditions (> vs >=)
- Confirm stack storage content (values vs indices)
- Verify calculation logic during pop operations
- Test boundary cases (empty stack, single element, etc.)
Optimization Strategies:
- Space optimization: use arrays to simulate stack
- Constant optimization: reduce unnecessary checks
- Parallelization: parallel processing in certain cases
- Cache-friendly: consider data access patterns