注意:
1.本博客仅供参考交流使用,请读者务必自行实践,切勿生搬硬套
2.由于笔者水平有限,若文中有错误或者可以改进之处,欢迎在评论区指出参考:scwMason的博客
Aiden邱秋秋 的博客
题目
Description
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的重量是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。
【备注:读入一行数字可用list(map(int,input().split()))来完成。因为数据较大,如果用for可能会超时】
Input
第一行是N种物品。第二行是背包容量V。接下去2行,第一行是各个物品的重量c[i],第二行是各个物品的价值w[i]。
Output
一个数字,价值总和。
Sample Input 1
3
2
1 2 3
1 2 3
Sample Output 1
2
这个题目最开始,可能是叫疯狂采药,其实是同一个题来着,也就是物品重量相当于采药所花时间,物品价值相当于药草价值,背包容量相当于总的采药时间,为了让这个问题更有哲学美感,我决定从采药的角度来叙述这个问题
疯狂采药(无限背包)和小心采药(01背包)的本质区别是什么?
01背包
本质是有限和无限的区别,同一颗药草可以采有限次和无限次的区别,当然我们知道由于总时间的限制,我们不可能真正的无限地采一个药草,但是当我们不清楚总时间或者总时间相当大的时候,采同一个药草仿佛真的可以认为是无限次,那么我们要怎么解决无限的问题呢?
在这里我提供两种解决无限问题的思路,一种过了OJ的测试(1530ms),另一种则没有(1950ms),要求是1600ms内。这两种方法,我叫第一种为转换视角法,第二种为一生万物法,看起来很中二,但是我觉得采药问题确实很有哲学艺术感。我们先二后一地来看一下。
Coding1:(超时版本,分析在后面)
n = int(input())
c = int(input())
w = list(map(int, input().split(" ")))
v = list(map(int, input().split(" ")))
f = [0] * (c + 1)
nw = [0] * 1000000
nv = [0] * 1000000
id = 0
for i in range(len(w)):
num = c // w[i]
if num != c * w[i]:
num += 1
s = 1
while num > 0:
id += 1
nw[id], nv[id] = s * w[i], s * v[i]
num -= s
s *= 2
for i in range(1, id + 1):
for j in range(c, nw[i] - 1, -1):
f[j] = max(f[j], f[j - nw[i]] + nv[i])
if i == id:
break
print(f[c])
可以发现我们先将w,v两个数组进行了扩充处理,变成nw,nv。
其实就是这么想的,既然我可以采这个药好多次,那么我就可以认为a药草其实本质上是花时为2w[a],价值为2v[a]的药草,然后是3,4…的集合,我多了好多种药草,那为什么程序里都只有偶数倍,没有奇数倍呢,是因为在这种问题里流传一句话,叫“1 2 4 8可以构成一切数(正整数)”,也就是在动态规划里,只需要考虑偶数倍,其实已经考虑了奇数倍在里面,大家可以体会一下。
所以我叫它一生万物,这是一种认为一个事物在时域上扩展后可以变成无数个事物的哲学思想。
Coding2:(通过版本,分析在后面)
def full_bag(c: int, w: list, v: list):
f = [0] * (c + 1)
for i in range(1, len(w)):
for j in range(w[i], c + 1):
f[j] = max(f[j], f[j - w[i]] + v[i])
return [f[c]]
n = int(input())
c = int(input())
w = list(map(int, input().split(" ")))
v = list(map(int, input().split(" ")))
w = [0] + w
v = [0] + v
print(full_bag(c, w, v)[0])
是的,通过版本是如此的简洁。所以没有简洁美的程序,一般都架不住考验。为什么能如此简洁呢,其实这和01背包问题的核心问题只有一句不一样。
for j in range(c, w[i] - 1, -1):(01)
for j in range(w[i], c + 1):(无限)
是的,如果我们在时间轴上换个方向看,有限就变成了无限。
在01背包中,我们采用一维数组逆序表示的原因是,我们一维空间无法保存i-1轮的状态,所以我们只能逆序,这样f[j-w[i]]就是上一轮的解,保证在i-1轮肯定没有选到过物品i,可以更新。
在完全背包中,我们的物品是无限拿的,所以要顺序执行,这样可能同一轮会选择多次,也就是同一物品选择多次
所以这两种解法,本质上来说呢,可以让我们知道,从一个角度看,时间是稠密丰富的,从另一个角度看,时间是简明流畅的