动态规划——01背包与完全背包问题
一、01背包:每个物品最多拿一个
1. 什么时候用01背包?
-
题目明确限制每个元素最多用一次。
-
要求选或不选得到某种极值(最大价值、能否凑满、方案数)。
2. 怎么把问题转化成01背包?
第一步:找“背包容量”
你要凑的目标值是什么?比如“总和的一半”、“子集和”、“总字符串长度”等,这就是背包的最大容量。
第二步:找“物品”
数组里的每个元素,比如石头重量、数字值、字符串(带有0和1的个数),它们都有重量,有的还有价值(价值通常等于重量,或者为1表示个数)。
第三步:定义dp含义
-
求最大价值:
dp[j]= 容量为j时的最大价值。 -
能否装满:
dp[j]= 容量j是否能恰好被填满(布尔值或int 0/1)。 -
装满的方案数:
dp[j]= 装满容量j的方法数。
由于物品只有一个,所以遍历背包容量的时候要倒序遍历,避免重复选择
3.例题:
1049.最后一块石头的重量II
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
-
如果
x == y,那么两块石头都会被完全粉碎; -
如果
x != y,那么重量为x的石头将会完全粉碎,而重量为y的石头新重量为y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
转化:两堆石头血拼,那不得让他们势均力敌?
所以问题变成:有一堆石头(物品),每个只能选一次,背包容量设为sum/2,求最多能装多重。最后剩下= sum - dp[target] - dp[target]。
int lastStoneWeightII(int *stones, int stonesSize)
{
int sum = 0;
for (int i = 0; i < stonesSize; i++)
{
sum += stones[i];
}
int target = sum / 2;
int *dp = (int *)calloc(1501, sizeof(int));
for (int i = 0; i < stonesSize; i++)
{
for (int j = target; j >= stones[i]; j--)
{
dp[j] = fmax(dp[j], dp[j - stones[i]] + stones[i]);
}
}
return sum - dp[target] - dp[target];
}
494.目标和
题目
给你一个非负整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
-
例如,
nums = [2, 1],可以在2之前添加'+',在1之前添加'-',然后串联起来得到表达式"+2-1"。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
转化:
假设加法的总和为 left ,那么减法对应的总和就是 sum - left 。所以我们要求的是left - (sum - left) = target,即 left= (target + sum) / 2,此时问题就转化为,用nums数组装满容量为left 的背包,有几种方法。
int findTargetSumWays(int *nums, int numsSize, int target)
{
int sum = 0;
for (int i = 0; i < numsSize; i++)
{
sum += nums[i];
}
int left = (sum + target) / 2; // left表示正数和
if (left * 2 != sum + target || sum < target || left < 0)
{
return 0;
}
int *dp = (int *)calloc(left + 1, sizeof(int));
dp[0] = 1;
for (int i = 0; i < numsSize; i++)
{
for (int j = left; j >= nums[i]; j--)
{
dp[j] += dp[j - nums[i]];
}
}
return dp[left];
}
二、完全背包:物品可以重复使用
1. 关键细节:求组合数 vs 求排列数
-
求组合数:先遍历物品,再遍历容量。
-
求排列数:先遍历容量,再遍历物品。
2.什么时候用完全背包?
-
物品可以无限次使用。
-
常见于硬币找零、整数拆分、爬楼梯(每次可跨任意步数)。
3. 怎么把问题转化成完全背包?
同样的三步法,但物品是无限的,所以容量正序。
4.例题
518. 零钱兑换 II
题目
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
思路
求凑出amount的硬币组合数。硬币无限个,完全背包。因为是组合数,所以代码外层循环是coins,内层是amount正序:
for (int i = 0; i < coinsSize; i++) {
for (int j = coins[i]; j <= amount; j++) {
dp[j] += dp[j - coins[i]];
}
}
377. 组合总和 Ⅳ
题目
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素排列的个数。
思路
虽然叫“组合总和”,但题目强调顺序不同算不同组合,其实是排列。所以先遍历容量再遍历物品:
for (int j = 0; j <= target; j++) { // 容量在外
for (int i = 0; i < numsSize; i++) { // 物品在内
if (j >= nums[i]) dp[j] += dp[j - nums[i]];
}
}
三、总结:背包问题转化
-
找容量:题目要你达到的“总数”、“目标值”、“限制条件”是啥?那就是背包容量。
-
找物品:数组里的每个元素有什么属性?它占据多少容量?能带来什么价值?
-
定规则:
-
每个物品只能用一次 → 01背包(容量倒序)。
-
每个物品无限用 → 完全背包(容量正序)。
-
求最大价值 →
max。 -
求能不能 →
dp[j]用布尔值。 -
求方案数 →
dp[0]=1,累加。
-
更多推荐


所有评论(0)