zoukankan      html  css  js  c++  java
  • 动态规划背包问题

    1.0-1背包问题
    给你一个可装载重量为W的背包和N个物品,每个物品有重量和价值两个属性。其中第i个物品的重量为wt[i],价值为val[i],现在让你用这个背包装物品,最多能装的价值是多少?

    举个简单的例子,输入如下:

    N = 3, W = 4
    wt = [2, 1, 3]
    val = [4, 2, 3]
    

    算法返回 6,选择前两件物品装进背包,总重量 3 小于W,可以获得最大价值 6。

    这个题目中的物品不可以分割,要么装进包里,要么不装,不能说切成两块装一半。这也许就是 0-1 背包这个名词的来历。解决这个问题没有什么排序之类巧妙的方法,只能穷举所有可能,按照套路,直接走流程就行了。

    动规标准套路:
    1.想明白状态有哪些,选择是什么:
    状态,如何才能描述一个问题局面?只要给定几个可选物品和一个背包的容量限制,就形成了一个背包问题,对不对?所以状态有两个,就是「背包的容量」和「可选择的物品」。再说选择,也很容易想到啊,对于每件物品,你能选择什么?选择就是「装进背包」或者「不装进背包」嘛
    题目中出现了 载重量w ,物品n ,每个物品有重量和价值,题目问的是,在一定载重量w的限定下, 最多能装的价值是多少。动态规划问题都是求最值的问题,这里求的是价值的最值,也就是说动态规划的结果就是价值的最值,再换句话说,就是dp[][]的含义一定是价值,那么剩下的就是限重w和可选择的物品,这也就是状态,当然对于每个物品的选择都是 装 or 不装进,而动态规划之中对于不同的选择就是之间穷举取极值。

    在明确了状态和选择之后,动态规划的基本套路就是:

    for 状态1 in 状态1的所有取值:
        for 状态2 in 状态2的所有取值:
            for ...
                dp[状态1][状态2][...] = 择优(选择1,选择2...)
    

    2.明确dp数组的定义:
    dp数组是什么?其实就是描述问题局面的一个数组。换句话说,我们刚才明确问题有什么「状态」,现在需要用dp数组把状态表示出来。首先看看刚才找到的「状态」,有两个,也就是说我们需要一个二维dp数组,一维表示可选择的物品,一维表示背包的容量。
    dp[i][w]的定义如下:对于前i个物品,当前背包的容量为w,这种情况下可以装的最大价值是dp[i][w]比如说,如果 dp[3][5] = 6,其含义为:对于给定的一系列物品中,若只对前 3 个物品进行选择,当背包容量为 5 时,最多可以装下的价值为 6。

    PS:为什么要这么定义?便于状态转移,或者说这就是套路,记下来就行了。
    根据这个定义,我们想求的最终答案就是****dp[N][W]。base case 就是dp[0][..] = dp[..][0] = 0,因为没有物品或者背包没有空间的时候,能装的最大价值就是 0。

    int dp[N+1][W+1]
    dp[0][..] = 0
    dp[..][0] = 0
    
    for i in [1..N]:
        for w in [1..W]:
            dp[i][w] = max(
                把物品 i 装进背包,
                不把物品 i 装进背包
            )
    return dp[N][W]
    

    3.确定状态转移方程:
    简单说就是,上面伪码中「把物品i装进背包」和「不把物品i装进背包」怎么用代码体现出来呢?这一步要结合对dp数组的定义和我们的算法逻辑来分析:

    dp[i][w]表示:对于前i个物品,当前背包的容量为w时,这种情况下可以装下的最大价值是dp[i][w]

    如果你没有把这第i个物品装入背包,那么很显然,最大价值dp[i][w]应该等于dp[i-1][w]。你不装嘛,那就继承之前的结果。

    如果你把这第i个物品装入了背包,那么dp[i][w]应该等于dp[i-1][w-wt[i-1]] + val[i-1]

    首先,由于i是从 1 开始的,所以对valwt的取值是i-1

    dp[i-1][w-wt[i-1]]也很好理解:你如果想装第i个物品,你怎么计算这时候的最大价值?换句话说,在装第****i个物品的前提下,背包能装的最大价值是多少?

    显然,你应该寻求剩余重量w-wt[i-1]限制下能装的最大价值,加上第i个物品的价值val[i-1],这就是装第i个物品的前提下,背包可以装的最大价值。

    综上就是两种选择,我们都已经分析完毕,也就是写出来了状态转移方程,可以进一步细化代码:

    for i in [1..N]:
        for w in [1..W]:
            dp[i][w] = max(
                dp[i-1][w],
                dp[i-1][w - wt[i-1]] + val[i-1]
            )
    return dp[N][W]
    

    最后一步****,把伪码翻译成代码,处理一些边界情况

    int knapsack(int W, int N, vector<int>& wt, vector<int>& val) {
        // vector 全填入 0,base case 已初始化
        vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
        for (int i = 1; i <= N; i++) {
            for (int w = 1; w <= W; w++) {
                if (w - wt[i-1] < 0) {
                    // 当前背包容量装不下,只能选择不装入背包
                    dp[i][w] = dp[i - 1][w];
                } else {
                    // 装入或者不装入背包,择优
                    dp[i][w] = max(dp[i - 1][w - wt[i-1]] + val[i-1], 
                                   dp[i - 1][w]);
                }
            }
        }
    
        return dp[N][W];
    }
    
    

    2.0-1背包问题的变体:
    给定一个只包含正整数非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

    首先回忆一下背包问题大致的描述是什么:

    给你一个可装载重量为W的背包和N个物品,每个物品有重量和价值两个属性。其中第i个物品的重量为wt[i],价值为val[i],现在让你用这个背包装物品,最多能装的价值是多少?

    那么对于这个问题,我们可以先对集合求和,得出sum,把问题转化为背包问题:

    给一个可装载重量为sum/2的背包和N个物品,每个物品的重量为nums[i]。现在让你装物品,是否存在一种装法,能够恰好将背包装满

    第一步要明确两点,「状态」和「选择」。状态就是「背包的容量」和「可选择的物品」,选择就是「装进背包」或者「不装进背包」。
    第二步要明确dp数组的定义dp[i][j] = x表示,对于前i个物品,当前背包的容量为j时,若xtrue,则说明可以恰好将背包装满,若xfalse,则说明不能恰好将背包装满。比如说,如果dp[4][9] = true,其含义为:对于容量为 9 的背包,若只是用前 4 个物品,可以有一种方法把背包恰好装满。或者说对于本题,含义是对于给定的集合中,若只对前 4 个数字进行选择,存在一个子集的和可以恰好凑出 9。

    根据这个定义,我们想求的最终答案就是dp[N][sum/2],base case 就是dp[..][0] = truedp[0][..] = false,因为背包没有空间的时候,就相当于装满了,而当没有物品可选择的时候,肯定没办法装满背包。

    第三步,根据「选择」,思考状态转移的逻辑。如果不把nums[i]算入子集,或者说你不把这第i个物品装入背包,那么是否能够恰好装满背包,取决于上一个状态dp[i-1][j],继承之前的结果。如果把nums[i]算入子集,或者说你把这第i个物品装入了背包,那么是否能够恰好装满背包,取决于状态dp[i - 1][j-nums[i-1]]。首先,由于i是从 1 开始的,而数组索引是从 0 开始的,所以第i个物品的重量应该是nums[i-1],这一点不要搞混。dp[i - 1][j-nums[i-1]]也很好理解:你如果装了第i个物品,就要看背包的剩余重量j - nums[i-1]限制下是否能够被恰好装满。换句话说,如果j - nums[i-1]的重量可以被恰好装满,那么只要把第i个物品装进去,也可恰好装满j的重量;否则的话,重量j肯定是装不满的。

    class Solution {
        public boolean canPartition(int[] nums) {
            int sum=0;
            for(int i=0;i<nums.length;i++){
                sum=sum+nums[i];
            }
            if(sum%2!=0){return false;}
            int target=sum/2;
    
            boolean[][] dp=new boolean[nums.length+1][target+1];
    
            for (int i = 0; i <= nums.length; i++){
                dp[i][0] = true;
            }
            
            for(int i=1;i<=nums.length;i++){
                for(int j=1;j<=target;j++){
                    if(j-nums[i-1]<0){
                        dp[i][j]=dp[i-1][j];
                    }else{
                        dp[i][j]=dp[i-1][j] | dp[i-1][j-nums[i-1]];
                    }
                }
            }
            return dp[nums.length][target];
    
        }
    }
    

    3.完全背包问题:
    leet518. 零钱兑换问题:

    有一个背包,最大容量为amount,有一系列物品coins,每个物品的重量为coins[i]每个物品的数量无限。请问有多少种方法,能够把背包恰好装满?

    这个问题和我们前面讲过的两个背包问题,有一个最大的区别就是,每个物品的数量是无限的,这也就是传说中的「完全背包问题」,没啥高大上的,无非就是状态转移方程有一点变化而已。

    第一步要明确两点,「状态」和「选择」。这部分都是背包问题的老套路了,我还是啰嗦一下吧:状态有两个,就是「背包的容量」和「可选择的物品」,选择就是「装进背包」或者「不装进背包」。

    for 状态1 in 状态1的所有取值:
        for 状态2 in 状态2的所有取值:
            for ...
                dp[状态1][状态2][...] = 计算(选择1,选择2...)
    

    第二步要明确dp数组的定义。首先看看刚才找到的「状态」,有两个,也就是说我们需要一个二维dp数组。

    dp[i][j]的定义如下:若只使用前i个物品,当背包容量为j时,有dp[i][j]种方法可以装满背包。换句话说,翻译回我们题目的意思就是:若只使用coins中的前i个硬币的面值,若想凑出金额j,有dp[i][j]种凑法

    base case 为dp[0][..] = 0, dp[..][0] = 1。因为如果不使用任何硬币面值,就无法凑出任何金额;如果凑出的目标金额为 0,那么“无为而治”就是唯一的一种凑法。最终想得到的答案就是dp[N][amount],其中Ncoins数组的大小.

    第三步,根据「选择」,思考状态转移的逻辑。注意,我们这个问题的特殊点在于物品的数量是无限的,所以这里和之前写的背包问题文章有所不同。如果你不把这第i个物品装入背包,也就是说你不使用coins[i]这个面值的硬币,那么凑出面额j的方法数dp[i][j]应该等于dp[i-1][j],继承之前的结果。如果你把这第i个物品装入了背包,也就是说你使用coins[i]这个面值的硬币,那么dp[i][j]应该等于dp[i][j-coins[i-1]]

    首先由于i是从 1 开始的,所以coins的索引是i-1时表示第i个硬币的面值。dp[i][j-coins[i-1]]也不难理解,如果你决定使用这个面值的硬币,那么就应该关注如何凑出金额j - coins[i-1]。比如说,你想用面值为 2 的硬币凑出金额 5,那么如果你知道了凑出金额 3 的方法,再加上一枚面额为 2 的硬币,不就可以凑出 5 了嘛。综上就是两种选择,而我们想求的dp[i][j]是「共有多少种凑法」,所以dp[i][j]的值应该是以上两种选择的结果之和

    class Solution {
        public int change(int amount, int[] coins) {
            int[][] dp=new int[coins.length+1][amount+1];
            for(int i=0;i<=coins.length;i++){
                dp[i][0]=1;
            }
    
            for(int i=1;i<=coins.length;i++){
                for(int j=1;j<=amount;j++){
                    if(j-coins[i-1]<0){
                        dp[i][j]=dp[i-1][j];
                    }else{
                        dp[i][j]=dp[i-1][j]+dp[i][j-coins[i-1]];
                    }
                }
            }
    
            return dp[coins.length][amount];
        }
    }
    
  • 相关阅读:
    小程序-地图-定位在我此时的位置
    小程序-地图-标注点固定在某一点
    小程序-列表块/类式ul-li格式(1)
    表单form-input标签禁止聚焦输入
    forEach与jdk8中的lambda, Stream
    put get & push pull
    事务背书 ACID, CAP, BASE
    懒汉式单例要加volatile吗
    SpringMVC DeferedResult和servlet3.1 AsyncContext异步请求
    netty在rpc MQ中的应用
  • 原文地址:https://www.cnblogs.com/shiji-note/p/14459958.html
Copyright © 2011-2022 走看看