动态规划_从爬楼梯、零钱兑换II、分割等和子集来看背包问题
一、爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
**示例 1:**
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
**示例 2:**
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
4. 1 阶 + 1 阶 + 1 阶
5. 1 阶 + 2 阶
6. 2 阶 + 1 阶
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/climbing-stairs
代码比较简单,这里定义的dp[i]的含义是爬到i+1阶的排列个数,这里定义的dp[0]相当于是爬到1阶的排列数。
转移方程dp[i] = dp[i-1] + dp[i-2]
,表示爬到i
阶的方法等于爬到i-1
阶的方法加上i-2
阶的方法之和。
class Solution {
public:
int climbStairs(int n) {
if(n == 1) return 1;
if(n == 2) return 2;
vector<int> dp(n,-1);
dp[0] = 1;
dp[1] = 2;
for(int i = 2; i < n ;i++)
{
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n-1];
}
};
如果将这道题泛化,一次爬楼梯的步数不再是1、2,而是一个数组step[n] = {1,2,3}。那么转移方程可以改为dp[i] = dp[i-1] + dp[i-2]+dp[i-3]
;更泛化一些可以理解为dp[i] = dp[i-1] + dp[i-j]; j = 1,2,3,....
,则可以得到如下代码。
for (int i = 1; i <= n; i++){
for(int step : steps){
if ( i < step ) continue;// 台阶少于跨越的步数
dp[i] = dp[i] + dp[i-step];
}
}
二、零钱兑换II
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 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
示例 2:
输入: amount = 3, coins = [2]
输出: 0
解释: 只用面额2的硬币不能凑成总金额3。
示例 3:
输入: amount = 10, coins = [10]
输出: 1
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/coin-change-2
这里就可以明显看到背包问题的特点,硬币无限,就相当于是完全背包问题了。对于背包问题不了解,可以搜索背包9讲。用给出的硬币凑出给定的数相当于是用给出的物品要填满给定容量的背包。这里dp[i][j]
表示前 i 个物品恰好装满 j 容量的方法数,也就是组合数。这里是dp[i][j] = dp[i-1][j] + dp[i][j - coins[i-1]] ;
而不是dp[i][j] = dp[i-1][j] + dp[i-1][j - coins[i-1]] ;
是因为这是个完全背包问题,一个硬币可以多次使用,dp[i][j - coins[i-1]
就包含了dp[i-1][j - coins[i-1]
的情况,所以即使你使用了硬币i,你只需要关心如何凑出金额 j - coins[i-1]
。
class Solution {
public:
int change(int amount, vector<int>& coins) {
int m = coins.size();
//dp[i][j]表示前i个物品恰好装满j容量的组合数
vector<vector<int>> dp(m+1,vector<int>(amount+1,0));//创建dp数组
//base case
for(int i = 0; i < m+1; i++) dp[i][0] = 1;//n个物品填满容量为0的背包有1种方式
for(int i = 1; i < m+1; i++){
for(int j = 1; j < amount+1;j++)
{ //coins[i-1]是因为coins是从0开始的
if(j - coins[i-1] < 0)//如果当前容量小于要放入的物品大小,那么就不放该物品
{
dp[i][j] = dp[i-1][j];//当前结果就等于不放该物品就能凑齐j的情况
}
else//如果可以放入
{
//当前结果就等于不用当前物品就凑齐的情况再加上用了当前物品刚好凑齐的情况
dp[i][j] = dp[i-1][j] + dp[i][j - coins[i-1]] ;
}
}
}
return dp[m][amount];
}
};
三、分割等和子集
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
每个数组中的元素不会超过 100
数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
示例 2:
输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/partition-equal-subset-sum
这道题其实本质也是个01背包问题,只要将数组中的所有元素求和,再除以2得到sum,就相当于问你使用这数组中的元素,每个元素只能用一次,能否凑出sum,如果所有元素和为奇数那么肯定不行,如果能凑出说明可以分割成两个元素和相等的子集。
这里dp[i][j] = dp[i - 1][j] || dp[i-1][j-nums[i-1]]
而不是dp[i][j] = dp[i - 1][j] || dp[i][j-nums[i-1]]
是因为这里数只能用一次,如果装进去就只能看dp[i-1][j-nums[i-1]]
(前i-1个数凑到了j-nums[i-1],也就是只差当前这个数就凑齐的情况)或者dp[i - 1][j]
(只使用前i-1个数就凑到了)是否为true,只要有一个为true,当前dp[i][j]
就为true。
代码如下:
class Solution {
public:
bool canPartition(vector<int>& nums) {
//如果能凑到元素之和的一半,说明可以找到两个相等子集
//dp[i][j]的含义为前i个数能恰好凑出数 j 时为true,否则为false
int n = nums.size();
int sum(0);
for(int i :nums)//求所有元素和
{
sum += i;
}
if(sum % 2 != 0) return false;//奇数肯定不行
sum /= 2;
vector<vector<bool>> dp(n+1,vector<bool>(sum+1,false));//初始化全为false
for (int i = 0; i < n + 1; i++) dp[i][0] = true;//当空间为0时,肯定可以凑出
for (int i = 1; i < n+1; i++)
{
for (int j = 1; j < sum+1; j++)
{
if (j - nums[i - 1] < 0)
{
// 背包容量不足,不能装入第 i 个物品
dp[i][j] = dp[i - 1][j];
} else
{
// 装入或不装入背包
dp[i][j] = dp[i - 1][j] || dp[i-1][j-nums[i-1]];
}
}
}
return dp[n][sum];
}
};