这两道题分别是动态规划中背包问题的一种,我们可以大致把背包问题分为01背包和完全背包以及组合背包问题。
1.
2.首先是背包分类的模板:
3.问题分类的模板:
链接:https://www.lintcode.com/problem/125/
题解:
仔细感觉就会发现其实是在填这个物品和容积所构成的一个表格(一定要对于物品和容量所开辟的表格多开一行一列,这样做的目的也是为了能够直接进行初始化),最终需要返回的是dp[n][m],也就是从前n个物品当中任意选择物品,且体积总和不超过m的最大价值。
class Solution{
public:
int backPackII(int m, vector<int> &A, vector<int> &V) {
// write your code here
int n = A.size();
vector<vector<int>> dp(n+1,vector<int>(m+1,0));
for(int i = 1;i<=n;++i)
{
for(int j = 1;j<=m;++j)
{
if(A[i-1] > j)
{
//肯定是放不下的
dp[i][j] = dp[i-1][j];
}
else
{
dp[i][j] = std::max(dp[i-1][j-A[i-1]]+V[i-1],dp[i-1][j]);
}
}
}
return dp[n][m];
}
};
这道题和sum和一样,如何找到这道题是一道01背包问题,是这道题的关键。
class Solution {
public:
//left - right = 0;
//left + right = sum;
//2left = sum -> left = sum/2
bool canPartition(vector<int>& nums) {
int sum = 0;
for(int& e: nums)
sum += e;
if(sum % 2 == 1)
return false;
int bigSize = sum /2;
vector<vector<int>> dp(nums.size()+1,vector<int>(bigSize+1,false));
dp[0][0] = true;
for(int i = 1;i<=nums.size();++i)
{
for(int j = 1;j<=bigSize;++j)
{
if(j < nums[i-1])
dp[i][j] = dp[i-1][j];
else
dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]];
}
}
return dp[nums.size()][bigSize];
}
};
target可能题目已经给出(显式),也可能是需要我们从题目的信息中挖掘出来(非显式)(常见的非显式target比如sum/2等)
class Solution {
public:
//这道题就是典型的隐式target,那么如何得到这个target呢?
//怎么看出来他是01背包问题呢?
//left - right = target
//数组和sum(固定的)
//left -(sum-left) = target
//left = (target+sum) / 2
//此时我们就得到了,那么背包的容量
//dp[i]表示当容量为i的时候,能装满有几种方式
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
for(int& e: nums)
sum += e;
if(abs(target) > sum)
return 0;
//此时我们可以假设sum 是5,target是2,那么此时就是无解
if((sum + target) % 2 == 1)
return 0;
int bigsize = (sum + target) / 2;//背包的容积
vector<int> dp(bigsize+1,0);
dp[0] = 1;
for(int i = 0;i<nums.size();++i)
{
//对于01背包写为一维的一定在内循环要写成倒叙,因为只有这样才能够保证每个物品不被重复选择
for(int j = bigsize;j >= nums[i];j--)
{
dp[j] += dp[j -nums[i]];
}
}
return dp[bigsize];
}
};
class Solution {
public:
//好像有那么一点点味道了,我们可以把这道题抽象成变态青蛙跳台阶的问题,我可以选择一次走1步,2步或者5步
//现在我要求得,走到第11步的时候跳跃步骤最少的方法
//最少的硬币个数
//dp[i]表示当金额为i时所需要的最小硬币个数
//这个dp数组存放的是从0一直到amount过程中到达每一个位置所需要的最小步数
int coinChange(vector<int>& coins, int amount) {
int Max = amount + 1;
vector<int> dp(amount + 1, Max);
dp[0] = 0;
for (int i = 1; i <= amount; ++i) {
for (int j = 0; j < coins.size(); ++j) {
if (coins[j] <= i) {
dp[i] = min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
};
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount+1,0);
dp[0] = 1;
//对于组合数是需要考虑到去重问题的,如果是这样进行循环那么就有可能会出现顺序不同,但是组合相同的结果
//总结一下就是先遍历物品在遍历金额会更好一些
for(int i = 0;i<coins.size();++i)
{
for(int j = 1;j<=amount;++j)
{
if(coins[i] <= j)
dp[j] += dp[j-coins[i]];
}
}
return dp[amount];
}
};
class Solution {
public:
//组合数和排列数是不一样的
//你仔细感觉这道题是不是和零钱兑换的题目很像
//根据测试用例可以知道,这道题是一个求排列数的题目
int combinationSum4(vector<int>& nums, int target) {
vector<int> dp(target+1,0);
//这里是不是还应该考虑一下dp的初始条件的情况
//因为你的递归这里的时候,dp[1] += dp[0] ,如果此时dp[0]初始化是0,那么dp[1]的组合数也是0,是错误的
dp[0] = 1;
for(int i = 1;i<=target;++i)
{
for(int j = 0;j<nums.size();++j)
{
//其实题目也给出了一个隐蔽的条件:保证答案符合32为整数范围
if(nums[j] <= i && dp[i-nums[j]] < INT_MAX -dp[i])
dp[i] += dp[i-nums[j]];
}
}
return dp[target];
}
};
LeetCode题目链接:https://leetcode-cn.com/problems/perfect-squares/
记住了:只要是求最小值初始化的时候就应该使用INT_MAX,因为如果初始化为0的话,可能会造成覆盖,出现错误
class Solution {
public:
int numSquares(int n) {
//和为n的完全平方数的最少数量,看到了这个最小数量就应该要想到是否这道题是完全
//对于这道题我们需要分析的就是何为物品何为容积
//根据测试用例也可以知道,每一个数都是可以被重复选择的
//这道题给我们提供了一种方式就是如何,来取得1,4,9这些数的方法
vector<int> dp(n+1,INT_MAX);
dp[0] = 0;
for(int i = 1;i<=n;++i) //可以看成容量
{
for(int j = 1;j*j <=i;++j) //可以看成物品
{
dp[i] = min(dp[i],dp[i-j*j]+1);
}
}
return dp[n];
}
};
LeetCode题目链接:https://leetcode-cn.com/problems/word-break/
题解:单词就是物品,字符串s就是背包,单词能否组成字符串s,就是问物品能不能把背包装满。
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
//能否使用字典里面给出来的字符串拼接出来我们想要的字符串s
//但我还是没想出来为什么这道题可以使用动态规划的思想来做
//dp[i]表示,当字符串长度为i的时候,能够切割出来1个或者多个单词在字典中找到
unordered_set<string> us(wordDict.begin(),wordDict.end());
vector<bool> dp(s.size()+1,false);
dp[0] = true; //你会发现这个是整个递归的七点,如果他是false,那么后面的许多结果都是不正确的
for(int i = 1;i<=s.size();++i)
{
for(int j = 0;j<i;++j)
{
//这里是要得到当字符串长度为i的时候,所有字串的可能
//前八位能否拆分取决于前五位能否拆分,加上五到八位是否属于字典
//这里你也就知道为什么解题答案要这么写了
string str = s.substr(j,i-j);//(起始位置,截取的个数)
if(us.find(str) != us.end() && dp[j])
dp[i] = true;
}
}
return dp[s.size()];
}
};
LeetCode题目链接:https://leetcode-cn.com/problems/climbing-stairs/
我很好奇为什么我第一次想这道题的想法却通过不了,他到底和我原来做的爬楼梯的方法有啥不同?真的蠢,那是在初始条件上有不同呀
①正常的动态规划思想
class Solution {
public:
//思考问题要全面呢,如果n<=2 这两种情况也要考虑到
int climbStairs(int n) {
if(n <= 2)
return n;
vector<int> dp(n+1,0);
dp[1] = 1;
dp[2] = 2;
for(int i = 3;i<=n;++i)
{
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
};
②空间复杂度更低一些
class Solution {
public:
int climbStairs(int n) {
//这道题就是很好的一道动态规划的斐波那契额数列的题
//f(n) = f(n-1)+f(n-2)
//这道题和真正的原来做过的爬台阶好像是由那么一点不一样的
//因为初始化的条件不一样
//这样做的最好方法就是优化了空间复杂度
if(n <= 2)
return n;
int f1 = 1;
int f2 = 2;
int ret = 0;
for(int i = 3;i<=n;++i)
{
ret = f1 + f2;
f1 = f2;
f2 = ret;
}
return ret;
}
};
参考Blog: