leetcode背包问题相关题目详解


前言

本文主要针对代码随想录中的动态规划问题进行分析,详细分析代码运行结果以及解题思路,若有错误请指正。具体内容会逐步更新。
参考链接: link


1、494目标和

题目如下:
给定一个非负整数数组,a1, a2, …, an, 和一个目标数S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
示例:
输入:nums: [1, 1, 1, 1, 1], S: 3
输出:5

思路分析

本题可以转化为01背包问题进行求解,但是有一些变化,对于01背包问题我也进行了详细的思路分析,具体链接为:link ,希望有所帮助。

本题主要思想就是添加符号使得表达式结果为目标值,但是可以稍作转换,具体思路如下:

首先有一个数组nums,以及一个目标值S。
对nums里的值添加‘+’与‘-’号,相当于把nums分为两部分,用一部分减去另一部分使得结果为S。
我们把这两部分记作left组合与right组合。
因此left组合与right组合满足sum(left)-sum(right)=S。
题目的目标就是在nums中找到这样的left组合与right组合。
同时left组合与right组合满足sum(left)+sum(right)=sum(nums)。
联立上述两个式子便可得到sum(left)=(S+sum(nums))/2。
其中S与sum(nums)都是已知的。
因此我们的目标就是找到和为target的left组合,其中target=(S+sum(nums))/2。
所以可以将其转化为01背包问题,其中物品重量和物品价值均为nums,题目要求的就是装满容量为target的书包有多少种方法。

根据上述分析,本题目标就是装满容量为target的书包有多少种方法,这与01背包不同,01背包是求装满背包的最大价值,但是二者在在思路上是一致的,只是递推关系式略有不同。

本题中的 dp[j] 为装满背包容量为j有多少种方法,下面分析它的递推关系式。
为了便于理解,先分析二维dp数组的求解过程,dp[i][j] 表示从物品0到物品 i 中选物品放满容量为j的背包的方法种数。可以分为不放物品i与放物品i。即不放物品i,从物品0到物品i-1中取物品放满容量为j的背包的方法种数dp[i-1][j] 与放物品i时,从物品0到物品i-1中取物品放满容量为j-nums[j]的背包的方法种数dp[i-1][j-nums[i]]之和。递推关系式如下:

dp[i][j]=dp[i-1][j]+dp[i-1][j-nums[i]]

转换为一维dp数组后,根据滚动数组的思想就变成了:

dp[j]=dp[j]+dp[j-nums[i]]
即dp[j]+=dp[j-nums[i]]

根据01背包的相关知识可知,上述递推关系式在代码中只能先遍历物品,再遍历背包,同时背包只能倒序遍历,具体原因请看另一篇介绍01背包思路的文章 link。文中详细介绍了原因。

代码及运行结果分析

根据递推关系式写出如下代码,为了便于理解,我将每次遍历后的dp数组打印出来:

#include<iostream>
#include<vector>
#include <iomanip>
using namespace std;

void print_dparr(vector<int>& dp)
{
		int col = dp.size();
        for (int j = 0;j < col;j++)
        {
            cout << std::left << setw(2) << dp[j] << " ";
        }
    cout << endl;
    cout << endl;
}

class solution
{
public:
	int getnum(vector<int>& nums,int s)
	{
		int sum = 0;
		for (int i = 0;i < nums.size();i++) 
			sum += nums[i];
		if ((s + sum) % 2 != 0) return 0;
		if (abs(s) > sum) return 0;
		int target = (s + sum) / 2;
		vector<int> dp(target + 1);
		dp[0] = 1;
		for (int i = 0;i < nums.size();i++)
		{
			for (int j = target;j >= nums[i];j--)
			{
				dp[j] += dp[j - nums[i]];
			}
			cout << "从物品0到物品" << i << "取物品放到背包中,方法总数:" << endl;
			print_dparr(dp);
		}
		return dp[target];
	}
};

int main()
{
	vector<int> nums = { 1,2,3,2,1 };
	int s = 3;
	solution s1;
	cout << s1.getnum(nums, s) << endl;
	return 0;
}

代码运行结果如下:

在这里插入图片描述

假设nums=[1,1,1,1,1],S=3时,代码运行结果如下:

在这里插入图片描述

读者可以将每次的运行结果与递推关系式相结合进行分析,便可以很好的理解本题的解题思路。

2、474一和零

题目:
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
示例 1:
输入:strs = [“10”, “0001”, “111001”, “1”, “0”], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {“10”,“0001”,“1”,“0”} ,因此答案是 4 。 其他满足题意但较小的子集包括 {“0001”,“1”} 和 {“10”,“1”,“0”} 。{“111001”} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。

思路分析

刚开始没有理解透彻01背包的原理时,对本题的解法还不太会,当熟悉01背包的思路之后就比较得心应手了。

本题最重要的一点就是物品的重量是二维的,接下来我会详细分析本题解题思路。

本题的目标为:给你一个二进制字符串数组 strs 和两个整数 m 和 n 。请你找出并返回 strs 的最大子集的大小,该子集中最多 有 m 个 0 和 n 个 1 。
其中strs数组表示物品,0和1的个数表示物品重量,由于返回的是最大子集的大小,因此可以将物品价值看作1。
以数组strs = [“10”, “0001”, “111001”, “1”, “0”]为例,我们可以得到它的物品重量和物品价值对应的表格如下:

重量weight(m,n)价值value
物品0 (10)1,11
物品1(0001)3,11
物品2(111001)2,41
物品3(1)0,11
物品4(0)1,01

当m=5,n=3时,我们要求的就是装满背包容量为(5,3)时的最大价值,这样就把题目转换成为了01背包问题。只是这次的物品重量变成了二维,相比于普通的01背包循环会多一层。下面确定递推关系式。

为了便于理解,我还是从二维dp数组(其实此时应该是三维了,为了便于理解我还是看作二维,请不要和之后的dp数组搞混)出发,dp[i][m,n] 表示从物品0到物品i中取物品放到背包容量为(m,n)的背包中的所得到的最大价值。这和普通的01背包问题思路一致,可以得到递推关系式如下:

dp[i][m,n] = max(dp[i - 1][m,n], dp[i - 1][m-weight_i_0,n-weight_i_1] + value[i]);
其中weight_i_0表示第i个物品中0的个数
其中weight_i_1表示第i个物品中1的个数
value[i]=1

对应的一维dp数组就可以很容易得到了。

dp[m,n] = max(dp[m,n], dp[m-weight_i_0,n-weight_i_1] + value[i]);
虽然我把他叫做一维dp数组,但是实际上是二维的,将它变成二维形式:
dp[m][n] = max(dp[m][n], dp[m-weight_i_0][n-weight_i_1] + value[i]);
其中weight_i_0表示第i个物品中0的个数
其中weight_i_1表示第i个物品中1的个数
value[i]=1

到了这一步其实已经和普通的01背包问题没有任何区别了,只需要注意要先遍历物品,再遍历背包,并且对背包的循环要多加一层,同时要倒序遍历,具体代码如下:

#include<iostream>
#include<vector>
#include <iomanip>
using namespace std;

void print_dparr(vector<vector<int>>& dp)
{
    int row = dp.size();
    int col = dp[0].size();
  
    for (int i = 0;i < row;i++)
    {
        for (int j = 0;j < col;j++)
        {
            cout <<std::left << setw(2) << dp[i][j] << " ";
        }
        cout << endl;
    }
    cout << endl;
}

class Solution {
public:
	int findMaxForm(vector<string>& strs, int m, int n) {
		vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
		int count = 0;
		for (auto str : strs)
		{
			int zerosNum = 0;
			int oneNum = 0;
			for (int i = 0;i < str.size();i++)
			{
				if (str[i] == '0') zerosNum++;
				else oneNum++;
			}
			for (int j = m;j >= zerosNum;j--)
			{
				for (int k = n;k >= oneNum;k--)
				{
					dp[j][k] = max(dp[j][k], dp[j - zerosNum][k - oneNum]+1);
				}
			}
			cout << "从物品0到物品" << count << "中取物品放到容量为"<<m<<","<<n << "的背包中的最大价值" << endl;
			count++;
			print_dparr(dp);
		}
		cout << "结果为:" << endl;
		return dp[m][n];
	}
};

int main()
{
	vector<string> strs = { "10", "0001", "111001", "1", "0" };
	Solution s;
	cout << s.findMaxForm(strs, 5, 3);
	return 0;
}

运行结果为:

在这里插入图片描述

以上就是全部解题思路,希望对读者理解本题有所帮助。

4、完全背包问题

有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。

思路分析

假设物品重量与价值如下:

重量weight价值value
物品0115
物品1320
物品2430

01背包和完全背包唯一不同就是体现在遍历顺序上,根据在01背包问题中的分析可知,背包遍历顺序为倒序时每个物品只能放入一次,如果背包遍历顺序为正序的话会导致同一个物品被放入多次,因此在完全背包问题中只需要将背包遍历顺序变成正序就行,但是此时需要注意01背包中只能先遍历物品,但是在完全背包问题中可以将物品遍历顺序与背包遍历顺序进行调换。

代码及运行结果分析

具体代码如下,首先考虑先遍历物品的情况:

#include<iostream>
#include<vector>
#include <iomanip>
using namespace std;

void print_dparr(vector<int>& dp)
{
	int col = dp.size();
	for (int j = 0;j < col;j++)
	{
		cout << std::left << setw(2) << dp[j] << " ";
	}
	cout << endl;
	cout << endl;
}
// 先遍历背包,再遍历物品
void test_CompletePack() {
	vector<int> weight = { 1, 3, 4 };
	vector<int> value = { 15, 20, 30 };
	int bagWeight = 4;
	vector<int> dp(bagWeight + 1, 0);

	for (int i = 0; i < weight.size(); i++) { // 遍历物品
		for (int j = 0; j <= bagWeight; j++) { // 遍历背包容量
			if (j - weight[i] >= 0) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
		}
		cout << "从物品0到物品" << i << "中取物品放入背包中的最大价值为" << endl;
		print_dparr(dp);
	}
	cout <<"结果为:"<< dp[bagWeight] << endl;
}
int main() {
	test_CompletePack();
}

运行结果为:

在这里插入图片描述

可以看到此时的结果为60,符合完全背包的理论,导致这种结果的原因在01背包问题中作者已经进行了阐述,在此再进行详细说明,首先我们分析第一次遍历的结果,即从物品0中取物品放入背包中的结果如下:

正序遍历的结果:
0 15 30 45 60
倒序遍历的结果:
0 15 15 15 15

从递推关系式出发:

初始化时dp[0] dp[1] dp[2]均为0,weight[0]=1,value[0]=15。
正序遍历的结果
dp[1] = dp[1 - weight[0]] + value[0] = dp[0] + 15 = 15
dp[2] = dp[2 - weight[0]] + value[0] = dp[1] + 15 = 30
dp[3] = dp[3 - weight[0]] + value[0] = dp[2] + 15 = 45
dp[4] = dp[4 - weight[0]] + value[0] = dp[3] + 15 = 60
正序遍历时计算 dp[2] 时把 dp[1]=15 加上去了,意味着物品0被放入了两次,计算 dp[3] 时把 dp[2]=30 加上去了,此时意味着
物品0放入了三次,其中dp[2]放入了两次,加上value[0]又放入了一次,故放入了3次,同理计算dp[4]将物品0放入了4次,当然这种情
况由于数据的原因导致总是重复放同一个物品,但是实际上也会有放入多次不同的物品的情况,读者只需要清楚在这种遍历顺序下会导致
同一物品被放入多次即可。

倒序遍历的结果
dp[4] = dp[4 - weight[0]] + value[0] = dp[3] + 15 = 15
dp[3] = dp[3 - weight[0]] + value[0] = dp[2] + 15 = 15
dp[2] = dp[2 - weight[0]] + value[0] = dp[1] + 15 = 15
dp[1] = dp[1 - weight[0]] + value[0] = dp[0] + 15 = 15
可以发现倒序遍历时每个物品只会被放入一次,因为每次计算中的dp[j-weight[0]]均为0

上述是基于先遍历物品的情况,接下来分析先遍历背包的情况,首先附上代码:

#include<iostream>
#include<vector>
#include <iomanip>
using namespace std;

void print_dparr(vector<int>& dp)
{
	int col = dp.size();
	for (int j = 0;j < col;j++)
	{
		cout << std::left << setw(2) << dp[j] << " ";
	}
	cout << endl;
	cout << endl;
}
// 先遍历背包,再遍历物品
void test_CompletePack() {
	vector<int> weight = { 1, 3, 4 };
	vector<int> value = { 15, 20, 30 };
	int bagWeight = 4;
	vector<int> dp(bagWeight + 1, 0);


	for (int j = 0; j <= bagWeight; j++) { // 遍历背包容量
		for (int i = 0; i < weight.size(); i++) { // 遍历物品
			if (j - weight[i] >= 0) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
		}
		cout << "从物品0到物品4中取物品放入背包容量为" << j << "时的最大价值" << endl;
		print_dparr(dp);
	}
	cout << "结果为:" << dp[bagWeight] << endl;
}
int main() {
	test_CompletePack();
}

运行结果为:

在这里插入图片描述

其实此时的计算过程就是按照下面的过程,与先遍历物品时的过程是一样的,因此得到的结果也是一致的。
dp[1] = dp[1 - weight[0]] + value[0] = dp[0] + 15 = 15
dp[2] = dp[2 - weight[0]] + value[0] = dp[1] + 15 = 30
dp[3] = dp[3 - weight[0]] + value[0] = dp[2] + 15 = 45
dp[4] = dp[4 - weight[0]] + value[0] = dp[3] + 15 = 60

可以看到此时的结果与先遍历物品时的结果一致,下面进行具体分析,为了便于理解,我把01背包对应的情况的代码运行结果也放在下面。

在这里插入图片描述

对比二者的结果可知,在01背包中由于背包遍历顺序是倒序的,这样就会导致每个物品只会放入一次,而在完全背包中把背包遍历顺序变成正序,此时根据递推公式可以知道dp[j] 是根据下标j之前所对应的dp[j]计算出来的,而先遍历物品和先遍历背包的都会先计算出前面的dp,再计算后面的dp,这样就会使得二者的结果是一样的,因此在完全背包问题中计算最大价值不管先遍历背包还是先遍历物品的结果都一样。

4、518零钱兑换

题目:
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 1:
输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

思路分析

注意此题为完全背包问题,完全背包问题即同一个物品可以被放进背包多次,根据之前01背包的分析可知,当背包遍历顺序为正序遍历时,会导致同一物品被放进背包多次,所以在完全背包问题中需要正序遍历背包。同时需要注意此时物品与背包的遍历顺序适应于不同的场景,下面会对这两种情况进行详细分析。

首先说结论,如果先遍历物品,那么最后结果求的是组合数,如果先遍历背包,那么最后求的是排列数,本题根据题目可知求的是组合数,下面进行具体分析。

递推关系式如下,递推关系式和494目标和的一样:

dp[j]=dp[j]+dp[j-nums[i]]
即dp[j]+=dp[j-nums[i]]

代码及运行结果分析

先遍历物品

先遍历物品的代码如下:

#include<iostream>
#include<vector>
#include <iomanip>
using namespace std;

void print_dparr(vector<int>& dp)
{
	int col = dp.size();
	for (int j = 0;j < col;j++)
	{
		cout << std::left << setw(2) << dp[j] << " ";
	}
	cout << endl;
	cout << endl;
}

class solution
{
public:
	int getres(vector<int>& coins, int& amount)
	{
		vector<int> dp(amount + 1, 0);
		dp[0] = 1;
		for (int i = 0;i < coins.size();i++)
		//for (int j = 0;j <= amount;j++)
		{
			//for (int i = 0;i < coins.size();i++)
			for (int j = 0;j <= amount;j++)
			{
				if (j >= coins[i]) 
					dp[j] += dp[j - coins[i]];
			}
			//cout << "从物品0到物品" << coins.size()-1 << "中取物品放满容量为"<<j<<"的背包的方法数" << endl;
			cout << "从物品0到物品" << i << "中取物品放满背包的方法数" << endl;
			print_dparr(dp);
		}
		cout << "结果为:" << endl;
		return dp[amount];
	}
};

int main()
{
	solution s;
	int amount = 5;
	vector<int> coins = { 1,2,5 };
	cout << s.getres(coins, amount) << endl;
	return 0;
}

运行结果为:

在这里插入图片描述

下面分析一下方法个数形成的具体过程:
以第一次遍历为例进行分析,即从物品0中取物品放满背包的方法数,此时的遍历代码为:

for (int j = 0;j <= amount;j++)
	{
		if (j >= coins[0]) 
			dp[j] += dp[j - coins[0]];
	}

具体形成工程如下:

coins[1]=1
dp[1]+=dp[1-conis[0]]
dp[2]+=dp[2-coins[0]]
dp[3]+=dp[3-conis[0]]
dp[4]+=dp[4-coins[0]]
dp[5]+=dp[5-conis[0]]
首先我们得知道dp[j] += dp[j - coins[0]]这个式子的含义,在确定递推关系式的时候其实以及说明了,
现在我们从另一个角度理解,以dp[1]+=dp[1-conis[0]]为例,这个式子表示在放入第0个物品后,装满容量
为1的背包的新增的方法数和装满容量为1-conis[0]的方法数一样,那么假设当前装满容量为1的背包的方法为
A,B,C,新增的方法就是装满容量为1-conis[0]的方法(假设为D,E),那么最后总的方法就是
A,B,C,(D+物品0),(E+物品0)。
带入具体数值可知,装满容量为1的背包的方法为(物品0+装满背包容量为0的方法),但是装满背包容量为0
的方法为NULL,因此装满容量为1的背包的方法为物品0,即为1。将这个思路推广到整个遍历过程可得下表。
012345
物品0NULL111111111111111
物品1NULL111、2111、121111、112、2211111、1112、122
物品2NULL111、2111、12、31111、112、22、1311111、1112、122、113、23

以上就是每种方法的形成过程,为了便于理解,我再对用物品0到物品1放入背包容量为3的背包的方法的形成过程进行分析,循环代码如下:

for (int j = 0;j <= amount;j++)
	{
		if (j >= coins[1]) 
			dp[j] += dp[j - coins[1]];
	}

此时dp[3]的形成过程如下:

coins[1]=2
dp[3] += dp[3 - coins[1]];
dp[3] += dp[1];
由上式可知放满容量为3的方法等于已经有的方法加上(物品1+放满背包容量为1的方法)这种方法,查询表
可知放满容量为1的方法为1,再加上物品1就是(12),与已有的方法组合再一起就是(11112),这就是
每种结果的形成过程。同理可以分析表中其他数据的来源。

先遍历背包

以上都是先遍历物品,接下来讨论先遍历背包的形式,代码如下:

#include<iostream>
#include<vector>
#include <iomanip>
using namespace std;

void print_dparr(vector<int>& dp)
{
	int col = dp.size();
	for (int j = 0;j < col;j++)
	{
		cout << std::left << setw(2) << dp[j] << " ";
	}
	cout << endl;
	cout << endl;
}

class solution
{
public:
	int getres(vector<int>& coins, int& amount)
	{
		vector<int> dp(amount + 1, 0);
		dp[0] = 1;
		//for (int i = 0;i < coins.size();i++)
		for (int j = 0;j <= amount;j++)
		{
			for (int i = 0;i < coins.size();i++)
			//for (int j = 0;j <= amount;j++)
			{
				if (j >= coins[i]) 
					dp[j] += dp[j - coins[i]];
			}
			cout << "从物品0到物品" << coins.size()-1 << "中取物品放满容量为"<<j<<"的背包的方法数" << endl;
			//cout << "从物品0到物品" << i << "中取物品放满背包的方法数" << endl;
			print_dparr(dp);
		}
		cout << "结果为:" << endl;
		return dp[amount];
	}
};

int main()
{
	solution s;
	int amount = 5;
	vector<int> coins = { 1,2,5 };
	cout << s.getres(coins, amount) << endl;
	return 0;
}

运行结果如下:

在这里插入图片描述

很明显与先遍历物品的结果不一样,下面讨论形成这种结果的原因,分析过程和上述一致,我将分析每个数据形成的过程。此时需注意,虽然我每次将一整行的dp数组打印了出来,但其实由于先遍历背包,因此每次求解的只有一个数据,其他的数据都是默认初始化的0或者已经求出来的数据,也就是说下一行的dp是在上一行的dp后面附加数据。

for (int i = 0;i < coins.size();i++)
	{
		if (j >= coins[i]) 
			dp[j] += dp[j - coins[i]];
	}
根据遍历过程进行逐行分析:
coins[0]=1,coins[1]=2,coins[2]=5,
dp[0]+= dp[0 - coins[i]];0小于所有的物品重量,因此值不变。此时放满容量为0的方法为NULL。
dp[1]+= dp[1 - coins[i]],只有coins[0]<=1,因此dp[1]+= dp[1 - coins[0]]
根据之前的分析过程可知,此时放满容量为1的方法为1。
dp[2]+= dp[2 - coins[i]],此时dp[2]+= dp[2 - coins[0]],dp[2]+=dp[2 - coins[1]]
此时放满背包容量为2的方法为211。
dp[3]+= dp[3 - coins[i]],此时dp[3]+=dp[3 - coins[0]],dp[3]+=dp[3 - coins[1]]
此时放满背包容量为3的方法为1211121

分析到这其实已经能看出结论了,注意计算dp[3]的过程出现了12、21,在组合中这两种算一种,但在排列中这就是两种排列了。出现这种情况的原因如下

dp[3]+= dp[3 - coins[i]],此时dp[3]+=dp[3 - coins[0]],dp[3]+=dp[3 - coins[1]]
首先dp[3]+=dp[3 - coins[0]],即再放入物品0后,dp[3]+=dp[2],即此时的方法为
(物品0+放满背包容量为2的方法)即12111
同理dp[3]+=dp[3 - coins[1]],即再放入物品1后,dp[3]+=dp[1],代表方法21。
从而使得此时求解的是排列数。

以上便是我对完全背包求解组合数和排列数的分析,希望对读者有所帮助。

4、待更新

思路分析

代码及运行结果分析


总结

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值