动态规划——背包问题(3)

动态规划——背包问题(3)

求解最佳方案数

例题

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i 件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出 最优选法的方案数。注意答案可能很大,请输出答案模 109+7 的结果。

输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。

接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式
输出一个整数,表示 方案数 模 109+7 的结果。

数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 6
输出样例:
2

思路

在这里插入图片描述

代码

N = 1010
MOD = int(1e9) + 7
f, g = [0] * N, [1] * N

for i in range(n) :
	v, w = map(int, input().split())
	for j in range(m, v - 1, -1) :
		maxv = max(f[j], f[j - v] + w)
		s = 0
		if maxv == f[j] : s = g[j]
		if maxv == f[j - v] + w : s = (s + g[j - v]) % MOD
		g[j] = s
		f[j] = maxv
print(g[m])

混合背包问题

例题

有 N 种物品和一个容量是 V 的背包。

物品一共有三类:

第一类物品只能用1次(01背包);
第二类物品可以用无限次(完全背包);
第三类物品最多只能用 si 次(多重背包);
每种体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。

si=−1 表示第 i 种物品只能用1次;
si=0 表示第 i 种物品可以用无限次;
si>0 表示第 i 种物品可以使用 si 次;
输出格式
输出一个整数,表示最大价值。

数据范围
0<N,V≤1000
0<vi,wi≤1000
−1≤si≤1000
输入样例
4 5
1 2 -1
2 4 1
3 4 0
4 5 2
输出样例:
8

思路

在这里插入图片描述

代码

N = 1010
f = [0] * N

n, m = map(int, input().split())

for i in range(n) :
	v, w, s = map(int, input().split())
	if s == 0 :
		for j in range(v, m + 1) :
			f[j] = max(f[j], f[j - v] + w)
	else :
		if s == -1 :
			s = 1
		k = 1
		while k <= s :
			for j in range(m, k * v - 1, -1) :
				f[j] = max(f[j], f[j - k * v] + k * w)
			s -= k
			k *= 2
		if s :
			for j in range(m, s * v - 1, -1) :
				f[j] = max(f[j], f[j - s * v] + s * w)
print(f[m])

有依赖的背包问题

例题

有 N 个物品和一个容量是 V 的背包。

物品之间具有依赖关系,且依赖关系组成一棵树的形状。如果选择一个物品,则必须选择它的父节点。

如下图所示:
QQ图片20181018170337.png

如果选择物品5,则必须选择物品1和2。这是因为2是5的父节点,1是2的父节点。

每件物品的编号是 i,体积是 vi,价值是 wi,依赖的父节点编号是 pi。物品的下标范围是 1…N。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

输出最大价值。

输入格式
第一行有两个整数 N,V,用空格隔开,分别表示物品个数和背包容量。

接下来有 N 行数据,每行数据表示一个物品。
第 i 行有三个整数 vi,wi,pi,用空格隔开,分别表示物品的体积、价值和依赖的物品编号。
如果 pi=−1,表示根节点。 数据保证所有物品构成一棵树。

输出格式
输出一个整数,表示最大价值。

数据范围
1≤N,V≤100
1≤vi,wi≤100
父节点编号范围:

内部结点:1≤pi≤N;
根节点 pi=−1;
输入样例
5 7
2 3 -1
2 2 1
3 5 1
4 7 2
3 6 2
输出样例:
11

思路

在这里插入图片描述

代码

N = 110
f = [[0] * N for _ in range(N)]
h = [-1] * N
e = [0] * N
ne = [-1] * N
v, w = [0] * N, [0] * N
idx = 0

def add(a, b) :
	global idx
	e[idx] = b
	ne[idx] = h[a]
	h[a] = idx
	idx += 1
	
def dfs(u) :
	for i in range(v[u], m + 1) : f[u][i] = w[u] # 当前节点必选
	i = h[u]
	while i != -1 : #枚举u的子树i组
		son = e[i]
		dfs(son)
		for j in range(m, v[u] - 1, -1) : #由于第i组不能重复选择,所以要倒序
			for k in range(j - v[u] + 1) : #枚举除去当前节点的体积,子节点还能分配的体积
				f[u][j] = max(f[u][j], f[u][j - k] + f[son][k])
		i = ne[i]
	
n, m = map(int, input().split())

root = 0
for i in range(1, n + 1) :
	v[i], w[i], p = map(int, input().split())
	if p == -1 :
		root = i
	else :
		add(p, i)
dfs(root)
print(f[root][m])

考察思维的一些背包题目

机器分配

总公司拥有 M 台 相同 的高效设备,准备分给下属的 N 个分公司。

各分公司若获得这些设备,可以为国家提供一定的盈利。盈利与分配的设备数量有关。

问:如何分配这M台设备才能使国家得到的盈利最大?

求出最大盈利值。

分配原则:每个公司有权获得任意数目的设备,但总台数不超过设备数 M。

输入格式
第一行有两个数,第一个数是分公司数 N,第二个数是设备台数 M;

接下来是一个 N×M 的矩阵,矩阵中的第 i 行第 j 列的整数表示第 i 个公司分配 j 台机器时的盈利。

输出格式
第一行输出最大盈利值;

接下 N 行,每行有 2 个数,即分公司编号和该分公司获得设备台数。

答案不唯一,输出任意合法方案即可。

数据范围
1≤N≤10,
1≤M≤15
输入样例:
3 3
30 40 50
20 30 50
20 25 30
输出样例:
70
1 1
2 1
3 1

  1. 思路
    在这里插入图片描述
  2. 代码
N = 10

f = [[0] * N for _ in range(N)]
a = [[0] * N for _ in range(N)]
ways = [0] * N

n, m = map(int, input().split())

for i in range(1, n + 1) :
	a[i][1 : m + 1] = list(map(int, input().split()))

for i in range(1, n + 1) :
	for j in range(m + 1) :
		for k in range(j + 1) :
			f[i][j] = (f[i][j], f[i - 1][j - k] + a[i][k])

print(f[n][m])

j = m
for i in range(n, 0, -1) :
	for k in range(j + 1) :
		if f[i][j] == f[i - 1][j - k] + a[i][k] :
			ways[i] = k
			j -= k
			break
for i in range(1, n + 1) :
	print(i, ways[i])

金明的预算方案

金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间。

更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过N元钱就行”。

今天一早,金明就开始做预算了,他把想买的物品分为两类:主件与附件,附件是从属于某个主件的,下表就是一些主件与附件的例子:

QQ截图20190313024710.png

如果要买归类为附件的物品,必须先买该附件所属的主件。

每个主件可以有0个、1个或2个附件。

附件不再有从属于自己的附件。

金明想买的东西很多,肯定会超过妈妈限定的N元。

于是,他把每件物品规定了一个重要度,分为5等:用整数1~5表示,第5等最重要。

他还从因特网上查到了每件物品的价格(都是10元的整数倍)。

他希望在不超过N元(可以等于N元)的前提下,使每件物品的价格与重要度的乘积的总和最大。

设第j件物品的价格为v[j],重要度为w[j],共选中了k件物品,编号依次为j1,j2,…,jk,则所求的总和为:

v[j1]∗w[j1]+v[j2]∗w[j2]+…+v[jk]∗w[jk](其中*为乘号)

请你帮助金明设计一个满足要求的购物单。

输入格式
输入文件的第1行,为两个正整数,用一个空格隔开:N m,其中N表示总钱数,m为希望购买物品的个数。

从第2行到第m+1行,第j行给出了编号为j-1的物品的基本数据,每行有3个非负整数v p q,其中v表示该物品的价格,p表示该物品的重要度(1~5),q表示该物品是主件还是附件。

如果q=0,表示该物品为主件,如果q>0,表示该物品为附件,q是所属主件的编号。

输出格式
输出文件只有一个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的最大值(<200000)。

数据范围
N<32000,m<60,v<10000
输入样例:
1000 5
800 2 0
400 5 1
300 5 1
400 3 0
500 2 0
输出样例:
2200

  1. 思路
    在这里插入图片描述
  2. 代码
N = 32010
M = 70
f = [0] * N
master = [[0, 0] for _ in range(M)]
serven = [[] for _ in range(M)]

m, n = map(int, input().split())

for i in range(1, n + 1) :
	v, p, q = map(int, input().split())
	if not q :
		master[i] = [v, v * p]
	else :
		serven[q].append([v, v * p])

for i in range(1, n + 1) :
	if master[i][0] :
		for j in range(m, -1, -1) :
			for k in range(1 << len(serven[i])) :
				v, w = master[i][0], master[i][1]
				for u in range(len(serven[i])) :
					if k >> u :
						v += serven[i][u][0]
						w += serven[i][u][1]
				if j >= v :
					f[j] = max(f[j], f[j - v] + w)
print(f[m])

货币系统

在网友的国度中共有 n 种不同面额的货币,第 i 种货币的面额为 a[i],你可以假设每一种货币都有无穷多张。

为了方便,我们把货币种数为 n、面额数组为 a[1…n] 的货币系统记作 (n,a)。

在一个完善的货币系统中,每一个非负整数的金额 x 都应该可以被表示出,即对每一个非负整数 x,都存在 n 个非负整数 t[i] 满足 a[i]×t[i] 的和为 x。

然而,在网友的国度中,货币系统可能是不完善的,即可能存在金额 x 不能被该货币系统表示出。

例如在货币系统 n=3, a=[2,5,9] 中,金额 1,3 就无法被表示出来。

两个货币系统 (n,a) 和 (m,b) 是等价的,当且仅当对于任意非负整数 x,它要么均可以被两个货币系统表出,要么不能被其中任何一个表出。

现在网友们打算简化一下货币系统。

他们希望找到一个货币系统 (m,b),满足 (m,b) 与原来的货币系统 (n,a) 等价,且 m 尽可能的小。

他们希望你来协助完成这个艰巨的任务:找到最小的 m。

输入格式
输入文件的第一行包含一个整数 T,表示数据的组数。

接下来按照如下格式分别给出 T 组数据。

每组数据的第一行包含一个正整数 n。

接下来一行包含 n 个由空格隔开的正整数 a[i]。

输出格式
输出文件共有 T 行,对于每组数据,输出一行一个正整数,表示所有与 (n,a) 等价的货币系统 (m,b) 中,最小的 m。

数据范围
1≤n≤100,
1≤a[i]≤25000,
1≤T≤20
输入样例:
2
4
3 19 10 6
5
11 29 13 19 17
输出样例:
2
5

  1. 思路
    一些性质:

    • a1, a2,…,an一定都可以被表示出来
    • 在最优解中,b1,b2,…, bn一定是从a1,a2, …,an中选择
    • b1,b2,…,bn一定不能被其他bi表示
  2. 代码

N = 25010

T = int(input())

for t in range(T) :
	f = [0] * N
	n = int(input())
	nums = list(map(int, input().split()))
	nums.sort()
	m = nums[n - 1]
	res = 0
	f[0] = 1
	for i in range(1, n + 1) :
		if not f[nums[i - 1]] : res += 1
		for j in range(nums[i - 1] -1, m + 1) :
			f[j] += f[j - nums[i - 1]]
			
	print(res)

能量石

岩石怪物杜达生活在魔法森林中,他在午餐时收集了 N 块能量石准备开吃。

由于他的嘴很小,所以一次只能吃一块能量石。

能量石很硬,吃完需要花不少时间。

吃完第 i 块能量石需要花费的时间为 Si 秒。

杜达靠吃能量石来获取能量。

不同的能量石包含的能量可能不同。

此外,能量石会随着时间流逝逐渐失去能量。

第 i 块能量石最初包含 Ei 单位的能量,并且每秒将失去 Li 单位的能量。

当杜达开始吃一块能量石时,他就会立即获得该能量石所含的全部能量(无论实际吃完该石头需要多少时间)。

能量石中包含的能量最多降低至 0。

请问杜达通过吃能量石可以获得的最大能量是多少?

输入格式
第一行包含整数 T,表示共有 T 组测试数据。

每组数据第一行包含整数 N,表示能量石的数量。

接下来 N 行,每行包含三个整数 Si,Ei,Li。

输出格式
每组数据输出一个结果,每个结果占一行。

结果表示为 Case #x: y,其中 x 是组别编号(从 1 开始),y 是可以获得的最大能量值。

数据范围
1≤T≤10,
1≤N≤100,
1≤Si≤100,
1≤Ei≤105,
0≤Li≤105
输入样例:
3
4
20 10 1
5 30 5
100 30 1
5 80 60
3
10 4 1000
10 3 1000
10 8 1000
2
12 300 50
5 200 0
输出样例:
Case #1: 105
Case #2: 8
Case #3: 500
样例解释
在样例#1中,有 N=4 个宝石。杜达可以选择的一个吃石头顺序是:

吃第四块石头。这需要 5 秒,并给他 80 单位的能量。
吃第二块石头。这需要 5 秒,并给他 5 单位的能量(第二块石头开始时具有 30 单位能量,5 秒后失去了 25 单位的能量)。
吃第三块石头。这需要 100 秒,并给他 20 单位的能量(第三块石头开始时具有 30 单位能量,10 秒后失去了 10 单位的能量)。
吃第一块石头。这需要 20 秒,并给他 0 单位的能量(第一块石头以 10 单位能量开始,110 秒后已经失去了所有的能量)。
他一共获得了 105 单位的能量,这是能获得的最大值,所以答案是 105。

在样本案例#2中,有 N=3 个宝石。

无论杜达选择吃哪块石头,剩下的两个石头的能量都会耗光。

所以他应该吃第三块石头,给他提供 8 单位的能量。

在样本案例#3中,有 N=2 个宝石。杜达可以:

吃第一块石头。这需要 12 秒,并给他 300 单位的能量。
吃第二块石头。这需要 5 秒,并给他 200 单位的能量(第二块石头随着时间的推移不会失去任何能量!)。
所以答案是 500。

  1. 思路
    最优解一定是按照一定顺序来吃:假定为a1, a2,…an的顺序。
    a i 和 a i + 1 是其中相邻的两个能量石 a_i和a_{i+1}是其中相邻的两个能量石 aiai+1是其中相邻的两个能量石,按照这个顺序吃,假设开始吃的时间为t,则获得的能量为 S 1 = e i − t ∗ l i + e i + 1 − ( t + s i ) ∗ l i + 1 S_1 = e_i-t*l_i + e_{i + 1} - (t + s_i) * l_{i + 1} S1=eitli+ei+1(t+si)li+1,如果交换顺序则 S 2 = e i + 1 − t ∗ l i + 1 + e i − ( t + s i + 1 ) ∗ l i S_2 = e_{i + 1}-t*l_{i+1} + e_{i} - (t + s_{i+ 1}) * l_{i} S2=ei+1tli+1+ei(t+si+1)li。如果按规定顺序,那么 S 1 > = S 2 S_1 >= S_2 S1>=S2,化简即 s i / l i < = s i + 1 / l i + 1 s_i / l_i <= s_{i + 1} / l_{i + 1} si/li<=si+1/li+1。则每个物品按照这样的顺序选取才能得到最大,下面就是0-1背包的板子。
    本题值得注意的是,本题状态表示是恰好时间j时能量最大值,因为能量会因为时间损耗。
  2. 代码
T = int(input())

for t in range(T) :
	n = int(input())
	stone = []
	m = 0
	for i in range(n) :
		s, e, l = map(int, input().split())
		stone.append([s, e, l])
		m += s
	f = [-100010] * (m + 1)
	f[0] = 0
	stone.sort(key = lambda x : x[0] / max(x[2], 1e-8))
	for i in range(n) :
		s, e, l = stone[i]
		for j in range(m, s - 1, -1) :
			f[j] = max(f[j], f[j - s] + e - (j - s) * l)
	res = 0
	for i in range(m + 1) :
		res = max(res, f[i])
	print(f'Case #{t + 1}: {res}')
  1. 总结

求最优解,与推进顺序相关,既有增量又有损耗的题目,可以假定一个顺序,然后假设顺序对调比较大小,得到一定单调性。方便后序加以选择

总结

背包问题就此告一段落,三篇博客已经涵盖了背包的大部分题型,奥力给!!!

posted @ 2022-12-13 13:47  chanxe  阅读(22)  评论(0编辑  收藏  举报