背包问题及动态规划小结

本文深入讲解了动态规划在不同场景的应用,包括资产包打包、考试策略制定及猜数字游戏等典型问题,通过实例剖析算法原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

动态规划题目小汇总

牛客网:资产包打包

  1. 题目描述

在金融资产交易中,经常涉及到资产包的挑选打包。在资产包打包过程中,每种类型的资产有固定的数量与价值,需选择某几种资产打包,使得资产包总价值最大。打包时每种资产只能整体打包,不能分割。假设现有可容纳M条资产的资产包,另外有 N N N种资产。资产 N a Na Na数量为 T a Ta Ta条,总价值为 V a Va Va元;资产 N b Nb Nb数量为 T b Tb Tb条,总价值为 V b Vb Vb元;资产 N c Nc Nc数量为 T c Tc Tc条,总价值为 V c Vc Vc元…;资产 N n Nn Nn数量为 T n Tn Tn,总价值为 V n Vn Vn。编写算法,挑选哪些类型资产放入资产包可使得资产包总价值最大?

输入描述:
资产总量,资产种类,每类资产条数,每类资产价值(逗号分隔);其中每类资产条数与每类资产价值为空格分隔。

总格式如下:
资产总量,资产种类,资产 A A A条数 资产 B B B条数 资产 C C C条数,资产 A A A价值 资产 B B B价值 资产 C C C价值!

举例,资产总量为12,资产种类3种,3种资产条数分别为4,5,7,三种资产价值分别是500元,600元,800元,
输入如下:
12,3,4 5 7,500 600 800

资产总量和资产种类都不超过1000,资产条数不超过1000,资产价值不超过10000,所有数值均为正整数。

输出描述:
资产包中资产最大总价值

示例1

输入
12,3,4 5 7,500 600 800

输出
1400

思路:
动态规划,和背包问题类似。 d p [ i ] dp[i] dp[i] 表示最大容量为 i i i 时的最大资产价值,那么dp用一维数组存储就可以。因此初始化dp数组的长度要用 资产总量T再+1(索引的原因)。
因此,对于每一种资产 n u m [ i ] num[i] num[i],每次都遍历更新容量在 n u m [ i ] num[i] num[i] 到最大容量T之间的最大资产价值。即有
d p [ j ] = m a x ( d p [ j ] , d p [ j − n u m b e r [ i ] ] + v a l u e [ i ] ) dp[j] = max(dp[j],dp[j-number[i]]+value[i]) dp[j]=max(dp[j],dp[jnumber[i]]+value[i])
因此最终的dp【-1】就是答案。

total,label,number,value = input().split(',')
number = list(map(int,number.split()))
value = list(map(int,value.split()))
#print(value)
dp = [0]*(int(total)+1)
for i in range(int(label)): # i是种类数
    for j in range(len(dp)-1,number[i]-1,-1): #j是number[i]到最大总量之间的数值
        dp[j] = max(dp[j],dp[j-number[i]]+value[i])
print(dp[-1])
  1. 还有一道题目,来自牛客网:考试策略 也用到经典的动态规划,我们先看看题目。

题目描述

小明同学在参加一场考试,考试时间2个小时。试卷上一共有 n n n 道题目,小明要在规定时间内,完成一定数量的题目。 考试中不限制试题作答顺序,对于 i i i 第道题目,小明有三种不同的策略可以选择:
(1)直接跳过这道题目,不花费时间,本题得0分。
(2)只做一部分题目,花费 p i pi pi 分钟的时间,本题可以得到 a i ai ai 分。
(3)做完整个题目,花费 q i qi qi 分钟的时间,本题可以得到 b i bi bi 分。
小明想知道,他最多能得到多少分。

输入描述:
第一行输入一个n,表示题目的数量。

接下来n行,每行四个数 p i p_i pi a i a_i ai q i q_i qi b i b_i bi。(1≤n≤100,1≤ p i p_i pi q i q_i qi≤120,0≤ a i a_i ai b i b_i bi≤1000)。

输出描述:
输出一个数,小明的最高得分。

示例1

输入
4
20 20 100 60
50 30 80 55
100 60 110 88
5 3 10 6

输出
94

这里输入的第一行是题目的数量,下面每一行都对应着一道题目,有两种策略(其实是三种策略,跳过此题,消耗0分钟,得分也是0,没有列出而已):1 做部分题目,消耗一定时间; 2 做完所有题目,消耗一定的时间。我们的策略就是在120分钟时间内尽可能多的拿到分数。

那么我们可以用 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示在 j 分钟内遇到第 i 道题目能得到的最大分数,那么就有如下递推公式:
其中p,a分别是第一种策略的消耗时间和所得分数,q,b分别是第二种策略的消耗时间和所得分数。
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − p ] + a , d p [ i − 1 ] [ j − q ] + b ) dp[i][j] = max(dp[i-1][j], dp[i-1][j-p]+a, dp[i-1][j-q]+b) dp[i][j]=max(dp[i1][j],dp[i1][jp]+a,dp[i1][jq]+b)
这个就是dp的核心公式,为了避免数组索引边界问题,要先判断 j 与 p 和 q 的大小。代码如下:

N = int(input())
lyst = []
for i in range(N):
    lyst.append([int(i) for i in input().split(' ')])
def maxScore(N, lyst, totalTime):
    resArr = [[0 for _ in range(totalTime+1)] for _ in range(N+1)]
    for i in range(1, N+1):
        for j in range(1, totalTime+1):
            p, a, q, b = lyst[i-1]
            if j>=q:
                resArr[i][j] = max(resArr[i-1][j], resArr[i-1][j-p]+a, resArr[i-1][j-q]+b)
            elif j>=p and j<q:
                resArr[i][j] = max(resArr[i-1][j], resArr[i-1][j-p]+a)
            else:
                resArr[i][j] = resArr[i-1][j]
    return resArr[-1][-1]
if __name__ == "__main__":
    print(maxScore(N, lyst, 120))
  1. 下面这个动态规划就有点难度了,链接在这里:
    leecode:猜数字大小

题目描述:

我们正在玩一个猜数游戏,游戏规则如下:
我从 1 到 n 之间选择一个数字,你来猜我选了哪个数字。每次你猜错了,我都会告诉你,我选的数字比你的大了或者小了。然而,当你猜了数字 x 并且猜错了的时候,你需要支付金额为 x 的现金。直到你猜到我选的数字,你才算赢得了这个游戏。

示例:
n = 10, 我选择了8.
第一轮: 你猜我选择的数字是5,我会告诉你,我的数字更大一些,然后你需要支付5块。
第二轮: 你猜是7,我告诉你,我的数字更大一些,你支付7块。
第三轮: 你猜是9,我告诉你,我的数字更小一些,你支付9块。
游戏结束。8 就是我选的数字。
你最终要支付 5 + 7 + 9 = 21 块钱。
给定 n ≥ 1,计算你至少需要拥有多少现金才能确保你能赢得这个游戏。

思路一:暴力法
首先,我们需要意识到我们在范围 ( 1 , n ) (1,n) (1,n) 中猜数字的时候,需要考虑最坏情况下的代价。也就是说要算每次都猜错的情况下的总体最大开销。
在暴力算法中,我们首先在 ( 1 , n ) (1,n) (1,n) 中任意挑选一个数字,假设它是个错误的猜测(最坏情况),我们需要用最小代价去猜到需要的数字。那么在一次尝试以后,答案要么在我们猜的数字的左边要么在右边,为了考虑最坏情况,我们需要考虑两者的较大值。因此,如果我们选择 i i i 作为第一次尝试,总体最小代价是: c o s t ( 1 , n ) = i + m a x ( c o s t ( 1 , i − 1 ) , c o s t ( i + 1 , n ) ) cost(1,n)=i + max(cost(1,i-1),cost(i+1,n)) cost(1,n)=i+max(cost(1,i1),cost(i+1,n))对于左右两段,我们分别考虑在段内选择一个数,并重复上面的过程来求得最小开销。使用如上方法,我们能求得从 i 开始猜,猜到答案的最小代价。同样地,我们遍历 (1,n) 中的所有数字并分别作为第一次尝试,求出每一个的代价,并输入最小值即为答案。

class Solution:
    def getMoneyAmount(self, n: int) -> int:
        return self.cal(1,n)
    def cal(self,low:int,high:int) -> int:
        if low >= high:
            return 0
        res = float('inf')
        for i in range(low,high+1):
            cost = i + max(self.cal(low,i-1),self.cal(i+1,high))
            res = min(res,cost)
        return res

思路二:dp
既然大问题能分解成子问题,那么我们自然就想到了动态规划:
i i i 为第一次尝试找到最小开销的过程可以被分解为找左右区间内最小开销的子问题。对于每个区间,我们重复问题拆分的过程,得到更多子问题,这启发我们可以用 DP 解决这个问题。我们需要使用一个 d p dp dp 矩阵,其中 d p ( i , j ) dp(i,j) dp(i,j) 代表在 ( i , j ) (i,j) (i,j) 中最坏情况下最小开销的代价。

现在我们只需要考虑如何求出这个 d p dp dp 数组。如果区间只剩下一个数 k k k ,那么猜中的代价永远为 0 ,因为我们区间里只剩下一个数字,也就是说,所有的 d p ( k , k ) dp(k,k) dp(k,k) 都初始化为 0 。然后,对于长度为 2 的区间,我们需要所有长度为 1 的区间的结果。由此我们可以看出,为了求出长度为 l e n len len 区间的解,我们需要所有长度为 l e n − 1 len−1 len1 的解。因此我们按照区间长度从短到长求出 d p dp dp 数组。现在,我们应该按照什么办法来求出 d p dp dp 矩阵呢?对于每个 d p ( i , j ) dp(i,j) dp(i,j) ,当前长度为 l e n = j − i + 1 len=j−i+1 len=ji+1 。我们遵照方法 1 中的办法,依次挑选每个数字作为第一次尝试的答案,可以求出最小开销:
c o s t ( i , j ) = p i v o t + m a x ( c o s t ( i , p i v o t − 1 ) , c o s t ( p i v o t + 1 , j ) ) cost(i,j)=pivot + max(cost(i,pivot-1),cost(pivot+1,j)) cost(i,j)=pivot+max(cost(i,pivot1),cost(pivot+1,j))
d p ( i , j ) = m i n 在 ( i , j ) 中 遍 历 所 有 的 p i v o t ( p i v o t + m a x ( d p ( i , p i v o t − 1 ) , d p ( p i v o t + 1 , j ) ) ) dp(i,j)=\mathop{min}\limits_{在(i,j)中遍历所有的pivot}(pivot + max(dp(i,pivot-1),dp(pivot+1,j))) dp(i,j)=(i,j)pivotmin(pivot+max(dp(i,pivot1),dp(pivot+1,j)))
复杂度分析:

时间复杂度: O ( n 3 ) O(n^3) O(n3)。我们遍历 d p dp dp 数组一遍需要 O ( n 2 ) O(n^2) O(n2) 的时间开销。
对于数组中每个元素,我们最多需要遍历 n n n 个数字。
空间复杂度: O ( n 2 ) O(n^2) O(n2)。需要创建 n 2 n^2 n2 空间的 d p dp dp 数组。

class Solution:
    def getMoneyAmount(self, n: int) -> int:
        dp = [[0 for _ in range(n+1)] for _ in range(n+1)]
        for step in range(2,n+1):
            for start in range(1,n-step+2):
                res = float('inf')
                for piv in range(start,start+step-1):
                    tem = piv + max(dp[start][piv-1],dp[piv+1][start+step-1])
                    res = min(res,tem)
                dp[start][start+step-1] = res
        return dp[1][n]

思路三:优化的dp
在暴力解中,对于范围 ( i , j ) (i,j) (i,j) 中的每一个数字,我们都需要分别考虑选为当前的
第一个猜测的代价,然后再分别考虑左右两个区间内的代价。但一个重要的发现是如果我们从范围 ( i , j ) (i,j) (i,j) 内选择数字作为第一次尝试,右边区间都比左边区间大,所以我们只需要从右边区间获取最大开销即可,因为它的开销肯定比左边区间的要大。为了减少这个开销,我们第一次尝试肯定从 ( i + j 2 , j ) (\frac{i+j}{2},j) (2i+j,j) 中进行选数。这样子,两个区间的开销会更接近且总体开销会更小。所以,我们不需要从 i i i j j j 遍历每个数字,只需要从 i + j 2 \frac{i+j}{2} 2i+j​ 到 j j j 遍历,且找到暴力解的最小开销即可。

class Solution:
    def getMoneyAmount(self, n: int) -> int:
        dp = [[0 for _ in range(n+1)] for _ in range(n+1)]
        for step in range(2,n+1):
            for start in range(1,n-step+2):
                res = float('inf')
                for piv in range(start+(step-1)//2,start+step-1):
                    tem = piv + max(dp[start][piv-1],dp[piv+1][start+step-1])
                    res = min(res,tem)
                dp[start][start+step-1] = res
        return dp[1][n]

优化后的dp尽管和未优化的dp复杂度相同,但是时间复杂度的系数能降低一半。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值