Skip to content

02773: 采药

dp, http://cs101.openjudge.cn/practice/02773

辰辰是个很有潜能、天资聪颖的孩子,他的梦想是称为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”

如果你是辰辰,你能完成这个任务吗?

输入

输入的第一行有两个整数T(1 <= T <= 1000)和M(1 <= M <= 100),T代表总共能够用来采药的时间,M代表山洞里的草药的数目。接下来的M行每行包括两个在1到100之间(包括1和100)的的整数,分别表示采摘某株草药的时间和这株草药的价值。

输出

输出只包括一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。

样例输入

70 3
71 100
69 1
1 2

样例输出

3

来源:NOIP 2005

解题思路:0-1背包问题(Knapsack problem)。内层循环(列)时间T,外层循环(行)药草M。

图解,可以参照《算法图解》,作何:Aditya Bhargava,译者:袁国忠。2016年出版。

python
T, M = map(int, input().split())
dp = [ [0] + [0]*T for _ in range(M+1)]

t = [0]
v = [0]
for i in range(M):
        ti, vi = map(int, input().split())
        t.append(ti)
        v.append(vi)

for i in range(1, M+1):			# 外层循环(行)药草M
        for j in range(0, T+1):	# 内层循环(列)时间T
                if j >= t[i]:
                        dp[i][j] = max(dp[i-1][j], dp[i-1][j-t[i]] + v[i])
                else:
                        dp[i][j] = dp[i-1][j]

print(dp[M][T])

空间优化的一维数组解法

python
T,M = map(int,input().split())
herb = []
for i in range(M):
    herb.append([int(x) for x in input().split()])

dp=[0]*(T+1)
for i in range(M):
    for j in range(T,0,-1):
        if j >= herb[i][0]:
            dp[j] = max(dp[j],dp[j-herb[i][0]]+herb[i][1])

print(dp[-1])

2021fall-cs101,陈飞宇。

背包问题的空间优化写法:事实上创建dp 时不需要用二维列表存储每增加一行数据时的所有运算结果。只需要开一个last_data 的一维列表记录“上一行”数据的运算结果即可;前面各行的数据可以丢弃。因为在背包问题的状态转移方程中只需要用到“上一行”的运算结果和当前的输入数据。

python
#T 采药总时间,M 草药数目
T,M = map(int,input().split())
#dp[j]用于存储当采药总时间为j 时,由目前已给出的草药数据可采得的最大价值
dp = [0]*(T+1)
last_data = list(dp)
#外层循环遍历草药数据(M)
for i in range(M):
    time,value = map(int,input().split())
    #内层循环遍历采药时间(T)
    for j in range(1,T+1):
        if j >= time:
            dp[j] = max(last_data[j],value + last_data[j-time])
        else:
            dp[j] = last_data[j]
    #用last_data 将本次运算结果存起来以供下一次循环运算使用
    last_data = list(dp)
print(dp[-1])

记忆式搜索

python
import math
from functools import lru_cache

@lru_cache(maxsize = None)
def fn(i, s):
  # i-th item, knapsack with available capacity s

  if (s < 0):
    return -math.inf
  if (i == len(vs)):
      return 0

  return max(fn(i+1, s), vs[i] + fn(i+1, s-ws[i]))


T, M = map(int, input().split())
ws = []
vs = []
for _ in range(M):
    t, v = map(int, input().split())
    ws.append(t)
    vs.append(v)

print(fn(0, T))
python
T,M = map(int, input().split())
herb = []
for i in range(M):
    herb.append([int(x) for x in input().split()])
    
dp=[0]*(T+1)
for i in range(M):
    for j in range(T, herb[i][0] - 1, -1):
        if j >= herb[i][0]:
            dp[j] = max(dp[j], dp[j-herb[i][0]]+herb[i][1])
            
print(dp[-1])
python
#T 采药总时间,M 草药数目
T,M = map(int,input().split())
#dp[j]用于存储当采药总时间为j 时,由目前已给出的草药数据可采得的最大价值
dp = [0]*(T+1)
last_data = list(dp)
#外层循环遍历草药数据(M)
for i in range(M):
    time,value = map(int,input().split())
    #内层循环遍历采药时间(T)
    for j in range(1,T+1):
        if j >= time:
            dp[j] = max(last_data[j],value + last_data[j-time])
        else:
            dp[j] = last_data[j]
    #用last_data 将本次运算结果存起来以供下一次循环运算使用
    last_data = list(dp)
print(dp[-1])
python
import math
from functools import lru_cache


@lru_cache(maxsize = None)
def fn(i, s):
  # i-th item, knapsack with available capacity s

  if (s < 0):
    return -math.inf
  if (i == len(vs)):
      return 0

  return max(fn(i+1, s), vs[i] + fn(i+1, s-ws[i]))


T, M = map(int, input().split())
ws = []
vs = []
for _ in range(M):
    t, v = map(int, input().split())
    ws.append(t)
    vs.append(v)

print(fn(0, T))

一般来说01背包如果限制T求最大W,那么dp时求T处的最大w;但是也可以求W处的最小t,并且时刻保持t<=T,最后寻找最大的有t的W。由于W本身没有限制,无法像t一样从T往0枚举,故自设一个限制100*100(采药的数据),从10000往0枚举。

这题T<=1000,显然枚T更优,但如果数据很刁钻,比如T很大,那可能这种方法更有效。

python
# 官祺云 24
T, n = map(int, input().split())
t, w = [0] * n, [0] * n
for i in range(n):
    t[i], w[i] = map(int, input().split())
dp = [float('inf')] * 10001
dp[0] = 0
for i in range(n):
    for j in range(10000, w[i] - 1, -1):
        if dp[j - w[i]] + t[i] <= T:
            dp[j] = min(dp[j], dp[j - w[i]] + t[i])
for i in range(10000, -1, -1):
    if dp[i] != float('inf'):
        print(i)
        break

使用 二维数组递归 + LRU Cache 来解决问题。

  1. 使用 二维数组 的方法:

如果你使用二维数组的动态规划方法,可以将状态转移视为 二维的背包问题。我们使用一个 dp[i][j] 数组来表示在前 i 个草药中,使用不超过 j 时间的最大价值。

动态规划转移方程:

  • dp[i][j] 表示使用前 i 个草药,且时间不超过 j 时的最大价值。

  • 对于每个草药

    (time, value)

    ,有两种选择:

    • 不采摘它,dp[i][j] = dp[i-1][j]
    • 采摘它,dp[i][j] = dp[i-1][j-time] + value,前提是 j >= time

二维数组实现:

python
def max_value_with_2d_array(T, M, herbs):
    # dp[i][j]表示前i个草药,时间不超过j时的最大价值
    dp = [[0] * (T + 1) for _ in range(M + 1)]

    for i in range(1, M + 1):
        time, value = herbs[i - 1]
        for j in range(T + 1):
            # 不采摘当前草药
            dp[i][j] = dp[i - 1][j]
            # 采摘当前草药,前提是时间允许
            if j >= time:
                dp[i][j] = max(dp[i][j], dp[i - 1][j - time] + value)
    
    return dp[M][T]

# 输入读取
T, M = map(int, input().split())  # 读取T和M
herbs = []

for _ in range(M):
    time, value = map(int, input().split())
    herbs.append((time, value))

# 计算并输出结果
print(max_value_with_2d_array(T, M, herbs))

说明:

  1. dp[i][j]:表示前 i 株草药,在总时间 j 内能获得的最大价值。
  2. 如果不采摘第 i 株草药,dp[i][j] 就等于 dp[i-1][j]
  3. 如果采摘第 i 株草药,且时间 j 足够,dp[i][j] 就等于 dp[i-1][j-time] + value

最终的结果就是 dp[M][T],即前 M 株草药,在总时间 T 内可以获得的最大价值。

时间复杂度:

  • O(M * T),其中 M 是草药的数量,T 是最大时间。

  1. 使用 递归 + LRU Cache 方法:

递归 + LRU Cache(最少最近使用缓存)是一种更灵活的解法,可以通过缓存中间计算的结果来避免重复计算。我们可以通过递归的方式,分别计算在某一时间限制下,是否选择某个草药,并且将结果缓存,以便重复使用。

递归思路:

  • 对于每一株草药

    (time, value)

    ,我们有两种选择:

    1. 不选择 这株草药,计算剩余时间内能获得的最大价值。
    2. 选择 这株草药,计算剩余时间内能获得的最大价值。

递归的基本形式是 max_value(time_left, idx),表示从第 idx 株草药开始,在剩余时间 time_left 内可以获得的最大价值。

递归 + LRU Cache 实现:

python
from functools import lru_cache

def max_value_with_recursion(T, M, herbs):
    @lru_cache(None)
    def helper(time_left, idx):
        if idx == M:  # 如果已经考虑完所有草药
            return 0
        time, value = herbs[idx]
        
        # 选择不采摘这株草药
        max_val = helper(time_left, idx + 1)
        
        # 选择采摘这株草药(前提是时间足够)
        if time_left >= time:
            max_val = max(max_val, helper(time_left - time, idx + 1) + value)
        
        return max_val
    
    return helper(T, 0)

# 输入读取
T, M = map(int, input().split())  # 读取T和M
herbs = []

for _ in range(M):
    time, value = map(int, input().split())
    herbs.append((time, value))

# 计算并输出结果
print(max_value_with_recursion(T, M, herbs))

说明:

  1. helper(time_left, idx):从草药 idx 开始,剩余时间为 time_left,返回能获得的最大价值。
  2. 缓存机制:使用 @lru_cache(None) 来缓存函数的计算结果,避免重复计算相同状态。
  3. 如果剩余时间不够,就跳过当前草药,递归处理下一个草药;如果可以采摘,则递归计算剩余时间内能获得的最大价值。

时间复杂度:

  • O(M * T),其中 M 是草药的数量,T 是最大时间,虽然有递归调用,但缓存机制使得每个状态只会被计算一次。

总结:

  • 二维数组方法:适合传统的动态规划,使用 dp[i][j] 来表示前 i 个草药,使用不超过 j 时间的最大价值。
  • 递归 + LRU Cache 方法:适合用递归思想求解,并通过缓存机制避免重复计算,适合不需要显式构造 DP 数组的情况。

对于这个问题,使用动态规划是最常见的解法,而递归 + LRU Cache 更加灵活,适合递归式问题的求解。