动态规划之 0-1 背包问题详解

个人博客:DoubleFJ の Blog

前言

背包问题是比较经典的动态规划算法题,之前没接触过算法都没听说过这个,也是后来在 leetcode 中刷题时才了解到,惭愧惭愧啊。算法的世界太奇妙,数学一直都是那么令人着迷。今天来总结一下这个 01 背包问题。注:这里的物品不可拆分。

动态规划

首先了解下什么是动态规划。动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程(multistep decision process)的优化问题时,提出了著名的最优化原理(principle of optimality),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。1957年出版了他的名著《Dynamic Programming》,这是该领域的第一本著作。

动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。

详情可见:动态规划

问题详解

问题描述

给定 N 种物品和一个背包。物品i的重量是 Wi,其价值位 Vi,背包的容量为 C。问应该如何选择装入背包的物品,使得转入背包的物品的总价值为最大。

问题分析

在选择物品的时候,对每种物品只有两种选择,要么装入,要么不装入。因此,此为一个 0-1 背包问题。
01 背包的递归公式为:

m[i,0] = m[0,j] = 0
m[i,j] = m[i-1,j] ,j < wi
m[i,j] = max(m[i-1,j-wi]+vi, m[i-1,j]) ,j >= wi

其中,m[i,j]为前 i 件物品中选择若干件,放入承重为 j 的背包中,得到的最大的价值。

wi 为第 i 件商品的重量。

vi 为第 i 件商品的价值。

例题讲解

有编号为 a,b,c,d,e 的五件物品,他们的重量分别为 4,5,6,2,2,价值分别为 6,4,5,3,6,现在给你一个承重为 10 的背包,怎么实现价值最大。

根据上述公式可以得到一个数据表,表从上向下生成:

nameweightvalue12345678910
a460006666666
b54000666661010
c65000666661011
d23033669991011
e26066991212151515

故可以根据公式码出如下实现代码:

    public static void main(String[] args) {

        int n = 5;// 5件物品,物品编号为a,b,c,d,e(下面为多加一件物品,第一个物品为虚拟的物品)
        int weight[] = { 0, 4, 5, 6, 2, 2 };// 物品的重量
        int value[] = { 0, 6, 4, 5, 3, 6 }; // 对应物品的价值
        int c = 10; // 背包容量
        int state[] = { 0, 0, 0, 0, 0, 0 };// 开始状态
        char name[] = { ' ', 'a', 'b', 'c', 'd', 'e' };
        int maxValue = getMaxValue(n, weight, value, state, c);
        System.out.println("最大价值为 = " + maxValue);
        System.out.print("放入的物品为 :");
        for (int i = 1; i <= 5; i++) {
            if (state[i] == 1) {
                System.out.print(name[i] + "  ");
            }
        }

        // System.out.println();
    }

    /**
     * 
     * @param n
     *            物品数量
     * @param weight
     *            物品对应重量(数组下标从0开始,故第一个物品为虚拟物品)
     * @param value
     *            物品对应价值(数组下标从0开始,故第一个物品为虚拟物品)
     * @param state
     *            物品的开始状态
     * @param c
     *            背包的容量
     * @return
     */
    public static int getMaxValue(int n, int weight[], int value[], int state[], int c) {
        // n 为物品的数量,数组时需要加 1,此时可以从 0,1,...n 个物品,共 n+1 个商品,其中第 0 个为虚构物品
        // 对于物品的价值,可以写成 2 维数组
        int m[][] = new int[n + 1][c + 1]; // n 为 0,1,2...(n-1),背包重量为 0,1,2...C
        int i, j;

        for (i = 0; i <= n; i++) {
            m[i][0] = 0;
        }
        for (j = 0; j <= c; j++) {
            m[0][j] = 0;
        }

        for (i = 1; i <= n; i++) {
            // System.out.println();
            for (j = 1; j <= c; j++) {
                if (j < weight[i]) { // 新的物品太重,无法放下
                    m[i][j] = m[i - 1][j];
                } else {// 分为放和不放 取较大值
                    m[i][j] = Math.max(m[i - 1][j - weight[i]] + value[i], m[i - 1][j]);
                }
                // System.out.print("m["+i+"]["+j+"]="+m[i][j]+" ");
                // System.out.print(m[i][j]+" ");
            }
        }

        // 根据其最大价值,反向推断是否添加了物品 i

        j = c;
        for (i = n; i > 0; i--) {
            if (m[i][j] > m[i - 1][j]) {// 物品 i 添加到了序列列表
                state[i] = 1;
                j = j - weight[i];
            } else { // 没有添加
                state[i] = 0;
            }
        }

        return m[n][c]; // 最大价值
    }

输出结果为:

最大价值为 = 15
放入的物品为 :a  d  e  

具体实现看上述代码即可,注释齐全,简单易懂。

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值