状态:dp[i][0]:没偷第i家的最多钱
dp[i][1]:偷了第i家时的最多钱
选择:不偷第i家:dp[i][0]=max(dp[i-1][0],dp[i-1][1])
偷第i家:dp[i][1]=dp[i-1][0]+nums[i]
注意到第i家仅与i-1家有关,所以可以只用2*2的数组
class Solution {
public:
int rob(vector<int>& nums) {
int sz=nums.size();
if(sz==1)
return nums[0];
int dp[2][2]={0,nums[0],0,0};
for(int i=1;i<sz;++i){
dp[1][0]=max(dp[0][0],dp[0][1]);
dp[1][1]=dp[0][0]+nums[i];
dp[0][0]=dp[1][0];
dp[0][1]=dp[1][1];
}
return max(dp[1][0],dp[1][1]);
}
};
- 思路:大于两家时,只能选择偷首尾两家之一(不一定非要偷其中一个,或许可以换个方向思考:选择不偷其中之一),即可分两种情况:第一个是不偷第一家,就变成了求从第二家开始到最后一家可以打劫的最大值(注意:不偷第一家不代表一定偷最后一家。),同理可得第二种情况
class Solution {
public:
int rob(vector<int>& nums) {
int sz=nums.size();
if(sz==1)
return nums[0];
if(sz==2)
return max(nums[0],nums[1]);
int dp1[2][2]={0,nums[0],0,0};//不偷最后一家
for(int i=1;i<sz-1;i++){
dp1[1][0]=max(dp1[0][0],dp1[0][1]);
dp1[1][1]=dp1[0][0]+nums[i];
dp1[0][0]=dp1[1][0];
dp1[0][1]=dp1[1][1];
}
int dp2[2][2]={0,nums[1],0,0};//不偷第一家
for(int i=2;i<sz;i++){
dp2[1][0]=max(dp2[0][0],dp2[0][1]);
dp2[1][1]=dp2[0][0]+nums[i];
dp2[0][0]=dp2[1][0];
dp2[0][1]=dp2[1][1];
}
return max(max(dp1[1][0],dp1[1][1]),max(dp2[1][0],dp2[1][1]));
}
};
好像,可以只使用一维数组,dp[i]即为打劫到第i家的最多钱数,dp[i]=max(dp[i-2]+nums[i],dp[i-1]),即(两种选择)打劫第i家和不打劫第i家取最大值
740.删除并获得点数meidium
思路:想到了将所有相等元素求和放在新的数组中,但是没搞清原来这样操作后就和打家劫舍一样了,从新数组第一个元素开始,dp[n]表示选择到第n个元素的最大点数,
dp[n]=max(dp[n-2]+nums[n],dp[n-1]),也是两种选择一种是选择当前点数,那么肯定n-1是不能选的,就是dp[n-2]+nums[n],还有就是不选择当前点数,
简直和打家劫舍一模一样。
class Solution {
public:
int deleteAndEarn(vector<int>& nums) {
int maxv=0;
for(int i:nums)
maxv=max(i,maxv);//找到最大值
vector<int> sum(maxv + 1);
for(int i:nums)//设置初值
sum[i]+=i;
//后边就是打家劫舍解法了,一模一样
if(maxv==1)
return sum[1];
if(maxv==2)
return max(sum[1],sum[2]);
int first=sum[1];
int second=max(sum[1],sum[2]);
int result=0;
for(int i=3;i<=maxv;++i){
result=max(first+sum[i],second);
first=second;
second=result;
}
return result;
}
};
class Solution {
public:
bool canJump(vector<int>& nums) {
int target=nums.size()-1;
int sz=0;//截止到现节点可以到达的最远位置
for(int i=0;i<=sz;++i){
if(i+nums[i]>sz)
sz=i+nums[i];
if(sz>=target)//最远位置可以到达目标位置
return true;
}
return false;
}
};
- 思路:dp[i]又来储存到达第i个节点所需最小步数,都初始化为最大值(也可以是数组大小),从前向后遍历,同时对每个节点可能的步数可以到达的位置取值
- 状态:result[i]:到第i个位置的最小步数
- 选择:每个位置可能的步数
class Solution {
public:
int jump(vector<int>& nums) {
int sz=nums.size();
if(sz==1)
return 0;
vector<int> result(sz,INT_MAX);//初始化为最大值
result[0]=0;
for(int i=0;i<sz;i++){//对每个位置
for(int j=1;j<=nums[i];j++){//每个位置可能的步数
if(j+i>sz-1)//当前位置加上步数超出范围就break
break;
result[i+j]=min(result[i+j],result[i]+1);//取最小值
}
}
return result[sz-1];
}
};
53.最大字序和easy
思路:从前向后加,记录过程中的最大值,若加的过程中小于零,就重置为零再开始加(因为求最大字序和不可能带着前面的小于零的和往后加呀,那只会使得后面的序列和变小,重置为零即舍弃前面的序列)
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int sz=nums.size();
if(sz==1)
return nums[0];//只有一个元素
int sum=nums[0];//加的过程中的和,初始为第一个元素
int result=sum;//过程中的最大值
for(int i=1;i<sz;++i){
if(sum<0)
sum=0;//如果和小于零了就重置
sum+=nums[i];
result=max(result,sum);//每次加都要更新最大值
}
return result;
}
};
- 第一反应:与和最大的连续子数组一样,只不过判断与之前的序列相乘还是开始新序列的判断条件由是否小于零变为是否等于零,但是,很明显不对
- 这不是一个最优子结构问题,当前状态的最优解不一定是由上一个状态的最优解转化而来的,对于负数,希望加入的是前面的序列积是一个越小越好的负数,对于正数,希望加入的是前面的序列和越大越好的整数,因为我们要维护上一个子序列的最小值和最大值,并在每个位置更新全局最大值
class Solution {
public:
int maxProduct(vector<int>& nums) {
int sz=nums.size();
int minv=nums[0];//当前子序列乘积最小值
int maxv=nums[0];//当前子序列乘积最大值
int result=nums[0];//全局子序列乘积最大值
int tmp=0;
for(int i=1;i<sz;i++){
//这里可以分开写,若nums[i]>0,应该与maxv相乘······
tmp=min(min(minv*nums[i],maxv*nums[i]),nums[i]);//暂存当前子序列乘积最小值
maxv=max(max(minv*nums[i],maxv*nums[i]),nums[i]);
minv=tmp;
result=max(result,maxv);//更新
}
return result;
}
};
1567. 乘积为正数的最长子数组长度medium
思路:当元素大于零,不改变序列正负,若为负数改变前面序列的正负性,讨论
class Solution {
public:
int getMaxLen(vector<int>& nums) {
int dpp=0;//当前乘积为正子序列长度
int dpn=0;//当前乘积为负子序列长度
int result=0;
for(int i=0;i<nums.size();++i){
if(nums[i]>0){
dpp++;
if(dpn==0)
dpn=0;
else
dpn++;
}
else if(nums[i]<0){
int tmp=dpp;
if(dpn==0){
dpp=0;
}
else{
dpp=dpn+1;
}
dpn=tmp+1;
}
else{
dpp=0;
dpn=0;
}
result=max(result,dpp);
}
return result;
}
};
虽然说经过一个半小时的奋斗通过了,很兴奋,第一个自己解决的hard的题目,但是看了题解之后发现自己还是不够优美,当然能做出来就很高兴了
自己的想法相当于是官方解法二和三的合体,不得不说双指针太妙了
- 官方题解思路:对于s中每个字母的位置i,如果从开始到i是可消除的, 那么总会存在j ,0<= j < i,使得从开始到j是可消除的,同时j+1到i,是一个可消除的单词
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
set<string> words;
for(string i:wordDict)//将单词放入set中便于查找
words.insert(i);
vector<bool> dp(s.size()+1,0);//标记每个位置是否可消除,dp[0]作为一个前置条件
dp[0]=1;
for(int i=1;i<=s.size();++i){
for(int j=0;j<i;j++){
if(dp[j]&&words.find(s.substr(j,i-j))!=words.end()){
dp[i]=1;
break;
}
}
}
return dp[s.size()];
}
};
- 我的思路:对于每一个可以消除的位置点(要记录最大可消除位置点),有两个选择,消除或者不消除,选择消除就重新开始单词序列。存储从开始到最后所有可能产生的单词序列,每到一个位置总要判断所有的序列中是否有可以消除的。
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
set<string> words;
for(string i:wordDict)
words.insert(i);
vector<string> ss;//存储所有可能产生的序列
ss.push_back("");
int maxp=0;//最大可消除位置
bool sign=0;//当前位置是否可被消除
for(int i=0;i<s.size();++i){
for(string& j:ss){
j+=s[i];
if(words.find(j)!=words.end())//当前位置有可被消除序列
sign=1;
}
if(sign==1){//将新的序列加入(对应可消除点重新开始序列)
maxp=i+1;
ss.push_back("");
sign=0;
}
}
if(maxp==s.size())
return true;
return false;
}
};
91. 解码方法medium
思路:dp[ i ] 就表示到当前位置最大解码方式。对于每个位置有两种操作,单独解码( dp[ i ] = dp [ i - 1 ] )或者与上一位置组合解码 (dp [ i ] = dp[ i - 2 ] ,dp[ i ] 的最终结果是这两种可能的和。若当前位置不为0,就可以单独解码,若组合起来不大于26就可以组合解码
class Solution {
public:
int numDecodings(string s) {
int a=0,b=1,c=0;
for(int i=0;i<s.size();++i){
c=0;
if(s[i]!='0')
c+=b;
if(i>0&&s[i-1]!='0'&&((s[i-1]-'0')*10+(s[i]-'0')<27))
c+=a;
a=b;
b=c;
}
return c;
}
};
96. 不同的二叉搜索树medium
思路:给定n,因为是连续的,所以可以把1~n每个数都作为根,dp[ n ](最大二叉搜索树个数)就等于这n种情况的个数和(每种情况的个数等于左子树个数和乘以右子树个数和),而对于每种情况又可以递归的进行计算,所以在递归过程中将计算过的保留下来就可以
class Solution {
public:
int numTrees(int n) {
vector<int> dp(n+1,0);
dp[0]=1;
return fun(dp,n);
}
int fun(vector<int>& dp,int n){
if(dp[n]!=0)
return dp[n];
int sum=0;
for(int i=1;i<=n;++i)
sum+=fun(dp,i-1)*fun(dp,n-i);
dp[n]=sum;
return dp[n];
}
};
理解上述解法之后很自然的就可以使用迭代计算
class Solution {
public:
int numTrees(int n) {
vector<int> dp(n+1,0);
dp[0]=1;
dp[1]=1;
int sum=0;
for(int i=1;i<=n;++i){
sum=0;
for(int j=1;j<=i;j++)
sum+=dp[j-1]*dp[i-j];
dp[i]=sum;
}
return dp[n];
}
};
两道简答题:
118杨辉三角easy
119杨辉三角 IIeasy这个非负索引。。。。就是代表从零开始。。。
120. 三角形最小路径和medium
这道题挺直白的。。
思路:对于每一行中的每个位置(除了收尾元素),都可能是由上一行中另两个位置转来,所以比较一下那个更小取哪个就好
class Solution {
public:
int minimumTotal(vector<vector<int>>& triangle) {
for(int i=1;i<triangle.size();++i){
int sz=triangle[i].size();
triangle[i][0]+=triangle[i-1][0];
for(int j=1;j<sz-1;++j)
triangle[i][j]+=min(triangle[i-1][j-1],triangle[i-1][j]);
triangle[i][sz-1]+=triangle[i-1][sz-2];
}
int min=triangle[triangle.size()-1][0];
for(int i:triangle[triangle.size()-1])
if(i<min)
min=i;
return min;
}
};
62. 不同路径medium
思路:从最上面一行走是一条路,最左面也是一条路,而其他位置可以由上方和左方两个位置到达,所以该位置就是这两个位置的和
class Solution {
public:
int uniquePaths(int m, int n) {
int arr[m][n];
for(int i=0;i<n;i++)
arr[0][i]=1;
for(int i=1;i<m;++i){
arr[i][0]=1;
for(int j=1;j<n;j++)
arr[i][j]=arr[i-1][j]+arr[i][j-1];
}
return arr[m-1][n-1];
}
};
- 可优化,仅使用一维数组存储上一行的信息即可
63. 不同路径 IImedium
思路:相比于上一题,只是当目前为止是障碍物时,直接不可达设置为0即可,下面使用二维数组实现的,可以优化为一维数组,
class Solution {
public:
int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
int m=obstacleGrid.size();
int n=obstacleGrid[0].size();
vector<vector<int>> dp;
for(int i=0;i<m;i++){
vector<int>tmp(n,0);
dp.push_back(tmp);
}
for(int i=0;i<n;i++){
if(obstacleGrid[0][i]==0){
dp[0][i]=1;
continue;
}
break;
}
for(int i=0;i<m;i++){
if(obstacleGrid[i][0]==0){
dp[i][0]=1;
continue;
}
break;
}
for(int i=1;i<m;++i){
for(int j=1;j<n;++j){
if(obstacleGrid[i][j]==0){
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
else
dp[i][j]=0;
}
}
return dp[m-1][n-1];
}
};
64. 最小路径和meidum
做完前两道,这个很简单,就是把和加起来就好了
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
int m=grid.size();
int n=grid[0].size();
vector<int> dp=grid[0];
for(int i=1;i<n;i++)
dp[i]+=dp[i-1];
for(int i=1;i<m;++i){
dp[0]+=grid[i][0];
for(int j=1;j<n;++j)
dp[j]=grid[i][j]+min(dp[j],dp[j-1]);
}
return dp[n-1];
}
};
5. 最长回文子串medium
思路:对于从i到j的子序列,如果s[ i ]==s [ j ]而且从i+1到j-1的子序列是回文子串那么他也是,在实现上因为要先知道所有较短的子串才能计算较长子串,所以
class Solution {
public:
string longestPalindrome(string s) {
int sz=s.size();
vector<vector<bool>> dp(sz,vector<bool>(sz));
int maxlen=1;//最长的长度
int begin=0;//开始的位置
for(int i=0;i<sz;++i)
dp[i][i]=1;
for(int L=2;L<=sz;++L){//先计算长度小的,从2开始
for(int i=0;i<sz;++i){
int j=i+L-1;//根据长度和当前位置算出子串的终点位置
if(j>=sz)
break;
if(s[j]!=s[i])
dp[i][j]=false;
else{
if(j-i<3)
dp[i][j]=true;
else
dp[i][j]=dp[i+1][j-1];
}
if(dp[i][j]&&L>maxlen){
maxlen=L;
begin=i;
}
}
}
return s.substr(begin,maxlen);
}
};
思路二:从中心向外扩展
516. 最长回文子序列medium
在上一题的基础上,如果两端的字符相同,就是在dp[i+1][j-1]的基础上加2,如果不相等,这两个不可能同时作为首尾存在在一个回文子序列中,所以
class Solution {
public:
int longestPalindromeSubseq(string s) {
int sz=s.size();
vector<vector<int>>dp(sz,vector<int>(sz,0));
int maxlen=1;
for(int i=0;i<sz;i++)//初始化为1
dp[i][i]=1;
for(int i=sz-2;i>=0;--i){//要知道dp[i][j]就必须知道dp[i+1][j]和dp[i][j-1]的情况,所以倒着遍历
for(int j=i+1;j<sz;++j){
if(s[i]==s[j])//首尾相等时
dp[i][j]=dp[i+1][j-1]+2;
else//收尾不等时
dp[i][j]=max(dp[i+1][j],dp[i][j-1]);
maxlen=max(maxlen,dp[i][j]);
}
}
return maxlen;
}
};
300. 最长递增子序列medium
思路:使用一维dp数组,dp[i]表示到以第i个元素结尾的最长递增子序列长度,它等于前所所有比他小的元素中长度最长的加一
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int sz=nums.size();
vector<int> dp(sz,1);
int maxlen=1;
for(int i=0;i<sz;i++){
for(int j=i-1;j>=0;j--){
if(j<=dp[i]-2)
break;
if(nums[i]>nums[j])
dp[i]=max(dp[i],dp[j]+1);
else if(nums[i]==nums[j]){
dp[i]=max(dp[i],dp[j]);
break;
}
else
continue;
}
maxlen=max(maxlen,dp[i]);
}
return maxlen;
}
};