概述
背包问题简而言之:你有一个背包 这个背包有一定的容积 要装入物品进去 不同物品体积不同 价值不同
背包的情况有两种 :01背包就是每种物品只有一个选了之后就是1不选就是0 完全背包就是每种物品个数可能有多个
装入的情况也有两种:恰好将背包装满的最大物品价值 背包可以不装满的最大价值
示例题目:https://www.nowcoder.com/share/jump/3925989451770378237514

先研究第一种情况 也就是不需要装满
很明显 物品的价值和体积分别要用一个一维数组记录 w、v
若是定义dp[i] 从前i个物品中选择 此时的最大价值 这样是不行的 因为没有东西表示你的背包容积 不知道背包容积去选择物品肯定不行
所以定义dp[i][j] 从前i个物品中选择 并且选择的物品的总体积不超过j 此时的最大价值 现在看能不能推导状态转移方程
i位置物品可以选也可以不选 不选 那么dp[i][j] = dp[i-1][j] 选那么dp[i][j] = w[i] + dp[i-1][j-v[i]] 因为选择了i 那么前面的方程容积肯定不能超过j-v[i] 但是j-v[i] 可能小于0 那么就在选择i物品这种情况之前判断一下j-v[i] >= 0 只有条件满足才可以选 最终dp[i][j] 是这两者之间的最大值
初始化 因为dp存在i-1 所以w、v最好也多开一个空间 让物品的下标从1开始标号 dp也多开一行一列 dp[0][0] = 0 因为其意思为从前0个物品中选 总体积不超过0的最大价值 价值就是0 第一行后面的位置 表示从前0个物品中选总体积不超过j 价值肯定也是0 因为选不到 第一列其他位置 物品存在 但是体积不可能为0 所以价值也是0
返回值返回dp[n][V] V表示背包的容积
第二种情况 背包恰好装满
相比于第一种情况只需要改动一部分即可
状态表示 定义dp[i][j] 从前i个物品中选择 并且选择的物品的总体积恰好为j 此时的最大价值
状态转移方程 这里有一个重点区别 若是选不到体积恰好为j的dp[i][j]应该填什么 显然这种情况是一种不可能的情况 也就是错误情况 这里用-1表示 -1表示错误 。若是不选i位置dp[i][j] = dp[i-1][j] 此时若是 dp[i-1][j] 为-1 也不影响 因为前面选不到体积恰好为 j 的 那么不选 i 之后的 dp[i][j] 肯定也选不到体积恰好为j的 所以也填入-1 若是选 i位置 区别于不需要将背包填满的情况 只需要添加一下条件 原本是dp[i][j] = w[i] + dp[i-1][j-v[i]] 但是dp[i-1][j-v[i]] 可能为-1 若是让-1进来这个方程 物品的价值就被改变了 很显然不合逻辑 并且从状态表示出发 若是 前面dp[i-1][j-v[i]]==-1 这里选了i体积也不能凑成为j的 因此方程不能为dp[i][j] = w[i] + dp[i-1][j-v[i]] 所以想要方程为这个 要加一个条件 就是 dp[i-1][j-v[i]] != -1 从这个两个中选最大值
初始化 dp[0][0] = 0 第一行后面的从前0个物品中选 全部选不到容积恰好为j的 都填-1 第一列下面的 为全部为0
返回值 返回dp[n][V]
代码:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
int n, V;
cin >> n >> V;
vector<int> v(n+1, 0), w(n+1, 0);
vector<vector<int>> dp(n+1, vector<int>(V+1, 0));
for (int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= V; j++)
{
dp[i][j] = dp[i-1][j];
if (j - v[i] >= 0) dp[i][j] = max(dp[i][j], w[i] + dp[i-1][j-v[i]]);
}
}
cout << dp[n][V] << endl;
for (int j = 1; j <= V; j++) dp[0][j] = -1;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= V; j++)
{
dp[i][j] = dp[i-1][j];
if (j - v[i] >= 0 && dp[i-1][j-v[i]] != -1) dp[i][j] = max(dp[i][j], w[i] + dp[i-1][j-v[i]]);
}
}
cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;
return 0;
}
空间优化 滚动数组
原dp是二维的 但是每次填表需要的两个位置在同一行 那么可以这样优化空间 开两个一维数组 大小为V+1 第一个数组记录i-1那一行 也就是此时填表需要的一行 第二个数组表示当前行 填完当前行之后将第一个数组赋值为当前数组 那么填后面的dp时 填到第二个数组里面 参照第一个数组 这样向下滚动的过程就是滚动数组 完成了空间的优化 这是最基本的滚动数组
本题用另一种更佳的方式优化 填当前位置的时候 其实只需要两个位置 i-1行的两个位置 那么可以只开一个V+1的一维数组 这个数组记录的是i-1行的信息 此时为i行开始填表 从后往前填 这样可以保证i-1行的我们需要的两个位置没有被覆盖 也就是确保i行的内容是正确的 这样完成空间优化
在上述代码上的修改实际上就是要消去dp表的行保存列 并且改一下填表顺序即可
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
int n, V;
cin >> n >> V;
vector<int> v(n+1, 0), w(n+1, 0);
vector<int> dp(V+1, 0);
for (int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
for (int i = 1; i <= n; i++)
{
for (int j = V; j >= 1; j--)
{
// dp[j] = dp[j]; // 去掉行之后就是这样的 那可以直接省略
if (j - v[i] >= 0) dp[j] = max(dp[j], w[i] + dp[j-v[i]]);
}
}
cout << dp[V] << endl;
for (int j = 1; j <= V; j++) dp[j] = -1;
for (int i = 1; i <= n; i++)
{
for (int j = V; j >= 1; j--)
{
// dp[j] = dp[j];
if (j - v[i] >= 0 && dp[j-v[i]] != -1) dp[j] = max(dp[j], w[i] + dp[j-v[i]]);
}
}
cout << (dp[V] == -1 ? 0 : dp[V]) << endl;
return 0;
}
题目
416. 分割等和子集 - 力扣(LeetCode)
class Solution
{
public:
bool canPartition(vector<int>& nums)
{
int n = nums.size();
int sum = 0;
for (auto& num : nums) sum += num;
if (sum % 2 != 0) return false;
vector<vector<bool>> dp(n+1, vector<bool>(sum/2+1, false));
for (int i = 1; i <= n; i++) dp[i][0] = true;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= sum / 2; j++)
{
dp[i][j] = dp[i-1][j];
if (j - nums[i-1] >= 0) dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]];
}
}
return dp[n][sum/2];
}
};
// 先分析 我们只需要从数组中选则一部分元素 使之和为sum / 2 sum为nums中元素总和
// 那么每个元素就面临着选与不选的问题 从此我们想到01背包 这是物品没有价值的01背包 只需要使体积为sum / 2
// 状态表示
// 定义dp[i][j] : 从前i个元素中选 看能否等于j dp中存的值为true或者false
// 状态转移方程
// 不选i位置 为dp[i-1][j] 选i位置 为dp[i-1][j-nums[i]] 当然要有条件限制 就是j - nums[i] >= 0
// 初始化
// dp表多开一行一列 此时要注意下标的映射关系
// dp[0][0] = true 第一行后面的全为false 因为不可能从0中选出不为0的数 第一列下面全为true 每一次都不选即可
// 返回值 返回dp[n][sum/2] 要注意只有sum为偶数才能这样动态规划 为奇数一定不能分割 直接返回false
// 空间优化
class Solution
{
public:
bool canPartition(vector<int>& nums)
{
int n = nums.size();
int sum = 0;
for (auto& num : nums) sum += num;
if (sum % 2 != 0) return false;
vector<bool> dp(sum / 2 + 1, false);
dp[0] = true;
for (int i = 1; i <= n; i++)
{
for (int j = sum / 2; j >= 1; j--)
{
dp[j] = dp[j];
if (j - nums[i-1] >= 0) dp[j] = dp[j] || dp[j-nums[i-1]];
}
}
return dp[sum/2];
}
};
494. 目标和 - 力扣(LeetCode)
// class Solution
// {
// public:
// int findTargetSumWays(vector<int>& nums, int target)
// {
// int sum = 0;
// for (auto& i : nums)
// sum += i;
// int a = (sum + target) / 2;
// if ((sum + target) % 2 || a < 0)
// return 0;
// int n = nums.size();
// vector<vector<int>> dp(n+1, vector<int>(a+1, 0));
// dp[0][0] = 1;
// for (int i = 1; i <= n; i++)
// {
// for (int j = 0; j <= a; j++) // j从0开始因为没有初始化第一列
// {
// dp[i][j] = dp[i-1][j];
// if (j - nums[i-1] >= 0)
// dp[i][j] += dp[i-1][j - nums[i-1]];
// }
// }
// return dp[n][a];
// }
// };
// 空间优化
class Solution
{
public:
int findTargetSumWays(vector<int>& nums, int target)
{
int sum = 0;
for (auto& i : nums)
sum += i;
int a = (sum + target) / 2;
if ((sum + target) % 2 || a < 0)
return 0;
int n = nums.size();
vector<int> dp(a+1);
dp[0] = 1;
for (int i = 1; i <= n; i++)
{
for (int j = a; j >= 0; j--) // j从0开始因为没有初始化第一列
{
if (j - nums[i-1] >= 0)
dp[j] += dp[j - nums[i-1]];
}
}
return dp[a];
}
};
// 转换:将nums中的数分为正负两个部分(a、b表示正负两部分绝对值的和)并且a-b=target(按照提议反推,假设a、b就是我们最终分出来的正负部分)
// 又因为a+b=sum,那么消去b,a = (sum + target)/ 2
// 这样一来就是01背包了,意思就是选择物品,使之总体积恰好为一个数
// 定义dp[i][j] 从前i个物品中挑选,体积恰好我j的选法
// 状态转移方程:
// 不选nums[i],那么dp[i][j]有dp[i-1][j]种选法;选nums[i],那么dp[i][j]有dp[i-1][j-nums[i]]种选法(要判断j-nums[i] >= 0)
// 最终dp[i][j]为两者相加
// 初始化
// 多开一行一列
// 第一行除了dp[0][0]全部初始化为0,dp[0][0]=1
// 第一列除了dp[0][0],看是什么情况:因为nums[i]可能为0,那么从前i个位置选体积为0就可能出现多种情况了,比如dp[1][0],但是nums[1]=0,后面也可能会出现,这样一来不能简单地初始化0或者1了。事实上这一行不用初始化,通过观察,选择这个位置要先判断j-nums[i] >= 0,而j为0,nums[i]>=0,只有nums[i]==0的时候第一列才会填表,但是现在使用的上一列的值(j-nums[i] = 0),没使用左上角,所以不用初始化
// 返回值
// dp[n][a]
1046. 最后一块石头的重量 - 力扣(LeetCode)
class Solution
{
public:
int lastStoneWeightII(vector<int>& stones)
{
int n = stones.size(), sum = 0;
for (int i = 0; i < n; i++)
sum += stones[i];
int target = sum / 2;
vector<int> dp(target + 1, 0);
for (int i = 1; i <= n; i++)
{
for (int j = target; j >= 1; j--)
{
if (j - stones[i - 1] >= 0)
dp[j] = max(dp[j], dp[j - stones[i - 1]] + stones[i - 1]);
}
}
return sum - 2 * dp[target];
}
};
// 问题转换:
// 本质上就是给数字前面加上+、-号然后相加求出最小和,和目标和类似
// 将整个数组中数字分为正负两部分,求出两者绝对值的最小差值
// 一个数字只有将其分为接近这个数字一半的两部分,这两部分的差值才是最小的
// 01背包
// 背包容积sum/2,物品体积、价值都是数字数值
完全背包_牛客题霸_牛客网
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int n, V;
cin >> n >> V;
vector<int> v(n), w(n);
for (int i = 0; i < n; i++)
{
cin >> v[i] >> w[i];
}
vector<vector<int>> dp(n + 1, vector<int>(V + 1, 0));
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= V; j++)
{
dp[i][j] = dp[i - 1][j];
if (j - v[i - 1] >= 0)
dp[i][j] = max(dp[i][j], dp[i][j - v[i - 1]] + w[i - 1]);
}
}
cout << dp[n][V] << endl;
for (int j = 1; j <= V; j++)
dp[0][j] = -1;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= V; j++)
{
dp[i][j] = dp[i - 1][j];
if (j - v[i - 1] >= 0 && dp[i][j - v[i - 1]] != -1)
dp[i][j] = max(dp[i][j], dp[i][j - v[i - 1]] + w[i - 1]);
}
}
cout << (dp[n][V] == -1 ? 0 : dp[n][V]) << endl;
return 0;
}
// 第一问
// 状态表示
// 定义dp[i][j]:从前i个物品中挑选,总体积不超过j的最大价值
// 状态转移方程
// dp[i][j] : 不选i位置为dp[i-1][j], 选i位置可以选多个i即max(dp[i-1][j-v[i]]+w[i], dp[i-1][j-2*v[i]]+2*w[i]......, dp[i-1][j-k*v[i]]+k*w[i])
// 所以dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]]+w[i], dp[i-1][j-2*v[i]+2*w[i]]......, dp[i-1][j-k*v[i]]+k*w[i]);
// 出现这种很多个方程的情况下,需要考虑简化,利用数学规律
// dp[i][j-v[i]] = max(dp[i-1][j-v[i]], dp[i-1][j-2*v[i]]+w[i], dp[i-1][j-3*v[i]]+2*w[i], ........, dp[i-1][j-x*v[i]]+(x-1)*w[i]);
// 观察第一个公式的右边部分除了第一个的后面部分,和下面公式的右边部分就差一个w[i]
// 所以dp[i][j] = max(dp[i-1][j], dp[i][j-v[i]]+w[i]);
// 初始化
// 多开一行一列,第一行全部为0,第一列全部为0
// 填表顺序
// 只能从上往下,从左往右,因为dp[i][j-v[i]]+w[i]需要用到同行中前面的值
// 返回值
// dp[n][V]
// 第二问
// 状态标识
// 定义dp[i][j]:从前i个物品中挑选,总体积恰好为j的最大价值
// 状态转移方程
// dp[i][j] = max(dp[i-1][j], dp[i][j-v[i]]+w[i]);
// 但是要注意一个点,就是dp[i][j-v[i]]可能存在这种情况,就是不能恰好填满,这个时候设置为-1,所以除了要判断j-v[i]>=0,还需要dp[i][j-v[i]]!=-1
// 初始化
// 多开一行一列,第一行第一个为0,剩下为-1(从前0个物品中选,总体积不可能恰好为一个非0的),第一列全部为0
// 返回值
// 要判断是不是-1
LCR 103. 零钱兑换 - 力扣(LeetCode)
// class Solution
// {
// public:
// const int INF = 0x3f3f3f3f;
// int coinChange(vector<int>& coins, int amount)
// {
// int n = coins.size();
// vector<vector<int>> dp(n+1, vector<int>(amount+1, INF));
// dp[0][0] = 0;
// for (int i = 1; i <= n; i++)
// dp[i][0] = 0;
// for (int i = 1; i <= n; i++)
// {
// for (int j = 1; j <= amount; j++)
// {
// dp[i][j] = dp[i-1][j];
// if (j - coins[i-1] >= 0 && dp[i][j - coins[i-1]] < INF)
// dp[i][j] = min(dp[i][j], dp[i][j - coins[i-1]] + 1);
// }
// }
// return dp[n][amount] >= INF ? -1 : dp[n][amount];
// }
// };
// 空间优化
class Solution
{
public:
const int INF = 0x3f3f3f3f;
int coinChange(vector<int>& coins, int amount)
{
int n = coins.size();
vector<int> dp(amount + 1, INF);
dp[0] = 0;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= amount; j++)
{
if (j - coins[i - 1] >= 0 && dp[j - coins[i - 1]] < INF)
dp[j] = min(dp[j], dp[j - coins[i - 1]] + 1);
}
}
return dp[amount] >= INF ? -1 : dp[amount];
}
};
// 看到无限想到完全背包
// 状态表示
// 定义dp[i][j]:从前i个硬币里面选,总金额恰好为j的最小硬币个数
// 状态转移方程
// 和完全背包模板一样
// dp[i][j] = min(dp[i-1][j], dp[i][j-coins[i]] + 1); (只有完全背包可以用这个数学规律)
// 初始化
// 多开一行一列,不可能的地方填0x3f3f3f3f(已经很大了)
// 返回值
// 先判断是否>=0x3f3f3f3f,大于则返回-1,否则返回dp[n][amount];
518. 零钱兑换 II - 力扣(LeetCode)
// class Solution
// {
// public:
// int change(int amount, vector<int>& coins)
// {
// int n = coins.size();
// vector<vector<unsigned int>> dp(n + 1, vector<unsigned int>(amount + 1, 0)); // 超出范围直接截断
// for (int i = 1; i <= n; i++)
// dp[i][0] = 1;
// for (int i = 1; i <= n; i++)
// {
// for (int j = 1; j <= amount; j++)
// {
// dp[i][j] = dp[i-1][j];
// if (j - coins[i-1] >= 0)
// dp[i][j] += dp[i][j - coins[i-1]];
// }
// }
// return dp[n][amount];
// }
// };
// 空间优化
class Solution
{
public:
int change(int amount, vector<int>& coins)
{
int n = coins.size();
vector<unsigned int> dp(amount + 1, 0); // 超出范围直接截断
dp[0] = 1;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= amount; j++)
{
dp[j] = dp[j];
if (j - coins[i - 1] >= 0)
dp[j] += dp[j - coins[i - 1]];
}
}
return dp[amount];
}
};
// 组合有多少种?每一次选的硬币变换了都要加上这个选法的种类
// 选与不选都是选法,都要加上去
279. 完全平方数 - 力扣(LeetCode)
class Solution
{
public:
const int INF = 0x3f3f3f3f;
int numSquares(int n)
{
int m = sqrt(n);
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for (int j = 1; j <= n; j++)
dp[0][j] = INF; // 因为是最少个数,所以将不可能的选法设置为大的数
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= n; j++)
{
dp[i][j] = dp[i - 1][j];
if (j - i * i >= 0)
{
dp[i][j] = min(dp[i][j], dp[i][j - i * i] + 1);
}
}
}
return dp[m][n];
}
};
// 就相当于从1到根号n的数种找一些数的平方和恰好等于n,求选的数的最少个数
// 完全背包
// dp[i][j] = (dp[i-1][j], dp[i-1][j-i^2]+1, dp[i-1][j-2*i^2]+2........, dp[i-1][j-n*i^2]+n);
// dp[i][j-i^2] = (dp[i-1][j-i^2], dp[i-1][j-2*i^2]+1, dp[i-1][j-3*i^2]+2........, dp[i-1][j-n*i^2]+n);
474. 一和零 - 力扣(LeetCode)
// class Solution
// {
// public:
// int findMaxForm(vector<string>& strs, int m, int n)
// {
// int len = strs.size();
// vector<pair<int, int>> cnt01(len);
// // 计算每个字符串的0、1个数
// for (int i = 0; i < len; i++)
// {
// int cnt0 = 0, cnt1 = 0;
// for (auto& c : strs[i])
// {
// if (c == '0') cnt0++;
// else cnt1++;
// }
// cnt01[i] = make_pair(cnt0, cnt1);
// }
// vector<vector<vector<int>>> dp(len + 1, vector<vector<int>>(m + 1, vector<int>(n + 1, 0)));
// for (int i = 1; i <= len; i++)
// {
// for (int j = 0; j <= m; j++)
// {
// for (int k = 0; k <= n; k++)
// {
// dp[i][j][k] = dp[i - 1][j][k];
// int a = cnt01[i - 1].first, b = cnt01[i - 1].second;
// if (j - a >= 0 && k - b >= 0)
// dp[i][j][k] = max(dp[i][j][k], dp[i - 1][j - a][k - b] + 1);
// }
// }
// }
// return dp[len][m][n];
// }
// };
// 实际上也是完全背包问题,只不过限定条件从一维变为了二维(可以理解为一维只需要保证体积不超,而现在重量也要保证):从数组中选一些字符串,这些字符串0、1的数量和不超过m、n
// 状态表示
// 定义dp[i][j][k]:从前i个字符串中选,0总和不超过j,1总和不超过k的选取字符串最大个数
// 状态转移方程
// 不选i位置,那么dp[i][j][k] = dp[i-1][j][k];
// 选i位置(a、b分别表示i位置字符串0、1的个数),那么dp[i][j][k] = dp[i-1][j-a][k-b] + 1(是最大个数,因此选一个是加1),但是需要保证下标合法
// 最终选两者最大值
// a、b可以提前计算
// 初始化
// 多开一个位置,全部初始化为0即可,从前0个中选使之0、1数量不超过j,k的最大长度明显选不到,所以初始化为0;从前i个中选,使之0,1数量不超过0,0因为没有空串,所以不可能选到,初始化为0即可,dp[0][0][0]这个位置显而易见是0
// 返回值dp[len][m][n]
// 空间优化需要从后往前,避免需要的数据被覆盖
class Solution
{
public:
int findMaxForm(vector<string>& strs, int m, int n)
{
int len = strs.size();
vector<pair<int, int>> cnt01(len);
// 计算每个字符串的0、1个数
for (int i = 0; i < len; i++)
{
int cnt0 = 0, cnt1 = 0;
for (auto& c : strs[i])
{
if (c == '0') cnt0++;
else cnt1++;
}
cnt01[i] = make_pair(cnt0, cnt1);
}
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for (int i = 1; i <= len; i++)
{
for (int j = m; j >= 0; j--)
{
for (int k = n; k >= 0; k--)
{
int a = cnt01[i - 1].first, b = cnt01[i - 1].second;
if (j - a >= 0 && k - b >= 0)
dp[j][k] = max(dp[j][k], dp[j - a][k - b] + 1);
}
}
}
return dp[m][n];
}
};
879. 盈利计划 - 力扣(LeetCode)
class Solution
{
public:
const int mode = 1e9 + 7;
int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit)
{
int m = group.size();
vector<vector<vector<int>>> dp(m + 1, vector<vector<int>>(n + 1, vector<int>(minProfit + 1, 0)));
for (int j = 0; j <= n; j++)
dp[0][j][0] = 1;
for (int i = 1; i <= m; i++)
{
for (int j = 0; j <= n; j++)
{
for (int k = 0; k <= minProfit; k++)
{
dp[i][j][k] = dp[i - 1][j][k];
if (j - group[i - 1] >= 0)
dp[i][j][k] += dp[i - 1][j - group[i - 1]][max(0, k - profit[i - 1])];
dp[i][j][k] %= mode;
}
}
}
return dp[m][n][minProfit];
}
};
// 二维01背包问题
// 状态表示
// 选任务,人数不超过n,并且利润超过minProfit的所有选法
// 状态转移方程
// 定义dp[i][j][k]: 从前i个任务中选,人数不超过j,利润超过k的选法
// 不选i位置任务,则dp[i][j][k] = dp[i-1][j][k]
// 选i位置任务,dp[i][j][k] += dp[i-1][j-group[i]][k-profit[i]]
// 其中j-group[i]不能小于0,小于0说明人数超过限制
// k - profit[i]可以小于0,小于0说明从前i-1任务中选利润超过负数的,显然随便选,但是下标可能越界因此要这样写max(0, k-profit[i]),
// 因为从前i-1个中选利润大于0的也是随便选
// 初始化
// 每一维多开一行
// dp[0][j][0] = 1,无论j是几都只有一种选法,就是不选
39. 组合总和 - 力扣(LeetCode)
class Solution
{
public:
int combinationSum4(vector<int>& nums, int target)
{
int n =nums.size();
vector<unsigned int> dp(target + 1);
dp[0] = 1;
for (int i = 1; i <= target; i++)
{
for (int j = 0; j < n; j++)
{
if (i - nums[j] >= 0)
dp[i] += dp[i - nums[j]];
}
}
return dp[target];
}
};
// 似包非包
// 看似是完全背包问题,但是这题每一种选法可以排列,完全背包解决不了
// 需要找重复子问题(多刷题总结经验)
// 状态表示
// dp[i]:凑成数i的组合总数
// 状态转移方程
// dp[i] += dp[i - nums[j]]
// 初始化
// dp[0] = 1;
// 返回值
// dp[target]
96. 不同的二叉搜索树 - 力扣(LeetCode)
class Solution
{
public:
int numTrees(int n)
{
vector<int> dp(n + 1);
dp[0] = 1;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= i; j++)
dp[i] += dp[i - j] * dp[j - 1];
return dp[n];
}
};
// 需要找重复子问题(多刷题总结经验)
// 状态表示
// dp[i]:以i为根节点能组成几种不同的二叉搜索树
// 选定一个数i(1~n)作为根节点,找以这个值为根节点的二叉搜素树,那么左、右子树的二叉搜索树个数就是重复子问题
// 状态转移方程
// 以i为根节点,左子树的根节点变化在1~i-1之间,右子树根节点变化在i+1~n之间

1497

被折叠的 条评论
为什么被折叠?



