Day 44 DP:322|518. 零钱兑换 I II, 377. 组合总和 Ⅳ

0-1背包

  • 每个物品只能使用一次
  • 非降维版本1 (dp[i][j]: 0,...,i)
    • 目标:dp[n-1][bagWeight]
    • 初始化根据物品0来确定
// 初始化 dp
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
for (int j = weight[0]; j <= bagweight; j++) {
    dp[0][j] = value[0];
}
for(int i = 1; i < weight.size(); i++) { // 遍历物品,从i=1开始
    for(int j = 0; j <= bagweight; j++) { // 遍历背包容量,可以顺序
        if (j < weight[i]) dp[i][j] = dp[i - 1][j]; // 需要update dp[i][j]
        else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
    }
}
  • 非降维版本2 (dp[i][j]: 前i个,0,...,i-1)
    • 目标:dp[n][bagWeight]
    • 初始化根据递推逻辑来处理,这样只需遍历全部物品即可。方便初始化
// 初始化 dp
vector<vector<int>> dp(weight.size()+1, vector<int>(bagweight + 1, 0));
for(int i = 1; i < weight.size()+1; i++) { // 遍历物品,从i=1开始, 对应物品i-1
    for(int j = 0; j <= bagweight; j++) { // 遍历背包容量,可以顺序
        if (j < weight[i-1]) dp[i][j] = dp[i - 1][j]; // 需要update dp[i][j]
        else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i-1]] + value[i-1]);
    }
}
  • 降维版本 (dp[j]: 对应非降维版本2)
    • 内层遍历背包需要逆序
// 初始化
vector<int> dp(bagWeight + 1, 0);
for(int i = 1; i < weight.size()+1; i++) { // 遍历物品
    for(int j = bagWeight; j >= weight[i-1]; j--) { // 遍历背包容量, 逆序
        dp[j] = max(dp[j], dp[j - weight[i-1]] + value[i-1]);
    }
}
  • 降维版本等价写法 (dp[j]: 对应非降维版本1)
// 初始化
vector<int> dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量, 逆序
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}
  • 遍历顺序
    • 非降维版本可以交换
    • 降维版本:先物品后背包 (背包逆序)

完全背包

  • 每个物品可以使用多次
  • 非降维版本1 (dp[i][j]: 0,...,i)
  • 非降维版本2 (dp[i][j]: 前i个,0,...,i-1)
    • 递推公式:对每个物品:(不选 vs 选了>=1次)
// 初始化 dp
vector<vector<int>> dp(weight.size()+1, vector<int>(bagweight + 1, 0));
for(int i = 1; i < weight.size()+1; i++) { // 遍历物品,从i=1开始, 对应物品i-1
    for(int j = 0; j <= bagweight; j++) { // 遍历背包容量,可以顺序
        if (j < weight[i-1]) dp[i][j] = dp[i - 1][j]; // 需要update dp[i][j]
        else dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i-1]] + value[i-1]);
    }
}
  • 降维版本 (对应非降维版本2)
vector<int> dp(bagWeight + 1, 0);
for(int i = 1; i < weight.size()+1; i++) { // 遍历物品
    for(int j = weight[i-1]; j <= bagWeight ; j++) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i-1]] + value[i-1]);
    }
}
  • 降维版本等价写法 (对应非降维版本1)
vector<int> dp(bagWeight + 1, 0);
for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}
  • 交换遍历顺序也可以
// 先遍历背包,再遍历物品
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
    for(int i = 0; i < weight.size(); i++) { // 遍历物品
        if (j - weight[i] >= 0) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}
  • 遍历顺序
    • 降维版本可交换,物品和背包都是顺序遍历

322. Coin Change

  • 思路
    • example
    • coins 中的所有值 互不相同
    • 完全背包
    • 先物品,再背包: 二维DP (都是顺序遍历)
class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        # 完全背包
        # dp[i][j]: coins[0,...,i-1] (方便处理初始化?), "amount" = j
        k = len(coins)
        # 注意初始化方式,后面需要取min,同时要满足累加逻辑。
        dp = [[amount+1 for _ in range(amount+1)] for _ in range(k+1)]
        dp[0][0] = 0 # 实际上dp[i][0] = 0 for any i
        for i in range(1, k+1): # 物品
            # coins[i-1]
            for j in range(amount+1): # 背包
                if coins[i-1] > j: # 不能使用coins[i-1]
                    dp[i][j] = dp[i-1][j] 
                else: # 可以使用coins[i-1] (可能多个)
                    dp[i][j] = min(dp[i-1][j], dp[i][j-coins[i-1]]+1)
                                    # 可用而不使用     可用且使用若干个
        return dp[k][amount] if dp[k][amount] != amount+1 else -1
  • 先物品,再背包:一维DP (都是顺序遍历)
class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        # 完全背包
        # dp[j]: 凑成金额j所需要的最小coins个数
        # 注意初始化方式,后面需要取min,同时要满足累加逻辑。
        dp = [amount+1 for _ in range(amount+1)]
        dp[0] = 0 
        for coin in coins: # 物品
            for j in range(amount+1): # 背包
                if coin > j: # 不能使用coin
                    dp[j] = dp[j] # 可删掉(但遍历下界是coin),保留在这里方便对比逻辑
                else: # 可以使用coin (可能多个)
                    dp[j] = min(dp[j], dp[j-coin]+1)
        return dp[amount] if dp[amount] != amount+1 else -1
  • 先物品,再背包: 二维DP (都是顺序遍历). 非前i个版本,初始化比较麻烦。
class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        n = len(coins)
        dp = [[amount+1 for _ in range(amount+1)] for _ in range(n)] 
        dp[0][0] = 0
        for j in range(amount+1):
            if j >= coins[0]:
                dp[0][j] = min(dp[0][j], dp[0][j-coins[0]] + 1)
        for i in range(1, n):
            coin = coins[i] 
            for j in range(amount+1):
                if j < coin:
                    dp[i][j] = dp[i-1][j] 
                else:
                    dp[i][j] = min(dp[i-1][j], dp[i][j-coin]+1)
        return dp[n-1][amount] if dp[n-1][amount] != amount+1 else -1
  • 先背包,再物品:二维DP (都是顺序遍历)
class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        # 完全背包
        # dp[i][j]: coins[0,...,i-1] (方便处理初始化?), "amount" = j
        k = len(coins)
        # 注意初始化方式,后面需要取min,同时要满足累加逻辑。
        dp = [[amount+1 for _ in range(amount+1)] for _ in range(k+1)]
        dp[0][0] = 0 # 实际上dp[i][0] = 0 for any i
        for j in range(amount+1): # 背包
            for i in range(1, k+1): # 物品
                # coins[i-1]
                if coins[i-1] > j: # 不能使用coins[i-1]
                    dp[i][j] = dp[i-1][j] 
                else: # 可以使用coins[i-1] (可能多个)
                    dp[i][j] = min(dp[i-1][j], dp[i][j-coins[i-1]]+1)
                                    # 不使用     使用(可能多个)
        return dp[k][amount] if dp[k][amount] != amount+1 else -1
  • 先背包,再物品:一维DP (都是顺序遍历)
class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        # 完全背包
        # dp[j]: 凑成金额j所需要的最小coins个数
        # 注意初始化方式,后面需要取min,同时要满足累加逻辑。
        dp = [amount+1 for _ in range(amount+1)] # !!!
        dp[0] = 0 # !!!
        for j in range(amount+1): # 背包
            for coin in coins: # 物品
                if coin > j: # 不能使用coin
                    dp[j] = dp[j] # 可删掉,保留在这里方便对比逻辑
                else: # 可以使用coin (可能多个)
                    dp[j] = min(dp[j], dp[j-coin]+1)
        return dp[amount] if dp[amount] != amount+1 else -1

518. 零钱兑换 II

  • 思路
    • example
    • coins 中的所有值 互不相同
    • 完全背包 + 组合(先物品后背包)
      • 物品:硬币(每个可以无限使用)
      • 背包:金额数
    • 如果要求返回全部方案组合,需要用dfs/回溯 搜索。这里只需要返回方案组合的数目,所以考虑用DP.
    • dp[j]: 凑成金额j的组合总数。
  • 复杂度. 时间:O(?), 空间: O(?)
class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        dp = [0 for _ in range(amount+1)]
        dp[0] = 1 # !!!
        for coin in coins:
            for j in range(coin, amount+1):
                dp[j] += dp[j-coin]
        return dp[amount] 
class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        n = len(coins)
        dp = [[0 for _ in range(amount+1)] for _ in range(n+1)]
        dp[0][0] = 1 
        for i in range(1, n+1):
            coin = coins[i-1]
            for j in range(amount+1):
                if j < coin:
                    dp[i][j] = dp[i-1][j]
                else:
                    dp[i][j] = dp[i-1][j] + dp[i][j-coin] 
        return dp[n][amount]  
  • 这里涉及到组合问题,需要去重先物品后背包可以去重。如果先背包后物品会有重复计算 (1+2 = 2+1 = 3).

377. 组合总和 Ⅳ

  • 思路
    • example
    • 类似零钱兑换 II,但是这里是要求排列总和(不是组合总和)
    • 如果要求返回全部方案,需要用回溯(排列问题)法。


    • dp[j]: 凑成j的排列总数
      • e.g., 3 = [(0) + 3] + [(1) + 2] + [(2) + 1]
        • dp[3] = dp[2] + dp[1] + dp[0]
    • 遍历顺序:先背包再物品
  • 复杂度. 时间:O(?), 空间: O(?)
class Solution:
    def combinationSum4(self, nums: List[int], target: int) -> int:
        dp = [0 for _ in range(target+1)]
        dp[0] = 1 # !!!
        for j in range(target+1):
            for num in nums:
                if j >= num:
                    dp[j] += dp[j-num] 
        return dp[target]  
  • 应该用爬楼梯框架来理解更容易。不要用背包问题框架去解释!!!
class Solution:
    def combinationSum4(self, nums: List[int], target: int) -> int:
        dp = [0 for _ in range(target+1)]
        dp[0] = 1
        for j in range(1, target+1):
            for num in nums:
                if j >= num:
                    dp[j] += dp[j-num] 
        return dp[target] 
  • 2D: 比较麻烦
class Solution:
    def combinationSum4(self, nums: List[int], target: int) -> int:
        n = len(nums) 
        dp = [[0 for _ in range(target+1)] for _ in range(n+1)]
        dp[0][0] = 1  # !!!
        for j in range(target+1):
            for i in range(1, n+1):
                if j < nums[i-1]:
                    dp[i][j] = dp[i-1][j]
                else:
                    for k in range(1, n+1):
                        if j >= nums[k-1]:
                            dp[i][j] += dp[i][j-nums[k-1]] 
        return dp[n][target]
  • Follow up: What if negative numbers are allowed in the given array? How does it change the problem? What limitation we need to add to the question to allow negative numbers?

例如,假设数组 nums 中含有正整数 a 和负整数 −b(其中 a>0,b>0,−b<0),则有 a×b+(−b)×a=0,对于任意一个元素之和等于 target 的排列,在该排列的后面添加 b 个 a 和 a 个 −b 之后,得到的新排列的元素之和仍然等于 target,而且还可以在新排列的后面继续 b 个 a 和 a 个 −b。因此只要存在元素之和等于 target 的排列,就能构造出无限长度的排列。
如果允许负数出现,则必须限制排列的最大长度,避免出现无限长度的排列,才能计算排列数。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容