Given an array of integers `A`, find the sum of `min(B)`, where `B` ranges over every (contiguous) subarray of `A`.
Since the answer may be large, return the answer modulo 10^9 + 7
.
Example 1:
Input: [3,1,2,4]
Output: 17
Explanation: Subarrays are [3], [1], [2], [4], [3,1], [1,2], [2,4], [3,1,2], [1,2,4], [3,1,2,4].
Minimums are 3, 1, 2, 4, 1, 1, 2, 1, 1, 1. Sum is 17.
Note:
1 <= A.length <= 30000
1 <= A[i] <= 30000
這道題給了一個數組,對於所有的子數組,找到最小值,並返回累加結果,並對一個超大數取余。由於我們只關心子數組中的最小值,所以對於數組中的任意一個數字,需要知道其是多少個子數組的最小值。就拿題目中的例子 [3,1,2,4] 來分析,開始遍歷到3的時候,其本身就是一個子數組,最小值也是其本身,累加到結果 res 中,此時 res=3,然后看下個數1,是小於3的,此時新產生了兩個子數組 [1] 和 [3,1],且最小值都是1,此時在結果中就累加了 2,此時 res=5。接下來的數字是2,大於之前的1,此時會新產生三個子數組,其本身單獨會產生一個子數組 [2],可以先把這個2累加到結果 res 中,然后就是 [1,2] 和 [3,1,2],可以發現新產生的這兩個子數組的最小值還是1,跟之前計算數字1的時候一樣,可以直接將以1結尾的子數組最小值之和加起來,那么以2結尾的子數組最小值之和就是 2+2=4,此時 res=9。對於最后一個數字4,其單獨產生一個子數組 [4],還會再產生三個子數組 [3,1,2,4], [1,2,4], [2,4],其並不會對子數組的最小值產生影響,所以直接加上以2結尾的子數組最小值之和,總共就是 4+4=8,最終 res=17。
分析到這里,就知道我們其實關心的是以某個數字結尾時的子數組最小值之和,可以用一個一維數組 dp,其中 dp[i] 表示以數字 A[i] 結尾的所有子數組最小值之和,將 dp[0] 初始化為 A[0],結果 res 也初始化為 A[0]。然后從第二個數字開始遍歷,若大於等於前一個數字,則當前 dp[i] 賦值為 dp[i-1]+A[i],前面的分析已經解釋了,當前數字 A[i] 組成了新的子數組,同時由於 A[i] 不會影響最小值,所以要把之前的最小值之和再加一遍。假如小於前一個數字,就需要向前遍歷,去找到第一個小於 A[i] 的位置j,假如j小於0,表示前面所有的數字都是小於 A[i] 的,那么 A[i] 是前面 i+1 個以 A[i] 結尾的子數組的最小值,累加和為 (i+1) x A[i],若j大於等於0,則需要分成兩部分累加,dp[j] + (i-j)xA[i],這個也不難理解,前面有 i-j 個以 A[i] 為結尾的子數組的最小值是 A[i],而再前面的子數組的最小值就不是 A[i] 了,但是還是需要加上一遍其本身的最小值之和,因為每個子數組末尾都加上 A[i] 均可以組成一個新的子數組,最終的結果 res 就是將 dp 數組累加起來即可,別忘了對超大數取余,參見代碼如下:
解法一:
class Solution {
public:
int sumSubarrayMins(vector<int>& A) {
int res = A[0], n = A.size(), M = 1e9 + 7;
vector<int> dp(n);
dp[0] = A[0];
for (int i = 1; i < n; ++i) {
if (A[i] >= A[i - 1]) dp[i] = dp[i - 1] + A[i];
else {
int j = i - 1;
while (j >= 0 && A[i] < A[j]) --j;
dp[i] = (j < 0) ? (i + 1) * A[i] : (dp[j] + (i - j) * A[i]);
}
res = (res + dp[i]) % M;
}
return res;
}
};
上面的方法雖然 work,但不是很高效,原因是在向前找第一個小於當前的數字,每次都要線性遍歷一遍,造成了平方級的時間復雜度。而找每個數字的前小數字或是后小數字,正是單調棧擅長的,可以參考博主之前的總結貼 [LeetCode Monotonous Stack Summary 單調棧小結](http://www.cnblogs.com/grandyang/p/8887985.html)。這里我們用一個單調棧來保存之前一個小的數字的位置,棧里先提前放一個 -1,作用會在之后講解。還是需要一個 dp 數組,跟上面的定義基本一樣,但是為了避免數組越界,將長度初始化為 n+1,其中 dp[i] 表示以數字 A[i-1] 結尾的所有子數組最小值之和。對數組進行遍歷,當棧頂元素不是 -1 且 A[i] 小於等於棧頂元素,則將棧頂元素移除。這樣棧頂元素就是前面第一個比 A[i] 小的數字,此時 dp[i+1] 更新還是跟之前一樣,分為兩個部分,由於知道了前面第一個小於 A[i] 的數字位置,用當前位置減去棧頂元素位置再乘以 A[i],就是以 A[i] 為結尾且最小值為 A[i] 的子數組的最小值之和,而棧頂元素之前的子數組就不受 A[i] 影響了,直接將其 dp 值加上即可。將當前位置壓入棧,並將 dp[i+1] 累加到結果 res,同時對超大值取余,參見代碼如下:
解法二:
class Solution {
public:
int sumSubarrayMins(vector<int>& A) {
int res = 0, n = A.size(), M = 1e9 + 7;
stack<int> st{{-1}};
vector<int> dp(n + 1);
for (int i = 0; i < n; ++i) {
while (st.top() != -1 && A[i] <= A[st.top()]) {
st.pop();
}
dp[i + 1] = (dp[st.top() + 1] + (i - st.top()) * A[i]) % M;
st.push(i);
res = (res + dp[i + 1]) % M;
}
return res;
}
};
再來看一種解法,由於對於每個數字,只要知道了其前面第一個小於其的數字位置,和后面第一個小於其的數字位置,就能知道當前數字是多少個子數組的最小值,直接相乘累加到結果 res 中即可。這里我們用兩個單調棧 st_pre 和 st_next,棧里放一個數對兒,由數字和其在原數組的坐標組成。還需要兩個一維數組 left 和 right,其中 left[i] 表示以 A[i] 為結束為止且 A[i] 是最小值的子數組的個數,right[i] 表示以 A[i] 為起點且 A[i] 是最小值的子數組的個數。對數組進行遍歷,當 st_pre 不空,且棧頂元素大於 A[i],移除棧頂元素,這樣剩下的棧頂元素就是 A[i] 左邊第一個小於其的數字的位置,假如棧為空,說明左邊的所有數字都小於 A[i],則 left[i] 賦值為 i+1,否則賦值為用i減去棧頂元素在原數組中的位置的值,然后將 A[i] 和i組成數對兒壓入棧 st_pre。對於 right[i] 的處理也很類似,先將其初始化為 n-i,然后看若 st_next 不為空且棧頂元素大於 A[i],然后取出棧頂元素t,由於棧頂元素t是大於 A[i]的,所以 right[t.second] 就可以更新為 i-t.second,然后將 A[i] 和i組成數對兒壓入棧 st_next,最后再遍歷一遍原數組,將每個 A[i] x left[i] x right[i] 算出來累加起來即可,別忘了對超大數取余,參見代碼如下:
解法三:
class Solution {
public:
int sumSubarrayMins(vector<int>& A) {
int res = 0, n = A.size(), M = 1e9 + 7;
stack<pair<int, int>> st_pre, st_next;
vector<int> left(n), right(n);
for (int i = 0; i < n; ++i) {
while (!st_pre.empty() && st_pre.top().first > A[i]) {
st_pre.pop();
}
left[i] = st_pre.empty() ? (i + 1) : (i - st_pre.top().second);
st_pre.push({A[i], i});
right[i] = n - i;
while (!st_next.empty() && st_next.top().first > A[i]) {
auto t = st_next.top(); st_next.pop();
right[t.second] = i - t.second;
}
st_next.push({A[i], i});
}
for (int i = 0; i < n; ++i) {
res = (res + A[i] * left[i] * right[i]) % M;
}
return res;
}
};
我們也可以對上面的解法進行空間上的優化,只用一個單調棧,用來記錄當前數字之前的第一個小的數字的位置,然后遍歷每個數字,但是要多遍歷一個數字,i從0遍歷到n,當 i=n 時,cur 賦值為0,否則賦值為 A[i]。然后判斷若棧不為空,且 cur 小於棧頂元素,則取出棧頂元素位置 idx,由於是單調棧,那么新的棧頂元素就是 A[idx] 前面第一個較小數的位置,由於此時棧可能為空,所以再去之前要判斷一下,若為空,則返回 -1,否則返回棧頂元素,用 idx 減去棧頂元素就是以 A[idx] 為結尾且最小值為 A[idx] 的子數組的個數,然后用i減去 idx 就是以 A[idx] 為起始且最小值為 A[idx] 的子數組的個數,然后 A[idx] x left x right 就是 A[idx] 這個數字當子數組的最小值之和,累加到結果 res 中並對超大數取余即可,參見代碼如下:
解法四:
class Solution {
public:
int sumSubarrayMins(vector<int>& A) {
int res = 0, n = A.size(), M = 1e9 + 7;
stack<int> st;
for (int i = 0; i <= n; ++i) {
int cur = (i == n) ? 0 : A[i];
while (!st.empty() && cur < A[st.top()]) {
int idx = st.top(); st.pop();
int left = idx - (st.empty() ? -1 : st.top());
int right = i - idx;
res = (res + A[idx] * left * right) % M;
}
st.push(i);
}
return res;
}
};
Github 同步地址:
https://github.com/grandyang/leetcode/issues/907
參考資料:
https://leetcode.com/problems/sum-of-subarray-minimums/
https://leetcode.com/problems/sum-of-subarray-minimums/discuss/170857/One-stack-solution
https://leetcode.com/problems/sum-of-subarray-minimums/discuss/222895/Java-No-Stack-solution.
[LeetCode All in One 題目講解匯總(持續更新中...)](https://www.cnblogs.com/grandyang/p/4606334.html)