求解方法
动态规划问题求解五部曲
1.dp数组、下标i,j的含义
2.递推公式
3.dp数组如何初始化
4.遍历顺序
5.打印dp数组(防止出错)
走楼梯问题
每次走楼梯,要么一次迈一步,要么一次迈两步,n阶楼梯有多少种走法?
分析:
最后一步要么走一步,要么走两步,n阶楼梯的走法就等于n-1阶的走法加上n-2阶的走法,即f(n)=f(n-1)+f(n+2)(递推公式)f(n)就是dp数组,代表这n阶楼梯的总走法,f(1)=1,f(2)=2[初始化]
背包问题
0-1背包
0-1背包问题是经典的动态规划问题之一。在i个物品中挑选,放入容量为m的书包中所达到的最大价值。其中每个物品只能挑选一次。
二维数组
dp[i][j],是指从i个物品中去选择,当背包容量为j时,所装的物品的最大价值。
书包容量为j时
不放物品i时,最大价值=dp[i-1][j] [从(i-1)件物品中去选择的最大价值]
包含物品i时,最大价值=max( dp[i-1][j] , dp[i-1][ j-weight[i] ]+value[i] )
如果说当书包已经从i-1件物品中选出最大value的组合,且容量所剩不大时,还想把物品i放进去,肯定是要把其他物品拿出来,但是拿出来的物品和刚放进去的物品i的value大小不同,不能直接确定总value一定变大还是变小,要有比较。
dp[i-1][ j-weight[i] ]+value[i] ,就是指提前预留好物品i的空间,书包中可以放其他物品的容量变成了j-weight[i],等价于书包容量为j-weight[i]时从i-1个物品中选出的最大价值:dp[i-1][ j-weight[i] ],再加上物品i的价值,这就是一定包含物品i的总value。
如果说书包容量不大,
并且拿出来的物品value比新加物品i的value大(物品i不放更好)时,这时候的最大价值就是不放物品i时的价值=dp[i-1][j];
同理如果拿出来的物品value比新加物品i的value小,此时的最大value就是一定包含物品i的情况;
但是当书包容量很大,物品i可以直接放进去,多加一个物品,书包中的价值肯定是变大了,此时的最大价值一定包含物品i,即dp[i-1][ j-weight[i] ]+value[i];
所以dp[i][j]=max(dp[i-1][j] , dp[i-1][ j-weight[i] ]+value[i] )
初始化
for (int i = 1; i < weight.size(); i++) { //j=0时
dp[i][0] = 0;
}
//i=0时
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}
遍历顺序
无论是先遍历物品还是书包都是一样的
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
if (j < weight[i]) //书包装不下物品i,j-weight[i]<0,数组会越界;
dp[i][j] = dp[i - 1][j];
else
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
一维数组
二维数组dp[i][j]=max( dp[i-1][j] , dp[i-1][ j-weight[i] ]+value[i] ),都是依照上一行(i-1)行数据进行更新数据,可以直接转化成一维数组,在for循环中一层层更新,根据上一轮的dp[j]进行更新。
这里要注意的是遍历顺序,在遍历书包时,不再是从左往右,而是从右往左。
这是因为dp[j] , dp[ j-weight[i] ]相当于二维数组中的dp[i-1][j] , dp[i-1][ j-weight[i] ],都是位于dp[j]的左上方。
如果从左往右遍历,当dp[j]用到是dp[j] , dp[ j-weight[i] ]已经发生了更新,此时变成了dp[i][j] , dp[i][ j-weight[i] ];因此应该是从右往左遍历,确保用到的dp[j] , dp[ j-weight[i] ]还没有发生更新。
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
1049. 最后一块石头的重量 II - 力扣(LeetCode)
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
int sum = 0;
for (int i = 0; i < stones.size(); i++) {
sum += stones[i];
}
//将题目转化成为两堆质量差不多的子集去抵消,总质量的一半
bool flag=false;
if (sum % 2 == 1) {
flag = true;
}
sum = sum / 2;//总质量的一半sum
int temp = 0;
//dp数组是从0-i石子中选出满足质量总和是sum,dp(质量)最大,value=weight
vector<int> dp(sum + 1, 0);
for (int i = 0; i < stones.size(); i++) {
for (int j = sum; j >= stones[i]; j--) {
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
temp = dp[j];
}
}
if (flag == true){
return sum*2+1 - 2*dp[sum];//总和为奇数时,除以2向下取整,这里+1
}
else
return 2*sum- 2*dp[sum];//题目求解的是两堆的差
}
};
完全背包
与0-1背包不同的是,这里每个物品能被多次选择。
二维数组
书包容量为j时
不放物品i时,最大价值=dp[i-1][j] [从(i-1)种物品中去选择的最大价值]
包含物品i时,最大价值=max( dp[i-1][j] , dp[ i ] [ j-weight[i] ] +value[i] )
这里是完全背包的特殊之处,每种物品不止选一次,在选择物品i之前,可能也选择过物品i,所以在留出物品i的容量之后,应该是在i种物品中去选择。
在计算dp[i][j]时,需要用到dp[ i ] [ j-weight[i] ] ,位于dp[i][j]的左侧,所以遍历顺序要从左往右。
for (int i = 1; i < n; i++) { // 遍历物品
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
if (j < weight[i])
dp[i][j] = dp[i - 1][j];
else
dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);
}
}
一维
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]);
}
}
例题
class Solution {
public:
int change(int amount, vector<int>& coins) {
//初始化一开始所有的dp数组都是0,对于第一排的不用特意赋值,因为dp[j]=dp[j-coins[i]]
//uint64_t是无符号64位整数,防止溢出
vector<uint64_t> dp(amount + 1, 0);
dp[0]=1;
for(int i=0;i<coins.size();i++){
for(int j=1;j<=amount;j++){
if(j>=coins[i]){
dp[j]+=dp[j-coins[i]];
}
}
}
return dp[amount];
}
};