说说几种背包问题(java实现)
背包问题是经典的动态规划问题。分成以下几种细说一下背包问题。代码为java实现。
1. 01背包问题
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。
定义一个二阶矩阵dp[N+1][V+1],dp[i][j]表示在 只能选择前i个物品,背包容量为j的情况下,背包中物品的最大价值
。
这里之所以要N+1和V+1,是因为第0行表示只能选择第0个物品的时候,即没有物品的时候第0列表示背包的体积为0的时候,即不能装任何东西的时候.
已知dp[i-1][j],要求dp[i][j],则需要状态转移方程
对于dp[i][j]有两种情况:
1.不选择当前的第i件物品/第i件物品比背包容量要大
dp[i][j] = dp[i-1][j]
2.选择当前的第i件物品(潜在要求第i件物品体积小于等于背包总容量),则能装入的物品最大价值为:当前物品的价值 加上 背包剩余容量在只能选前i-1件物品的情况下的最大价值。表示为:
dp[i][j] = dp[i-1][j-v[i]] + w[i]
dp[i][j]在两种情况中选择比较大的情况作为当前的最优解;
即:
if(j >= v[i]):
dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i])
else:
dp[i][j] = dp[i-1][j]
以上的动态规划需要一个二维的dp空间。我们可以优化为一个一维的dp数组。此时,dp[v]就表示在v大小的背包空间下,可以放入的最大的物品价值 。此时的物品依次放入。当我们已知第i-1个物品放入后,v的最大值,那么加入第i个物品时,最大值要么是放入第i个物品,要么是原来的值。表达式:
dp[v] = Math.max( dp[v - v[i] ) + w[i] , dp[v])
参考代码如下:
import java.util.Scanner;
public class Main{
public static void main(String[] args) throws Exception {
// 读入数据的代码
Scanner reader = new Scanner(System.in);
// 物品的数量为N
int N = reader.nextInt();
// 背包的容量为V
int V = reader.nextInt();
// 一个长度为N的数组,第i个元素表示第i个物品的体积;
int[] v = new int[N + 1] ;
// 一个长度为N的数组,第i个元素表示第i个物品的价值;
int[] w = new int[N + 1] ;
for (int i=1 ; i <= N ; i++){
//读入是第一个数索引为1
// 接下来有 N 行,每行有两个整数:v[i],w[i],用空格隔开,分别表示第i件物品的体积和价值
v[i] = reader.nextInt();
w[i] = reader.nextInt();
}
reader.close() ;
// 二维dp
int[][] dp = new int[N+1][V+1];
dp[0][0] = 0;
//此处i从1开始是因为上面v[i]和w[i]是从1开始存的
for(int i = 1; i <= N; i++){
for(int j = 0; j <= V; j++){
if(j >= v[i]){
dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-v[i]] + w[i]);
}else{
dp[i][j] = dp[i-1][j];
}
}
}
//压缩为一维dp数组
int[] dp = new int[V+1];
dp[0] = 0;
for(int i = 1; i <= N; i++){
for(int j = V; j >= v[i]; j--){
dp[j] = Math.max(dp[j], dp[j-v[i]] + w[i]);
}
}
2.完全背包问题
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用
。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。
可以模仿01背包,dp[i][j]记录前i个物品放入j空间的最大值。此时,由于一个物品可是使用无限次数,dp[i][j]应该从dp[i][j-v[i]]来而不是dp[i-1][j-v[i]]来。表达式为
dp[i][j]=Math.max(dp[i][j-v[i]]+w[i],dp[i-1][j])
同样,完全背包问题一样可以优化至一维dp空间。 01背包和完全背包使最常用的背包问题。很多其他背包问题可以转换为这两个问题来思考。 参考代码如下:
//数据读取和上一题01背包相同
//模仿01背包,多维的dp。dp[i][j]记录前i个物品放入j空间的最大值。
int[][] dp=new int[N+1][V+1];
for(int i=1;i<=N;i++){
for(int j=0;j<=V;j++){
if(j>=v[i]){
//此时的状态方程,比较的大小
//注意i-1和i的状态区别
dp[i][j]=Math.max(dp[i][j-v[i]]+w[i],dp[i-1][j]);
}else{
dp[i][j]=dp[i-1][j];
}
}
}
System.out.println(dp[N][V]);
//初步优化为一维数组
//dp[v]表示在v空间下装入的最大价值
int[] dp=new int[V+1];
for(int i=1;i<=N;i++){
for(int j = V; j >= v[i]; j --){
//第i个物品可以使用0-k个,和不使用第i个物品的最大值比较,找到较大的一个
//逐一找最大值,增加循环,用变量k遍历计算最大值。
for(int k = 0; j-k* v[i] >= 0; k ++){
dp[j] = Math.max(dp[j] , dp[j - k * v[i]] + k * w[i]);
}
}
}
System.out.println(dp[V]);
//再优化。
int[] dp=new int[V+1];
for(int i=1;i<=N;i++){
//和01背包相比在于01背包从后向前遍历,由于使用到之前的状态,从后向前时前面的状态为0,确保了一个物品只使用了一次。
//完全背包使用从前向后遍历,前面的状态先遍历。此时后面的状态再计算时,使第i个物品重复使用。
for(int j = v[i]; j <= V; j ++){
dp[j] = Math.max(dp[j], dp[j - v[i]] + w[i]);
}
}
System.out.println(dp[V]);
3 多重背包问题
有 N 种物品和一个容量是 V 的背包。第 i 种物品最多有 si 件
,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
思路:
参考完全背包问题,就是将完全背包中的数量限制改为si. (体现在for循环中)
优化:
利用二进制,将背包数量分解,变为01背包问题。(他人处学习来的思路和代码)。
参考代码:
//直接做法。
int[] dp = new int[V+1];
for(int i = 1; i <= N; i ++){
for(int j = V; j >= v[i]; j --){
//此处加入一个限制条件k<=s[i]即可
//k可以从1开始,因为0就是dp[j]
for(int k = 1; j - k * v[i] >= 0 && k <= s[i]; k ++ ){
dp[j] = Math.max(dp[j], dp[j - k * v[i]] + k * w[i]);
}
}
}
System.out.println(dp[V]);
//参考地址https://www.acwing.com/solution/content/7495/
//优化的多重背包问题。
//将数量s分解。例如有一个体积为v,值为w,数量为7的。则分解为[v,w],[2v,2w],[4v,4w]三个物体
//将所有物体数量都分解。则化解为了01背包问题。
int maxN = 200002;
int[] v = new int[maxN];
int[] w = new int[maxN];
Scanner jin = new Scanner (System.in);
void run(){
int n = jin.nextInt();
int m = jin.nextInt();
int p = 1;
for (int i = 1; i <= n ; i++){
int V = jin.nextInt();
int W = jin.nextInt();
int S = jin.nextInt();
int k = 1;
while (S > k){
v[p] = V*k;
w[p] = W*k;
S -= k;
k *= 2;
p++;
}
if (S > 0){
v[p] = V*S;
w[p] = W*S;
p ++;
}
}
//到此为止,原来所有的物体分解为共p个独立的物体,每个限制使用1次,化解为01背包问题。
//共有p个物体,放入总体积m的背包。体积为v,价值为w
int res = dp(p, m);
System.out.println(res);
}
//同01背包问题。
int dp(int n, int m){
int[] f = new int[maxN];
for (int i= 1; i <= n ; i ++){
for (int j = m ; j>= v[i] ; j--){
f[j] = Math.max(f[j], f[j - v[i]] + w[i]);
}
}
return f[m];
}
public static void main(String[] args) {new solution().run();}
//利用优先队列优化多重背包问题。看到过大佬用过这个思路。
//(待学习)
4.混合背包问题
有 N 种物品和一个容量是 V 的背包。
物品一共有三类:
第一类物品只能用1次(01背包);
第二类物品可以用无限次(完全背包);
第三类物品最多只能用 si 次(多重背包);
每种体积是 vi,价值是 wi。
si=−1 表示第 i 种物品只能用1次;
si=0 表示第 i 种物品可以用无限次;
si>0 表示第 i 种物品可以使用 si 次;
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
思路:
分类讨论。01背包可以看作只使用一次的多重背包问题。
参考代码:
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
int N = sc.nextInt(); // 物品个数
int V = sc.nextInt(); // 背包总容量
int[] dp = new int[V + 1];
for(int i = 0; i < N; i++){
int v = sc.nextInt(); // 体积
int w = sc.nextInt(); // 价值
int s = sc.nextInt(); // 数量
if(s == 0){
// 完全背包问题
for(int j = v; j <= V; j++){
dp[j] = Math.max(dp[j], dp[j - v] + w);
}
}else{
// 多重背包问题,01背包是多重背包的特例,可以一并处理
s = Math.abs(s);
for(int j = 1; s >= j; s -= j, j *= 2){
for(int k = V; k >= j * v; k--){
dp[k] = Math.max(dp[k], dp[k - j * v] + j * w);
}
}
if(s > 0){
for(int j = V; j >= s * v; j--){
dp[j] = Math.max(dp[j], dp[j - s * v] + s * w);
}
}
}
}
System.out.println(dp[V]);
}
5.二维背包问题
有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。
每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi。初始的01背包问题多加入了一个限制
求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。
输出最大价值。
思路: 与01背包一样,二维dp扩充为三维dp
public int two_dimension_knapsack_problem_1(int N, int V, int M, int[] v, int[] m, int[] w){
int[][][] dp = new int[N+1][V+1][M+1];
for(int i = 1; i <= N; i++){
for(int j = 1; j <= V; j++){
for(int k = 1; k <= M; k++){
if(j < v[i] || k < m[i]){
// 客观条件限制,不能选择当前物品N
dp[i][j][k] = dp[i-1][j][k];
}else {
//基本相同,多个限制
dp[i][j][k] = Math.max(dp[i-1][j][k], dp[i-1][j-v[i]][k-m[i]] + w[i]);
}
}
}
}
return dp[N][V][M];
注:许多代码与思路来源于anwin网站与相关题目题解。