Given an array of integers `A`, consider all non-empty subsequences of `A`.
For any sequence S, let the width of S be the difference between the maximum and minimum element of S.
Return the sum of the widths of all subsequences of A.
As the answer may be very large, return the answer modulo 10^9 + 7.
Example 1:
Input: [2,1,3]
Output: 6
Explanation: Subsequences are [1], [2], [3], [2,1], [2,3], [1,3], [2,1,3].
The corresponding widths are 0, 0, 0, 1, 1, 2, 2.
The sum of these widths is 6.
Note:
1 <= A.length <= 20000
1 <= A[i] <= 20000
這道題給了我們一個數組,並且定義了一種子序列的寬度,就是非空子序列中最大值和最小值的差值,讓我們算出所有的子序列的寬度之和,而且提示了結果可能是個超大數,要對 1e9+7 取余。由於要求是子序列,所以不必像子數組那樣必須要連續,並且我們只關心最大值和最小值,其他的數字並不 care。由於子序列並不存在順序之分,所以我們可以開始就對輸入數組進行排序,並不會影響最終的結果。想到這里博主的思路就斷了,難道要生成所有的子序列,然后一個一個的計算差值么,這種思路對得起 Hard 標簽么?但一時半會又想不出什么其他好思路,其實這道題的最優解法相當的 tricky,基本有點腦筋急轉彎的感覺了。在解題之前,我們首先要知道的是一個長度為n的數組,共有多少個子序列,如果算上空集的話,共有 2^n 個。那么在給數組排序之后,對於其中任意一個數字 A[i],其前面共有i個數是小於等於 A[i] 的,這i個數字共有 2^i 個子序列,它們加上 A[i] 都可以組成一個新的非空子序列,並且 A[i] 是這里面最大的數字,那么在寬度計算的時候,就要加上 A[i] x (2^i),同理,A[i] 后面還有 n-1-i 個數字是大於等於它的,后面可以形成 2^(n-1-i) 個子序列,每個加上 A[i] 就都是一個新的非空子序列,同時 A[i] 是這些子序列中最小的一個,那么結果中就要減去 A[i] x (2 ^ (n-1-i))。對於每個數字都這么計算一下,就是最終要求的所有子序列的寬度之和了。可能你會懷疑雖然加上了 A[i] 前面 2^i 個子序列的最大值,那些子序列的最小值減去了么?其實是減去了的,雖然不是在遍歷 A[i] 的時候減去,在遍歷之前的數字時已經將所有該數字是子序列最小值的情況減去了,同理,A[i] 后面的那些 2^(n-1-i) 個子序列的最大值也是在遍歷到的時候才加上的,所以不會漏掉任何一個數字。在寫代碼的時候有幾點需要注意的地方,首先,結果 res 要定義為 long 型,因為雖然每次會對 1e9+7 取余,但是不能保證不會在取余之前就已經整型溢出,所以要定義為長整型。其次,不能直接算 2^i 和 2^(n-1-i),很容易溢出,即便是長整型,也有可能溢出。那么解決方案就是,在累加i的同時,每次都乘以個2,那么遍歷到i的時候,也就乘到 2^i 了,防止溢出的訣竅就是每次乘以2之后就立馬對 1e9+7 取余,這樣就避免了指數溢出,同時又不影響結果。最后,由於這種機制下的 2^i 和 2^(n-1-i) 不方便同時計算,這里又用了一個 trick,就是將 A[i] x (2^(n-1-i)) 轉換為了 A[n-1-i] x 2^i,其實二者最終的累加和是相等的:
sum(A[i] * 2^(n-1-i)) = A[0]*2^(n-1) + A[1]*2^(n-2) + A[2]*2^(n-3) + ... + A[n-1]*2^0
sum(A[n-1-i] * 2^i) = A[n-1]*2^0 + A[n-2]*2^1 + ... + A[1]*2^(n-2) + A[0]*2^(n-1)
可以發現兩個等式的值都是相等的,只不過順序顛倒了一下,參見代碼如下:
解法一:
class Solution {
public:
int sumSubseqWidths(vector<int>& A) {
long res = 0, n = A.size(), M = 1e9 + 7, c = 1;
sort(A.begin(), A.end());
for (int i = 0; i < n; ++i) {
res = (res + A[i] * c - A[n - i - 1] * c) % M;
c = (c << 1) % M;
}
return res;
}
};
我們也可以換一種寫法,使用兩個累加和 leftSum 和 rightSum,其中 leftSum[i] 表示數組范圍 [0, i] 內的數字之和,rightSum[i] 表示數組范圍 [i, n-1] 內的數字之和,然后每次在i位置,累加 (rightSum - leftSum) x 2^i 到結果 res,也能得到同樣正確的結果,這是為啥呢?我們只要將 (rightSum - leftSum) x 2^i 展開,就能明白了:
sum((rightSum - leftSum) * 2^i) =
(A[n-1] - A[0]) * 2^0 +
(A[n-1] + A[n-2] - A[1] - A[0]) * 2^1 +
(A[n-1] + A[n-2] + A[n-3] - A[2] - A[1] - A[0]) * 2^2 +
... +
(A[n-1] + A[n-2] - A[1] - A[0]) * 2^(n-3)
(A[n-1] - A[0]) * 2^(n-2)
=
A[n-1] * (2^(n-1) - 2^0) + A[n-2] * (2^(n-2) - 2^1) + ... + A[0] * (2^0 - 2^(n-1))
=
sum(A[i] * 2^i - A[i] * 2^(n-1-i))
我們發現,最終還是轉化成了跟解法一中一樣的規律,參見代碼如下:
解法二:
class Solution {
public:
int sumSubseqWidths(vector<int>& A) {
long res = 0, n = A.size(), M = 1e9 + 7, c = 1;
int leftSum = 0, rightSum = 0, left = 0, right = n - 1;
sort(A.begin(), A.end());
while (left < n) {
leftSum += A[left++];
rightSum += A[right--];
res = (res + (rightSum - leftSum) * c) % M;
c = (c << 1) % M;
}
return res;
}
};
討論:做完了這道題之后,博主就在想,如果將子序列變成子數組,其他不變,該怎么做?稍稍想了一下,發現是 totally different story,這道題的方法完全就不能使用了,因為子數組是不能排序的,也不能不連續,雖然只改了幾個字,但是完全是兩道題。博主能想到的就是建立一個類似分段樹的結構,保存每個連續區間的最大值和最小值,從而解決問題。各位看官大神有什么好想法請留言討論哈~
Github 同步地址:
https://github.com/grandyang/leetcode/issues/891
參考資料:
https://leetcode.com/problems/sum-of-subsequence-widths/
https://leetcode.com/problems/sum-of-subsequence-widths/discuss/162318/O(nlogn)-solution
[LeetCode All in One 題目講解匯總(持續更新中...)](https://www.cnblogs.com/grandyang/p/4606334.html)