动规规划-完全背包问题

有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。 首先回顾一下0-1背包问题,它和完全背包问题的区别就是是否可以多次取;那么我们再看下0-1背包的状态转移方程:dp[n][j] = max (dp[n-1][j] , dp[n-1][j-v[n]]+v[n]) ,其中,dp[n-1][j] = dp[n-1][j-0v[n]]+0v[n] 。我们很容易发现0-1背包的状态方程建立是考虑取不取的问题,那么完全背包问题就考虑取几个的问题,因此只需要改下0-1背包的状态方程就可以,将0、1改成k,k表示某个物品最多取k次。dp[n][j] = max (dp[n-1][j-countarr[n]]+countarr[n]) ,其中0<=count<=j/arr[n]

1、 本题为经典的完全背包问题,但是下列传统思路的代码会超时,需要进一步改进!实际上,复杂度的提升是决策层循环带来的,因此将决策层优化就能提升效率。

class Solution:
    def back_pack_i_i_i(self, a , v , m: int) -> int:
        # write your code here
        # 动规思路:dp[n][j] 表示前n个物品取不超过j的体积的最大价值
        # dp[n][j] = max (dp[n-1][j-count*arr[n]]+count*arr[n]) ,其中0<=count<=j/arr[n]

        dp = [[-1] * (m + 1) for _ in range(len(a))]

        for j in range(m+1):
            count = j // a[0]
            for c in range(count, -1, -1):  #决策层循环
                if c * a[0] <= j:
                    dp[0][j] = c * v[0]
                    break
                else:
                    dp[0][j] = 0

        def func(n, j):
            if n == 0:
                return dp[n][j]

            res = 0
            count = j // a[n]
            for c in range(count + 1):  #决策层循环
                if dp[n - 1][j - c * a[n]] == -1:
                    dp[n - 1][j - c * a[n]] = func(n - 1, j - c * a[n])
                res = max(res, dp[n - 1][j - c * a[n]] + c * v[n])
            return res

        return func(len(a) - 1, m)

 其实决策层那里经过数学推导可以得到:dp[n][j]=max(dp[n-1][j],dp[n][j-v[n]]+v[n]); 这与0-1背包非常相似,我们看下0-1背包的方程:dp[n][j] = max (dp[n-1][j] , dp[n-1][j-v[n]]+v[n])。其实区别就是在后面取最后一个物品的时候算前n个而不是n-1个。01背包考虑是放或者不放,所以考虑前n-1个物品的最大值与最后一个物品取不取的关系,而完全背包是不放或者放多件的关系,考虑的是前n件物品的最大值与还能不能放的问题。反正记住一点就行,完全背包的max中第二项考虑前n个,其他与0-1背包一样,代码如下:

class Solution:
    import sys  # 导入sys模块
    sys.setrecursionlimit(100000)  # 将默认的递归深度修改为100000
    def back_pack_i_i_i(self, a , v , m: int) -> int:
        # write your code here
        # 动规思路:dp[n][j] 表示前n个物品取不超过j的体积的最大价值
        # dp[n][j] = max (dp[n-1][j] , dp[n][j-v[n]]+v[n])
        if(len(a)==0):
            return 0

        dp = [[-1] * (m + 1) for _ in range(len(a))]

        for j in range(m+1):
            count = j // a[0]
            for c in range(count, -1, -1):  #优化决策层循环
                if c * a[0] <= j:
                    dp[0][j] = c * v[0]
                    break
                else:
                    dp[0][j] = 0

        def func(n, j): 
            if n == 0:
                return dp[n][j]
            if j==0:
                return 0

            if dp[n-1][j]==-1:
                dp[n - 1][j] = func(n-1,j)
            res = dp[n - 1][j]

            if j>=a[n]:
                if dp[n][j-a[n]]==-1:
                    dp[n][j - a[n]] = func(n,j-a[n])
                res = max(res,dp[n][j - a[n]]+v[n])
            dp[n][j] = res
            return res

        return func(len(a) - 1, m)

该题是经典的求方案数,我们知道动态规划善于用来解决 求最值、求方案数、求可行性三种题目,那么本题就是求方案数。但是本题要看清楚是只能取一次还是能够重复取,如果是重复取的话就是完全背包问题。先用0-1背包构建状态方程,然后再改n-1为n。请参考下面代码:

class Solution:
    def change(self, amount: int, coins ) -> int:

        # 动规思路:dp[n][j]表示前n个数凑成j的方案数
        # dp[n][j] = max(dp[n-1][j],dp[n-1][j-a[n]]) 0-1背包问题
        # dp[n][j] = max(dp[n-1][j],dp[n][j-a[n]])   完全背包问题
        if amount==0:
            return 1

        dp = [[-1] * (amount + 1) for _ in range(len(coins) + 1)]

        # 老是初始化出错,记得完全背包初始化时要考虑多个
        for j in range(amount+1):
            if j >= coins[0] and j // coins[0] == j / coins[0]:
                dp[0][j] = 1
            else:
                dp[0][j] = 0

        def func(n, j):
            if n == 0:
                return dp[n][j]
            if j == 0:
                return 1

            if dp[n - 1][j] == -1:
                dp[n - 1][j] = func(n - 1, j)
            res = dp[n - 1][j]

            if j >= coins[n]:
                if dp[n][j - coins[n]] == -1:
                    dp[n][j - coins[n]] = func(n, j - coins[n])
                res = dp[n - 1][j] + dp[n][j - coins[n]] 
            dp[n][j] = res
            return res

        return func(len(coins) - 1, amount)
class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        #完全背包问题 dp[i][j] 前i个数凑成j所需的最少银币数
        #dp[i][j] = min(dp[i-1][j],dp[i][j-coins[i]])
        if amount==0:
            return 0

        dp = [[99999999]*(amount+1) for _ in range(len(coins)+1)]

        for j in range(amount+1):
            if j//coins[0]==j/coins[0]:
                dp[0][j] = j//coins[0]
            # else:
                # dp[0][j] = 0  # 注意 不能赋值为0 直接不用赋值就行

        for i in range(1,len(coins)):
            for j in range(0,amount+1):  # 注意遍历开始条件
                if j>=coins[i]:
                    dp[i][j] = min(dp[i-1][j],dp[i][j-coins[i]]+1)
                else:
                    dp[i][j] = dp[i-1][j]

        res = dp[len(coins)-1][amount]
        return -1 if res==99999999 else res

注意此题和上一题的区别,上一题是完全背包的典型例题,不考虑排列方式,本题比上一题难在排列方式,如果不考虑排列代码如上面零钱兑换那题:凡是考虑装满背包的方案数的问题都是考虑 dp[i] += dp[i-num[j]] 的所有方案,dp[i] 表示总和为i的所有排列数。

class Solution:
    def combinationSum4(self, nums: List[int], target: int) -> int:
        #动规思路:前缀完全背包动态问题 dp[i][j]表示前i个数为j的组合个数
        #方程建立: dp[i][j] = max(dp[i-1][j],dp[i][j-nums[i]])
        n = len(nums)
        dp = [[-1]*(target+1)  for _ in range(n+1)]

        #初始化 
        for j in range(target+1):
            if nums[0]==j:
                dp[0][j] = 1
            else:
                dp[0][j] = 0

        for i in range(n): 
            dp[i][0] = 0

        #递归方程
        def func(i,j):
            if i==0 or j==0:
                return dp[i][j]

            if dp[i-1][j]==-1:
                dp[i-1][j]=func(i-1,j)
            res = dp[i-1][j]

            if j>=nums[i]:
                if dp[i][j-nums[i]]==-1:
                    dp[i][j-nums[i]]=func(i,j-nums[i])
                # res = dp[i-1][j] + dp[i][j-nums[i]]  #这样写就是不考虑排列方式
                res = dp[i-1][j] + 复杂的排列组合公式
            dp[i][j] = res 
            return res 

        res = func(n-1,target)
        return res

考虑组合方式的代码如下:

class Solution:
    def numSquares(self, n: int) -> int:
        #转化为完全背包问题,物品就是完全平方数

        nums = []
        for i in range(1,n+1):
            if(i*i<=n):
                nums.append(i*i)
            else:
                break
        m = len(nums)
        dp = [[999999]*(n+1) for _ in range(m+1) ]
        for j in range(n+1):
            if (j//nums[0]==j/nums[0]):
                dp[0][j] = j//nums[0]

        for i in range(1,m):
            for j in range(n+1):
                if j>=nums[i]:
                    dp[i][j] = min(dp[i-1][j],dp[i][j-nums[i]]+1)
                else:
                    dp[i][j] = dp[i-1][j]

        return dp[m-1][n]

猜你喜欢

转载自blog.csdn.net/cj151525/article/details/128997563