动态规划问题——0/1背包问题(Java实现)

1、问题描述

0-1背包问题:

        给定N件物品和一个容量为V的背包。放入第i件物品耗费的空间为C[i] ,得到的价值是 W[i] 。

        问:哪些物品装入背包可使价值总和最大?最大是多少?

2、基本思路

2.1  基本思路

   这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。

分析:

面对每个物品,我们只有选择拿与不拿两种选择,不能够选择装入物品的一部分,也不能装入同一物品多次。

解决方法:

声明一个二维数组F[N + 1 , V + 1] ,F[i ][ v] 表示  前i件物品恰放入一个容量恰为v的背包可以获得的最大价值。通过分析可得出F[i , v]的计算方法,

1) 当 v < W[i] 时, 说明背包容量不足以放下第i件物品,只能选择不拿,此时:

            F[i ][ v] = F[i -1][ v]

2)   当 v >= W[i] 时,这是背包容量可以放下第i件物品,可以选择拿还是不拿,判断标准:拿这件物品是否能获取更大的价值。

  • 如果拿,则 F[i ][ v] = F[ i - 1][ v - C[i] ] +W[i];
  • 如果不拿,则 F[i ][ v] = F[i -1 ][ v]。

究竟拿不拿,取决于哪种情况使价值最大,由此可得到状态转移方程:

//如果容量为j的背包放得下第i个物体
if(j >= weight[i]) {
    //两种选择:放与不放第i个物体,策略:哪个使价值最大就选用哪种策略
    F[i][j] = max(F[i - 1][j - weight[i]] + value[i], F[i - 1][j]);
}else {
    //放不下,只能选择不放第i个物体
    F[i][j] = F[i - 1][j];
}

我们最终要求的就是: F[ N ] [ V ],表示 N件物品恰放入一个容量恰为V的背包可以获得的最大价值。

举例分析:假设有5个物体,背包容量为10,物体的体积分别是2 2 6 5 4,物体的收益分别是6 3 5 4 6。假设要求F[1][3],则需要求解的是:

F[1][0] = F[0][0] = 0 ; F[1][1] = F[0][1] = 0 ; F[1][2] = max { F[1][2-2] + 6, F[0][2] } = 6;

F[1][3] = max {F[1][3-2] + 6 , F[0][3]} = 6;

因此,我们求解F[N][V]的时候,我们可以逆序枚举,因为在计算F[i,j]前,它的F[i - 1][j - weight[i]]  和  F[i - 1][j] 一定已经计算出来了。


2.2 复杂度分析

逆序枚举时间复杂度为O(N*V),且由于使用了二维数组 F[N + 1 , V + 1] ,其空间复杂度为O((N+1)*(V+1))。

其时间复杂度已经不能再优化了,但是空间复杂度可以优化到O(V),具体看2.4节。


2.3 如何确定哪些物品构成最大价值?

在3.1节我们已经可以确定每一种状态可获得的最大价值,但是并不清楚具体选择哪几样物品能够获得最大价值。


那么,如何确定哪几样物品能够获得最大价值呢?

另起一个数组isAdd[N + 1],isAdd[i] = false表示不拿第i个物体,否则表示拿了。

F[i][j],它是最优值,那么如果:

  • F[i][j] = F[i - 1][j],说明有没有第i个物品都一样,第i个物品没有放,则isAdd[i] = false;
  • 否则,说明放了第i个物体,则isAdd[i] = true。

具体实现代码可以看3.2节。


2.4 空间复杂度优化(降维)

空间复杂度可以优化为O(V),只适用于求最大价值,但是如果要求解哪些物体构成最大价值,则不可以降维。

二维的伪代码如下:


考虑3.1节基本思路的具体实现,每一次都有一个主循环i = 1 .. N,每次算出来二维数组F[ i ][0 .. V]的所有值。那么,如果只用一维数组F[0 .. V],能不能保证 第i次循环结束后F[v]中表示的就是我们定义的状态F[i][v]呢?

降成一维的伪代码:


为什么能够二维降一维?

看上面的伪代码,其实对于外层循环的每一个i值,其实是不需要记录的。在进行第i次循环时,所有的F[0 ... V]都还未更新时,F[v]此时记录的表示前i-1个物品在背包空间为 v 时的最大价值,这样就相当于还记录着F[i-1,v]和F[i-1,v-Ci]。

那么为何内层循环要递减遍历?

第i次循环时,刚开始F[0 ... V]还未更新时,F[v]此时记录的表示前i-1个物品在背包空间为 v 时的最大价值,那么如何保证F[v]在进行更新时,每一次用到的最优值都是未更新前的? 进行逆序遍历,就可以保证更新正确。

举例说明:假设一个物品A价值100,体积为2,正在进行第i次外层循环,假设第i-1次的F[v]全为0,按照[0 ... V]的顺序遍历,那么v = 2时,F[2] = max(F[2], F[0] + 1000) = 1000;当j = 4时,F[4] = max(F[4], F[2] + 1000) = 2000,此时max用到的F[2]是已经更新过的,所以正序遍历会造成错误的结果。

那么为什么逆序遍历就不会出错了呢?

按照上面的例子,按照[V ... 0]的顺序进行遍历,先求F[4] = max(F[4], F[2] + 1000) = 1000,此时max用到的F[2]就是第i-1次循环的结果。

不适用的情况

虽然降维有效降低了空间复杂度,但是如果题目要求算出哪些物体组成最大价值的话,还是得用二维数组。具体可看2.3节。

java具体实现代码在3.3小节。

2.5 扩展——改成“恰好装满背包”

求最优解的背包问题中,有两种不太相同的问法:

  • 有的题目要求“恰好装满背包”时的最优解
  • 有的题目则没有要求必须把背包装满

一种实现方法的区别仅在初始化的时候有所不同:

  • 对于第一种问法,要求恰好装满背包,那么在初始化时除了F[0]为0,其余F[1 .. V]均设为-INF,这样就可以确保最终得到的F[V]是一种恰好装满背包的最优解。
  • 如果没有要求必须把背包装满,只是希望价格尽量大,初始化时应该将F[0 .. V]全部设为0。

如何理解?

  • 初始化的F数组事实上就是在没有任何物体放进背包的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可以在什么也不装且价值为0的情况下被“恰好装满”,其它容量的背包均没有合法解,属于未定义的状态,应该赋值为-INF。
  • 如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值全部为0。

为什么取负无穷大为不合法的状态?

因为状态选择方程中,F[v]是通过max函数取值较大的那个,所以将不合法的状态设为负无穷,则这种状态怎么样都不会被选到。

一个常数优化


===================================================================

待添加...

===================================================================

3、Java实现

3.1 二维数组实现

package pack9jiang;

import java.util.Scanner;

/**
 * 使用二维数组非递归的方法求解0/1背包问题
 */
public class ZeroOnePack {
    // N表示物体的个数,V表示背包的载重
    int N,V;
    //用于存储每个物体的重量,下标从1开始
    private int[] weight;
    //存储每个物体的收益,下标从1开始
    private int[] value;
    //二维数组,用来保存每种状态下的最大收益
    private int[][] F;

    /**
     * 使用非递归方式,求解F[0 .. N][0 .. V],即for循环从下至上求解
     */
    public void ZeroOnePackNonRecursive() {
        //对二维数组F进行初始化
        for(int j = 0; j <= V; j++) {
            F[0][j] = 0;
        }

        //注意边界问题,i是从1开始的,j是从0开始的
        //因为F[i - 1][j]中i要减1
        for(int i = 1; i <= N; i++) {
            for(int j = 0; j <= V; j++) {
                //如果容量为j的背包放得下第i个物体
                if(j >= weight[i]) {
                    F[i][j] = Math.max(F[i - 1][j - weight[i]] + value[i], F[i - 1][j]);
                }else {
                    //放不下,只能选择不放第i个物体
                    F[i][j] = F[i - 1][j];
                }
            }
        }

        //打印所有结果,我们要求的是F[N][V]
        for(int i = 0; i <= N; i++) {
            for(int j = 0; j <= V; j++) {
                System.out.print(F[i][j] + " ");
            }
            System.out.println();
        }
    }


    /**
     * 求解F[n][m]这个最优值具体选取哪几样物品能获得最大价值,但只会输出一种情况
     * @param n     表示前n个物体,n <= N
     * @param v     表示背包的容量,v <= V
     */
    public void printResult(int n, int v) {
        boolean[] isAdd = new boolean[n + 1];

        for(int i = n; i >= 1; i--) {
            if(F[i][v] == F[i-1][v])
                isAdd[i] = false;
            else {
                isAdd[i] = true;
                v -= weight[i];
            }
        }

        for(int i = 1; i <= n; i++) {
            System.out.print(isAdd[i] + " ");
        }
        System.out.println();
    }

    /**
     * 输入格式:
     5 10
     2 2 6 5 4
     6 3 5 4 6
     * result:15
     * 第一行是物体个数、背包总空间;
     * 第二行是每个物体的空间;
     * 第三行是每个物体的收益。
     */
    public void init() {
        Scanner sc = new Scanner(System.in);
        N = sc.nextInt();
        V = sc.nextInt();

        //下标从1开始,表示第1个物品
        weight = new int[N + 1];
        value = new int[N + 1];
        F= new int[N + 1][V + 1];//注意是 N + 1,因为需要一个初始状态F[0][0],表示前0个物品放进空间为0的背包的最大收益

        for(int i = 1; i <= N; i++) {
            weight[i] = sc.nextInt();
        }

        for(int i = 1; i <= N; i++) {
            value[i] = sc.nextInt();
        }
    }

    public static void main(String[] args) {
        ZeroOnePack zop = new ZeroOnePack();
        zop.init();
        zop.ZeroOnePackNonRecursive();
        zop.printResult(zop.N,zop.V);
    }
}


3.2 确定哪些物品构成最大值

在3.1的代码基础上添加以下代码:

/**
     * 求解F[n][m]这个最优值具体选取哪几样物品能获得最大价值,但只会输出一种情况
     * @param n     表示前n个物体,n <= N
     * @param v     表示背包的容量,v <= V
     */
    public void printResult(int n, int v) {
        boolean[] isAdd = new boolean[n + 1];

        for(int i = n; i >= 1; i--) {
            if(F[i][v] == F[i-1][v])
                isAdd[i] = false;
            else {
                isAdd[i] = true;
                v -= weight[i];
            }
        }

        for(int i = 1; i <= n; i++) {
            System.out.print(isAdd[i] + " ");
        }
        System.out.println();
    }

3.3 降维

package pack9jiang;

import java.util.Scanner;

/**
 * 0/1背包的降维,将空间复杂度降为O(V)
 */
public class ZeroOnePackExtend1 {
    // N表示物体的个数,V表示背包的载重
    int N,V;
    //用于存储每个物体的重量,下标从1开始
    private int[] weight;
    //存储每个物体的收益,下标从1开始
    private int[] value;
    //降成一维数组,用来保存每种状态下的最大收益
    private int[] F;

    /**
     * 使用非递归方式
     */
    public void ZeroOnePackNonRecursive() {
        //对一维数组F进行初始化
        for(int i = 0; i <= V; i++) {
            F[i] = 0;
        }

        //注意边界问题,i是从1开始的
        for(int i = 1; i <= N; i++) {
            for(int j = V; j >= 0; j--) {
                //降序遍历
                if(j >= weight[i]) {
                    F[j] = Math.max(F[j - weight[i]] + value[i], F[j]);
                }else {
                    //可以省略
                    F[j]= F[j];
                }
            }
        }

        //打印所有结果,我们要求的是F[V]
        for(int i = 0; i <= V; i++) {
            System.out.print(F[i] + " ");
        }
        System.out.println();
    }


    /**
     * 输入格式:
     5 10
     2 2 6 5 4
     6 3 5 4 6
     * result:15
     * 第一行是物体个数、背包总空间;
     * 第二行是每个物体的空间;
     * 第三行是每个物体的收益。
     */
    public void init() {
        Scanner sc = new Scanner(System.in);
        N = sc.nextInt();
        V = sc.nextInt();

        //下标从1开始,表示第1个物品
        weight = new int[N + 1];
        value = new int[N + 1];
        F= new int[V + 1];//注意是 V + 1

        for(int i = 1; i <= N; i++) {
            weight[i] = sc.nextInt();
        }

        for(int i = 1; i <= N; i++) {
            value[i] = sc.nextInt();
        }
    }

    public static void main(String[] args) {
        ZeroOnePackExtend1 zope1 = new ZeroOnePackExtend1();
        zope1.init();
        zope1.ZeroOnePackNonRecursive();
    }
}


3.4 扩展——改成“恰好装满背包”

package pack9jiang;

import java.util.Scanner;

/**
 * 0/1背包问题变种:求恰好装满背包的最优解
 */
public class ZeroOnePackExtend2 {
    // N表示物体的个数,V表示背包的载重
    int N,V;
    //用于存储每个物体的重量,下标从1开始
    private int[] weight;
    //存储每个物体的收益,下标从1开始
    private int[] value;
    //降成一维数组,用来保存每种状态下的最大收益
    private int[] F;
    int INF = 9999;

    /**
     * 使用非递归方式
     */
    public void ZeroOnePackNonRecursive() {
        //仅在初始化有区别
        F[0] = 0;
        for(int i = 1; i <= V; i++) {
            F[i] = -INF;
        }

        //注意边界问题,i是从1开始的
        for(int i = 1; i <= N; i++) {
            for(int j = V; j >= 0; j--) {
                //降序遍历
                if(j >= weight[i]) {
                    F[j] = Math.max(F[j - weight[i]] + value[i], F[j]);
                }else {
                    //可以省略
                    F[j]= F[j];
                }
            }
        }

        //打印所有结果,我们要求的是F[V]
        for(int i = 0; i <= V; i++) {
            System.out.print(F[i] + " ");
        }
        System.out.println();
    }


    /**
     * 输入格式:
     5 10
     2 2 6 5 4
     6 3 5 4 6
     * result:14
     * 第一行是物体个数、背包总空间;
     * 第二行是每个物体的空间;
     * 第三行是每个物体的收益。
     */
    public void init() {
        Scanner sc = new Scanner(System.in);
        N = sc.nextInt();
        V = sc.nextInt();

        //下标从1开始,表示第1个物品
        weight = new int[N + 1];
        value = new int[N + 1];
        F= new int[V + 1];//注意是 V + 1

        for(int i = 1; i <= N; i++) {
            weight[i] = sc.nextInt();
        }

        for(int i = 1; i <= N; i++) {
            value[i] = sc.nextInt();
        }
    }

    public static void main(String[] args) {
        ZeroOnePackExtend2 zope2 = new ZeroOnePackExtend2();
        zope2.init();
        zope2.ZeroOnePackNonRecursive();
    }
}


  • 51
    点赞
  • 230
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值