# Leetcode刷題學習筆記 -- Monotonic Stack ## 簡介 stack中保持著一定的順序,例如遞增或是遞減。 如果要新增的element比top還大/小。就把top pop出去。然後繼續往下比,直到保持一定的遞增或是遞減的順序。通常是找序列中比自己後面,但是值比自己還大/小的題目。 ## 使用時機? ### 1. 需要==往前或是往後==尋找,==下一個比自己大或比自己小==的元素就可以使用monotonic stack #### PLE(Previous Less Element) Good reference : [Sum of Subarray Minimums](https://leetcode.com/problems/sum-of-subarray-minimums/solutions/178876/stack-solution-with-very-detailed-explanation-step-by-step/?orderBy=most_votes) 找出之前比自己還小的值。 + stack 存放index。因為存index有很多好處,其中之一就是可以算和target的差距。 + 如果stack為empty則填入-1,也是有利計算差距。 ```cpp vector<int> nums; vector<int> ple(nums.size()); stack<int> st; // 存放index for(int i = 0; i < nums.size(); ++i) { while(!st.empty() && nums[st.top()] > nums[i]) st.pop(); ple[i] = st.empty() ? -1 : st.top(); st.push(i); // 將目前index推入stack } ``` #### NLE(Next less element) 找出之後比自己還小的值。 + code snippet一樣,只是for反過來尋找 + 如果stack為empty則填入nums.size() ```cpp vector<int> nums; int sz = nums.size(); vector<int> nle(sz); stack<int> st; for(int i = nums.size() - 1; i >= 0; --i) { while(!st.empty() && nums[st.top()] > nums[i]) st.pop(); nle[i] = st.empty() ? sz : st.top(); st.push(i); } ``` 另一種寫法,如果沒辦法從後面走訪數列的話 ```cpp vector<int> nums; int sz = nums.size(); vector<int> nle(sz, sz); stack<int> st; for(int i = 0; i < sz; ++i) { while(!st.empty() && nums[st.top()] > nums[i]) { nle[st.top()] = i; st.pop(); } st.push(i); } ``` ### 2. 往前或是往後看的最大/小值 剛剛第一種情況是往前或是往後看,第一個比自己大或小的值。另一種情況是往前看或是往後看,找出最大值。如果自己本身是最大值就是自己。 以下的code是從自己往後看,看到最大值的index。 ```cpp= vector<int> nums; int sz = nums.size(); vector<int> bwdMax(sz); // index stack<int> st; // index for(int i = 0; i < sz; ++i) { // stack中比目前的值還小,就丟掉,因為接下來的index只會看到這個 while(!st.empty() && nums[i] >= nums[st.top()]) st.pop(); if(st.empty()) st.push(i); // 如果沒了就是看到自己 bwdMax[i] = st.top(); } ``` 以下的code使從自己往前看,看到最大值的index。 ```cpp= vector<int> nums; int sz = nums.size(); vector<int> fwdMax(sz); // index stack<int> st; // index for(int i = sz - 1; i >= 0; --i) { while(!st.empty() && nums[i] >= nums[st.top()]) st.pop(); if(st.empty()) st.push(i); fwdMax[i] = st.top(); } ``` 後來想想,其實找fordward max或是backward max不需要使用stack,只要用以下簡單的方法即可。 ```cpp= int maxv = -1; vector<int> bwd(sz); for(int i = 0; i < sz; ++i) { maxv = max(maxv, nums[i]); bwd[i] = maxv; } ``` ### Example #### [42. Trapping Rain Water](https://leetcode.com/problems/trapping-rain-water/) 給你一個vector<int> height,下雨過後會有多少雨水留在裡面。 > 1. 雨水留在裡面的多少會由,這格往前往後看的最大值決定。 > 2. 所以使用monotonic stack來計算每格往前往後看的最大值。 ```cpp= class Solution { public: int trap(vector<int>& height) { auto sz = height.size(); stack<int> st; vector<int> bwdMax(sz), fwdMax(sz); // backward max value index for(int i = 0; i < sz; ++i) { while(!st.empty() && height[i] >= height[st.top()]) st.pop(); if(st.empty()) st.push(i); bwdMax[i] = st.top(); } // clean the stack while(!st.empty()) st.pop(); // forward max value index for(int i = sz - 1; i >= 0; --i) { while(!st.empty() && height[i] >= height[st.top()]) st.pop(); if(st.empty()) st.push(i); fwdMax[i] = st.top(); } // calculate each rain int ans{0}; for(int i = 0; i < sz; ++i) { ans += min(height[fwdMax[i]], height[bwdMax[i]]) - height[i]; } return ans; } }; ``` 也可以使用以下簡單的方法 ```cpp= class Solution { public: int trap(vector<int>& height) { int sz = height.size(); vector<int> fwd(sz), bwd(sz); int maxv = -1; for(int i = 0; i < sz; ++i) { maxv = max(maxv, height[i]); bwd[i] = maxv; } maxv = -1; for(int i = sz - 1; i >= 0; --i) { maxv = max(maxv, height[i]); fwd[i] = maxv; } int ans{0}; for(int i = 0; i < sz; ++i) { ans += min(fwd[i], bwd[i]) - height[i]; } return ans; } }; ``` ### [739. Daily Temperatures[Medium]](https://leetcode.com/problems/daily-temperatures/) 給你一個每日溫度的數列vector<int> temperatures。求出過給天後,會比現在還溫暖。例如: Input: temperatures = [73,74,75,71,69,72,76,73] Output: [1,1,4,2,1,1,0,0] 解題思路: > 這是一個典型的單調遞增stack的問題。 ```cpp= vector<int> dailyTemperatures(vector<int>& temperatures) { stack<pair<int, int>> st; // 使用pair來紀錄 auto n = temperatures.size(); vector<int> ans(n, 0); for(int i = 0; i < n; ++i) { if(!st.empty()) { while(!st.empty() && temperatures[i] > st.top().first) { auto item = st.top(); ans[item.second] = i - item.second; st.pop(); } } st.push({temperatures[i], i}); } return ans; } ``` 2022/07/06 和上面的相比,stack只需存index即可。 2022/10/29 因為這題是找出後面第一個比自己大的值,所以是monotonic stack ```cpp= vector<int> dailyTemperatures(vector<int>& temp) { stack<int> s; //index int sz = temp.size(); vector<int> rtn(sz); for(int i = sz - 1; i >= 0; --i) { while(!s.empty() && temp[i] >= temp[s.top()]) s.pop(); rtn[i] = s.empty() ? 0 : s.top() - i; s.push(i); } return rtn; } ``` #### [901. Online Stock Span](https://leetcode.com/problems/online-stock-span/description/) 找出price左邊連續比它還小的數值。 > 1. 一開始有想到monotone stack但是怎麼寫都是不對。原因是少了一個變數。這邊使用了stack<pair<int, int>>。多了一個資訊。統計這個數字的span。全部的span加起來剛好是輸入price的個數。 > 2. 使用monotone stack是因為不需要找比自己還大的數字。 > 3. 可以看成我們在整理這個vector > [100] > [100, 80] > [100, 80, 60] > [100, 80, [60, 70]] --> [100, 80, 70(2)] > [100, 80, 70(2), 60] > [100, 80, [70(2), 60, 75]] --> [100, 80, 75(4)] > [100, [80, 75(4), 85]] --> [100, 85(6)] ```cpp class StockSpanner { stack<pair<int, int>> st; // value, span public: StockSpanner() { } int next(int price) { int ret{1}; while(!st.empty() && st.top().first <= price) { ret += st.top().second; st.pop(); } st.emplace(price, ret); return ret; } }; ``` 2022/10/29 因為這題是找出前面比自己還要大的數,所以使用monotonic stack。 ```cpp class StockSpanner { vector<int> prices; stack<int> st; int idx{0}; public: StockSpanner() { } int next(int price) { while(!st.empty() && prices[st.top()] <= price) st.pop(); int rtn = st.empty() ? idx + 1: idx - st.top(); prices.push_back(price); st.push(idx); idx++; return rtn; } }; ``` ### 3. 找出以第i個位置的山峰或是山谷 應用前面的找出前面和後面比自己高或是比自己低的位置,如果左右邊都應用的話,就是找出以第i個位置的山峰或是山谷。 例如: [1,4,3,7,4,5],可以畫出以下的圖 ![](https://hackmd.io/_uploads/ryqUZL7fT.png) 這邊可以發現,對index = 2 來說,如果以他為local minimal的subarray,可以得到。 ![](https://hackmd.io/_uploads/B1XBM8mf6.png) 同理,對index = 4來說,以他為local minimal的subarray,可以得到 ![](https://hackmd.io/_uploads/ryTAf8mMT.png) ### Example #### [1793. Maximum Score of a Good Subarray](https://leetcode.com/problems/maximum-score-of-a-good-subarray/description) Maximum score的定義是,subarray的最小值,乘上subarray的長度。另外Good subarray的定義是,必須包含index = k。 > 1. 因為Score的定義是,subarray的最小值乘上長度。所以算是找local minimal => 使用monotonic stack找出區域山谷。 > 2. 最後看每個index所構成的區域山谷,使否有包含index = k。 ```cpp= class Solution { public: int maximumScore(vector<int>& nums, int k) { int sz = nums.size(); vector<int> left(sz, -1), right(sz, sz), st; for(int i = 0; i < sz; ++i) { while(!st.empty() && nums[i] < nums[st.back()]) { right[st.back()] = i; st.pop_back(); } st.push_back(i); } st.clear(); for(int i = sz - 1; i >= 0; --i) { while(!st.empty() && nums[i] < nums[st.back()]) { left[st.back()] = i; st.pop_back(); } st.push_back(i); } int ans{}; for(int i = 0; i < sz; ++i) { if(left[i] < k && right[i] > k) ans = max(ans, nums[i] * (right[i] - left[i] - 1)); } return ans; } }; ``` #### [84. Largest Rectangle in Histogram](https://leetcode.com/problems/largest-rectangle-in-histogram/) 找出histogram中最大面積的長方形。 > 1. 因為長方形的高度會被,最矮的數值決定。=> 找出以此index的區域山谷。 > 2. 找出以每個index為山谷之後,就可以依序找出每個面積,得到最大值。 ```cpp= class Solution { public: int largestRectangleArea(vector<int>& heights) { int sz = heights.size(); vector<int> fle(sz, sz), ble(sz, -1); stack<int> st; for(int i = 0; i < sz; ++i) { while(!st.empty() && heights[i] < heights[st.top()]) { fle[st.top()] = i; st.pop(); } st.push(i); } st = move(stack<int>()); for(int i = sz - 1; i >= 0; --i) { while(!st.empty() && heights[i] < heights[st.top()]) { ble[st.top()] = i; st.pop(); } st.push(i); } int ans{}; for(int i = 0; i < sz; ++i) { int left = i - ble[i]; int right = fle[i] - i; ans = max(ans, heights[i] * (left + right - 1)); } return ans; } }; ``` ## Problems ### [907. Sum of Subarray Minimums](https://leetcode.com/problems/sum-of-subarray-minimums/description/) 給你一個vector<int>找出所有subarray的組合中,每個subarray的最小值的和為多少? > + 這題參考了 [Sum of Subarray Minimums](https://leetcode.com/problems/sum-of-subarray-minimums/solutions/178876/stack-solution-with-very-detailed-explanation-step-by-step/?orderBy=most_votes) 對monotinic stack有更深一層的了解。 > + 另外參考了官神的[答案](https://github.com/wisdompeak/LeetCode/tree/master/Stack/907.Sum-of-Subarray-Minimums),因為官神對於計算個數比較直覺。 > 計算個數參考以下的註解。 > + 因為要知道subarray最小值,所以某個subarray的最小值取決於,這個subarray的左邊和右邊數值都是比目前的值還要大。 > + 找出自己前面和後面的最小值 ==> monotonic stack > + why two pass? 因為條件和自己前後都有關係。 ```cpp // [3, 1, 2, 4] // [3] --> ans += 3, 找出比自己小的左右兩邊的邊界 // [3, 1], [1], [1, 2], [1, 2, 4], [3, 1, 2], [3, 1, 2, 4] --> ans += 1 // [2], [2, 4] --> ans += 2 // [4] --> ans += 4 int sumSubarrayMins(vector<int>& arr) { int sz = arr.size(); vector<int> ple(sz), nle(sz); // PLE stack<int> st; for(int i = 0; i < sz; ++i) { while(!st.empty() && arr[st.top()] > arr[i]) st.pop(); ple[i] = st.empty() ? -1 : st.top(); st.push(i); } // clear the stack while(!st.empty()) st.pop(); // NLE // 為什麼是大於等於? // 因為等於的最小值也是和arr[i]一樣。 // 但是如果,兩邊都使用>= 會有重複問題,所以只要一邊就可以。 // ex : [71, 55, 82, 55], 如果ple和nle都使用>= // idx1 [71, 55], [55], // [82], [82, 55] // idx3 [71, 55, 82, 55] -->重複計算 for(int i = sz - 1; i >= 0; --i) { while(!st.empty() && arr[st.top()] >= arr[i]) st.pop(); nle[i] = st.empty() ? sz : st.top(); st.push(i); } // count the result // [3, 1, 2, 4] // -1, ,1 ,4 // i - ple[i] : 前面有幾個element : 2 --> [1], [3, 1] // nle[i] - i : 後面有幾個element : 3 --> [], [2], [2, 4] long rtn{0}; long mod = 1e9 + 7; for(int i = 0; i < sz; ++i) { long count = (i - ple[i]) * (nle[i] - i) % mod; rtn = (rtn + arr[i] * count) % mod; } return rtn; } ``` ### [316. Remove Duplicate Letters](https://leetcode.com/problems/remove-duplicate-letters/description/?envType=daily-question&envId=2023-09-26) ```cpp= class Solution { public: string removeDuplicateLetters(string s) { stack<char> st; unordered_set<char> seen; unordered_map<char, int> last; // 最後出現的位置 for(int i = 0; i < s.size(); ++i) last[s[i]] = i; for(int i = 0; i < s.size(); ++i) { if(seen.count(s[i])) continue; // 目前的char比s[i]還大,且後面還會有,就丟掉 while(!st.empty() && s[i] < st.top() && last[st.top()] > i) { seen.erase(st.top()); st.pop(); } seen.insert(s[i]); st.push(s[i]); } string rtn{}; while(!st.empty()) { rtn = st.top() + rtn; st.pop(); } return rtn; } }; ``` ### [2866. Beautiful Towers II](https://leetcode.com/problems/beautiful-towers-ii/description/) 給你一個vector<int> maxHeights,當用每個index當成mountain array的top,則兩邊勢必要配合top做修正,修正完之後才會成為mountain array,返回最大的mountain array sum。 > 1. 參考[官方答案](https://leetcode.cn/problems/beautiful-towers-ii/solutions/2563770/mei-li-ta-ii-by-leetcode-solution-2j4s/)。 > 2. 我們把每個index都當成mountain top。 > 3. 如果兩邊遇到比自己大的就會變成maxHeights[i]。所以可以容易計算出sum += (j - i) * maxHeights[i]; > 4. 遇到比自己小的(j),則會被j的左邊或是右邊的sum決定。 > 5. 所以問題變成找出比自己小的index。 > 6. 另外卡住的點是,以前使用monotonic stack會在pop之後更新資料,但是現在是pop完之後才處理,所以方向改變了。 ```cpp= class Solution { public: long long maximumSumOfHeights(vector<int>& maxHeights) { int sz = maxHeights.size(); vector<long long> right(sz), left(sz); stack<int> st; for(int i = 0; i < sz; ++i) { while(!st.empty() && maxHeights[i] < maxHeights[st.top()]) st.pop(); if(st.empty()) left[i] = (long long)(i + 1) * maxHeights[i]; else left[i] = left[st.top()] + (long long)(i - st.top()) * maxHeights[i]; st.push(i); } st = move(stack<int>()); long long res{}; for(int i = sz - 1; i >= 0; --i) { while(!st.empty() && maxHeights[i] < maxHeights[st.top()]) st.pop(); if(st.empty()) right[i] = (long long)(sz - i) * maxHeights[i]; else right[i] = right[st.top()] + (long long)(st.top() - i) * maxHeights[i]; st.push(i); res = max(res, left[i] + right[i] - maxHeights[i]); } return res; } }; ``` ### [1944.Number of Visible People In a Queue](https://leetcode.com/problems/number-of-visible-people-in-a-queue/description/) > 1. 這題感覺就是monotonic stack的題目,但是就是思考不出來怎麼處理中間比較矮的人。 > 2. 參考[lee215解答](https://leetcode.com/problems/number-of-visible-people-in-a-queue/solutions/1359707/java-c-python-stack-solution-next-greater-element/) ```cpp= class Solution { // 題目是往右看,可以看到幾個人 public: vector<int> canSeePersonsCount(vector<int>& heights) { int sz = heights.size(); vector<int> res(sz), st; for(int i = 0; i < sz; ++i) { // 前一個比自己小的(st.back()),因為會被我擋住(i), // 所以丟出來,不用在比了 while(!st.empty() && heights[st.back()] <= heights[i]) res[st.back()]++, st.pop_back(); // 前一個比自己大的(st.back())可以看到自己(i) if(!st.empty()) res[st.back()]++; st.push_back(i); } return res; } }; ``` ###### tags: `leetcode` `刷題`