LeetCode-动态规划-背包问题

1 背包问题就是动态规划

        动态规划介绍:算法-动态规划-《算法导论3rd-P215》_hclbeloved的博客-CSDN博客

        背包问题是一种动态规划的问题,且常用的是“从底到上”的动态规划。有的时候问题本身可能无法直接看出是背包问题,不过可以对问题进行抽象,底层逻辑如果相同,便可当作“背包问题”进行求解。

        写在前面,参考链接:力扣

1.1 按照背包的类型分类:

        0-1背包:每个元素最多被选择一次;求解的常是最值、选择个数、选择存在问题。

        完全背包:每个元素可以被重复选择;求解的常是最值问题。

        组合背包:每个元素可以被重复选择,且考虑元素的相对顺序,求解的是组合的数量;

        排列背包:每个元素可以被重复选择,但需要考虑元素的相对顺序,求解的是排列的数量;

1.2 按照背包所求解的问题的类型进行分类:

        (1)求解最值(最大价值;最少零钱数目;)

        (2)选择存在问题(等和子集是否存在;)

        (3)排列组合问题(排列、组合的数量)

 1.3 根据背包类型,选择循环模板;根据问题类型,选择dp类型

1.3.1 循环模板的选择        

        (1)0-1背包:416. 分割等和子集

        二维dp:外循环nums,内循环target,target正序且target>=nums[i];

        一维dp:外循环nums,内循环target,target倒序且target>=nums[i];

        (2)完全背包:322. 零钱兑换279. 完全平方数

         一维dp:外循环nums,内循环target,target正序且target>=nums[i];

        (3)组合背包(不考虑顺序):518. 零钱兑换 II

        一维dp:外循环nums,内循环target,target正序且target>=nums[i];

        注意:求解模板与完全背包相同的原因是二者都可以重复选择元素。

        (4)排列背包(考虑顺序):377. 组合总和 Ⅳ

         一维dp:外循环target内循环nums,target正序且target>=nums[i];

1.3.2 dp类型

        (1)求解最值(最大价值;最少零钱数目;)

        dp[i] = max/min(dp[i], dp[i-nums]+1)或dp[i] = max/min(dp[i], dp[i-num]+nums)

        (2)存在问题(等和子集是否存在;)

        dp[i]=dp[i] | dp[i-num]

        (3)排列、组合问题(排列、组合的数量)

        dp[i]+=dp[i-num]

2 0-1背包

        题目:背包所能容纳的最大价值

        有两个数组weights, values分别保存一些物品的重量和价值,现有一个容量为weight的背包,请设计一个方法来选择合适的物品放入背包,使其内部的价值最大。

2.1 从底到上的二维动态规划

        分析:

2.1.1 状态定义

        dp[i][j]:表示对于前i个物品,当背包的容量为j时,能装下的最大的价值;

2.1.2 base case:

        dp[...][0]表示当背包的容量为0时,能装下的最大价值为0;

        dp[0][...]表示当没有物品选择时,能装下的最大价值为0;

2.1.3 状态转移方程

        现在要求解dp[i][j]:前i个物品,背包容量为j时,能装下的最大价值。

        注意:第 i 个物品对应的索引是 i - 1。

        这里分为两种情况:

        第一:第 i 个物品的重量大于当前背包的容量 j,即 weights[i-1] > j,此时无法将第 i 个物品放入背包。即:dp[i][j] = dp[i-1][j];

        第二:第 i 个物品的重量小于等于当前背包的容量 j,即 weights[i-1] <= j,此时有可以将第 i 个物品放入背包,也可以不放入,即:dp[i][j] = max(dp[i-1][j], values[i-1] + dp[i-1][j - weights[i-1]])。

2.1.4 代码实现

        注意:内层 for 循环使用的是从“当前元素的重量正序“背包的目标weight”。

int knapsack(vector<int>& weights, vector<int>& values, int weight) 
{
	int n = weights.size();
	// dp[i][j]:前i个物品,背包容量为j时,能装下的最大价值
	// base case 已初始化
	vector<vector<int>> dp(n+1, vector<int>(weight+1, 0));

	for (int i = 1; i <= n; ++i) 
	{
	    // weight 为背包的目标容量
		for (int j = 1; j <= weight; ++j) 
		{
			// 第 i 个物品对应的索引是 i - 1
			if (j < weights[i-1]) 
			{
				//背包的当前容量 j 小于 当前物品的重量,无法装入当前的背包
				dp[i][j] = dp[i-1][j];
			} 
			else 
			{
				// 装⼊或者不装⼊背包,择优
				dp[i][j] = max(dp[i-1][j], values[i-1] + dp[i-1][j - weights[i-1]]);
			}
		}
	}

	return dp[n][weight];
}

2.2 从底到上的一维动态规划

        由上面的二维动态规划可以知道,在计算dp[i][j]时,实际上只和dp[i-1][j]以及dp[i-1][j - weights[i-1]]相关,即仅与上一层(i-1)层相关。

        注意:内层 for 循环使用的是从“背包的目标weight”逆序当前元素的重量”。

int knapsack(vector<int>& weights, vector<int>& values, int weight) 
{
	// dp[i]:背包容量为j时,能装下的最大价值
	// base case 已初始化
	vector<int> dp(weight+1, 0);

	// 物品的索引从0开始
	for (int i = 0; i < weights.size(); ++i) 
	{
	    // weight 为背包的目标容量
	    // j >= weights[i] 保证背包的当前容量 j 大于等于 当前物品的重量
	    // 采用逆序保证 dp[j - weights[i]] 是在第 i-1 层所求得的值
		for (int j = weight; j >= weights[i]; --j) 
		{
			// 装⼊或者不装⼊背包,择优
			dp[j] = max(dp[j], values[i] + dp[j - weights[i]]);
		}
	}

	return dp[weight];
}

2.3 0-1背包题目

2.3.1 分割等和子集

剑指 Offer II 101. 分割等和子集 416. 分割等和子集

        代码如下:

class Solution {
public:
    //0-1背包的一维dp解决方法 
    bool canPartition(vector<int>& nums) {
        int n = nums.size(), m = 0;
        for (int x: nums) 
            m += x;
        if (m % 2) 
            return false;
        //目标和 m 为背包的目标容量; nums中的值为物品的重量; 物品的价值在这里并未体现
        m /= 2;
        //dp[j]表示是否可以组成j的和
        vector<bool> dp(m + 1);
        dp[0] = true;
        for (int i = 0; i < n; ++i)
        {
            for (int j = m; j >= nums[i]; --j)
            {
                dp[j] = dp[j] | dp[j - nums[i]];
            }
        }


        return dp[m]; 
    }
};

2.3.2 目标和

494. 目标和

class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for (int n : nums)
        {
            sum += n;
        }            

        if (target > sum || target < -sum)
            return 0;
        
        // 目标和 m 为背包的目标容量; nums中的值为物品的重量; 物品的价值在这里并未体现
        // 假设选出几个数使其变为负数,选出的这几个数的和假设为 x,则有
        // (sum-x)-x=target, sum-target = 2x
        sum -= target;
        if ((sum & 0x01) == 1)
            return 0;

        sum /= 2;
        //dp[j]表示是否可以组成j的和的数量
        vector<int> dp(sum+1, 0);
        dp[0] = 1;
        for (int n : nums)
        {
            for (int j = sum; j >= n; --j)
            {
                dp[j] += dp[j-n];
            }
        }

        return dp[sum]; 
    }
};

2.3.3 最后一块石头的重量II

1049. 最后一块石头的重量 II

class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        /*
        最后一块石头的重量:从一堆石头中,每次拿两块重量分别为x,y的石头,若x=y,则两块石头均粉碎;若x<y,两块石头变为一块重量为y-x的石头求最后剩下石头的最小重量(若没有剩下返回0)
        问题转化为:把一堆石头分成两堆,求两堆石头重量差最小值
        进一步分析:要让差值小,两堆石头的重量都要接近sum/2;我们假设两堆分别为A,B,A<sum/2,B>sum/2,若A更接近sum/2,B也相应更接近sum/2
        进一步转化:将一堆stone放进最大容量为sum/2的背包,求放进去的石头的最大重量MaxWeight,最终答案即为sum-2*MaxWeight。

        至此转化为了 0-1 背包问题: stones 中的数据既代表了重量,又代表了价值。背包的目标容量为 sum/2 。
        其实这个题和《目标和 https://leetcode-cn.com/problems/target-sum/》很相似,不过本题比“目标和”的题目更近了一步,本题相当于“目标和”的加强版,
        加强的部分在于使得“目标和”尽可能的小。
        
        链接:https://leetcode-cn.com/problems/partition-equal-subset-sum/solution/yi-pian-wen-zhang-chi-tou-bei-bao-wen-ti-a7dd/      
        */
        int sum = accumulate(stones.begin(), stones.end(), 0);
        int target = sum / 2;
        vector<int> dp(target + 1);
        
        for (int stone : stones)
            for (int i = target; i >= stone; i--)
                dp[i] = max(dp[i], dp[i - stone] + stone);
                
        return sum - 2 * dp[target];
    }
};

3 完全背包

        完全背包使用的也是“从底向上”的动态规划,且基本上都是一维的动态规划,这里与0-1背包不同的是base case是初始化为0的,比如下面的dp[0] = 0;对比上面的0-1背包,常常对base case初始化为1或者true。

        注意:完全背包的内层 for 循环使用的是从“当前元素的重量正序“背包的目标weight”。

3.1 完全背包题目

3.1.1 零钱兑换

        题目:322. 零钱兑换       

        代码实现具体如下:

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        //用完全背包的解决方法
        //amount为背包的目标容量
        //coins中的值为重量,价值就是选择这一个硬币时硬币的数量,即:选择这个硬币时,价值为 1
		vector<int> dp(amount+1,amount+1);
		dp[0]=0;
        for (auto& coin : coins)
        {
            for(int i=1;i<=amount;i++)
            {
                //当前背包的容量如果大于coin,则可以选择放入背包,也可以选择不放入,进行比较后,选出最小值
                if(i>=coin) 
                    dp[i] = min(dp[i], 1 + dp[i-coin]);
            }
        }

		return dp[amount]==amount+1?-1:dp[amount];
    }
};

3.1.2 完全平方数

279. 完全平方数

class Solution {
public:
    /* 完全背包的最值问题
    * 完全平方数最小为1,最大为sqrt(n),故题目转换为在nums=[1,2.....sqrt(n)]中选任意数平方和为target=n
    * nums中的值为重量,价值就是选择这一个数字时的数量,即:选择这个数字时,价值为 1
    * 等价题目:https://leetcode-cn.com/problems/coin-change/
    */
    int numSquares(int n) {
        vector<int> dp(n + 1, INT_MAX); //dp[i]:和为i的完全平方数的最小数量
        dp[0] = 0;
        for (int num = 1; num <= sqrt(n); num++)
        {
            for (int i = 0; i <= n; i++)
            {
                if (i >= num * num)
                    dp[i] = min(dp[i], 1 + dp[i - num * num]);
            }
        }
        return dp[n];
    }
};

4 组合背包

外循环nums,内循环target,target正序且target>=nums[i];

dp[i]+=dp[i-num]

518. 零钱兑换 II

class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount+1,0);
        //base case
        dp[0] = 1;
        
        for (int coin : coins) 
        {
            // 记录每添加一种面额的零钱,总金额j的变化
            for (int j = coin; j <= amount; j++) 
            {
                // 在上一钟零钱状态的基础上增大
                // 例如对于总额5,当只有面额为1的零钱时,只有一种可能 5x1
                // 当加了面额为2的零钱时,除了原来的那一种可能外
                // 还加上了组合了两块钱的情况,而总额为5是在总额为3的基础上加上两块钱来的
                // 所以就加上此时总额为3的所有组合情况
                dp[j] += dp[j - coin];
            }
        }
        return dp[amount];
    }
};

5 排列背包

一维dp:外循环target内循环nums,target正序且target>=nums[i];

dp[i]+=dp[i-num]

377. 组合总和 Ⅳ

class Solution {
public:
    //与518. 零钱兑换 i 有些相似:https://leetcode-cn.com/problems/coin-change-2/
    // 零钱兑换 i 不考虑组合中元素的顺序,是组合问题;此处考虑了元素的相对顺序,是排列问题

    /*
    这道题中,给定数组 nums 和目标值 target,要求计算从 nums 中选取若干个元素,使得它们的和等于 target 的方案数。
    其中,nums 的每个元素可以选取多次,且需要考虑选取元素的顺序。由于需要考虑选取元素的顺序,因此这道题需要计算的是选取元素的排列数。

    可以通过动态规划的方法计算可能的方案数。用 dp[x] 表示选取的元素之和等于 x 的方案数,目标是求 dp[target]。
    动态规划的边界是 dp[0]=1。只有当不选取任何元素时,元素之和才为 0,因此只有 1 种方案。

    当 1≤i≤target 时,如果存在一种排列,其中的元素之和等于 i,则该排列的最后一个元素一定是数组 nums 中的一个元素。
    假设该排列的最后一个元素是 num,则一定有 num≤i,对于元素之和等于 i−num 的每一种排列,在最后添加 num 之后即可得到一个元素之和等于 i 的排列,
    因此在计算 dp[i] 时,应该计算所有的 dp[i−num] 之和。

    由此可以得到动态规划的做法:
    初始化 dp[0]=1;
    遍历 i 从 1 到 target,对于每个 i,进行如下操作:
    遍历数组 nums 中的每个元素 num,当 num≤i 时,将 dp[i−num] 的值加到 dp[i]。
    最终得到 dp[target] 的值即为答案。

    上述做法是否考虑到选取元素的顺序?答案是肯定的。因为外层循环是遍历从 1 到 target 的值,内层循环是遍历数组 nums 的值,
    在计算 dp[i] 的值时,nums 中的每个小于等于 i 的元素都可能作为元素之和等于 i 的排列的最后一个元素。例如,1 和 3 都在数组 nums 中,
    计算 dp[4] 的时候,排列的最后一个元素可以是 1 也可以是 3,因此 dp[1] 和 dp[3] 都会被考虑到,即不同的顺序都会被考虑到。


    参考链接:https://leetcode-cn.com/problems/combination-sum-iv/solution/zu-he-zong-he-iv-by-leetcode-solution-q8zv/
    */
    int combinationSum4(vector<int>& nums, int target) {
        //从底到上的动态规划
        vector<unsigned int> dp(target+1);
        dp[0] = 1;
        for (int i = 1; i <= target; i++)
        {
            for (int num : nums)
            {
                if (num <= i) 
                    dp[i] += dp[i - num];
            }
        }
        return dp[target];
    }
};

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值