雙周賽 126。

題目

輸入長度 n 的整數陣列 nums,還有正整數 k。

一個陣列的力量值等於其總和為 k 的子序列的數量。

求 nums 所有子序列的力量值總和
答案可能很大,先模 10^9 + 7 後回傳。

解法

這題有點妙,計算力量值本身就要找子序列。所有子序列的力量值相當於找子序列的子序列
看看 n <= 100,光是要生子序列的 2^n 都會超時,更別想子序列的子序列。


與其暴力生成所有子序列,不如找找看那些總和為 k 的子序列會對答案貢獻幾次。

舉個例子:

nums = [1,1,5], k = 5

很明顯一定要包含 5,他的子序列才可能滿足 k。

[1,1,5], [1,_,5], [_,1,5], [_,_,5]
這四個子序列分別都可以貢獻一個子序列 [5]
答案 = 4

再來看看範例 1:

nums = [1,2,3], k = 3

[1,2] 和 [3] 都滿足 k。看看他們各出現幾次:

[1,2,3], [1,2,_] 各貢獻一次 [1,2]
[1,2,3], [1,_,3], [_,2,3], [_,_,3] 各貢獻一次 [3]
答案 = 2 + 4 = 6

好像發現了一點規律:如果有個長度為 size 的子序列滿足 k,則他的貢獻次數 = 2^(N - size)。
更嚴謹地說,如果有一子序列 [x1,x2] 滿足 k,每多出一個數 xi,都會使得子序列的數量翻倍,而且他們都必定包含 [x1,x2]:

最初 [x1,x2]
加入 x3
[x1,x2,x3], [x1,x2,_]
加入 x4
[x1,x2,x3,x4], [x1,x2,_,x4], [x1,x2,x3,_], [x1,x2,_,_]


如此一來,問題就轉換成:

  • 找到長度 size,且滿足 k 的子序列有幾個
  • 根據 size 計算貢獻

在 nums 中找特定總和的子序列是比較常見的問題。
定義 dp(i, sm):已選的子序列總和為 sm,在 nums[i..(N-1)] 有幾種選法可以滿足 k。
我們同時需要知道已選的長度,因此要多個狀態,變成 dp(i, sm, size)。

轉移:nums[i] 選或不選,兩個加起來。

  • 選 nums[i]:dp(i + 1, sm + nums[i], size + 1)
  • 不選 nums[i]:dp(i + 1, sm, size)

BASE:當 i = N 且 sm = k 時,滿足條件,根據 cnt 計算貢獻次數;否則當 sm > k 或 i = N 時不合法,回傳 0。


在沒有選擇元素的的情況下,在 nums[0..(N-1)] 中滿足 k 的子序列貢獻次數。
答案入口就是 dp(0, 0, 0)。

注意:乍看之下子序列總和可能很大,但是因為超過 k 的部分都直接剪枝掉,因此只會保留總和 0~k 的狀態。
而 i 和 size 最多為 n,複雜度 O(N^2 * k),最多 10^6 個狀態。

時間複雜度 O(N^2 * k)。
空間複雜度 O(N^2 * k)。

class Solution:
    def sumOfPower(self, nums: List[int], k: int) -> int:
        MOD = 10 ** 9 + 7
        N = len(nums)
        
        @cache
        def dp(i, sm, size):
            if i == N and sm == k:
                return 2 ** (N - size)
            
            if i == N or sm > k:
                return 0
            
            res = dp(i + 1, sm, size) # no take
            res += dp(i + 1, sm + nums[i], size + 1) # take
            return res % MOD
        
        ans = dp(0, 0, 0)
        dp.cache_clear()
        
        return ans % MOD