LeetCode----->dp系列
198. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
思路:
dp[i]代表0~i位置所偷得的最大收获
对于某个位置i来说,如果不选择盗窃,则在i位置所偷得的收获就是dp[i-1],因为dp[i-1]代表了0~i-1的最大收获,而i位置未偷,因此dp[i]=dp[i-1];
如果选择偷窃,则在i位置所得的收获就是nums[i]+dp[i-2],因为在i偷窃了,就一定不能在i-1偷窃。对i-1位置来说,如果未发生盗窃,则0~i-1的偷窃最大值也出现在i-2,因为dp[i-1]=dp[i-2]
class Solution { public: int rob(vector<int>& nums) { if(nums.empty()) return 0; vector<int> dp(nums.size()+1,0); dp[1]=nums[0]; for(int i=2;i<dp.size();i++) dp[i]=max(dp[i-1],nums[i-1]+dp[i-2]); return dp.back(); } };
213. 打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
输入: [1,2,3,1]
输出: 4
解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
思路:
这个题和上一题的区别在于,最后一家与第一家是相邻的,也就是说如果最优方案中偷取了第一家,则最后一家一定不能偷取。
如果要偷取最后一家,则第一家一定不能偷取。
基于上述思路,将整个数组分为两种情况:即[0,end-1]和[1,end]。这样在两种情况中,对一种end不存在,对第二种0不存在,也就不存在首位相邻的问题。
剩下的只要分别求出两次的最大值,择最优值即为解。
class Solution { public: int rob(vector<int>& nums) { if(nums.empty()) return 0; if(nums.size()<=2) { return max(nums[0],nums.back()); } vector<int> dp1(nums.size(),0); vector<int> dp2(nums.size(),0); dp1[1]=nums[0]; dp2[1]=nums[1]; for(int i=2;i<dp1.size();i++) { dp1[i]=max(dp1[i-1],dp1[i-2]+nums[i-1]); } for(int i=2;i<dp2.size();i++) { dp2[i]=max(dp2[i-1],dp2[i-2]+nums[i]); } return max(dp1.back(),dp2.back()); } };
信件错排
题目描述:
NowCoder每天要给很多人发邮件。有一天他发现发错了邮件,把发给A的邮件发给了B,把发给B的邮件发给了A。于是他就思考,要给n个人发邮件,在每个人仅收到1封邮件的情况下,有多少种情况是所有人都收到了错误的邮件?
即没有人收到属于自己的邮件。
思路:
dp[i]为i封邮件全错的数量。当在某位置i时:
1).首先在i-1个位置中任选一个位置j,这样有i-1种选法
2).然后对位置j来说,首先将它与i交换,这样j位置放i,我们不看位置j,只要求出其余共i-1个位置的错排数量即可
3).对于其余i-1个位置来说,他们的数量并不是简单的dp[i-1],这是因为现在信件共有i-1封,位置共有i-1个,但位置中i位置对于任何信件都是错排的,也就是说剩余i-1封信件任意选一封放在i位置都是错排的,而对于dp[i-1]来说,某个位置k只有i-2封信件是错排的,因此他们的数量不是简单的dp[i-1];
4).现在对这i-1 封信件与i-1个位置进行计算:考虑换出来的信件j,如果将j放在i位置,则错排数量有dp[i-2]个;而如果不将j放在i,相当于说j不能放在i,也就是说j只能放在其他的共i-2个位置,而其他的信件也只能放在除了本身位置外的i-2个位置,在这种情况下,对每个元素都只能选择i-2个位置,这就相当于dp[i-1]了。
因此综上:
dp[i]=(i-1)*(dp[i-1]+dp[i-2]);//其中dp[i-1]是j不放在i上的数量;dp[i-2]是j放在i上的错排数量;而选出j的方式共有i-1个.
母牛生产
题目描述:假设农场中成熟的母牛每年都会生 1 头小母牛,并且永远不会死。第一年有 1 只小母牛,从第二年开始,母牛开始生小母牛。每只小母牛 3 年之后成熟又可以生小母牛。给定整数 N,求 N 年后牛的数量。
思路:
对于某年i来说:
其牛的来源有两部分:
1) i-1年的牛
2) 新出生的牛
对于1)来说,很简单,i-1的牛的数量为dp[i-1]
而对于2)来说,新出生的牛来源于两部分,一部分为在i年之前就成长为大母牛的母牛生产的牛的数量,另一部分为在i年刚刚所成长为大母牛的母牛所生产的数量。对于i-3年来说,其大母牛一定会在i年生产,而对于i-3年的小母牛来说,其中包括在i-3年刚刚出生的小母牛与在i-3年之前出生的小母牛。i-3年之前出生的小母牛,一定会在i年前转变为大母牛,因此他们也一定会在i年生小母牛。而对于i-3年出生的小母牛,他们一定在第i年转变为大母牛,因此他们也一定会在i年生出小母牛。
或者这样理解,i年出生的小母牛的数量来源于i-1年的大母牛生产的小母牛与在第i年成长为大母牛的小母牛所生产的数量。对于i-1年的大母牛,他们一定出生于i-3年之前,因此在第i-3年的牛中,所有的大母牛和i-3年出生之前的小母牛都能在i-1年转变为大母牛。而对于第i年成长为大母牛的小母牛,他们一定出生在i-3年,因此i-3年所有的牛均可以在第i年生产。
因此:
Dp[i]=Dp[i-1]+Dp[i-3];
64. 最小路径和
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
输入: [ [1,3,1], [1,5,1], [4,2,1] ] 输出: 7 解释: 因为路径 1→3→1→1→1 的总和最小。
思路:
由于只能从上到下与从左到右,因此对某个点(i,j)来说,到达此点的最小值路径和只可能是从上方到达此点或从左方到达此点。
dp[i][j]=min(dp[i-1][j],dp[i][j-1])+grid[i][j]:上方或左方的最小值,再加上此点的路径权值,即为到达(i,j)的最短路径和
class Solution { public: int minPathSum(vector<vector<int>>& grid) { if(grid.empty()||grid[0].empty()) return 0; vector<vector<int>> dp(grid.size()+1,vector<int>(grid[0].size()+1)); dp[1][1]=grid[0][0]; for(int i=1;i<dp[0].size();i++) dp[0][i]=INT_MAX; for(int i=1;i<dp.size();i++) dp[i][0]=INT_MAX; for(int row=1;row<dp.size();row++) { for(int col=1;col<dp[row].size();col++) { if(row==1&&col==1) continue; dp[row][col]=min(dp[row-1][col],dp[row][col-1])+grid[row-1][col-1]; } } return dp.back().back(); } };
62. 不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
问总共有多少条不同的路径?
思路:
哈皮题目,要个锤子思路
[i,j]等于[i,j-1]和[i-1][j]的路径之和
class Solution { public: int uniquePaths(int m, int n) { if(!m||!n) return 0; vector<vector<int>> dp(m+1,vector<int>(n+1,0)); dp[1][1]=1; for(int i=1;i<dp.size();i++) { for(int j=1;j<dp[i].size();j++) { if(i==1&&j==1) continue; dp[i][j]=dp[i-1][j]+dp[i][j-1]; } } return dp.back().back(); } };
303. 区域和检索 - 数组不可变
给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。
示例:
给定 nums = [-2, 0, 3, -5, 2, -1],求和函数为 sumRange()
sumRange(0, 2) -> 1
sumRange(2, 5) -> -1
sumRange(0, 5) -> -3
思路:
用一个缓存缓存每个点的求和结果,这样每次查询就不用了再for来重新计算
class NumArray { public: NumArray(vector<int>& nums) { num=nums; if(num.size()>0) { sum.resize(nums.size(),0); sum[0]=nums[0]; for(int i=1;i<sum.size();i++) sum[i]=sum[i-1]+nums[i]; } } int sumRange(int i, int j) { if(num.empty()) return 0; if(i<0) { if(j>=sum.size()) return sum.back(); else return sum[j]; } if(j>=num.size()) { return sum.back()-sum[i]+num[i]; } return sum[j]-sum[i]+num[i]; } private: vector<int> num; vector<int> sum; };
413. 等差数列划分
如果一个数列至少有三个元素,并且任意两个相邻元素之差相同,则称该数列为等差数列。
数组 A 包含 N 个数,且索引从0开始。数组 A 的一个子数组划分为数组 (P, Q),P 与 Q 是整数且满足 0<=P<Q<N 。
如果满足以下条件,则称子数组(P, Q)为等差数组:
元素 A[P], A[p + 1], ..., A[Q - 1], A[Q] 是等差的。并且 P + 1 < Q 。
函数要返回数组 A 中所有为等差数组的子数组个数
A = [1, 2, 3, 4] 返回: 3, A 中有三个子等差数组: [1, 2, 3], [2, 3, 4] 以及自身 [1, 2, 3, 4]
思路:
最开始的思路是:
用dp[i][j]表示从i到j的子序列是否是等差数列。如果dp[i][j-1]==1(即i到j-1是等差数列)且A[j]-A[j-1]==A[j-1]-A[j-2](新增数也能等差),则令dp[i][j]=1;否则令dp[i][j]=0
最后统计所有dp[i][j](j-i>=3)的和。
class Solution { public: int numberOfArithmeticSlices(vector<int>& A) { if(A.size()<3) return 0; vector<vector<int>> dp(A.size(),vector<int>(A.size())); for(int i=0;i<=dp.size()-2;i++) { dp[i][i+1]=1; } for(int i=0;i<=dp.size()-3;i++) { for(int j=i+2;j<dp.size();j++) { if(dp[i][j-1]==1) { int diff=A[j-1]-A[j-2]; int val=A[j]-A[j-1]; if(val==diff) dp[i][j]=1; else dp[i][j]=0; } else dp[i][j]=0; } } int ans=0; for(int i=0;i<=dp.size()-3;i++) { for(int j=i+2;j<dp.size();j++) ans+=dp[i][j]; } return ans; } };
虽然能通过,但这个方法毕竟是O(n^2)且使用二维dp,时空都不敢恭维。
看了下别人的思路,的确比这个简单多了。
别人的方法:
dp[i]表示新增一个数字A[i]后,新增加的等差数列数量。
这样如果A[i]-A[i-1]==A[i-1]-A[i-2],则dp[i]=1+dp[i-1];
这里的1代表新增的一个三个数字的等差数列,而dp[i-1]代表,如果dp[i-1]有x个等差数列,现在新增一个A[i]且A[i]的引入也能等差,则i-1位置的新增的等差数列加上一个新的数又组成了x个新的等差数列。
这么说太抽象,举个例子:
0 1 2 3 4
1 3 5 7 9
在2位置,dp[2]=1,然后当i到3时,123又组成了一个新的等差,这就是上面所说的1
而原来0 1 2是一个等差数列,现在加了个3,这个3也能等差,因此0 1 2 (新增的3)也能组成一个新的等差数列
这样dp[3]=1+dp[2]=2
class Solution { public: int numberOfArithmeticSlices(vector<int>& A) { if(A.size()<3) return 0; vector<int> dp(A.size(),0); if(A[2]-A[1]==A[1]-A[0]) dp[2]=1; int sum=dp[2]; for(int i=3;i<dp.size();i++) { if(A[i]-A[i-1]==A[i-1]-A[i-2]) dp[i]=1+dp[i-1]; sum+=dp[i]; } return sum; } };
343. 整数拆分
给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
输入: 2 输出: 1 解释: 2 = 1 + 1, 1 × 1 = 1。
思路:
dp[i]代表对i分割完后的乘积的最大值。对某个数i来说,可以首先考虑将他分为两部分,一部分为j,另一部分为i-j。在这两部分中又可以继续分割,但由于我们已经保存了dp[i]与dp[i-j],因此可以直接取得这个值。但是dp[j]表示的是对i分割之后的最大值,对于j我们也可以不分割,因为i已经分为两部分了,因此对j与i-j不分割也是符合条件的。因此我们要从中选取最大的乘积值作为dp[i]的值,这下就有四种情况:
1).dp[j]*dp[i-j]:两部分都继续分割
2).j*dp[i-j]:只分割右部分
3).dp[j]*(i-j):只分割左部分
4).j*(i-j):对这两部分都不分割
在这四个部分中选出一个最大值,这个最大值就是在j这个点分割成左右两部分所得的乘积的最大值,但j这个点并不一定是最优,因此还要与dp[i]做对比
class Solution { public: int integerBreak(int n) { vector<int> dp(n+1); if(!n) return 0; if(n<=2) return 1; if(n==3) return 2; dp[0]=0; dp[1]=1; dp[2]=1; dp[3]=2; for(int i=4;i<dp.size();i++) { for(int j=1;j<=i-j;j++) { dp[i]=max(dp[i],dp[j]*dp[i-j]); } } return dp.back(); } };
279. 完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...
)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
思路:
一开始的思路是dp[i]保存i的次数,然后用j从1开始遍历,直到i-j>=j不满足为止,这样O(n^2)就能解决,但在n较大时仍会超时
class Solution { public: int numSquares(int n) { if(n<=0) return 0; vector<int> dp(n+1); int start=1; for(int i=1;i<=n;i++) { int len=INT_MAX; if(start*start==i) { len=1; start++; } for(int j=1;j<=i-j;j++) { int l1=dp[j]; int l2=dp[i-j]; if(l1+l2<len) len=l1+l2; } dp[i]=len; } return dp.back(); } };
看了下最优解,虽然仍然是O(n^2),但内层循环的确优化了很多。
思路是:
i一定是一个大的完全平方数加上某个数得到的,这个某个数我们在前面的遍历过程中已经保存到了dp[i]了,现在只要遍历得到这个完全平方数就好了。
dp[i]=min(dp[i],dp[i-j*j]+1)
这个1就是一个完全平方数
class Solution { public: int numSquares(int n) { if(n<=0) return 0; vector<int> dp(n+1,0); dp[0]=0; int start=1; for(int i=1;i<=n;i++) { dp[i]=dp[i-1]+1; for(int j=1;i-j*j>=0;j++) dp[i]=min(dp[i],dp[i-j*j]+1); } return dp.back(); } };
91. 解码方法
一条包含字母 A-Z 的消息通过以下方式进行了编码:
'A' -> 1
'B' -> 2
...
'Z' -> 26
给定一个只包含数字的非空字符串,请计算解码方法的总数。
思路:
对于i位置来说,它可以自己表示一个数,或与前面的数组成一个数。
当自己表示一个数时,有dp[i-1]种,因为相当于是在前面dp[i-1]种中的后面加一个数而已
当自己与前面的数组成一个数时,有dp[i-2]种,因为相当于是在前面dp[i-2]种中的后面增加一个由s[i-1]与s[i]组成的
但是对于s[i],可能有几种特殊情况:
1).s[i]是0.这样它无法单独组成一个数,此时只有第二种情况,也就是与前面的数组成一个数,但要注意如果与前一个数组成的数不满足要求,则应该return 0
2.s[i-1]与s[i]无法组成一个数,可能是因为s[i-1]s[i]超过界限或s[i-1]==0,这样就只有情况1
class Solution { public: int numDecodings(string s) { vector<int> dp(s.size()+1,0); if(s.empty()||s[0]=='0') return 0; dp[1]=1; dp[0]=1; for(int i=2;i<dp.size();i++) { int idx=i-1; int tmp=stoi(s.substr(idx-1,2)); if(s[idx]=='0') { if(s[idx-1]=='0'||(tmp<=0||tmp>26)) return 0; dp[i]=dp[i-2]; } else { dp[i]=dp[i-1]; if(s[idx-1]!='0'&&tmp>0&&tmp<=26) { dp[i]+=dp[i-2]; } } } return dp.back(); } };
300. 最长上升子序列
给定一个无序的整数数组,找到其中最长上升子序列的长度。
输入: [10,9,2,5,3,7,101,18]
输出: 4 解释: 最长的上升子序列是 [2,3,7,101],
它的长度是 4
。
思路:
在O(n^2)的情况下,可以考虑使用dp来解决这个问题。
对于dp[i],表示以num[i]结尾的序列中的子序列长度,则对i+1来说,我们遍历0~i中的每个dp[j],如果num[i+1]>num[j],则更新dp[i+1]=dp[j]+1,但并不能停止,dp[i+1]应该为所有能递增的子序列中最长的
dp[i]=max(dp[i],dp[j]+1);j为满足num[i]>num[j]的j的位置
这是使用dp搜索的思路,实际上可以考虑贪心的思想。
对于某个元素num[i],它越小,它与后面的序列组成上升子序列的机会越大,因此我们可以用一个数组tail来找出某个最长上升子序列
比如:
[10,9,2,5,3,7,101,18]
class Solution { public: int lengthOfLIS(vector<int>& nums) { if(nums.empty()) return 0; vector<int> tail; tail.push_back(nums[0]); for(int i=1;i<nums.size();i++) { int val=nums[i]; if(val>tail.back())//>= { tail.push_back(val); continue; } int l=0;int h=tail.size()-1; int mid=(l+h)/2; while(l<=h) { mid=(l+h)/2; if(tail[mid]<val) l=mid+1; else h=mid-1; } tail[l]=val; } return tail.size(); } };
另外这里刚好是使用二分寻找插入位置的模板:
while中的条件为l<=h:
因为第一要考虑数组中只有一个元素额的情况。
第二即使l==h,mid==l,这是也依然要判断,因为寻找的是插入位置,l==h时还要再判断一次才能找到正确的点
还有
满足要求的点一定是l
因为最后一次循环一定是l==h,此时如果num[mid],也就是num[l](l==mid==h)大于val,则对h=h-1,但第一个大于val的地点还是在l
如果num[l]小于等于val,则对l=l+1,这样num[l]也是第一个大于val的地点。
因此num[l]一定是满足要求的点。
int l=0; int h=tail.size()-1; int mid=(l+h)/2; while(l<=h) { mid=(l+h)/2; if(tail[mid]<val) l=mid+1; else h=mid-1; } tail[l]=val;
646. 最长数对链
给出 n 个数对。 在每一个数对中,第一个数字总是比第二个数字小。
现在,我们定义一种跟随关系,当且仅当 b < c 时,数对(c, d) 才可以跟在 (a, b) 后面。我们用这种形式来构造一个数对链。
给定一个对数集合,找出能够形成的最长数对链的长度。你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。
输入: [[1,2], [2,3], [3,4]]
输出: 2 解释: 最长的数对链是 [1,2] -> [3,4]
思路:
首先对数链按照首元素排序,这样方便进行dp.
dp[i]表示i位置组成的数链最大长度,得到dp[i]应该遍历0~i-1的dp数组,如果满足i数链的第一元素大于j数链的第二元素,则可以组成一条数对链。
在0~i-1的遍历过程中,应该找一个最长的作为i位置的数链最大长度
class Solution { public: int findLongestChain(vector<vector<int>>& pairs) { sort(pairs.begin(),pairs.end(),cmp); vector<int> dp(pairs.size(),1); for(int i=1;i<dp.size();i++) { for(int j=0;j<i;j++) { if(pairs[i][0]>pairs[j][1]) dp[i]=max(dp[i],dp[j]+1); } } int ans=INT_MIN; for(int i=0;i<dp.size();i++) ans=max(dp[i],ans); return ans; } static bool cmp(vector<int>& num1,vector<int>& num2) { return num1[0]<num2[0]; } };
376. 摆动序列
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。
输入: [1,7,4,9,2,5]
输出: 6
解释: 整个序列均为摆动序列。
思路:
最开始想到的是使用类似贪心的思想:
以[1,17,5,10,13,15,10,5,16,8]为例子:
一开始摆动序列是[1 17 5 10],此时遇到13,不能组成摆动序列,但在此位置期待的是下降,而13比10更大,相比于10来说,13后面的数有更大概率与13组成一组下降,因此更新10为13.同理如果在期待上升的时候遇到了更小的数,则更新back()为这个更小的数,因为更有可能与后面的序列组成上升序列。
这里有一个问题:
这个想法默认的是nums[0]一定加入摆动序列,且最开头的趋势是由nums[p1](第一个不与nums[0]相等的数)决定的,比如1 17就决定了一定是上升开头,这是正确的吗?
假设nums[p1]与后面的数可以组成一个最长摆动序列,如[8 7 6 8 5 6 5],其中7 6 8 5 6 5可以组成最长子序列。开头默认是上升,期待的是下降,而当遇到6时,已经将7更新为6,这个操作实际上相当于将nums[0]顶替了nums[p1](即7的位置)而与6 8 5 6 5组成了摆动。
class Solution { public: int wiggleMaxLength(vector<int>& nums) { if(nums.empty()) return 0; if(nums.size()==1) return 1; int p1=0; while(p1<nums.size()&&nums[p1]==nums[0]) p1++; if(p1>=nums.size()) return 1; int flag=(nums[p1]-nums[0]>0)?-1:1; vector<int> aux; aux.push_back(nums[0]); aux.push_back(nums[p1]); for(int i=p1+1;i<nums.size();i++)//1,7,4,9,2,5 { if(flag==-1) { if(nums[i]-nums[i-1]>=0) aux.back()=nums[i]; else { aux.push_back(nums[i]); flag=-flag; } } else { if(nums[i]-nums[i-1]<=0) aux.back()=nums[i]; else { aux.push_back(nums[i]); flag=-flag; } } } return aux.size(); } };
最优解有一种更好的方法,使用dp来做这个题:
up记录当前位置以上升作为结尾的摆动最长长度,down记录当前位置以下降作为结尾的摆动最长长度。
当nums[i]>nums[i-1]时,说明此时是上升,此时up记录的还是i-1时的状态,而如果要将nums[i]加入摆动,则应该与前面以下降结尾的子序列组成摆动,更新up=down+1;
当nums[i]<nums[i-1]时,同理,更新down=up+1/
class Solution { public: int wiggleMaxLength(vector<int>& nums) { if(nums.empty()) return 0; vector<int> dp(nums.size(),0); dp[0]=1; int up=1; int down=1; for(int i=1;i<nums.size();i++) { if(nums[i]==nums[i-1]) continue; else if(nums[i]>nums[i-1]) up=down+1; else down=up+1; } return max(up,down); } };
最长公共子序列
对于两个子序列 S1 和 S2,找出它们最长的公共子序列。
定义一个二维数组 dp 用来存储最长公共子序列的长度,其中 dp[i][j] 表示 S1 的前 i 个字符与 S2 的前 j 个字符最长公共子序列的长度。考虑 S1i 与 S2j 值是否相等,分为两种情况:
当 S1i==S2j 时,那么就能在 S1 的前 i-1 个字符与 S2 的前 j-1 个字符最长公共子序列的基础上再加上 S1i 这个值,最长公共子序列长度加 1,即 dp[i][j] = dp[i-1][j-1] + 1。
当 S1i != S2j 时,此时最长公共子序列为 S1 的前 i-1 个字符和 S2 的前 j 个字符最长公共子序列,或者 S1 的前 i个字符和 S2 的前 j-1 个字符最长公共子序列,取它们的最大者,即 dp[i][j] = max{ dp[i-1][j], dp[i][j-1] }。
综上,最长公共子序列的状态转移方程为:
0-1 背包
有一个容量为 N 的背包,要用这个背包装下物品的价值最大,这些物品有两个属性:体积 w 和价值 v。
定义一个二维数组 dp 存储最大价值,其中 dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值。设第 i件物品体积为 w,价值为 v,根据第 i 件物品是否添加到背包中,可以分两种情况讨论:
第 i 件物品没添加到背包,总体积不超过 j 的前 i 件物品的最大价值就是总体积不超过 j 的前 i-1 件物品的最大价值,dp[i][j] = dp[i-1][j]。
第 i 件物品添加到背包中,dp[i][j] = dp[i-1][j-w] + v。
第 i 件物品可添加也可以不添加,取决于哪种情况下最大价值更大。因此,0-1 背包的状态转移方程为:
空间优化
在程序实现时可以对 0-1 背包做优化。观察状态转移方程可以知道,前 i 件物品的状态仅与前 i-1 件物品的状态有关,因此可以将 dp 定义为一维数组,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。此时:
因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w],以防将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。
416. 分割等和子集
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
每个数组中的元素不会超过 100
数组的大小不会超过 200
思路:
一开始最开始想到的是先sort,sort后利用一个滑动窗口,当小于sum-滑动窗口之和时,right++;当小于sum-滑动窗口之和时,left++;直到滑动窗口之和等于sum-滑动窗口之和。
这个思路有一个问题在于假设的是(一群中间大小的和)==(较大数字的和+较小数字的和)
比如1 2 44 55 98.实际上45+55+1==98+2;而按这个思路滑动窗口是无法找到合适的分界线的。
实际上这个题的真正意思是:
两个子集的和相等,也就是说整个数组的和必须为偶数。
如果数组的和为奇数,则不可能分成两个完全相等的部分。
而实际上问题就转换为了,在这个数组中,寻找若干个数的和等于整个数组和的一半,则其他数的和一定和这若干个数的和相等。
这实际上是个背包问题,在整个数组和的一半这样大的背包中,尽可能的放数字使价值最大,如果价值最大的时候该价值等于背包大小,则说明可以;否则说明无法在这个数组中找到若干个数使他们的和等于数组和的一半
class Solution { public: bool canPartition(vector<int>& nums) { if(nums.empty()) return false; int sum=0; for(int i=0;i<nums.size();i++) sum+=nums[i]; if(sum%2!=0) return false; sum=sum/2; vector<int> dp(sum+1,0); for(int i=0;i<nums.size();i++) { for(int j=sum;j>=nums[i];j--) dp[j]=max(dp[j],dp[j-nums[i]]+nums[i]); } if(dp.back()==sum) return true; else return false; } };
494. 目标和
给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
输入: nums: [1, 1, 1, 1, 1], S: 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
一共有5种方法让最终目标和为3。
思路:
大佬的DP太巧妙了,这也能当背包做?
设nums内满足要求的正数和为x,负数和为y,则:
x+y=target
x-y=sum
解得2*x=target+sum------------->x=(target+sum)/2;
即在nums中,找到能组成和为(target+sum)/2的组合的个数。
这相当于一个容量为(target+sum)/2的背包,然后去nums中找到若干个数放进去。
从表达式来看,如果target+sum是奇数,则一定不可能,因为nums中的数都是偶数。
也就是说只要target+sum是偶数,就一定能找到找到若干件物品使物品价值和为(target+sum)/2;
而普通的背包问题是找寻背包最大价值,现在已知只要target+sum为偶数,就一定能找到了,也就是说最大价值一定就是(target+sum)/2了,这就等价于背包问题。
另外背包问题求解的是最大价值,而这里求解的是最多的方法数。
令dp[i]为能组成i的组合数。
则dp[j]=dp[j]+dp[j-in[i]];//右边的dp[j]为i-1状态下,也就是说不放i,在j容量中只在前i-1个物品中寻找物品放置,使和为j的组合数
//dp[j-in[i]]为放入i物品,此时相当于背包中已经有了一个i了,要使和为x,则还要用j-in[i]的容量中放入j-in[i]个物品,共dp[j-in[i]]种。
另外注意,0不会影响和,但它也有正负号,因此先把它提出来,最后用2^count去乘结果。
class Solution { public: int findTargetSumWays(vector<int>& nums, int S) { int count=0; vector<int> in; long sum=0; for(int i=0;i<nums.size();i++) { if(!nums[i]) count++; else { in.push_back(nums[i]); sum+=nums[i]; } } if((sum+S)%2!=0) return 0; if(sum<S) return 0; int val=(sum+S)/2; vector<int> dp(val+1,0); dp[0]=1; for(int i=0;i<in.size();i++) { for(int j=dp.size()-1;j>=in[i];j--) dp[j]=dp[j]+dp[j-in[i]]; } return dp.back()*(1<<count); } };
474. 一和零
在计算机界中,我们总是追求用有限的资源获取最大的收益。
现在,假设你分别支配着 m 个 0 和 n 个 1。另外,还有一个仅包含 0 和 1 字符串的数组。
你的任务是使用给定的 m 个 0 和 n 个 1 ,找到能拼出存在于数组中的字符串的最大数量。每个 0 和 1 至多被使用一次。
输入: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
输出: 4
解释: 总共 4 个字符串可以通过 5 个 0 和 3 个 1 拼出,即 "10","0001","1","0" 。
思路:
多维费用的01背包问题,但本质上还是一个01背包。
dp[i][j]表示i个0,j个1所能获取的最大字符串数量。
这样
dp[i][j]=max(dp[i][j],dp[i-nums0[idx][j-nums1[idx]]+1);//右边的dp[i][j]是不放,dp[i-nums0[idx][j-nums1[idx]]+1是放入该字符串
class Solution { public: int findMaxForm(vector<string>& strs, int m, int n) { vector<int> num0(strs.size(),0); vector<int> num1(strs.size(),0); for(int i=0;i<strs.size();i++) { int count0=0; int count1=0; for(int j=0;j<strs[i].size();j++) { if(strs[i][j]=='0') count0++; else count1++; } num0[i]=count0; num1[i]=count1; } vector<vector<int>> dp(m+1,vector<int>(n+1,0)); for(int i=1;i<=strs.size();i++) { int idx=i-1; for(int r0=m;r0>=num0[idx];r0--) { for(int r1=n;r1>=num1[idx];r1--) { dp[r0][r1]=max(dp[r0][r1],dp[r0-num0[idx]][r1-num1[idx]]+1); } } } return dp.back().back(); } };
322. 零钱兑换
题目描述:
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
思路:
完全背包问题,dp[j]表示i时刻下,凑成j背包容量所用的最少硬币个数。
而我们对于每一次循环(i,j),在未更新dp[j]时,此时的dp[j]还代表(i-1,j)的状态,但j以前的dp[j]都表示i下的状态了。
因此,对于dp[j],由于小于j的部分已经是i时刻,也就是说对于小于j的背包,已经是用0~i个物品来塞了,也就是说小于j的dp[j]都是最优解,而运行到j时,我们的选择是,要么一件i都不放,要么就可能放入若干件。对于1件都不放的情况,实际上就是dp[j],因为未更新前的dp[j]还表示的是i-1时刻下的j容量的最优解,也就是一件i都不放的情况;而如果选择放,由于j以前的已经是i时刻了,也就是说,j以前的背包里已经可能有若干个i物品了,因此此时我们考虑的只是要不要再加一个物品。
这里可能有点绕,可能你会有一个问题,既然可能i物品放若干个,那为什么状态转移方程里只是放一个的情况,也就是dp[j-coins[i]]+1这种呢?
假设最优情况是连放两个,此时应该为dp[j-2*coins[i]]+2(这个+2代表连放2个i硬币),而这实际上是与dp[j-coins[i]]+1相等的。因为当我们遍历到j-coins[i]时,此时的最优情况是放入一个i硬币,因此为dp[i-cins[i]]+1,而又遍历到i时,由于最优情况是连放两个,而在j-coins[i]已经放了一个了,因此现在还缺一个,也就是dp[j-coins[i]+1=dp[j-2*coins[i]]+2;
因此,对于(i,j),实际上我们考虑的问题的关键是:要么一个不放,要么就是追加一个i物品.
对于这个题来说:
当dp[j]==INT_MAX的时候表示无法凑到这个钱数,因此分开讨论:
如果dp[j-coins[i]]==INT_MAX,代表dp[j]只能不放i硬币,因为前面的最优解不存在
否则,说明存在,则可以放i硬币,则选一个最优的解。
class Solution { public: int coinChange(vector<int>& coins, int amount) { vector<int> dp(amount+1,INT_MAX); dp[0]=0; for(int i=0;i<coins.size();i++) { for(int j=coins[i];j<=amount;j++) { if(dp[j-coins[i]]==INT_MAX) continue; else dp[j]=min(dp[j],dp[j-coins[i]]+1); } } if(dp.back()==INT_MAX) return -1; else return dp.back(); } };
139. 单词拆分
给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
思路;
由于词典内的单词可以多次使用,因此相当于一个完全背包,然后由于单词是有顺序的,因此将物品的选择放到内循环。
dp[i]表示容量为i时能否由dict内的部分单词组成这句话。
只有当s.substr(i-len,i)==word时,代表这个物品i放进去是没有问题的,但可以选择放也可以选择不放:dp[i]=dp[i]||dp[i-len]
class Solution { public: bool wordBreak(string s, vector<string>& wordDict) { vector<bool> dp(s.size()+1,false); dp[0]=true; for(int i=1;i<dp.size();i++) { for(int j=0;j<wordDict.size();j++) { auto tmp=wordDict[j]; int len=tmp.size(); if(i-len>=0&&tmp==s.substr(i-len,len)) dp[i]=dp[i-len]; } } return dp.back(); } };