【Lintcode】python - [563] Backpack V 個人解法筆記

題目出處

563 · Backpack V

難度

medium

個人範例程式碼

from typing import (
    List,
)

class Solution:
    """
    @param nums: an integer array and all positive numbers
    @param target: An integer
    @return: An integer
    """
    def back_pack_v(self, nums: List[int], target: int) -> int:
        # write your code here
        if not nums:
            return 0

        # [1,1,1,1] has 4 ans, remove 1 has 4 choice

        n = len(nums)
        dp = [[0] * (target +1) for _ in range(2)]
        dp[0][0] = 1
        prefix_sum = 0

        for i in range(1, n+1): # take next category of coin
            dp[i%2][0] = 1
            prefix_sum += nums[i-1] 
            for j in range(1, min(target, prefix_sum)+1): # find current max
                dp[i%2][j] = dp[(i-1)%2][j]
                last_volume = j - nums[i-1]
                if last_volume >= 0: # add take new one coin method
                    dp[i%2][j] += dp[(i-1)%2][last_volume]

        return dp[n%2][target]

算法說明

本題是背包系列問題的第 5 題,建議初學者可以從第一題開始去學:
第 1 題:最基本的背包問題,不重複 size,物品只有一個,計算組合可能性
第 2 題:最基本的背包問題,不重複 size,物品只有一個,計算最大價值
第 3 題:完全背包問題,有重複 size,物品無限數量,計算最大價值
第 4 題:有重複 size,物品無限數量,計算可能組合 ,「(1,2,3) 與 (2,3,1) 視為相同答案」
第 5 題:有重複 size,物品有限數量,計算可能組合 ,「(1,2,3) 與 (2,3,1) 視為相同答案」
第 6 題:有重複 size,物品無限數量,計算可能「排列」 ,也就是說「(1,2,3) 與 (2,3,1) 視為不同答案」
第 7 題:有重複物品,有限制物品數數量上限,問可能組合
第 8 題:有重複物品,有限制物品數數量上限,問最大價值
第 9 題:變化問題,有重複 size,計算機率
第 10 題:變化問題,只是把 size 面額提高

第五題,我們變成只有有限數量的物品可以拿,求可能的方法總數。
因為拿的順序會影響結果,例如:「(2,2,3) 與 (3,2,2) 是同一個結果」,
我們一開始就先 nums.sort()

但這邊要注意陷阱「同 size 的不同物品,是為不同組合」,例如: [1,1,1,1], target = 3,要算 4 個結果 (可以想像我們是在決定拿掉哪一個)。

外圈 for 我們去控制目前新拿的物品種類
內圈 for 控制目前背包最大的容量

dp 的運作

dp[i][j],j 代表目前的大小,i 反映我們目前選擇的物件。

優化空間 - 滾動數組

這裡我們先放上優化前的 code (會有「空間超過的問題」)

from typing import (
    List,
)

class Solution:
    """
    @param nums: an integer array and all positive numbers
    @param target: An integer
    @return: An integer
    """
    def back_pack_v(self, nums: List[int], target: int) -> int:
        # write your code here
        if not nums:
            return 0

        # [1,1,1,1] has 4 ans, remove 1 has 4 choice

        n = len(nums)
        dp = [[0] * (target +1) for _ in range(n+1)]
        dp[0][0] = 1

        for i in range(1, n+1): # take next category of coin
            dp[i][0] = 1
            for j in range(1, target+1):
                dp[i][j] = dp[i-1][j]
                last_volume = j - nums[i-1]
                if last_volume >= 0: # add take new one coin method
                    dp[i][j] += dp[i-1][last_volume]

        return dp[-1][-1]

我們仔細觀察我們 dp 的運算過程,基本上每一層新的 dp[i][],
我們都只會參考 dp[i-1][] 的資料而已,(也就說,我們只參考前一層),

因此我們可以利用這個特性節省空間,宣告空間 i = 2 即可,
而取 i 的變化時,我們取 mod 2,

最後的答案就位於 dp[n%2][volume] 中。

小優化時間

在內層的 for 迴圈,我們其實每次不一定有必要都算到 target 那麼大,
我們可以知道,多一個新的物品,最大可能的 size 就是前面全選的合。

因此,在內層的 for 迴圈中,
我們可以多取一個 「min(target, prefix_sum) + 1」,

可以節省大量的明明算不到 target,卻總是算到 target 位置的時間。

input handling

如果沒有 nums,回傳 0,
其他一同在 dp 內處理

Boundary conditions

用兩層 for 來控制搜尋範圍,
外層 for size (表示這個新的物品,我們決定拿與不拿有可能有哪些組合。)
內層 for volume (表示我們目前背包的大小)

Reference