本文仅为个人刷题时笔记!
现在前面:
dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值
理解 这句话,差不多花了我一天时间,人不聪明,但要努力
比如322,不停的思考,为什么预处理是[0 , n],为什么二维循环是[1 ,n],为什么返回又是dp[n][amount]。具体看322
70. 爬楼梯
简单题 dp[i] = dp[i - 1] + dp[i - 2] 但是注意越界问题,这个公式只适用于n >= 3
不需要开数组的做法:
int cur ,pre1 ,pre2;
pre1 = pre2 = 1;
for(int i = 2;i <= n;i++)
{
cur = pre1 + pre2;
pre2 = pre1;
pre1 = cur;
}
198. 打家劫舍
二刷了,已经完全会了。但是代码太臃肿了。
我的想法是写出dp[ 0] = nums[0]和dp[1] = max(nums[0],nums[1])的值,循环从2开始。但官大想法是dp[0] = 0,dp[1] = nums[1],相当于用dp[0]这个过度值,就不用再分类讨论了。
413. 等差数列划分 爷做出来了啊啊啊啊啊啊啊啊啊啊
64. 最小路径和 爷又做出来了啊啊啊啊啊 就是代码太臃肿了,不需要预处理,直接在循环了进行 if else判断
542. 01 矩阵
思路很清晰但是不会写代码,通过这题,又学到一招。
很明显,0的位置dp还是0,1的位置要么接壤1要么接壤0,接壤0 就好办了,直接0位置的dp + 1,接壤1 尤其是多个1,就只需要看四个方向哪个1的dp值最小。但是这时我就懵了,min函数只能判断俩,四个值怎么办。
大佬的思路就是两遍动态搜索,先从左上到右下,遇到 0 就赋值 0 ,1的位置,只要不再左,上两个位置的边界(因为j - 1或 i - 1会越界)那么先将自身和上,左两个位置取最小值。
if(j > 0) dp[i][j] = min(dp[i][j] , dp[i][j - 1] + 1);
if(i > 0) dp[i][j] = min(dp[i][j],dp[i - 1][j] + 1);
再从右下到左上一次动态搜索,取自身,下,右两个位置最小值
if(j < m - 1) dp[i][j] = min(dp[i][j] , dp[i][j + 1] + 1);
if(i < n - 1) dp[i][j] = min(dp[i][j],dp[i + 1][j] + 1);
dp要先初始化为INT_MAX .
221. 最大正方形
有点乱,没写出来(写了一天了脑子快炸了)
首先要明确,dp[i][j] 是指,以第map[i][j]为右下角的正方形。那么好办了,直接循环判断每个1出现的地方,如果是i == 0 || j == 0这种边界,那dp[i][j] = 1,否则 dp[i][j] = min(min(dp[i - 1][j], dp[i][j - 1]) , dp[i-1][j-1] ) + 1;
然后就是取最大值了,注意最后求的是面积,dp是边长.
279. 完全平方数
dfs必超时,,但是dfs我写出来了啊。。。所以贴个代码纪念一下。
class Solution {
public:
int t ;
int minn = INT_MAX;
int numSquares(int n)
{
t = sqrt(n);
if(t * t == n) return 1;
dfs(n ,0);
return minn;
}
void dfs(int n ,int ans)
{
if(n == 0)
{
cout<<3<<" "<<n<<":"<<ans<<endl;
if(ans < minn)
{
minn = ans;
}
return;
}
for(int i = t; i >= 1 ; i--)
{
if(n < 0) continue;
n = n - i * i;
ans++;
cout<<1<<" "<<n<<":"<<ans<<endl;
dfs(n,ans);
n = n + i * i;
ans--;
cout<<2<<" "<<n<<":"<<ans<<endl;
}
}
};
正片开始:
两层循环,第一次循环是遍历每个1~ n,第二次从1 ~ sqrt(n),得到每个dp值。
91. 解码方法
思路还是比较乱。理一理。
在i这个位置,他的结果取决于前面一个数。
(1)如果当前这个数s[i -1] != ‘0’,那么dp[i] = dp[i -1],
(2)如果当前这个数s[i- 1] 又可以和s[i -2]合并if(i > 1 && s[i - 2] != '0' && ((s[i - 2] - '0') * 10 + s[i - 1] - '0' ) <= 26)
dp[i] = dp[i -2]
满足以上两种情况则把两种都加起来,dp[i]+=dp[i - 1] ; dp[i] += dp[i - 2];
最后return dp[n]
关于这个i ,循环中,dp[i]代表是字符s[i - 1]的dp值,意思是从s[0]到s[i - 1]这段字符串中的正确结果
139. 单词拆分
假设例子是 catsandog
(1)dp[i]意思看上面,所以i = = 4,判断的是cat及其子串的部分
(2)善用find函数 ,把字典放入unordered_set,unordered_set<string> wordDictSet(wordDict.begin(),wordDict.end());
,使用find函数查找子串是否存在。
(3)对于每一次的判断条件,每次从j开始切割 j从0到 i这个位置不断遍历子串,但是必须保证dp[j] == true,这样才保证切割的位置前的串都是正确满足字典的串。
300. 最长递增子序列
经典中的经典题了。用代码来分析。
class Solution {
public:
int lengthOfLIS(vector<int>& nums)
{
int n = nums.size();
vector<int> dp(n + 1,1);
if(n == 0) return 0;
//这题关键在于要套俩循环
//第一次循环是指以nums[i]为最后一个数,求 i 之前最长递增子序列
//第二次循环是遍历i之前所有数,把i之前每个小于nums[i]的值都得到
//才不会落下
for(int i = 0;i < n;i++)
{
for(int j = 0;j < i;j++)
{
if(nums[i] > nums[j])
{
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
return *max_element(dp.begin(),dp.end());//stl好东西
}
};
1143. 最长公共子序列
啊啊啊啊很接近了啊啊啊但还是不会写啊啊啊啊
老想着预处理预处理,啊不是我又不要把整个dp输出,dp开大一层最外层设为0就好了啊。
思路就是形成一个二维dp,text1.size()行text2.size()列,dp[i] [j]代表的是 text1[0] ~text1[i] 中与text2[0] ~text2[j] ,最大子序列的值
(1)如果当前c1 == c2,那只要在dp[i- 1][j - 1] + 1就行
(2)如果不相等,那就只能继承以前的最大值dp[i][j] = max(dp[i - 1][j] ,dp[i][j -1]);
,相当于,反正不相等,两边少这个不相等的字符都不影响,只要继承到一个最大的值
416分割等和子集
开始了,背包问题。但是没想到怎么转换成背包问题
思路:在数组里找数装入背包,让背包的总数和 = 全数据 / 2。
有一些例外要单独列出
(1)总数和为单数
(2)数组中最大数 > sum/ 2,显然,数组中每个数都要用到,这个最大值无论放在哪里,都会导致一边不满足
(3)数组中只有一个数
以上全部返回false;
dp[i][j]意思是,从nums[0] ~ nums[i]中选取任意数,也可以是 0 个,使得总和为 j
首先预处理边界 i==0,很明显,dp[i][0] = true,只要i 不越界,无论 i 为多少,不去就行,总和为 0 一定满足,其次,就是数组中第一个值dp[0][nums[0]] = true,取第一个值一定可以为 真。
对于 i > 0 和 j > 0,就要对比每次选取的数据和j 的值
- j > nums[i]
可以选nums[i] dp[i][j] = dp[i−1][j−nums[i]]
也可不选 nums[i] dp[i][j] = dp[i- 1][j]
两者去或就行,只有一者为true,dp[i][j]就可以满足 为true - j < nums[j]
不能选 nums[i] dp[i][j] = dp[i- 1][j]
474. 一和零
这是一个两个背包的问题,所以要开三维数组。当然也可以压缩,先看三维的
dp[i][j][k] 从 strs[0] ~strs[1]选取若干个字符串,所有字符串0个数和cnt0 == j,cnt1 == k
关于得到一个字符串 0 和 1 ,这里使用的辅函数,辅函数返回类型为pair<int,int>
pair<int,int> count(const string& s)
{
int cnt0,cnt1;
cnt1 = cnt0 = 0;
for(int i =0;i < s.size();i++)
{
if(s[i] == '1')
{
cnt1++;
}
else cnt0++;
}
// cout<<cnt0<<" "<<cnt1<<endl;
return make_pair(cnt0,cnt1);//最后要返回pair<int,int>
}
所以数据的使用方法是auto [cnt0,cnt1] = count(strs[i - 1])
,注意是 i -1啊
然后就是经典背包规划,但是这道题,取strs[ i -1]有两个条件, j >= cnt0 && k >= cnt1 。所以,dp[i][j][k]直接记作dp[i - 1][j][k],只有当满足了这个条件,才进行最大值判断。
322. 零钱兑换
几乎跟416一模一样,就是max换成min,但是自己在写二维dp的时候很吃力,很多细节不到位,记录一下不断debug的过程。几个小时啊啊啊啊啊
首先明确一点:dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值(这句话多看几遍**)
class Solution {
public:
int coinChange(vector<int>& coins, int amount)
{
int n = coins.size();
vector<vector<long>> dp(n + 1,vector<long>(amount + 1,INT_MAX ));
//全部置为INT_MAX,我们求的是最小值
for(int i = 0;i <= n;i++)
{
dp[i][0] = 0;
}
第一次预处理,从前i个物品中,只要不取,j == 0一定可以满足且dp值为 0
for(int j = 1;j <= amount;j++)
从1开始,因为前0个一个不取也是0,也就是说dp[0][0] = 0,这个在上面已处理
{
dp[0][j] = INT_MAX;
}
第二次预处理,前0个物品,怎么选都没得啊,所以全部预处理为INT_MAX
到此,两个边界i 和 j都处理完了
for(int i = 1;i <= n;i++)从1开始循环,而且可以等于n
表示前1个物品到前n个物品,这里的循环的i代表的数量数目,选取硬币的个数
{
for(int j = 1;j <= amount;j++)
{
if(j >= coins[i - 1])
这里的i-1代表的是第i - 1个啊 比如i == 1,前1个硬币在coins中是coins[0]
{
// cout<<"*";
dp[i][j] = min(dp[i - 1][j] ,dp[i][j - coins[i - 1]] + 1 );
}
else dp[i][j] = dp[i - 1][j];
}
}
return dp[n][amount] == INT_MAX ? -1 : dp[n][amount];
}
};
72. 编辑距离
没思路
对于边界:i == 0 dp[0][j] = j,等价于第一个串要插入j个字符才能成为前word2[j]
对于判断第i位与第j位是否相等(word1[i - 1] == word2[j - 1]),相等则不用加1,不相等要 + 1:
word1[i - 1] == word2[j - 1]) ? 0 : 1
650. 只有两个键的键盘
也是完全没有什么思路啊。。。
这份题解已经很清晰了。。然而还有个疑问,为什么遍历j只要第一次找到 一个j满足是i的因数就可以跳出循环了,不用比较最小值吗?
评论区数学证明:
10. 正则表达式匹配
看清题,* 的意思是,可以匹配零个或多个前面的那一个元素。
那么将 *与前面那个元素组合起来匹配
动规一共要分三个大情况去考虑。 *,字符,.
(1) p[j] == ‘*’
分两种情况,一是j - 1前一个字符匹配上了,另一个是没有匹配上
先看第一种
s[i - 1] == p[j - 1] ,p[j - 1]p[j]这个组合还可以继续用,s[i - 1]可以直接当做删除不影响结果
比如
s : a a b
p : a * b
0 位置匹配上 ,来到位置1,1位置时,s[1] == p[0],s[1]可以直接删掉不影响
dp[i][j] = dp[i - 1][j]
如果没有匹配上,那就将整个p[j - 1]p[j]组合扔掉 dp[i][j] |= dp[i][j - 2];
(2)p[j] == ‘.’ 那么直接不用比较了,dp[i][j = dp[i - 1][j - 1]
(3)都是字符 比较两个是否相等 s[i] == p[j]
121. 买卖股票的最佳时机
思路:如何卖出赚的最多?那就是尽可能的在最低点买进,最高点卖出。我们假设每个i都是最佳卖出的时机,对此遍历。我们需要找一个不超过i这个点的最小值,maxprofit = max(maxprofit,price - minprice);
188. 买卖股票的最佳时机 IV
难的啊,真的很难。
这题需要维护两个二维数组
buy[i][j] :表示在第 i 天的,已进行 j 笔交易,且当前手上持有股票,这种情况下的最大利润。
sell[i][j] :表示在第 i 天的,已进行 j 笔交易,且当前手上不持有股票,这种情况下的最大利润。
买入不算交易,卖出才算一次交易。
(1)边界处理:buy[0][j] 和 sell[0][j]都是第0 天,没得交易 直接赋值 INT_MIN/2,这里要除以2,,因为buy[0][j] 和 sell[0][j]也是要进行±prices[i - 1]运算的,直接复制INT_MIN会溢出。
(2)关于遍历的交易次数。不一定是k次。加入每天都在交易,一天买一天卖,实际交易次数只有 n / 2(卖出才算交易,只有手上有股票才可以卖出)。所以 k = min(n / 2, k)
(3)对于每个buy[i][j] ,有两种情况。此时手上有股票,可以是是i - 1刚卖出,i天又买了股票,也可是是i-1就已经有股票一直没卖。代码表示为
buy[i][j] = max(sell[i - 1][j] - prices[i - 1], buy[i - 1][j]);
(注意i是第i天,那么对应prices[i - 1],prices是从0 到 n-1)
对于,sell[i][j],可以是从i - 1天开始手中就没有股票,也可能是i -1持有,第i天卖出,sell[i][j] = max(buy[i - 1][j - 1] + prices[i - 1], sell[i - 1][j]);
看上面两个代码 ,j - 1,那么 j 参与循环只能从1开始遍历到k,所以要单独处理buy[i][0]这种buy[i][0] = max(sell[i - 1][0] - prices[i - 1] ,buy[i - 1][0]);
不存在sell[i][0]这种情况,卖出必须进行一次交易即j - 1的情况,不可能出现buy[i - 1][- 1] + prices[i - 1]
(4)最后返回手上第n天没有股票的sell[n]情况。手上没有多余的股票肯定比手上买入没卖出盈利的利润高。但是只需要return *max_element(sell[n].begin(), sell[n].end());
,,题目只是说最多k次交易,具体几次不在乎,只需要求得最大值
309. 最佳买卖股票时机含冷冻期
(我要成为股神(bushi))
二维数组,记录三个状态
dp[i][0]:i天手上有股票的最大利益
dp[i][1]:i天手上无股票且处于冷冻期的最大利益
dp[i][2]:i天手上无股票且不处于冷冻期的最大利益
dp[i][0] :可能是i - 1天就有的股票,也可以是i天买的股票
dp[i][0] = max(dp[i - 1][0] ,dp[i - 1][2] - prices[i]);
dp[i][1] :只能是因为昨天把股票卖了
dp[i][1] = dp[i - 1][0] + prices[i];
dp[i][2] :可能是昨天冷冻期今天解冻,可能是昨天就没股票
dp[i][2] = max(dp[i - 1][1],dp[i - 1][2]);
返回return max(dp[n - 1][1],dp[n - 1][2]);
,dp[n - 1][0]不必考虑,手上留着卖不出的股票一定比卖出得到收益低
213. 打家劫舍 II
这题的关键是分区间和处理边界。
(1)偷了第一家就不能偷最后一家。分开两个区间偷,求最大
max(robchange(nums, 0 , n - 2) ,robchange(nums,1 , n -1));
(2)dp数组循环的边界处理
dp[0] = nums[0],无疑问
关于dp[1],如果可以偷最后一家,那么nums[0]不可以偷,相当于从1开始,dp[1] = nums[1],,但是最后一家不可以偷,dp[1] = max(nums[0] ,nums[1])
其他问题处理就跟无环一样处理
class Solution {
public:
int rob(vector<int>& nums)
{
int n = nums.size();
if(n == 0) return 0;
else if(n == 1) return nums[0];
else if(n == 2) return max(nums[0] ,nums[1]);
return max(robchange(nums, 0 , n - 2) ,robchange(nums,1 , n -1));
}
int robchange(vector<int>& nums,int start,int end)
{
vector<int> dp(nums.size() + 1);
for(int i = start; i <= end ;i++ )
{
if(i == 0)
{
dp[0] = nums[0];
}
else if(i == 1)
{
if(end == nums.size() - 1)
{
dp[1] = nums[1];
}
else
{
dp[1] = max(nums[0] ,nums[1]);
}
}
else dp[i] = max(dp[i - 2] + nums[i] ,dp[i - 1]);
}
return dp[end];
}
};
53. 最大子序和
奇耻大辱,,居然没写出来。直接上代码把
class Solution {
public:
int maxSubArray(vector<int>& nums)
{
int n = nums.size();
vector<int> dp(n + 1);
dp[0] = nums[0];
int ans = dp[0];
for(int i = 1; i < n;i++)
{
dp[i] = max(nums[i], dp[i - 1] + nums[i]);
ans = max(ans, dp[i]);
}
return ans;
}
};
一直在考虑如果当前nums[i]不加,dp[i]该如何维护,不加就直接dp[i] = nums[i]就好了啊,,最大值需要新开一个变量,不一定是dp[n - 1],求的是最大子序列,不等于一定加到最后一个值.
343. 整数拆分
动规不出来啊–
遍历找最大值。对于任意一个i ,一半拆分成 j,另一半就是i - j,这个 i *(i - j)也许是正确答案,但如果不是,就意味着需要继续按同样的方法拆分 i - j。用代码表示就是curmax = max(curmax, max(j * (i - j) ,j * dp[i - j] ));
583. 两个字符串的删除操作
跟72几乎一样
动规部分稍微改改就行
if(word1[i - 1] == word2[j - 1])
{
dp[i][j] = dp[i - 1][j - 1];
}
else
{
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + 1;
}
646. 最长数对链
看了题解才知道,其实这题本质上就是求最长递增子序列的变种。
先排序,按照数组对的第一个元素从小到大排序。这里记录一下Lambda表达式的写法
sort(pairs.begin(),pairs.end(),[](vector<int>& v1 ,vector<int>& v2)
{
return v1[0] < v2[0];
});
dp[i] 表示 以 pairs[i]数组对为结尾的最长数对链
两层循环,外层遍历每一个数组对,以该数组对为结尾的结果。内层j < i数组对中找能够在以pair[i]为结尾的数组对往前填充数组,尽可能的延长该数对链。
for(int i = 1;i < n;i++)
{
for(int j = 0;j < i;j++)
{
if(pairs[i][0] > pairs[j][1])
{
dp[i] = max(dp[i] , dp[j] + 1);
}
}
}
376. 摆动序列
周赛写了1955,看懂1955后这题会写了,记录一下,泪目
class Solution {
public:
int wiggleMaxLength(vector<int>& nums)
{
int n = nums.size();
if(n == 1) return 1;
else if (n == 2 && nums[0] != nums[1]) return 2;
vector<vector<int>> dp(n + 1,vector<int>(2,1) );
for(int i = 1;i < n;i++)
{
if(nums[i] - nums[i - 1] > 0)
{
for(int j = 0;j < i;j++)
{
dp[i][0] = max(dp[i][0] ,dp[j][1] + 1);
}
}
else if(nums[i] - nums[i - 1] < 0)
{
for(int j = 0;j < i;j++)
{
dp[i][1] = max(dp[i][1] ,dp[j][0] + 1);
}
}
else
{
for(int j = 0;j < i;j++)
{
dp[i][1] = max(dp[i][1] ,dp[j][1]);
dp[i][0] = max(dp[i][0] ,dp[j][0]);
}
}
}
return *max_element(dp[n - 1].begin(),dp[n - 1].end());
}
};
494. 目标和
回溯做的,顺便写一下回溯的debug过程。
回溯法1.0版
void dfs(vector<int>& nums, int target ,int step)
{
if(step == nums.size() && target == 0)
{
ans++;
return;
}
dfs(nums,target - nums[step],step+1);
dfs(nums,target + nums[step],step+1);
}
错解,这里 && 导致调用dfs的时候可能会出现没有return的情况,递归就失败了
回溯法2.0版
void dfs(vector<int>& nums, int target ,int step)
{
if(step == nums.size())
{
if( target == 0)
{
ans++;
return;
}
}
dfs(nums,target - nums[step],step+1);
dfs(nums,target + nums[step],step+1);
}
错解,跟上面一样,满足条件下return到上一层,外层if没有return无法回溯。
回溯法最终版
void dfs(vector<int>& nums, int target ,int step)
{
if(step == nums.size())
{
if(target == 0)
{
ans++;
return;
}
return;
}
dfs(nums,target - nums[step],step+1);
dfs(nums,target + nums[step],step+1);
}
正解,都可以返回到上一层了。
但是这里记录一下官方的方法,不需要return,通过If - else 让语句全部执行完毕自动返回。
接下来是重点,动规方法。
所有的数据会被分为两个部分,一个前面带 + ,一个带 - 。假设带 - 好的数据之和为neg,neg与带正好部分sum - neg的关系就是
(sum−neg)−neg=sum−2⋅neg=target
也就是
neg= (sum−target)/ 2
这样就转换成了背包问题,其中dp[i][j] 表示在数组 nums 的前 i 个数中选取元素,使得这些元素之和等于 j 的方案数。假设数组nums 的长度为 n,则最终答案为 dp[n][neg]。接下来就是套路,不赘述了。
714. 买卖股票的最佳时机含手续费
股票会写了,我是股神(bushi
把关于边界和循环边界的思考记录一下。
dp[0][0]是指第一天无股票状态下最大收益,那就是0
dp[0][1]是指第一天有股票状态下最大收益,那就是刚买的,为 -prices[0]
这里是prices[0]开始,i循环应该是[1,n),每天的买入卖出操作是±prices[i],最后返回的是dp[n - 1][0];