
🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》
✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介:

在算法学习的过程中,动态规划一直是一个既重要又容易让人感到"头疼"的专题.它不像简单模拟或基础数据结构那样直观,很多时候需要我们从题目中抽象出状态,再通过状态之间的关系推导出转移方程.而在众多动态规划模型中,背包问题无疑是最经典、最具有代表性的一类问题.它不仅频繁出现在各类算法竞赛、笔试面试和刷题训练中,也常常被用来帮助初学者理解动态规划的核心思想.对于很多刚接触动态规划的同学来说,背包问题的难点并不只在于写出代码,而在于如何判断题目属于哪一种背包模型,如何定义状态,如何确定遍历顺序,以及如何从二维状态优化到一维滚动数组.尤其是在 0-1 背包和完全背包中,容量循环方向的不同常常会直接影响结果是否正确;而在多重背包、混合背包等进阶模型中,物品数量和选择方式的变化又会带来新的思考.因此,掌握背包问题不能只靠死记硬背模板,更需要理解每一种模型背后的逻辑和适用场景.本文将围绕"背包问题经典模型与解题套路"这一主题展开,系统梳理背包问题中的常见类型、核心状态转移思路以及实际解题时的分析方法.我们会从最基础的 0-1 背包开始,逐步延伸到完全背包、多重背包、分组背包等经典变形,并结合常见题型总结出一套可复用的解题套路.希望通过本文的讲解,读者不仅能够看懂背包问题的公式和代码,更能真正理解动态规划"从局部选择推导全局最优"的思想,在之后遇到类似问题时,能够快速识别模型、明确状态设计,并写出清晰可靠的解法.废话不多说,下面跟着小编的节奏🎵一起去疯狂的学习吧!
目录
1.背包问题背景介绍
背包问题最早来源于一个非常经典的现实场景:假设有一个容量有限的背包,以及若干个具有不同重量和价值的物品,我们希望在不超过背包容量的前提下,选择一些物品放入背包,使得背包中物品的总价值尽可能大.这个问题看似简单,却很好地抽象出了现实生活中常见的"资源有限、收益最大化"问题,因此逐渐成为算法领域中研究动态规划的重要模型之一.
从本质上看,背包问题是一类典型的组合优化问题.它关注的是如何在给定约束条件下,从多个选择中挑选出最优方案.这里的"背包容量"可以理解为有限资源,例如时间、空间、资金、成本、体力等;而"物品价值"则可以对应收益、利润、满意度、优先级等目标.因此,背包问题并不局限于真正的"背包装物品",它还可以对应许多实际场景,例如预算分配、任务选择、项目投资、资源调度、商品采购等.
在算法学习中,背包问题之所以重要,是因为它非常适合作为动态规划思想的入门和进阶模型.它具有清晰的状态划分和选择过程:对于每一个物品,我们都需要考虑"选"或者"不选";对于每一种容量状态,我们都需要判断当前选择是否能够带来更优结果.通过不断比较不同选择下的最优值,就可以逐步得到整个问题的最优解.这种由小问题推导大问题、由局部最优状态构建全局最优答案的过程,正是动态规划的核心思想.
随着问题条件的变化,背包问题也衍生出了多种经典模型.比如,每个物品只能选择一次时,就是 0-1 背包问题;每个物品可以选择无限次时,就是完全背包问题;每个物品有固定数量限制时,就是多重背包问题;物品被划分为若干组、每组最多只能选择一个时,就是分组背包问题.虽然这些模型在限制条件上有所不同,但它们的核心思路都是围绕状态定义、状态转移和最优选择展开的.
因此,学习背包问题不仅是为了掌握某一类题目的解法,更重要的是通过它理解动态规划的建模方法.只要能够看清题目中的"容量限制""选择对象"和"最优目标",很多看似复杂的问题都可以转化为背包模型来解决.这也是背包问题在算法竞赛、笔试面试和日常刷题中频繁出现的重要原因.
背包问题概述
背包问题(Knapsack problem)是一种组合优化的 NP 完全问题.
问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高.
根据物品的个数,分为如下几类:
- 01 背包问题:每个物品只有一个
- 完全背包问题:每个物品有无限多个
- 多重背包问题:每件物品最多有 si 个
- 混合背包问题:每个物品会有上面三种情况......
- 分组背包问题:物品有 n 组,每组物品里有若干个,每组里最多选一个物品
其中上述分类里面,根据背包是否装满,又分为两类:
- 不一定装满背包
- 背包一定装满
优化方案:
- 空间优化 - 滚动数组
- 单调队列优化
- 贪心优化
根据限定条件的个数,又分为两类:
- 限定条件只有一个:比如体积 -> 普通的背包问题
- 限定条件有两个:比如体积 + 重量 -> 二维费用背包问题
根据不同的问法,又分为很多类:
- 输出方案
- 求方案总数
- 最优方案
- 方案可行性
其实还有很多分类,但是我们仅需了解即可.
因此,背包问题种类非常繁多,题型非常丰富,难度也是非常难以捉摸.但是,尽管种类非常多,都是从 01 背包问题演化过来的.所以,一定要把01背包问题学好.
2.01背包(OJ题)

算法思路:解法(动态规划):
我们先解决第一问:
1.状态表示:
dp[i][j] 表示:从前 i 个物品中挑选,总体积不超过j,所有的选法中,能挑选出来的最大价值.
2.状态转移方程:
线性 dp 状态转移方程分析方式,一般都是根据最后一步的状况,来分情况讨论:
i. 不选第 i 个物品:相当于就是去前 i - 1 个物品中挑选,并且总体积不超过 j.此时 dp[i][j] = dp[i - 1][j];
ii. 选择第 i 个物品:那么我就只能去前 i - 1 个物品中,挑选总体积不超过 j - v[i] 的物品.此时 dp[i][j] = dp[i - 1][j - v[i]] + w[i].但是这种状态不一定存在,因此需要特判一下.
综上,状态转移方程为:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]).
3.初始化:
我们多加一行,方便我们的初始化,此时仅需将第一行初始化为 0 即可.因为什么也不选,也能满足体积不小于 j 的情况,此时的价值为 0.
4.填表顺序:
根据状态转移方程,我们仅需从上往下填表即可.
5.返回值:
根据状态表示,返回 dp[n][V].
接下来解决第二问:
第二问仅需微调一下 dp 过程的五步即可.
因为有可能凑不齐 j 体积的物品,因此我们把不合法的状态设置为 -1.
1.状态表示:
dp[i][j] 表示:从前 i 个物品中挑选,总体积正好等于 j,所有的选法中,能挑选出来的最大价值.
2.状态转移方程:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]).
但是在使用 dp[i - 1][j - v[i]] 的时候,不仅要判断 j >= v[i],又要判断 dp[i - 1][j - v[i]] 表示的情况是否存在,也就是 dp[i - 1][j - v[i]] != -1.
3.初始化:
我们多加一行,方便我们的初始化:
i. 第一个格子为 0,因为正好能凑齐体积为 0 的背包;
ii. 但是第一行后面的格子都是 -1,因为没有物品,无法满足体积大于 0 的情况.
4.填表顺序:
根据状态转移方程,我们仅需从上往下填表即可.
5.返回值:
由于最后可能凑不成体积为 V 的情况,因此返回之前需要特判一下.
空间优化:
背包问题基本上都是利用滚动数组来做空间上的优化:
i. 利用滚动数组优化;
ii. 直接在原始代码上修改.
在01背包问题中,优化的结果为:
i. 删掉所有的横坐标;
ii. 修改一下 j 的遍历顺序.






核心代码
cpp
#include <iostream>
#include <string.h>
using namespace std;
//定义数组最大容量,满足题目数据范围
const int N = 1010;
//全局变量:n=物品数量,V=背包最大容量,v[]=物品体积,w[]=物品价值
int n, V, v[N], w[N];
//dp二维数组:dp[i][j]表示前i个物品,背包容量为j时的最大价值
int dp[N][N];
int main()
{
//1.输入数据:读取物品总数n 和 背包最大容量V
cin >> n >> V;
//循环读取n个物品的体积v[i]和价值w[i](物品下标从1开始)
for(int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
//第一问:经典01背包(不要求装满背包)
//遍历每一个物品
for(int i = 1; i <= n; i++)
//遍历每一种背包容量
for(int j = 0; j <= V; j++)
{
//状态1:不选第i个物品,价值等于前i-1个物品、容量j的最大价值
dp[i][j] = dp[i - 1][j];
//状态2:如果背包容量足够放下第i个物品,尝试选这个物品
if(j >= v[i])
//取「不选」和「选」两种情况的最大值
dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
}
//输出结果:n个物品、背包满容量V时的最大价值(不要求装满)
cout << dp[n][V] << endl;
//第二问:01背包(要求必须装满背包)
//重置dp数组为0,重新计算必须装满的场景
memset(dp, 0, sizeof dp);
//核心初始化:0个物品时,容量j>0无法装满,赋值为-1(表示非法状态)
//dp[0][0]=0:0个物品、容量0,价值为0(合法,刚好装满)
for(int j = 1; j <= V; j++) dp[0][j] = -1;
//遍历物品和背包容量,逻辑同上
for(int i = 1; i <= n; i++)
for(int j = 0; j <= V; j++)
{
//不选第i个物品
dp[i][j] = dp[i - 1][j];
//必须满足:容量足够 + 前一个状态是合法的(能装满)
if(j >= v[i] && dp[i - 1][j - v[i]] != -1)
dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
}
//输出结果:如果dp[n][V]=-1说明无法装满,输出0;否则输出最大价值
cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;
return 0;
}
完整测试代码
cpp
#include <iostream>
#include <string.h>
#include <sstream>
using namespace std;
// 定义数组最大容量,满足题目数据范围
const int N = 1010;
// 全局变量:n=物品数量,V=背包最大容量,v[]=物品体积,w[]=物品价值
int n, V, v[N], w[N];
// dp二维数组:dp[i][j]表示前i个物品,背包容量为j时的最大价值
int dp[N][N];
// 封装求解函数,方便测试多组数据
void solve(istream& in)
{
// 1. 输入数据:读取物品总数n 和 背包最大容量V
in >> n >> V;
// 循环读取n个物品的体积v[i]和价值w[i]
for(int i = 1; i <= n; i++)
in >> v[i] >> w[i];
// 第一问:经典01背包,不要求装满背包
memset(dp, 0, sizeof dp);
for(int i = 1; i <= n; i++)
for(int j = 0; j <= V; j++)
{
// 不选第i个物品
dp[i][j] = dp[i - 1][j];
// 选第i个物品
if(j >= v[i])
dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
}
cout << "不要求装满背包的最大价值:" << dp[n][V] << endl;
// 第二问:01背包,要求必须装满背包
memset(dp, 0, sizeof dp);
// 初始化非法状态
// dp[0][0] = 0,表示0个物品装满容量0,合法
// dp[0][j] = -1,表示0个物品无法装满容量j
for(int j = 1; j <= V; j++)
dp[0][j] = -1;
for(int i = 1; i <= n; i++)
for(int j = 0; j <= V; j++)
{
// 不选第i个物品
dp[i][j] = dp[i - 1][j];
// 选第i个物品
// 要求前一个状态必须合法
if(j >= v[i] && dp[i - 1][j - v[i]] != -1)
dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]);
}
if(dp[n][V] == -1)
cout << "必须装满背包的最大价值:0" << endl;
else
cout << "必须装满背包的最大价值:" << dp[n][V] << endl;
}
int main()
{
// 测试用例1
// 4个物品,背包容量5
// 物品:
// 体积 价值
// 1 2
// 2 4
// 3 4
// 4 5
//
// 不要求装满:可选体积2+3,价值4+4=8
// 必须装满:同样可选体积2+3,价值8
string test1 =
"4 5\n"
"1 2\n"
"2 4\n"
"3 4\n"
"4 5\n";
cout << "====== 测试用例1 ======" << endl;
stringstream ss1(test1);
solve(ss1);
cout << endl;
// 测试用例2
// 3个物品,背包容量6
// 物品体积分别为2、4、5
// 可以选2+4刚好装满,最大价值为10
string test2 =
"3 6\n"
"2 3\n"
"4 7\n"
"5 8\n";
cout << "====== 测试用例2 ======" << endl;
stringstream ss2(test2);
solve(ss2);
cout << endl;
// 测试用例3
// 3个物品,背包容量7
// 物品体积为2、4、6
// 不要求装满时,可以选6,价值8
// 必须装满时,无法刚好凑出7,所以输出0
string test3 =
"3 7\n"
"2 3\n"
"4 5\n"
"6 8\n";
cout << "====== 测试用例3 ======" << endl;
stringstream ss3(test3);
solve(ss3);
cout << endl;
return 0;
}

核心代码
cpp
//空间优化后
#include <iostream>
#include <string.h>
using namespace std;
//定义数组最大容量,适配题目数据范围
const int N = 1010;
//全局变量定义
//n:物品数量 V:背包最大容量
//v[]:存储每个物品的体积 w[]:存储每个物品的价值
int n, V, v[N], w[N];
//一维dp数组:滚动数组优化,dp[j]表示背包容量为j时的最大价值
int dp[N];
int main()
{
//1.输入数据
//读取物品总数n 和 背包最大容量V
cin >> n >> V;
//循环读取n个物品的体积和价值(物品下标从1开始)
for(int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
//第一问:01背包(不要求装满背包)
//遍历每个物品(外层循环:逐个选择物品)
for(int i = 1; i <= n; i++)
//逆序遍历背包容量!核心:防止同一个物品被重复选取(01背包关键)
//从最大容量V往下遍历到当前物品体积v[i]
for(int j = V; j >= v[i]; j--)
//状态转移方程:
//dp[j] = 不选第i个物品的价值(原值) vs 选第i个物品的价值(dp[j-v[i]]+w[i])
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
//输出结果:背包容量为V时的最大价值(不要求装满)
cout << dp[V] << endl;
//第二问:01背包(必须装满背包)
//重置dp数组为0,重新计算必须装满的场景
memset(dp, 0, sizeof dp);
//核心初始化:必须装满的特殊处理
//dp[0]=0:容量0,价值0(合法,刚好装满)
//dp[j>0]=-1:标记为非法状态(0个物品无法装满容量>0的背包)
for(int j = 1; j <= V; j++) dp[j] = -1;
//遍历每个物品
for(int i = 1; i <= n; i++)
//依旧逆序遍历背包容量
for(int j = V; j >= v[i]; j--)
//必须判断:前一个状态是合法的(能装满),才能进行状态转移
if(dp[j - v[i]] != -1)
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
//输出结果:
//如果dp[V]=-1 → 无法装满背包,输出0
//否则输出装满背包时的最大价值
cout << (dp[V] == -1 ? 0 : dp[V]) << endl;
return 0;
}
完整测试代码
cpp
#include <iostream>
#include <string.h>
#include <sstream>
using namespace std;
// 定义数组最大容量,适配题目数据范围
const int N = 1010;
// 全局变量定义
// n:物品数量 V:背包最大容量
// v[]:存储每个物品的体积 w[]:存储每个物品的价值
int n, V, v[N], w[N];
// 一维dp数组:滚动数组优化
// dp[j]表示背包容量为j时的最大价值
int dp[N];
// 封装求解函数,方便多组测试
void solve(istream& in)
{
// 1. 输入数据
in >> n >> V;
for(int i = 1; i <= n; i++)
in >> v[i] >> w[i];
// 第一问:01背包,不要求装满背包
memset(dp, 0, sizeof dp);
for(int i = 1; i <= n; i++)
for(int j = V; j >= v[i]; j--)
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
cout << "不要求装满背包的最大价值:" << dp[V] << endl;
// 第二问:01背包,必须装满背包
memset(dp, 0, sizeof dp);
// 初始化:
// dp[0] = 0,表示容量为0时可以刚好装满
// dp[j] = -1,表示容量j暂时无法被刚好装满
for(int j = 1; j <= V; j++)
dp[j] = -1;
for(int i = 1; i <= n; i++)
for(int j = V; j >= v[i]; j--)
if(dp[j - v[i]] != -1)
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
cout << "必须装满背包的最大价值:"
<< (dp[V] == -1 ? 0 : dp[V]) << endl;
}
int main()
{
// 测试用例1:
// 4个物品,背包容量5
//
// 物品:
// 体积 价值
// 1 2
// 2 4
// 3 4
// 4 5
//
// 不要求装满:
// 选择体积2和3的物品,总体积5,总价值8
//
// 必须装满:
// 同样选择体积2和3的物品,刚好装满,总价值8
string test1 =
"4 5\n"
"1 2\n"
"2 4\n"
"3 4\n"
"4 5\n";
cout << "====== 测试用例1:可以刚好装满 ======" << endl;
stringstream ss1(test1);
solve(ss1);
cout << endl;
// 测试用例2:
// 3个物品,背包容量6
//
// 物品:
// 体积 价值
// 2 3
// 4 7
// 5 8
//
// 不要求装满:
// 可以选择体积2和4的物品,总价值10
//
// 必须装满:
// 体积2+4=6,刚好装满,总价值10
string test2 =
"3 6\n"
"2 3\n"
"4 7\n"
"5 8\n";
cout << "====== 测试用例2:选择多个物品刚好装满 ======" << endl;
stringstream ss2(test2);
solve(ss2);
cout << endl;
// 测试用例3:
// 3个物品,背包容量7
//
// 物品:
// 体积 价值
// 2 3
// 4 5
// 6 8
//
// 不要求装满:
// 最优选择是体积6的物品,价值8
//
// 必须装满:
// 无法组合出总体积7,所以输出0
string test3 =
"3 7\n"
"2 3\n"
"4 5\n"
"6 8\n";
cout << "====== 测试用例3:无法刚好装满 ======" << endl;
stringstream ss3(test3);
solve(ss3);
cout << endl;
// 测试用例4:
// 5个物品,背包容量10
//
// 物品:
// 体积 价值
// 2 6
// 2 3
// 6 5
// 5 4
// 4 6
//
// 不要求装满:
// 可以选择体积2、2、6,总体积10,总价值14
//
// 必须装满:
// 同样可以刚好装满,最大价值14
string test4 =
"5 10\n"
"2 6\n"
"2 3\n"
"6 5\n"
"5 4\n"
"4 6\n";
cout << "====== 测试用例4:多个组合比较最优值 ======" << endl;
stringstream ss4(test4);
solve(ss4);
cout << endl;
// 测试用例5:
// 2个物品,背包容量1
//
// 物品体积都大于背包容量
//
// 不要求装满:
// 什么都不选,最大价值为0
//
// 必须装满:
// 无法装满容量1,输出0
string test5 =
"2 1\n"
"2 10\n"
"3 20\n";
cout << "====== 测试用例5:所有物品都放不下 ======" << endl;
stringstream ss5(test5);
solve(ss5);
cout << endl;
return 0;
}

3.分割等和子集(OJ题)

算法思路:解法(动态规划):
先将问题转化成我们熟悉的题型.
如果数组能够被分成两个相同元素之和相同的子集,那么原数组必须有下面几个性质:
i. 所有元素之和应该是一个偶数;
ii. 数组中最大的元素应该小于所有元素总和的一半;
iii. 挑选一些数,这些数的总和应该等于数组总和的一半.
根据前两个性质,我们可以提前判断数组能够被划分.根据最后一个性质,我们发现问题就转化成了01背包的模型:
i. 数组中的元素只能选择一次;
ii. 每个元素面临被选择或者不被选择的处境;
iii. 选出来的元素总和要等于所有元素总和的一半.
其中,数组内的元素就是物品,总和就是背包.
那么我们就可以用背包模型的分析方式,来处理这道题.
请大家注意,不要背状态转移方程,因为题型变化之后,状态转移方程就会跟着变化.我们要记住的是分析问题的模式.用这种分析问题的模式来解决问题.
1.状态表示:
dp[i][j] 表示在前 i 个元素中选择,所有的选法中,能否凑成总和为 j 这个数.
2.状态转移方程:
老规矩,根据最后一个位置的元素,结合题目的要求,分情况讨论:
i. 不选择 nums[i]:那么我们是否能够凑成总和为 j,就要看在前 i - 1 个元素中选,能否凑成总和为 j.根据状态表示,此时 dp[i][j] = dp[i - 1][j];
ii. 选择 nums[i]:这种情况下是有前提条件的,此时的 nums[i] 应该是小于等于 j.因为如果这个元素都比要凑成的总和大,选择它就没有意义呀.那么我们是否能够凑成总和为 j,就要看在前 i - 1 个元素中选,能否凑成总和为 j - nums[i].根据状态表示,此时 dp[i][j] = dp[i - 1][j - nums[i]].
综上所述,两种情况下只要有一种能够凑成总和为 j,那么这个状态就是 true.因此,状态转移方程为:
dp[i][j] = dp[i - 1][j]
if(nums[i - 1] <= j) dp[i][j] = dp[i][j] || dp[i - 1][j - nums[i]]
3.初始化:
由于需要用到上一行的数据,因此我们可以先把第一行初始化.
第一行表示不选择任何元素,要凑成目标和 j.只有当目标和为 0 的时候才能做到,因此第一行仅需初始化第一个元素 dp[0][0] = true
4.填表顺序:
根据状态转移方程,我们需要从上往下填写每一行,每一行的顺序是无所谓的.
5.返回值:
根据状态表示,返回 dp[n][aim] 的值.
其中 n 表示数组的大小,aim 表示要凑的目标和.
6.空间优化:
所有的背包问题,都可以进行空间上的优化.
对于01背包类型的,我们的优化策略是:
i. 删掉第一维;
ii. 修改第二层循环的遍历顺序即可.






核心代码
cpp
//解法类:判断数组是否可以分割成两个子集,使得两个子集的元素和相等
class Solution
{
public:
//核心函数:nums为输入的整数数组
bool canPartition(vector<int>& nums)
{
//n:数组元素个数 sum:数组所有元素的总和
int n = nums.size(), sum = 0;
//遍历数组,计算所有元素的总和
for(auto x : nums) sum += x;
//关键判断:如果总和是奇数,一定无法平分,直接返回false
if(sum % 2) return false;
//目标和:将数组平分,每个子集的和必须等于 sum/2
//问题转化为:能否从数组中选出若干数,使得它们的和恰好等于 aim
int aim = sum / 2;
//二维dp数组定义:
//dp[i][j] 表示:从前 i 个元素中挑选,能否凑出和为 j(true=能,false=不能)
//行数:n+1(前0~n个元素),列数:aim+1(和为0~aim)
vector<vector<bool>> dp(n + 1, vector<bool>(aim + 1));
//dp初始化:
//前i个元素,凑和为0,一定可以实现(不选任何元素),所以全部赋值为true
for(int i = 0; i <= n; i++) dp[i][0] = true;
//动态规划填表:从上到下,从左到右遍历
for(int i = 1; i <= n; i++) //遍历前i个元素
for(int j = 1; j <= aim; j++)//遍历目标和j
{
//情况1:不选第i个元素
//能否凑出和j,等于前i-1个元素能否凑出和j
dp[i][j] = dp[i - 1][j];
//情况2:选第i个元素(前提:当前目标和j >= 第i个元素的值)
//注意:数组下标从0开始,第i个元素对应 nums[i-1]
if(j >= nums[i - 1])
//两种情况满足其一即可,用 或运算(||)
dp[i][j] = dp[i][j] || dp[i - 1][j - nums[i - 1]];
}
//返回结果:前n个元素,能否凑出和为aim(能则可以平分数组)
return dp[n][aim];
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
using namespace std;
// 解法类:判断数组是否可以分割成两个子集,使得两个子集的元素和相等
class Solution
{
public:
// 核心函数:nums为输入的整数数组
bool canPartition(vector<int>& nums)
{
// n:数组元素个数 sum:数组所有元素的总和
int n = nums.size(), sum = 0;
// 遍历数组,计算所有元素的总和
for(auto x : nums) sum += x;
// 关键判断:如果总和是奇数,一定无法平分,直接返回false
if(sum % 2) return false;
// 目标和:将数组平分,每个子集的和必须等于 sum / 2
// 问题转化为:能否从数组中选出若干数,使得它们的和恰好等于 aim
int aim = sum / 2;
// 二维dp数组定义:
// dp[i][j] 表示:从前 i 个元素中挑选,能否凑出和为 j
vector<vector<bool>> dp(n + 1, vector<bool>(aim + 1));
// dp初始化:
// 前i个元素,凑和为0,一定可以实现,即不选任何元素
for(int i = 0; i <= n; i++)
dp[i][0] = true;
// 动态规划填表
for(int i = 1; i <= n; i++)
for(int j = 1; j <= aim; j++)
{
// 情况1:不选第i个元素
dp[i][j] = dp[i - 1][j];
// 情况2:选第i个元素
if(j >= nums[i - 1])
dp[i][j] = dp[i][j] || dp[i - 1][j - nums[i - 1]];
}
// 返回结果:前n个元素,能否凑出和为aim
return dp[n][aim];
}
};
void printVector(const vector<int>& nums)
{
cout << "[";
for(int i = 0; i < nums.size(); i++)
{
cout << nums[i];
if(i != nums.size() - 1)
cout << ", ";
}
cout << "]";
}
void test(vector<int> nums, bool expected)
{
Solution solution;
bool result = solution.canPartition(nums);
cout << "测试数组:";
printVector(nums);
cout << endl;
cout << "程序结果:";
cout << (result ? "true" : "false") << endl;
cout << "预期结果:";
cout << (expected ? "true" : "false") << endl;
cout << "测试结论:";
if(result == expected)
cout << "通过";
else
cout << "未通过";
cout << endl << endl;
}
int main()
{
// 测试用例1:
// nums = [1, 5, 11, 5]
// 总和 = 22,目标和 = 11
// 可以选择 [11] 或 [5, 5, 1] 凑出11
// 所以可以分割成两个和相等的子集
test({1, 5, 11, 5}, true);
// 测试用例2:
// nums = [1, 2, 3, 5]
// 总和 = 11,是奇数
// 奇数无法平分,所以返回false
test({1, 2, 3, 5}, false);
// 测试用例3:
// nums = [1, 2, 3, 4]
// 总和 = 10,目标和 = 5
// 可以选择 [1, 4] 或 [2, 3] 凑出5
// 所以可以平分
test({1, 2, 3, 4}, true);
// 测试用例4:
// nums = [2, 2, 3, 5]
// 总和 = 12,目标和 = 6
// 无法从数组中凑出6
// 所以不能平分
test({2, 2, 3, 5}, false);
// 测试用例5:
// nums = [100, 100]
// 总和 = 200,目标和 = 100
// 可以分成 [100] 和 [100]
test({100, 100}, true);
// 测试用例6:
// nums = [1, 1, 1, 1]
// 总和 = 4,目标和 = 2
// 可以分成 [1, 1] 和 [1, 1]
test({1, 1, 1, 1}, true);
// 测试用例7:
// nums = [3, 3, 3, 4, 5]
// 总和 = 18,目标和 = 9
// 可以选择 [4, 5] 凑出9
// 所以可以平分
test({3, 3, 3, 4, 5}, true);
return 0;
}

核心代码
cpp
//空间优化后
//解题思路:将问题转化为 01 背包可行性问题
class Solution
{
public:
//函数功能:判断能否将数组分成两个子集,使元素和相等
bool canPartition(vector<int>& nums)
{
//n:数组元素个数 sum:数组所有元素的总和
int n = nums.size(), sum = 0;
//遍历计算数组总和
for(auto x : nums) sum += x;
//核心判断:总和为奇数,一定无法平分,直接返回 false
if(sum % 2) return false;
//目标和:问题转化为 -> 能否选出若干元素,和恰好等于 sum/2
int aim = sum / 2;
//一维滚动数组优化:
//dp[j] 表示:能否凑出 元素和为 j 的子集(true=能,false=不能)
vector<bool> dp(aim + 1);
//初始化:和为 0 一定可以凑出(不选任何元素),这是合法状态
dp[0] = true;
//外层循环:遍历每一个元素(相当于01背包的「物品」)
for(int i = 1; i <= n; i++)
//内层循环:逆序遍历目标和(01背包一维优化核心!)
//逆序遍历:保证每个元素只被选一次,避免重复选取
for(int j = aim; j >= nums[i - 1]; j--)
//状态转移方程:
//dp[j] = 不选当前元素(保持原值) || 选当前元素(dp[j-当前元素值])
dp[j] = dp[j] || dp[j - nums[i - 1]];
//最终结果:能否凑出和为 aim 的子集,能则可以平分数组
return dp[aim];
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
using namespace std;
// 空间优化后
// 解题思路:将问题转化为 01 背包可行性问题
class Solution
{
public:
// 函数功能:判断能否将数组分成两个子集,使元素和相等
bool canPartition(vector<int>& nums)
{
// n:数组元素个数 sum:数组所有元素的总和
int n = nums.size(), sum = 0;
// 遍历计算数组总和
for(auto x : nums) sum += x;
// 核心判断:总和为奇数,一定无法平分,直接返回 false
if(sum % 2) return false;
// 目标和:问题转化为 -> 能否选出若干元素,和恰好等于 sum / 2
int aim = sum / 2;
// 一维滚动数组优化:
// dp[j] 表示:能否凑出元素和为 j 的子集
vector<bool> dp(aim + 1);
// 初始化:和为 0 一定可以凑出,即不选任何元素
dp[0] = true;
// 外层循环:遍历每一个元素
for(int i = 1; i <= n; i++)
// 内层循环:逆序遍历目标和
// 逆序遍历保证每个元素只被使用一次
for(int j = aim; j >= nums[i - 1]; j--)
dp[j] = dp[j] || dp[j - nums[i - 1]];
// 最终结果:能否凑出和为 aim 的子集
return dp[aim];
}
};
void printVector(const vector<int>& nums)
{
cout << "[";
for(int i = 0; i < nums.size(); i++)
{
cout << nums[i];
if(i != nums.size() - 1)
cout << ", ";
}
cout << "]";
}
void test(vector<int> nums, bool expected)
{
Solution solution;
bool result = solution.canPartition(nums);
cout << "测试数组:";
printVector(nums);
cout << endl;
cout << "程序结果:";
cout << (result ? "true" : "false") << endl;
cout << "预期结果:";
cout << (expected ? "true" : "false") << endl;
cout << "测试结论:";
if(result == expected)
cout << "通过";
else
cout << "未通过";
cout << endl << endl;
}
int main()
{
// 测试用例1:
// nums = [1, 5, 11, 5]
// 总和 = 22,目标和 = 11
// 可以选择 [11] 或 [1, 5, 5] 凑出 11
// 所以可以分割成两个和相等的子集
test({1, 5, 11, 5}, true);
// 测试用例2:
// nums = [1, 2, 3, 5]
// 总和 = 11,是奇数
// 奇数无法平分,所以返回 false
test({1, 2, 3, 5}, false);
// 测试用例3:
// nums = [1, 2, 3, 4]
// 总和 = 10,目标和 = 5
// 可以选择 [1, 4] 或 [2, 3] 凑出 5
test({1, 2, 3, 4}, true);
// 测试用例4:
// nums = [2, 2, 3, 5]
// 总和 = 12,目标和 = 6
// 无法从数组中选出若干元素凑出 6
test({2, 2, 3, 5}, false);
// 测试用例5:
// nums = [100, 100]
// 总和 = 200,目标和 = 100
// 可以分成 [100] 和 [100]
test({100, 100}, true);
// 测试用例6:
// nums = [1, 1, 1, 1]
// 总和 = 4,目标和 = 2
// 可以分成 [1, 1] 和 [1, 1]
test({1, 1, 1, 1}, true);
// 测试用例7:
// nums = [3, 3, 3, 4, 5]
// 总和 = 18,目标和 = 9
// 可以选择 [4, 5] 凑出 9
test({3, 3, 3, 4, 5}, true);
// 测试用例8:
// nums = [2, 4, 6, 8]
// 总和 = 20,目标和 = 10
// 可以选择 [2, 8] 或 [4, 6] 凑出 10
test({2, 4, 6, 8}, true);
// 测试用例9:
// nums = [1, 2, 5]
// 总和 = 8,目标和 = 4
// 无法凑出 4
test({1, 2, 5}, false);
return 0;
}

4.目标和(OJ题)

算法思路:解法(动态规划):
本题可以直接用暴搜的方法解决.但是稍微用数学知识分析一下,就能转化成我们常见的背包模型的问题.
设我们最终选取的结果中,前面加 + 号的数字之和为 a,前面加 - 号的数字之和为 b,整个数组的总和为 sum,于是我们有:
a + b = suma - b = target
上面两个式子消去 b 之后,可以得到 a = (sum + target) / 2
也就是说,我们仅需在 nums 数组中选择一些数,将它们凑成和为 (sum + target) / 2 即可.
问题就变成了分割等和子集 这道题.
我们可以用相同的分析模式,来处理这道题.
1.状态表示:
dp[i][j] 表示:在前 i 个数中选,总和正好等于 j,一共有多少种选法.
2.状态转移方程:
老规矩,根据最后一个位置的元素,结合题目的要求,我们有选择最后一个元素或者不选择最后一个元素两种策略:
i. 不选 nums[i]:那么我们凑成总和 j 的总方案,就要看在前 i - 1 个元素中选,凑成总和为 j 的方案数.根据状态表示,此时 dp[i][j] = dp[i - 1][j];
ii. 选择 nums[i]:这种情况下是有前提条件的,此时的 nums[i] 应该是小于等于 j.因为如果这个元素都比要凑成的总和大,选择它就没有意义呀.那么我们能够凑成总和为 j 的方案数,就要看在前 i - 1 个元素中选,能否凑成总和为 j - nums[i].根据状态表示,此时 dp[i][j] = dp[i - 1][j - nums[i]].
综上所述,两种情况如果存在的话,应该要累加在一起.因此,状态转移方程为:
dp[i][j] = dp[i - 1][j]
if(nums[i - 1] <= j) dp[i][j] += dp[i - 1][j - nums[i - 1]]
3.初始化:
由于需要用到上一行的数据,因此我们可以先把第一行初始化.
第一行表示不选择任何元素,要凑成目标和 j.只有当目标和为 0 的时候才能做到,因此第一行仅需初始化第一个元素 dp[0][0] = 1
4.填表顺序:
根据状态转移方程,我们需要从上往下填写每一行,每一行的顺序是无所谓的.
5.返回值:
根据状态表示,返回 dp[n][aim] 的值.
其中 n 表示数组的大小,aim 表示要凑的目标和.
6.空间优化:
所有的背包问题,都可以进行空间上的优化.
对于01背包类型的,我们的优化策略是:
i. 删掉第一维;
ii. 修改第二层循环的遍历顺序即可.






核心代码
cpp
//解题思路:将问题转化为 01背包求组合方案数 问题
class Solution
{
public:
//函数功能:给数组元素添加 +/- 号,计算凑出target的方案总数
int findTargetSumWays(vector<int>& nums, int target)
{
//计算数组所有元素的总和sum
int sum = 0;
for(auto x : nums) sum += x;
//核心公式推导:
//设正数和为A,负数和为B,则 A - B = target,A + B = sum
//联立得:A = (sum + target) / 2
//问题转化为:从数组中选若干数,和为A,求方案数
int aim = (sum + target) / 2;
//边界条件判断:
//1.(sum+target)必须是偶数,否则无法整除,无方案
//2.目标和aim不能为负数,否则无方案
if(aim < 0 || (sum + target) % 2) return 0;
//n:数组元素个数
int n = nums.size();
//二维dp数组定义:
//dp[i][j] 表示:前i个元素中挑选,凑出和为j的 方案总数
vector<vector<int>> dp(n + 1, vector<int>(aim + 1));
//dp初始化:
//0个元素,凑和为0,有1种方案(不选任何元素)
dp[0][0] = 1;
//动态规划填表:从上到下遍历
for(int i = 1; i <= n; i++) //遍历前i个元素
for(int j = 0; j <= aim; j++)//遍历目标和j
{
//情况1:不选第i个元素
//方案数 = 前i-1个元素凑出j的方案数
dp[i][j] = dp[i - 1][j];
//情况2:选第i个元素(前提:j >= 当前元素值)
//数组下标从0开始,第i个元素对应 nums[i-1]
if(j >= nums[i - 1])
//方案数累加:选 + 不选 两种情况的总和
dp[i][j] += dp[i - 1][j - nums[i - 1]];
}
//返回结果:前n个元素,凑出和为aim的总方案数
return dp[n][aim];
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
using namespace std;
// 解题思路:将问题转化为 01背包求组合方案数 问题
class Solution
{
public:
// 函数功能:给数组元素添加 +/- 号,计算凑出target的方案总数
int findTargetSumWays(vector<int>& nums, int target)
{
// 计算数组所有元素的总和sum
int sum = 0;
for(auto x : nums) sum += x;
// 边界条件判断:
// 如果 |target| > sum,说明无论怎么加减都无法凑出 target
if(abs(target) > sum) return 0;
// 核心公式推导:
// 设正数和为A,负数和为B
// A - B = target
// A + B = sum
// 联立得:A = (sum + target) / 2
//
// 问题转化为:
// 从数组中选若干数,使其和为 A,求方案数
if((sum + target) % 2) return 0;
int aim = (sum + target) / 2;
// aim不能为负数
if(aim < 0) return 0;
// n:数组元素个数
int n = nums.size();
// 二维dp数组定义:
// dp[i][j] 表示:前i个元素中挑选,凑出和为j的方案总数
vector<vector<int>> dp(n + 1, vector<int>(aim + 1));
// dp初始化:
// 0个元素,凑和为0,有1种方案,即不选任何元素
dp[0][0] = 1;
// 动态规划填表
for(int i = 1; i <= n; i++)
for(int j = 0; j <= aim; j++)
{
// 情况1:不选第i个元素
dp[i][j] = dp[i - 1][j];
// 情况2:选第i个元素
if(j >= nums[i - 1])
dp[i][j] += dp[i - 1][j - nums[i - 1]];
}
// 返回结果:前n个元素,凑出和为aim的总方案数
return dp[n][aim];
}
};
void printVector(const vector<int>& nums)
{
cout << "[";
for(int i = 0; i < nums.size(); i++)
{
cout << nums[i];
if(i != nums.size() - 1)
cout << ", ";
}
cout << "]";
}
void test(vector<int> nums, int target, int expected)
{
Solution solution;
int result = solution.findTargetSumWays(nums, target);
cout << "测试数组:";
printVector(nums);
cout << endl;
cout << "target:" << target << endl;
cout << "程序结果:" << result << endl;
cout << "预期结果:" << expected << endl;
cout << "测试结论:";
if(result == expected)
cout << "通过";
else
cout << "未通过";
cout << endl << endl;
}
int main()
{
// 测试用例1:
// nums = [1, 1, 1, 1, 1], target = 3
//
// 一共有5种方案:
// -1 +1 +1 +1 +1 = 3
// +1 -1 +1 +1 +1 = 3
// +1 +1 -1 +1 +1 = 3
// +1 +1 +1 -1 +1 = 3
// +1 +1 +1 +1 -1 = 3
test({1, 1, 1, 1, 1}, 3, 5);
// 测试用例2:
// nums = [1], target = 1
// 只有一种方案:+1 = 1
test({1}, 1, 1);
// 测试用例3:
// nums = [1], target = -1
// 只有一种方案:-1 = -1
test({1}, -1, 1);
// 测试用例4:
// nums = [1, 2, 3], target = 0
// sum = 6
// aim = (6 + 0) / 2 = 3
// 能凑出3的组合有:[3]、[1,2]
// 所以方案数为2
test({1, 2, 3}, 0, 2);
// 测试用例5:
// nums = [2, 3, 5, 6], target = 4
// sum = 16
// aim = (16 + 4) / 2 = 10
// 能凑出10的组合有:[2,3,5]
// 所以方案数为1
test({2, 3, 5, 6}, 4, 1);
// 测试用例6:
// nums = [1, 2, 7], target = 4
// sum = 10
// aim = (10 + 4) / 2 = 7
// 能凑出7的组合有:[7]
// 所以方案数为1
test({1, 2, 7}, 4, 1);
// 测试用例7:
// nums = [1, 2, 7], target = 5
// sum = 10
// sum + target = 15,是奇数
// 无法转化为整数目标和,所以方案数为0
test({1, 2, 7}, 5, 0);
// 测试用例8:
// nums = [1, 2, 3], target = 10
// target绝对值大于sum,无论如何都凑不出来
test({1, 2, 3}, 10, 0);
// 测试用例9:
// nums = [0, 0, 1], target = 1
//
// 0可以加正号,也可以加负号:
// +0 和 -0 数值一样,但属于不同符号方案
//
// 两个0分别有2种选择,共 2 * 2 = 4 种
// 最后1取正号
// 所以总方案数为4
test({0, 0, 1}, 1, 4);
return 0;
}

核心代码
cpp
//空间优化后
//解题思路:转化为 01背包求组合方案数,用滚动数组优化空间
class Solution
{
public:
//函数功能:给数组元素添加 +/- 符号,计算凑出目标值target的总方案数
int findTargetSumWays(vector<int>& nums, int target)
{
//计算数组所有元素的总和
int sum = 0;
for(auto x : nums) sum += x;
//核心公式推导:
//设正数和为A,负数和为B → A - B = target,A + B = sum
//联立方程得:A = (sum + target) / 2
//问题转化:从数组中选若干数,和为A,求总方案数
int aim = (sum + target) / 2;
//边界条件判断,不满足则直接返回0种方案:
//1.目标和aim为负数,无合法方案
//2.sum+target是奇数,无法整除,无合法方案
if(aim < 0 || (sum + target) % 2) return 0;
//n:数组元素个数
int n = nums.size();
//一维滚动数组优化:
//dp[j] 表示:凑出元素和为 j 的总方案数
vector<int> dp(aim + 1);
//初始化:凑出和为0,只有1种方案(不选任何元素)
dp[0] = 1;
//外层循环:遍历每一个元素(01背包的「物品」)
for(int i = 1; i <= n; i++)
//内层循环:逆序遍历目标和(01背包一维优化核心!)
//逆序遍历:保证每个元素只能被选一次,避免重复选取
for(int j = aim; j >= nums[i - 1]; j--)
//状态转移方程:
//不选当前元素:保持dp[j]原值
//选当前元素:方案数 += 凑出 j-当前元素值 的方案数
dp[j] += dp[j - nums[i - 1]];
//返回结果:凑出和为aim的总方案数
return dp[aim];
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
using namespace std;
// 空间优化后
// 解题思路:转化为 01背包求组合方案数,用滚动数组优化空间
class Solution
{
public:
// 函数功能:给数组元素添加 +/- 符号,计算凑出目标值target的总方案数
int findTargetSumWays(vector<int>& nums, int target)
{
// 计算数组所有元素的总和
int sum = 0;
for(auto x : nums) sum += x;
// 边界判断1:
// 如果 target 的绝对值大于 sum,说明无论怎么加减都不可能凑出 target
if(abs(target) > sum) return 0;
// 边界判断2:
// sum + target 必须是偶数,否则无法得到整数 aim
if((sum + target) % 2) return 0;
// 核心公式推导:
// 设正数和为A,负数和为B
// A - B = target
// A + B = sum
// 联立得:A = (sum + target) / 2
int aim = (sum + target) / 2;
// aim 为负数,无合法方案
if(aim < 0) return 0;
// n:数组元素个数
int n = nums.size();
// 一维滚动数组优化:
// dp[j] 表示:凑出元素和为 j 的总方案数
vector<int> dp(aim + 1);
// 初始化:凑出和为0,只有1种方案,即不选任何元素
dp[0] = 1;
// 外层循环:遍历每一个元素
for(int i = 1; i <= n; i++)
// 内层循环:逆序遍历目标和
// 逆序遍历保证每个元素只能使用一次
for(int j = aim; j >= nums[i - 1]; j--)
// 状态转移:
// 不选当前元素:dp[j]保持不变
// 选当前元素:累加dp[j - nums[i - 1]]
dp[j] += dp[j - nums[i - 1]];
// 返回结果:凑出和为aim的总方案数
return dp[aim];
}
};
void printVector(const vector<int>& nums)
{
cout << "[";
for(int i = 0; i < nums.size(); i++)
{
cout << nums[i];
if(i != nums.size() - 1)
cout << ", ";
}
cout << "]";
}
void test(vector<int> nums, int target, int expected)
{
Solution solution;
int result = solution.findTargetSumWays(nums, target);
cout << "测试数组:";
printVector(nums);
cout << endl;
cout << "target:" << target << endl;
cout << "程序结果:" << result << endl;
cout << "预期结果:" << expected << endl;
cout << "测试结论:";
if(result == expected)
cout << "通过";
else
cout << "未通过";
cout << endl << endl;
}
int main()
{
// 测试用例1:
// nums = [1, 1, 1, 1, 1], target = 3
//
// 一共有5种方案:
// -1 +1 +1 +1 +1 = 3
// +1 -1 +1 +1 +1 = 3
// +1 +1 -1 +1 +1 = 3
// +1 +1 +1 -1 +1 = 3
// +1 +1 +1 +1 -1 = 3
test({1, 1, 1, 1, 1}, 3, 5);
// 测试用例2:
// nums = [1], target = 1
// 只有一种方案:+1 = 1
test({1}, 1, 1);
// 测试用例3:
// nums = [1], target = -1
// 只有一种方案:-1 = -1
test({1}, -1, 1);
// 测试用例4:
// nums = [1, 2, 3], target = 0
//
// sum = 6
// aim = (6 + 0) / 2 = 3
//
// 能凑出3的组合有:
// [3]
// [1, 2]
//
// 所以方案数为2
test({1, 2, 3}, 0, 2);
// 测试用例5:
// nums = [2, 3, 5, 6], target = 4
//
// sum = 16
// aim = (16 + 4) / 2 = 10
//
// 能凑出10的组合有:
// [2, 3, 5]
//
// 所以方案数为1
test({2, 3, 5, 6}, 4, 1);
// 测试用例6:
// nums = [1, 2, 7], target = 4
//
// sum = 10
// aim = (10 + 4) / 2 = 7
//
// 能凑出7的组合有:
// [7]
//
// 所以方案数为1
test({1, 2, 7}, 4, 1);
// 测试用例7:
// nums = [1, 2, 7], target = 5
//
// sum = 10
// sum + target = 15,是奇数
// 无法转化为整数目标和,所以无方案
test({1, 2, 7}, 5, 0);
// 测试用例8:
// nums = [1, 2, 3], target = 10
//
// target 的绝对值大于 sum
// 无论如何添加 + / - 都无法凑出 10
test({1, 2, 3}, 10, 0);
// 测试用例9:
// nums = [0, 0, 1], target = 1
//
// 两个0都可以选择 +0 或 -0
// 虽然数值不变,但符号方案不同
//
// 第一个0:2种选择
// 第二个0:2种选择
// 数字1:只能取 +1
//
// 总方案数 = 2 * 2 = 4
test({0, 0, 1}, 1, 4);
// 测试用例10:
// nums = [0, 0, 0, 0, 1], target = 1
//
// 四个0分别都有 +0 / -0 两种选择
// 总方案数 = 2^4 = 16
test({0, 0, 0, 0, 1}, 1, 16);
// 测试用例11:
// nums = [1, 2, 1], target = 0
//
// sum = 4
// aim = (4 + 0) / 2 = 2
//
// 能凑出2的组合有:
// [2]
// [1, 1]
//
// 所以方案数为2
test({1, 2, 1}, 0, 2);
return 0;
}

5.最后一块石头的重量||(OJ题)

算法思路:解法(动态规划):
先将问题转化成我们熟悉的题型.
- 任意两块石头在一起粉碎,重量相同的部分会被丢掉,重量有差异的部分会被留下来.那就相当于在原始的数据的前面,加上加号或者减号,使最终的结果最小即可.也就是说把原始的石头分成两部分,两部分的和越接近越好.
- 又因为当所有元素的和固定时,分成的两部分越接近数组总和的一半,两者的差越小.
因此问题就变成了:在数组中选择一些数,让这些数的和尽量接近 sum / 2,如果把数看成物品,每个数的值看成体积和价值,问题就变成了01背包问题.
1.状态表示:
dp[i][j] 表示在前 i 个元素中选择,总和不超过 j,此时所有元素的最大和.
2.状态转移方程:
老规矩,根据最后一个位置的元素,结合题目的要求,分情况讨论:
i. 不选 stones[i]:那么我们是否能够凑成总和为 j,就要看在前 i - 1 个元素中选,能否凑成总和为 j.根据状态表示,此时 dp[i][j] = dp[i - 1][j];
ii. 选择 stones[i]:这种情况下是有前提条件的,此时的 stones[i] 应该是小于等于 j.因为如果这个元素都比要凑成的总和大,选择它就没有意义呀.那么我们是否能够凑成总和为 j,就要看在前 i - 1 个元素中选,能否凑成总和为 j - stones[i].根据状态表示,此时 dp[i][j] = dp[i - 1][j - stones[i]] + stones[i].
综上所述,我们要的是最大价值.因此,状态转移方程为:
dp[i][j] = dp[i - 1][j]
if(j >= stones[i]) dp[i][j] = dp[i][j] + dp[i - 1][j - stones[i]] + stones[i]
3.初始化:
由于需要用到上一行的数据,因此我们可以先把第一行初始化.
第一行表示没有石子.因此想凑成目标和 j,最大和都是 0.
4.填表顺序:
根据状态转移方程,我们需要从上往下填写每一行,每一行的顺序是无所谓的.
5.返回值:
a. 根据状态表示,先找到最接近 sum / 2 的最大和 dp[n][sum / 2];
b. 因为我们要的是两堆石子的差,因此返回 sum - 2 * dp[n][sum / 2].
6.空间优化:
所有的背包问题,都可以进行空间上的优化.
对于01背包类型的,我们的优化策略是:
i. 删掉第一维;
ii. 修改第二层循环的遍历顺序即可.







核心代码
cpp
//解题思路:将问题转化为 01背包问题 - 把石头分成重量最接近的两堆,求最小重量差
class Solution
{
public:
//函数功能:根据石头粉碎规则,返回最后可能的最小石头重量
int lastStoneWeightII(vector<int>& stones)
{
//1.准备工作:计算所有石头的总重量
int sum = 0;
for(auto x : stones) sum += x;
//n:石头数量
//m:背包容量 = 总重量的一半(目标:凑出不超过m的最大重量)
int n = stones.size(), m = sum / 2;
//2.动态规划核心
//二维dp数组定义:
//dp[i][j] 表示:从前i个石头中挑选,总重量不超过j时,能凑出的最大重量
vector<vector<int>> dp(n + 1, vector<int>(m + 1));
//遍历所有石头(外层循环:逐个处理物品)
for(int i = 1; i <= n; i++)
//遍历所有背包容量(内层循环:逐个处理容量)
for(int j = 0; j <= m; j++)
{
//情况1:不选第i个石头
//最大重量 = 前i-1个石头、容量j的最大重量
dp[i][j] = dp[i - 1][j];
//情况2:选第i个石头(前提:当前容量j >= 石头重量)
if(j >= stones[i - 1])
//取「不选」和「选」两种情况的最大值
//选的话:剩余容量j-stones[i-1]的最大重量 + 当前石头重量
dp[i][j] = max(dp[i][j], dp[i - 1][j - stones[i - 1]] + stones[i - 1]);
}
//3.返回结果
//总重量 - 2倍的最大凑出重量 = 两堆石头的最小重量差(即最后剩余石头的重量)
return sum - 2 * dp[n][m];
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
using namespace std;
// 解题思路:将问题转化为 01背包问题
// 把石头分成重量最接近的两堆,求最小重量差
class Solution
{
public:
// 函数功能:根据石头粉碎规则,返回最后可能的最小石头重量
int lastStoneWeightII(vector<int>& stones)
{
// 1. 准备工作:计算所有石头的总重量
int sum = 0;
for(auto x : stones) sum += x;
// n:石头数量
// m:背包容量 = 总重量的一半
// 目标:凑出不超过 m 的最大重量
int n = stones.size(), m = sum / 2;
// 2. 动态规划核心
// dp[i][j] 表示:
// 从前 i 个石头中挑选,总重量不超过 j 时,能凑出的最大重量
vector<vector<int>> dp(n + 1, vector<int>(m + 1));
// 遍历所有石头
for(int i = 1; i <= n; i++)
// 遍历所有背包容量
for(int j = 0; j <= m; j++)
{
// 情况1:不选第 i 个石头
dp[i][j] = dp[i - 1][j];
// 情况2:选第 i 个石头
if(j >= stones[i - 1])
dp[i][j] = max(
dp[i][j],
dp[i - 1][j - stones[i - 1]] + stones[i - 1]
);
}
// 总重量 - 2倍的最大凑出重量
// = 两堆石头的最小重量差
return sum - 2 * dp[n][m];
}
};
void printVector(const vector<int>& stones)
{
cout << "[";
for(int i = 0; i < stones.size(); i++)
{
cout << stones[i];
if(i != stones.size() - 1)
cout << ", ";
}
cout << "]";
}
void test(vector<int> stones, int expected)
{
Solution solution;
int result = solution.lastStoneWeightII(stones);
cout << "测试石头重量:";
printVector(stones);
cout << endl;
cout << "程序结果:" << result << endl;
cout << "预期结果:" << expected << endl;
cout << "测试结论:";
if(result == expected)
cout << "通过";
else
cout << "未通过";
cout << endl << endl;
}
int main()
{
// 测试用例1:
// stones = [2, 7, 4, 1, 8, 1]
//
// 总重量 sum = 23
// 目标容量 m = 23 / 2 = 11
//
// 可以凑出接近一半的重量 11:
// 例如 [2, 4, 1, 1, 3] 不存在,这里更直观地看:
// 可以选 [7, 4] = 11
//
// 两堆重量分别为 11 和 12
// 最小差值 = 1
test({2, 7, 4, 1, 8, 1}, 1);
// 测试用例2:
// stones = [31, 26, 33, 21, 40]
//
// 总重量 sum = 151
// 目标容量 m = 75
//
// 能凑出的最接近一半的重量是 73
// 另一堆重量是 78
// 最小差值 = 5
test({31, 26, 33, 21, 40}, 5);
// 测试用例3:
// stones = [1, 2]
//
// 两块石头互相粉碎后剩余重量:
// 2 - 1 = 1
test({1, 2}, 1);
// 测试用例4:
// stones = [1, 1]
//
// 两块一样重的石头可以完全抵消
// 最后剩余重量为 0
test({1, 1}, 0);
// 测试用例5:
// stones = [10]
//
// 只有一块石头,无法粉碎
// 最后剩余重量就是 10
test({10}, 10);
// 测试用例6:
// stones = [3, 3, 3, 3]
//
// 总重量 sum = 12
// 可以分成 [3, 3] 和 [3, 3]
// 两堆重量相等,差值为 0
test({3, 3, 3, 3}, 0);
// 测试用例7:
// stones = [1, 3, 5, 7]
//
// 总重量 sum = 16
// 可以分成 [1, 7] 和 [3, 5]
// 两堆重量都是 8
// 最小差值为 0
test({1, 3, 5, 7}, 0);
// 测试用例8:
// stones = [2, 2, 2, 7]
//
// 总重量 sum = 13
// 目标容量 m = 6
// 最接近一半的重量可以凑出 6,即 [2, 2, 2]
// 另一堆重量为 7
// 最小差值为 1
test({2, 2, 2, 7}, 1);
return 0;
}

核心代码
cpp
//空间优化后
//解题思路:将石头分成重量尽可能接近的两堆,差值即为最小剩余重量
//本质转化为:01背包求不超过总重量一半的最大装载重量
class Solution
{
public:
//函数功能:返回按规则粉碎后,最后剩余石头的最小可能重量
int lastStoneWeightII(vector<int>& stones)
{
//1.准备工作
//计算所有石头的总重量sum
int sum = 0;
for(auto x : stones) sum += x;
//n:石头数量;m:背包容量(目标:不超过sum/2的最大重量)
int n = stones.size(), m = sum / 2;
//2.动态规划(一维滚动数组优化)
//dp[j] 定义:背包容量为j时,能装下的石头最大重量
vector<int> dp(m + 1);
//外层循环:遍历每一块石头(01背包的「物品」)
for(int i = 1; i <= n; i++)
//内层循环:逆序遍历背包容量(01背包一维优化核心!)
//逆序遍历:保证每块石头只能被选择一次,避免重复选取
for(int j = m; j >= stones[i - 1]; j--)
//状态转移方程:
//不选当前石头:保持dp[j]原值
//选当前石头:dp[j-当前石头重量] + 当前石头重量
//取两种情况的最大值
dp[j] = max(dp[j], dp[j - stones[i - 1]] + stones[i - 1]);
//3.返回结果
//总重量 - 2倍的最大装载重量 = 两堆石头的重量差(即最终剩余的最小重量)
return sum - 2 * dp[m];
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
using namespace std;
// 空间优化后
// 解题思路:将石头分成重量尽可能接近的两堆,差值即为最小剩余重量
// 本质转化为:01背包求不超过总重量一半的最大装载重量
class Solution
{
public:
// 函数功能:返回按规则粉碎后,最后剩余石头的最小可能重量
int lastStoneWeightII(vector<int>& stones)
{
// 1. 准备工作
// 计算所有石头的总重量sum
int sum = 0;
for(auto x : stones) sum += x;
// n:石头数量;m:背包容量
// 目标:不超过sum/2的最大重量
int n = stones.size(), m = sum / 2;
// 2. 动态规划:一维滚动数组优化
// dp[j] 定义:背包容量为j时,能装下的石头最大重量
vector<int> dp(m + 1);
// 外层循环:遍历每一块石头
for(int i = 1; i <= n; i++)
// 内层循环:逆序遍历背包容量
// 逆序遍历保证每块石头只能被选择一次
for(int j = m; j >= stones[i - 1]; j--)
dp[j] = max(dp[j], dp[j - stones[i - 1]] + stones[i - 1]);
// 总重量 - 2倍的最大装载重量
// = 两堆石头的重量差
return sum - 2 * dp[m];
}
};
void printVector(const vector<int>& stones)
{
cout << "[";
for(int i = 0; i < stones.size(); i++)
{
cout << stones[i];
if(i != stones.size() - 1)
cout << ", ";
}
cout << "]";
}
void test(vector<int> stones, int expected)
{
Solution solution;
int result = solution.lastStoneWeightII(stones);
cout << "测试石头重量:";
printVector(stones);
cout << endl;
cout << "程序结果:" << result << endl;
cout << "预期结果:" << expected << endl;
cout << "测试结论:";
if(result == expected)
cout << "通过";
else
cout << "未通过";
cout << endl << endl;
}
int main()
{
// 测试用例1:
// stones = [2, 7, 4, 1, 8, 1]
//
// 总重量 sum = 23
// 背包容量 m = 23 / 2 = 11
//
// 可以凑出一堆重量 11,例如 [7, 4]
// 另一堆重量为 12
// 最小差值 = 12 - 11 = 1
test({2, 7, 4, 1, 8, 1}, 1);
// 测试用例2:
// stones = [31, 26, 33, 21, 40]
//
// 总重量 sum = 151
// 背包容量 m = 75
//
// 最优划分可以让两堆重量尽量接近
// 最终最小差值为 5
test({31, 26, 33, 21, 40}, 5);
// 测试用例3:
// stones = [1, 2]
//
// 两块石头互相粉碎后剩余重量为 1
test({1, 2}, 1);
// 测试用例4:
// stones = [1, 1]
//
// 两块一样重的石头可以完全抵消
// 最后剩余重量为 0
test({1, 1}, 0);
// 测试用例5:
// stones = [10]
//
// 只有一块石头,无法粉碎
// 最后剩余重量就是 10
test({10}, 10);
// 测试用例6:
// stones = [3, 3, 3, 3]
//
// 总重量 sum = 12
// 可以分成 [3, 3] 和 [3, 3]
// 两堆重量相等
// 最小差值为 0
test({3, 3, 3, 3}, 0);
// 测试用例7:
// stones = [1, 3, 5, 7]
//
// 总重量 sum = 16
// 可以分成 [1, 7] 和 [3, 5]
// 两堆重量都是 8
// 最小差值为 0
test({1, 3, 5, 7}, 0);
// 测试用例8:
// stones = [2, 2, 2, 7]
//
// 总重量 sum = 13
// 背包容量 m = 6
// 可以凑出一堆重量 6,即 [2, 2, 2]
// 另一堆重量为 7
// 最小差值为 1
test({2, 2, 2, 7}, 1);
// 测试用例9:
// stones = [5, 5, 5]
//
// 总重量 sum = 15
// 背包容量 m = 7
// 最多只能凑出 5
// 另一堆重量为 10
// 最小差值为 5
test({5, 5, 5}, 5);
// 测试用例10:
// stones = [6, 7, 8, 9]
//
// 总重量 sum = 30
// 背包容量 m = 15
// 可以凑出 15,例如 [6, 9] 或 [7, 8]
// 两堆重量都是 15
// 最小差值为 0
test({6, 7, 8, 9}, 0);
return 0;
}


🚀真正的勇者不是流泪的人,而是含泪奔跑的人!
敬请期待下一篇文章内容【动态规划算法】(完全背包问题从状态定义到空间优化)
每日心灵鸡汤:"艰难困苦、玉汝于成."
很喜欢一段话:人生这条路很长,未来星辰大海般璀璨,不必踌躇于过去的半亩方塘,那些所谓的遗憾,可能是一种成长,那些曾受过的伤,终会化作照亮前路的光.总有一天你会明白真正能治愈你的,从来不是时间,而是你心里那段释怀和格局,只要你的内心不慌乱,连世界都难影响你.我们唯一应该厌倦的,是那些黯淡无光的日子,改变自己内心是第一步,把思想化作行动,拥有独立人格,提升自己,幸福会如阳光一样,出现在黎明破晓时刻.
