动态规划(常见背包问题)

01背包问题

问题简述:在 N 件物品中选中若干件放入背包中,在不超过背包体积 V 的情况下,使得背包的价值最大
条件:每个物品只能选一次
每个物品的状态只有选或者不选两种状态

举个例子:
N 件商品和体积为 V 的背包,物品的体积和价值 viwi,(i表示第几件物品)求解将哪些物品放进去使得在不超过背包容量时的价值最大

思路:定义状态表达式dp[i][j]表示在只有前i个物品的状态下,体积为j的的情况下的背包的最大价值,则当前物品就存在选或者不选两种状态,不选:dp[i][j] = dp[i - 1][j] , 选:dp[i][j] = dp[i - 1][j - vi] + wi,则最后当前这个物品的状态就选取两种状态的最大值dp[i][j] = max (选,不选)

代码:

public class Main{
//n 代表物品个数,m表示背包体积
    public static void main(String[] args) {
        int[][] dp = new int[n+1][m+1]; //定义状态
        for (int i = 1; i <= n; i++) {  //外层循环的是物品
            for (int j = 1; j <= m; 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];
                }
            }
        }
        System.out.println(dp[n][m]);

查看上面的代码,时间复杂度和空间复杂度都是O(n*m)进一步进行优化,时间复杂度是不可能进行优化了,当前物品的状态只和它前一个物品的状态有关,因此我们可以降维为一维数组,用来存储前一个物品的状态,j将空间复杂度降为O(n)

但是需要注意的是,循环体积的时候必须从后往前进行循环,只有从后往前循环 dp[ j - v[i] ] 才是上一个状态的值,如果是从前往后,我们使用的dp[j-v[i]]就是已经更新的值

int[] dp = new int[m+1];
for (int i = 1; i <= n; i++) {
    for (int j = m; j >= v[i]; j--) { //直接进行判断
        dp[j] = Math.max(dp[j],dp[j-v[i]]+w[i]);
    }
}
System.out.println(dp[m]);

由于int类型的数组没有赋值,默认情况下就是0,所以我们推的dp[i][j]表示的是在不超过体积j的情况下最大值,倘若使得dp[0] = 0,dp[1 … m] 为 负无穷,这样就可以保证所有的状态都是由dp[0]推出来的, 那么此时dp[i][j]就表示的是体积恰好是 j 的情况下的最大价值,这时直接打印dp[i][j]有可能不是我们需要求解的值,要想输出正确的值,我们需要遍历下数组,寻找最大值

int[] dp = new int[m+1];
dp[0] = 0;
for (int i = 1; i <= m; i++) {
   dp[i] = Integer.MIN_VALUE;
}
for (int i = 1; i <= n; i++) {
   for (int j = m; j >= v[i]; j--) { //直接进行判断
       dp[j] = Math.max(dp[j],dp[j-v[i]]+w[i]);
   }
}
int res = 0;
for (int i = 0; i <= m; i++) res = Math.max(res,dp[i]); 
System.out.println(res);

完全背包问题

问题论述: 在 N 件物品中选中若干件放入背包中,在不超过背包体积 V 的情况下,使得背包的价值最大
条件:物品可以无限次的选择

举例:
有 n 件物品和体积为 m 的背包,可以无限次的使用物品, 求解将哪些物品放进去使得在不超过背包容量时的价值最大

思路:其实和01背包问题思路一致,不过是物品不仅仅只有选或者不选两种状态,而是选1个,2个,…状态,其实也很简单了,加个for循环就ok啦

int[] dp = new int[m+1];
for (int i = 1; i <= n; i++) {
   //每件物品只选一次,但是现在只有空间足够的情况下,可以选多件同一个物品
   for (int j = m; j >= v[i]; j--) {
       for (int k = 0; k * v[i] <= j; k++) {
           dp[j] = Math.max(dp[j],dp[j-k*v[i]]+k*w[i]);
       }
   }
}
System.out.println(dp[m]);

此时观察上慢代码的时间复杂度就不仅仅是O(n*m),可不可以进行优化,我们先想想01
背包问题,逆序的进行循环是为了dp[j - v[i]]是表示上一层的状态值,也就是说当前物品只能选一次,但是现在物品可以无限次的进行选择,那么我们就可以顺序进行循环,求解当前的状态值可以使用这一层中已经更新的状态进行填充,也就是说逆序是保证当前物品只能选取一次,而顺序当前物品可以无限次的进行选取

int[] dp = new int[m+1];
for (int i = 1; i <= n; i++) {
//可以利用之前已经改变状态进行填充
  for (int j = v[i]; j <= m; j++) {
       dp[j] = Math.max(dp[j],dp[j-v[i]]+w[i]);
   }
}
System.out.println(dp[m]);

多重背包问题

问题论述:在 N 件物品中选中若干件放入背包中,在不超过背包体积 V 的情况下,使得背包的价值最大
条件:物品可以有限次的选择

举例:
有n件物品,背包体积是m,在不超过背包体积的情况下,求解背包的最大价值

思路:同01背包问题一致,每件物品都是有限次的进行选取,可以选1件,2件,好多件,就是在循环体积的时候,加一个for循环就ok啦

代码:

int[] dp = new int[m+1];
//外层循环物品
for (int i = 1; i <= n; i++) {
    for (int j = m; j >= v[i]; j--) {
        for (int k = 1; k <= s[i]; k++) { //s[i]代表该件商品的可以选取的个数
            if (k*v[i] <= j)
                dp[j] = Math.max(dp[j],dp[j-k*v[i]]+k*w[i]);
        }
    }
}
System.out.println(dp[m]);

这段代码的时间复杂度超过O(n*m),我们可以利用二进制的方法对其进行优转化为01背包问题
比如说二进制位3位 (111)可以表示0 ~ 7之间的任何数字,那么我们就可以利用这个特点,对有限次的选择进行优化,倘若一个物品可以选择的次数是7,倘若拆开,那么这件物品就只有选与不选两种状态,由此就可以利用01背包进行解决问题,

0:000 不选
1:001 选1件 1
2:010 选2件 2
3:011 选3件 1+2
4:100 选4件 4
5:101 选5件 1+4
6:110 选6件 2 + 4
7:111 选7件 1+2+4

这个是恰好的情况,倘若是物品的个数是10,那就需要1111, 1 + 2 + 4 +(10 - 1 - 2 - 4)也就是需要1,2,4,3这四个数就可以组成0 ~ 10以内的数字,注意不能是1,2,4,8,如果是8的话,那么就是组合成0 ~ 15之间的数字,显然我们最大数字是10

代码:

/*
s[i]表示第i种物品的个数,可以选择的有限次数
l表示对重新拆解的物品进行体积和价值的存储
n表示物品种类
m表示背包体积
*/
List<List<Integer>> l = new LinkedList<>();

for (int i = 1; i <= n; i++) {
	//对有限次的物品进行拆分
    for (int j = 1; j <= s[i]; j*=2) {
        s[i] -= j;
        l.add(Arrays.asList(j*v[i],j*w[i]));
    }
    if (s[i] > 0) {
        l.add(Arrays.asList(s[i]*v[i],s[i]*w[i]));
    }
}
//01背包问题,外层是物品的种类,内层是体积
for (List<Integer> list : l) {
    for (int j = m; j >= list.get(0); j--) {
        dp[j] = Math.max(dp[j],dp[j-list.get(0)]+list.get(1));
    }
}
System.out.println(dp[m]);

混合背包问题

问题论述:在 N 件物品中选中若干件放入背包中,在不超过背包体积 V 的情况下,使得背包的价值最大
条件:同时存在物品只能选一次,选无限次,选有限次

思想:这个其实就是上述三种情况的混合使用,只要上述的三种情况理解,那么这个种情况信手拈来,就是根据不同的条件进行不同的操作

代码:

/*
s = −1 表示第 i 种物品只能用1次;
s = 0 表示第 i 种物品可以用无限次;
s > 0 表示第 i 种物品可以使用s次;
*/
Scanner sc = new Scanner(System.in);
List<List<Integer>> list = new ArrayList<>();
for (int i = 1; i <= n; i++) {
    int v = sc.nextInt();
    int w = sc.nextInt();
    int s = sc.nextInt();
    //如果是0-1背包
    if (s < 0) {
        list.add(Arrays.asList(v,w,-1));
    }
    //如果是完全背包
    else if (s == 0) {
        list.add(Arrays.asList(v,w,0));
    }
    //多重背包(利用二进制)
    else {
        for (int j = 1; j <= s; j*=2) {
            s -= j;
            list.add(Arrays.asList(v*j, w*j, -1));
        }
        if (s > 0) list.add(Arrays.asList(s*v, s*w, -1));
    }
}
int[] dp = new int[m+1];
for (List<Integer> myList : list) {
    //0-1背包
    if (myList.get(2) == -1) {
        for (int k = m; k >= myList.get(0); k--) {
            dp[k] = Math.max(dp[k],dp[k-myList.get(0)] + myList.get(1));
        }
    }
    else {
        //完全背包
        for (int k = myList.get(0); k <= m; k++) {
            dp[k] = Math.max(dp[k],dp[k-myList.get(0)] + myList.get(1));
        }
    }
}
System.out.println(dp[m]);

总结:以上就是常见的背包问题,01背包是最基础的,背包的变形都是在01背包的基础上进行条件的改变,注意的点就是,进行推当前物品的状态时,是不是由dp[0],这一个状态推来的,如果是那么就需要在遍历一次寻找最大值; 如果不是,那最后一个值就是我们所寻求的最大值

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值