动态规划-背包问题等

背包问题九讲

 

01背包总结+传授个人经验

0-1 背包问题:给定 n 种物品和一个容量为 C 的背包,物品 i 的重量是 wi,其价值为 vi 。

问:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?


分析一波,面对每个物品,我们只有选择拿取或者不拿两种选择,不能选择装入某物品的一部分,也不能装入同一物品多次。


解决办法:声明一个 大小为  m[n][c] 的二维数组,m[ i ][ j ] 表示 在面对第 i 件物品,且背包容量为  j 时所能获得的最大价值 ,那么我们可以很容易分析得出 m[i][j] 的计算方法,

(1). j < w[i] 的情况,这时候背包容量不足以放下第 i 件物品,只能选择不拿

m[ i ][ j ] = m[ i-1 ][ j ]

(2). j>=w[i] 的情况,这时背包容量可以放下第 i 件物品,我们就要考虑拿这件物品是否能获取更大的价值。

如果拿取,m[ i ][ j ]=m[ i-1 ][ j-w[ i ] ] + v[ i ]。 这里的m[ i-1 ][ j-w[ i ] ]指的就是考虑了i-1件物品,背包容量为j-w[i]时的最大价值,也是相当于为第i件物品腾出了w[i]的空间。

如果不拿,m[ i ][ j ] = m[ i-1 ][ j ] , 同(1)

究竟是拿还是不拿,自然是比较这两种情况那种价值最大。


由此可以得到状态转移方程:

if(j>=w[i])
m[i][j]=max(m[i-1][j],m[i-1][j-w[i]]+v[i]);
else
m[i][j]=m[i-1][j];

例:0-1背包问题。在使用动态规划算法求解0-1背包问题时,使用二维数组m[i][j]存储背包剩余容量为j,可选物品为i、i+1、……、n时0-1背包问题的最优值。绘制
价值数组v = {8, 10, 6, 3, 7, 2},

重量数组w = {4, 6, 2, 2, 5, 1},

背包容量C = 12时对应的m[i][j]数组。


(第一行和第一列为序号,其数值为0)
如m[2][6],在面对第二件物品,背包容量为6时我们可以选择不拿,那么获得价值仅为第一件物品的价值8,如果拿,就要把第一件物品拿出来,放第二件物品,价值10,那我们当然是选择拿。m[2][6]=m[1][0]+10=0+10=10;依次类推,得到m[6][12]就是考虑所有物品,背包容量为C时的最大价值。

 public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int count = scanner.nextInt();
        int capacity = scanner.nextInt();
        int[] values = IntStream.range(0, count).map(num -> scanner.nextInt()).toArray();
        int[] weights = IntStream.range(0, count).map(num -> scanner.nextInt()).toArray();

        //0-1背包标准解答,求解二维数组;
        solve01BagGeneral(count,capacity,values,weights);

        //0-1背包优化解答,求解一维数组;
        solve01BagOptimization(count,capacity,values,weights);
    }

    private static void solve01BagGeneral(int count, int capacity, int[] values, int[] weights) {
        
    int[][] plans = new int[count+1][capacity+1];
        for(int i=1; i<=count; i++ ) {
            for(int j=1; j<=capacity; j++) {
                if (j >= weights[i-1]) {
                    plans[i][j] = Math.max(plans[i-1][j], plans[i-1][j-weights[i-1]] + values[i-1] );
                }else {
                    plans[i][j] = plans[i-1][j];
                }
            }
        }
        Stream.of(plans).forEach(t -> System.out.println(Arrays.toString(t)));
        int[] x = new int[count];
        int c=capacity;
        for(int i=count; i>0; i--) {
            if (plans[i][c] == plans[i-1][c]) {
                x[i-1] = 0;
            }else {
                x[i-1] = 1;
                c -= weights[i-1];
            }
        }
        System.out.println(Arrays.toString(x));
    }    

到这一步,可以确定的是可能获得的最大价值,但是我们并不清楚具体选择哪几样物品能获得最大价值。

另起一个 x[ ] 数组,x[i]=0表示不拿,x[i]=1表示拿。

m[n][c]为最优值,如果m[n][c]=m[n-1][c] ,说明有没有第n件物品都一样,则x[n]=0 ; 否则 x[n]=1。当x[n]=0时,由x[n-1][c]继续构造最优解;当x[n]=1时,则由x[n-1][c-w[i]]继续构造最优解。以此类推,可构造出所有的最优解。(这段全抄算法书,实在不知道咋解释啊。。)

  int[] x = new int[count];
        int c=capacity;
        for(int i=count; i>0; i--) {
            if (plans[i][c] == plans[i-1][c]) {
                x[i-1] = 0;
            }else {
                x[i-1] = 1;
                c -= weights[i-1];
            }
        }

二、优化空间复杂度
先考虑一下上面的状态转移方程如何实现,肯定有一个主循环i = 1...N,每次算出来二维数组dp[i][0..V]的所有值。那么如果只用一个数组f[0...V],能不能保证第i次循环结束后f[v]就是我们定义的状态f[i][v]呢?

f[i][v]是由f[i-1][v]和f[i-1][v-c[i]]两个子问题递推而来,能否保证在推f[i][v]时(也即在第i次主循环中推f[v]时)能够得到f[i-1][v]和f[i-1][v-c[i]]的值呢?

事实上,这要求在每次主循环中我们以v=V...0的顺序推f[v],这样才能保证推f[v]时f[v-c[i]]保存的是状态f[i-1][v-c[i]]的值。伪代码如下:

for i in 0 ... N
  for v = V ... 0
    f[v] = max{f[v], f[v-c[i]] + w[i]}

    private static void solve01BagOptimization(int count, int capacity, int[] values, int[] weights) {
        int[] dp = new int[capacity+1];
        for(int i=1; i<=count; i++ ) {
            for(int j=capacity; j>=weights[i-1]; j--) {
                dp[j]=Math.max(dp[j],dp[j-weights[i-1]]+values[i-1]);
            }
        }
        System.out.println(Arrays.toString(dp));
    }

三、完全背包问题

题目
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价格是w[i].求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大

思路
这个问题类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略 已非取或不取两种,而且右取0件、取1件、取2件...等很多种。如果仍然按照01背包的思路,令dp[i][v]表示前i种物品恰好放入一个容量为v的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程:

dp[i][v] = max{dp[i-1][v - k * c[i]] + k * w[i] | 0 <= k * c[i]<= v}

private static void solveFullBagGeneral(int count, int capacity, int[] values, int[] weights) {
    int[][] plans = new int[count+1][capacity+1];
    for(int i=1; i<=count; i++ ) {
        for(int j=1; j<=capacity; j++) {
            int max=0;
            for( int k=0; (k * weights[i-1])<=capacity; k++ ){
                if (j >= k* weights[i-1]) {
                    max = Math.max(max, plans[i - 1][j - k * weights[i - 1]] + k * values[i - 1]);
                }
            }
            plans[i][j]=max;
        }
    }
    Stream.of(plans).forEach(t -> System.out.println(Arrays.toString(t)));
}

 四、完全背包优化解;

转化为01背包求解
最简单的想法是:考虑到第i种物品最多选V/c[i]件,于是可以把第i种物品转换为V/c[i]件费用及价值均不变的物品,然后求解这个01背包问题。但是这样完全没有改进时间复杂度,但这毕竟给了我们将完全背包转换为01背包问题的思路:将一种物品拆成多件物品

O(VN)的算法
这个算法使用一维数组,先看伪代码:

for i = 1 ... N
  for v = 0 ... V
    f[v] = max{f[v], f[v-cost] + weight}

你会发现,这个伪代码与01背包的伪代码只有v的循环次序不同而已。为什么这样一改就行呢?首先,想想为什么01背包问题中要按照v=V...0的逆序来循环。这是因为要保证第i次循环中的状态f[i][v]是由状态f[i-1][v-c[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果f[i-1][v-c[i]]。而现在完全背包的特点恰好是每种物品可选无限件,所以在考虑“加选一件dii种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果f[i][c-v[i]],所以就可以并且必须采用v=0...V的顺序循环


原文链接:https://blog.csdn.net/wzy_1988/article/details/12260343

 https://www.cnblogs.com/zyacmer/p/9961710.html

原文地址:https://www.cnblogs.com/yuluoxingkong/p/14934270.html