LeetCode 907 - Sum of Subarray Minimums
題目
題目連結:https://leetcode.com/problems/sum-of-subarray-minimums/
給定一個序列,求出所有子區間的最小值的和。
範例說明
Example 1:
1 | Input: arr = [3,1,2,4] |
Example 2:
1 | Input: arr = [11,81,94,43,3] |
想法
由 O(N^3) 到 O(N^2) 的想法
首先,最簡單的方法為枚舉每個區間,再求出區間最小值並總和。
以 i
為左界,以 j
為右界:
1 | int sum = 0; |
可以容易地發現,對於相同的左界 i
來說,每次右界 j
都只增加一個數字。因此只要將新增的數字與上一輪計算的 minValue
進行更新,就可以不用內層的 k
迴圈了。
1 | int sum = 0; |
如此一來可以得到一個 O(N^2)
的算法。
由 O(N^2) 到 O(N) 的想法
我們改變迴圈的順序,改為先枚舉右邊界再枚舉左邊界,注意迴圈 i
的順序以保證對於同一個右邊界 j
,i
的改變是使區間變大的:
1 | int sum = 0; |
對於每一個右邊界 j
所形成的區間,可以觀察出只有由右而左遞減的數字才是有用的,舉例來說,以 [3, 1, 7, 5]
的 5
為右邊界,所形成的區間如下:
Subarray | Decreasing (<-) | Minimum | Sum |
---|---|---|---|
[ 5] |
[ 5] |
5 | 5 |
[ 7, 5] |
[ x, 5] |
5 | 10 |
[ 1, 7, 5] |
[ 1, x, 5] |
1 | 11 |
[3, 1, 7, 5] |
[x, 1, x, 5] |
1 | 12 |
可以發現數字 7
對於以 5
為右邊界的區間來說並沒有貢獻,因為所有以 5
為右邊界的區間都會有數字 5
;
而對於以 5
為右邊界,左邊界比數字 1
還要小的數字來說,所有的區間都會包含數字 1
,因此比數字 1
大的數字也沒有用。
因而產生了一個由右而左的遞減序列。
有了這個性質,我們可以維護一個由右而左遞減的序列,以知道哪些數字是有用的。知道了哪些數字是有用的外,還需要知道這些數字的用處有多大,以很快的計算出以 j
為右界的區間的最小值的和。
可以發現上表中,最後計算出來的 Sum
是 12 = 1 * 2 + 5 * 2
,假設:
- 右區間變大,增加一個數字
4
:Decreasing -> [x, 1, x, x, 4]
,Sum = 1 * 2 + 4 * 3 = 14
- 右區間變大,增加一個數字
6
:Decreasing -> [x, 1, x, x, 4, 6]
,Sum = 1 * 2 + 4 * 3 + 6 * 1 = 20
。 - 右區間變大,增加一個數字
0
:Decreasing -> [x, x, x, x, x, x, 0]
,Sum = 0 * 7 = 0
。 - 右區間變大,增加一個數字
8
:Decreasing -> [x, x, x, x, x, x, 0, 8]
,Sum = 0 * 7 + 8 * 1
。
可以發現,一個數字的貢獻(下面稱作 weight
)即是他左邊的 x
的數量加一。也等於看成是他左邊所有數的 weight
加一。
最後,由左而右,每次將右區間變大(增加一個數字),並且維護由右而左遞減的序列以及一個值 Sum
。因為每個數字最多只會進入、離開棧一次,所以總時間複雜度為 O(N)
。
實作細節
要實作一個單調遞增/遞減的序列,並且只有在同一邊增加數字的操作時,要使用的資料結構是 stack
,或是可以稱為 單調棧(Monotone Stack)。
要利用棧維護一個由右而左遞減的序列,每當右區間變大時,不斷將棧頂部比此元素大的數字都移除即可。
本題除了要紀錄加入棧的元素的值外,還額外需要紀錄每個數字的貢獻 weight
,以快速的計算區間最小值的和。
最後,在 由 O(N^2) 到 O(N) 的想法 的最開始,筆者並沒有特別提到為何要將迴圈的順序改變。改變迴圈的順序主要是想要將區間變成由小到大,也就是當 j
變大時,右區間變大,因此區間的大小是增加的。區間大小變大的好處是代表我們已經看過了較小區間的資訊,才有可能可以依照那些資訊來快速的找出答案。
也就是現在的想法 將右邊界由左而右,維護一個由右而左遞減的序列,其實也可以改為 將左邊界由右而左,維護一個由左而右的遞增序列。兩個方法都是可行的!
程式碼
1 | /** |