01背包问题
递归
对于每一个物品,如果他的重量不在承载范围内,就跳过此物品;如果他的重量在承载范围内,可以有两个选择:放或不放。对这两种方案得到的价格取大者,然后对下一个物品进行同样操作,从而能得到最终的最高价格。于是按照这个思路可以采取递归的算法实现。
自定义一个函数ks(n,c) 其中n用于控制遍历的下标,当n==-1时意味着所有物品都操作完毕,给result赋值0,结束递归。c用于记录当前的背包承重大小,当c==0时意味着背包刚好全部放满,那么这时候同样结束递归。这里采取从后往前的遍历,所以n的初始值是len(w)-1即最后一个物品的下标,c的初始值就是背包的承重total。
if-elif-else的分支结构判断了:是否退出递归,当前物品不能放,当前物品能放
具体看以下代码
w = [1,2,4,2,5]
v = [5,3,5,3,2]
def ks(n,c):
if n == -1 or c == 0:
result=0
elif w[n]>c:
result = ks(n-1,c)
else:
tmp1 = ks(n-1,c)
tmp2 = v[n] + ks(n-1,c-w[n])
result = max(tmp1,tmp2)
return result
total = int(input())
n = len(w)-1
print(ks(n,total))
动态规划
算法思路
创建一个二维dp表,每个格子dp[i][j]所代表的意思是
前 i
个物品在容量不超过 j
的情况下能取得的最大价值。
所以在定义的时候我们可以直接把所有格子设为0.
dp=[[0 for i in range(V+1)] for j in range(N+1)]
接下来就是填表,每一个格子的值都由他前一个格子所决定,当填到格子dp[N][V]也就是最右下角的格子,这就是我们要的答案。
我们对每一个物品限额进行遍历,对于每一个物品i,用j遍历每种不同的背包容量,对于每一种限额的背包,会有如下情况:
- 他不能放入背包:即这个物品的容量超出了当前背包的容量限额,因此把这个格子填上从上从前一个格子继承的价值,因为不放入第j个物品,那么他的价值就是放入前j-1个物品时能达到的最大价值dp[i-1][j]
- 他可以放入背包:但是你可以有两种选择
- 不放入背包:那么就把这个格子填上从上一个格子继承的价值 dp[i-1][j]
- 放入背包:这时候你需要考虑放入后容量的变化,在放入之后,背包可能还会有一些空余的容量是j-volume[i-1],对于这个容量的背包,找到其在存放当前物品之前,也就是i-1的时候,所能达到的最大价值dp[i-1][j-volume[i-1]]然后加上当前这个物品的价值price[i-1]
最后从这两种选择中选择价值大的情况填入。
上面的volume[i-1]和price[i-1]为什么要减1?因为我们把物品数据存放起来的时候下标是从0开始的而填表时真正开始向背包装物品是在i和j为1的时候。
代码实现
python3和C++代码
N,V=map(int, input().split())
volume=[]
price=[]
dp=[[0 for i in range(V+1)] for j in range(N+1)]
for i in range(N):
v,p=map(int, input().split())
volume.append(v)
price.append(p)
for i in range(1,N+1):
for j in range(1,V+1):
if volume[i-1]<=j:
dp[i][j]=max(dp[i-1][j],dp[i-1][j-volume[i-1]]+price[i-1])
else:
dp[i][j]=dp[i-1][j]
print(dp[N][V])
#include <bits/stdc++.h>
using namespace std;
int main()
{
int N, V;
cin >> N >> V;
vector<int> volume(N, 0);
vector<int> price(N, 0);
vector<vector<int>> dp(N + 1, vector<int>(V + 1, 0));
for (int i = 0; i < N; ++i)
cin >> volume[i] >> price[i];
for (int i = 1; i <= N; ++i)
{
for (int j = 1; j <= V; ++j)
{
if (volume[i - 1] <= j)
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - volume[i - 1]] + price[i - 1]);
else
dp[i][j] = dp[i - 1][j];
}
}
cout << dp[N][V] << endl;
return 0;
}
转一维
#include <bits/stdc++.h>
using namespace std;
int main() {
int N, V;
cin >> N >> V;
vector<int> w(N + 1), p(N + 1);
vector<int> dp(V + 1, 0);
for (int i = 1; i <= N; i++) {
cin >> w[i] >> p[i];
}
for (int i = 1; i <= N; i++) {
for (int j = V; j >= w[i]; j--) {
dp[j] = max(dp[j], dp[j - w[i]] + p[i]);
}
}
cout << dp[V] << endl;
return 0;
}
完全背包问题
完全背包问题和0-1背包的本质区别就是,完全背包的一个物品可以选择任意多次(每次递推到第i个物品的状态时,就算选了一次第i个物品,我后续还是可以选它),所以状态转移方程和0-1背包的区别就是,完全背包是i,0-1背包是i-1(i代表可选物品的范围)
代码实现
N, V = map(int, input().split())
w = []
p = []
dp = [[0 for i in range(V + 1)] for j in range(N + 1)]
for i in range(N):
a, b = map(int, input().split())
w.append(a)
p.append(b)
for i in range(1, N + 1):
for j in range(1, V + 1):
dp[i][j] = dp[i - 1][j] # 不选择当前物品
k = 0
while k * w[i - 1] <= j: #选择当前物品k次
dp[i][j] = max(dp[i][j], dp[i-1][j-k*w[i-1]]+k*p[i - 1])
k += 1
print(dp[N][V])
#include <bits/stdc++.h>
using namespace std;
int main()
{
int N, V;
cin >> N >> V;
vector<int> volume(N, 0);
vector<int> price(N, 0);
vector<vector<int>> dp(N + 1, vector<int>(V + 1, 0));
for (int i = 0; i < N; ++i)
cin >> volume[i] >> price[i];
for (int i = 1; i <= N; ++i)
{
for (int j = 1; j <= V; ++j)
{
dp[i][j] = dp[i - 1][j];
int k=0;
while(k*volume[i-1]<=j){
dp[i][j]=max(dp[i][j],dp[i-1][j-k*volume[i-1]]+k*price[i-1]);
k++;
}
}
}
cout << dp[N][V] << endl;
return 0;
}
转一维
#include <bits/stdc++.h>
using namespace std;
int main() {
int N, V;
cin >> N >> V;
vector<int> w(N + 1), p(N + 1);
vector<int> dp(V + 1, 0);
for (int i = 1; i <= N; i++) {
cin >> w[i] >> p[i];
}
for (int i = 1; i <= N; i++) {
for (int j = w[i]; j <= V; j++) {
dp[j] = max(dp[j], dp[j - w[i]] + p[i]);
}
}
cout << dp[V] << endl;
return 0;
}
两种背包问题的代码对比
左边的是01背包问题,右边的是完全背包问题
其实差别仅仅是内层for循环的条件。
- 01背包问题 使用反向遍历,确保每个物品在每次遍历中仅被考虑一次。
- 完全背包问题 使用正向遍历,允许每个物品被多次考虑,适应物品的多次选择情况。