day36 动态规划04

107 阅读4分钟

1049. 最后一块石头的重量 II

文章讲解

思路:

参考分隔等和子集,本题试图凑出sum//2,如果凑出来,y-x=y-y=0,如果凑不出来,就取dp[n][target]时候能凑出的最大值
class Solution:
    def lastStoneWeightII(self, stones: List[int]) -> int:
        # 计算背包容量
        n = len(stones)
        total = sum(stones)
        target = total // 2 # 另一堆是total-target>target

        # dp定义和初始化:dp[i][j]表示前i堆石头是否有子集恰好装满容量为j的背包
        dp = [[False] * (target+1) for _ in range(n+1)]
        # 背包容量为0时候,空集能装满
        for i in range(n+1):
            dp[i][0] = True

        # 遍历石头
        for i in range(1, n+1):
            for j in range(1, target+1):
                if j < stones[i-1]:
                    dp[i][j] = dp[i-1][j]
                else:
                    dp[i][j] = dp[i-1][j] or dp[i-1][j-stones[i-1]]

        # 逆序遍历,找到第一个装满的最大容量j
        for j in range(target,-1,-1):
            if dp[n][j]:
                return total - 2 * j # 最大容量为j,恰好被装满
        return 0

滚动数组:


class Solution:
    def lastStoneWeightII(self, stones: List[int]) -> int:
        # 计算背包容量
        n = len(stones)
        total = sum(stones)
        target = total // 2 # 另一堆是total-target>target

        # dp定义和初始化:dp[i][j]表示前i堆石头是否有子集恰好装满容量为j的背包
        dp = [0] * (target+1)
        # 背包容量为0时候,空集能装满
        dp[0] = True

        # 遍历石头
        for i in range(n):
            for j in range(target, stones[i-1]-1,-1): # 可以j=stones[i-1]
                dp[j] = dp[j] or dp[j-stones[i-1]]

        # 逆序遍历,找到第一个装满的最大容量j
        for j in range(target,-1,-1):
            if dp[j]:
                return total - 2 * j # 最大容量为j,恰好被装满
        return 0

494. 目标和

文章讲解

思路1:回溯

回溯,每个位置有+elem -elem两种选择
class Solution:
    def findTargetSumWays(self, nums: List[int], target: int) -> int:
        self.target = target
        self.nums = nums
        self.count = 0 # 满足target的表达式数量

        self.backtrack(0, 0)
        return self.count
        
    def backtrack(self, index: int, current_sum: int)->None:
        # base case
        if index == len(self.nums):
            if current_sum == self.target:
                self.count += 1
            return 

        # +1 or -1
        self.backtrack(index+1, current_sum + self.nums[index])
        self.backtrack(index+1, current_sum - self.nums[index])

剪枝:

class Solution:
    def findTargetSumWays(self, nums: List[int], target: int) -> int:
        self.target = target
        self.nums = nums
        self.count = 0  # 满足target的表达式数量
        self.memo = {}  # 备忘录
        self.backtrack(0, 0)
        return self.count

    def backtrack(self, index: int, current_sum: int) -> None:
        # 备忘录:检查当前状态是否已经计算过
        if (index, current_sum) in self.memo:
            self.count += self.memo[(index, current_sum)]
            return

        # base case
        if index == len(self.nums):
            if current_sum == self.target:
                self.count += 1
                self.memo[(index, current_sum)] = 1
            else:
                self.memo[(index, current_sum)] = 0
            return

        # 剪枝
        remaining_sum = sum(self.nums[index:])
        if current_sum + remaining_sum < self.target or current_sum - remaining_sum > self.target:
            self.memo[(index, current_sum)] = 0
            return

        # +1 or -1
        original_count = self.count
        self.backtrack(index + 1, current_sum + self.nums[index])
        self.backtrack(index + 1, current_sum - self.nums[index])
        self.memo[(index, current_sum)] = self.count - original_count

思路2:dp

-   将问题转化为01背包问题
-   设所有数字和为sum,添加正号的数字和为x,则添加负号的数字和为sum-x
-   根据题目要求:x - (sum-x) = target
-   可得:x = (target + sum)/2
class Solution:
    def findTargetSumWays(self, nums: List[int], target: int) -> int:
        # 0-1背包
        total = sum(nums)
        n = len(nums)

        # base case: target+total能被2整除;target不能大于total
        if abs(target) > total:
            return 0 # 0种方法
        if (total+target)%2 !=0:
            return 0
        
        # dp初始化:dp[j]表示装满容量为j的背包有多少种方法
        bag_size = (total + target) //2 
        dp = [0] * (bag_size + 1)
        dp[0] = 1 # 空包时候空集算1种方法

        # 迭代
        for i in range(n):
            for j in range(bag_size, nums[i-1]-1, -1):
                dp[j] += dp[j-nums[i-1]]
        return dp[bag_size]

474. 一和零

文章讲解

思路:

定义:dp[i][j]表示在i个0j个1的条件下,最多的字符串数量
    背包容量:m个0n个1
    物品:字符串
    物品重量:字符串有多少个0和多少个1
    物品价值:1,求得是字符串的数量,每个字符串价值为1
初始化:dp[0][0]都是0吧
递推:
    不取当前字符:dp[i][j] 不变
    取当前字符:dp[i][j] = dp[i-zerosNum][j-onesNum] + 1
遍历方向:顺序遍历每个字符串;逆序遍历容量m和n,防止重复计算物品(0-1背包特点)
举例:
输入: strs = ["10", "0", "1"], m = 1, n = 1
输出: 2
解释: 最大的子集是 {"0", "1"} ,所以答案是 2 。
dp = [[0,0],[0,0]]
迭代字符1: "10"
    zerosNum = 1
    onesNum = 1
        
    i=1;j=1;
        dp[1][1] = max(dp[1][1], dp[1-1][1-1]+1) = max(0, 1) = 1
迭代字符串2: "0"
    zerosNum = 1
    onesNum = 0
    
    i=1;j=1;
        dp[1][1] = max(dp[1][1], dp[1-1][1-0]+1) = max(1, 1) = 1
    i=1;j=0;
        dp[1][0] = max(dp[1][0], dp[1-1][0-0]+1) = max(0, 1) = 1
        
迭代字符串3: "1"
    zerosNum =0
    onesNum = 1
    
    i=1;j=1;
        dp[1][1] = max(dp[1][1], dp[1-0][1-1]+1) = max(1, 2) = 2
    i=0;j=1;
        dp[0][1] = max(dp[0][1], dp[0-0][1-1]+1) = max(0, 1) = 1
  
 dp[1][1] = 2 

注意点:
-   理解每个字符串是一个物品
-   m和n是两个维度的背包容量
-   需要倒序遍历背包容量
class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        # dp数组
        dp = [[0] * (n+1) for _ in range(m+1)]

        # 遍历每个字符
        for c in strs:
            # 统计0和1的数量
            zerosNum = c.count("0")
            onesNum = c.count("1")
            # 逆序遍历容量m和n
            for i in range(m, zerosNum-1, -1):
                for j in range(n, onesNum-1, -1):
                    dp[i][j] = max(dp[i][j], dp[i-zerosNum][j-onesNum] + 1)
        return dp[m][n]

时间复杂度: O(kmn), k是字符串数组长度

空间复杂度: O(mn)