1143. 最长公共子序列
给定两个字符串 text1
和 text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0
。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
- 例如,
"ace"
是"abcde"
的子序列,但"aec"
不是"abcde"
的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。
示例 2:
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。
回溯
- 状态定义 dfs(i,j):text1 的前 i 个字母和 text2 的前 j 个字母的最长公共子序列长度
- 转移方程:
- text[i] == text[j] dfs(i,j) = dfs(i-1,j-1) + 1 (二者当前字符相等时,出于贪心思想:都要选,LCS长度+1,然后看-1子串的LCS长度)
- text[i] != text[j] dfs(i,j) = max(dfs(i-1,j) , dfs(i,j-1)) (二者字符不等时,要么 i 往前一位,要么 j 往前一位,不用二者都往前,这种情况别上面二者包括了)
- 严格证明见下图
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int i = text1.length()-1,j = text2.length()-1;
function<int(int,int)> dfs = [&](int i,int j){
if(i<0||j<0) return 0;//如果下标不合法,必没有相同子序列,也即相同子序列长度为0
if(text1[i]==text2[j]) return dfs(i-1,j-1)+1;
else return max(dfs(i-1,j),dfs(i,j-1));
};
return dfs(i,j);
}
};
记忆化搜索
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int i = text1.length()-1,j = text2.length()-1;
vector<vector<int>> cache(i+1,vector<int>(j+1,-1));
function<int(int,int)> dfs = [&](int i,int j){
if(i<0||j<0) return 0;
if(cache[i][j]==-1)
{
if(text1[i]==text2[j]) cache[i][j] = dfs(i-1,j-1)+1;
else cache[i][j] = max(dfs(i-1,j),dfs(i,j-1));
}
return cache[i][j];
};
return dfs(i,j);
}
};
递推
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
vector<vector<int>> f(text1.length()+1,vector<int>(text2.length()+1));
for(int i=1;i<=text1.length();i++){
for(int j = 1;j<=text2.length();j++){
if(text1[i-1] == text2[j-1]) f[i][j] = f[i-1][j-1]+1;
else f[i][j] = max(f[i-1][j],f[i][j-1]);
}
}
return f[text1.length()][text2.length()];
}
};
空间优化
这题注意,从三个状态量转移而来:
- f [i-1] [j-1]
- f [i] [j-1]
- f [i-1] [j]
倒序遍历无法实现,正序遍历可以直接获得后两个状态量,可以每一次内层j循环中一个变量pre记录修改前的f [i] [j],这样下一轮j循环中该变量就表示f [i-1] [j-1]了
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
vector<int> f(text2.length()+1);
int pre = 0;
for(int i=1;i<=text1.length();i++){
pre = f[0];//每轮i一开始都将其pre从f[i-1][text2.length()]还原为f[i][0]
for(int j = 1;j<=text2.length();j++){
int tmp = f[j];//内层j循环中,记录下来未修改时的f[i][j]
if(text1[i-1] == text2[j-1]) f[j] = pre+1;
else f[j] = max(f[j],f[j-1]);
pre = tmp;//此时修改过后就变成f[i-1][j],j+1后该变量就变成了f[i-1][j-1]
}
}
return f[text2.length()];
}
};
72. 编辑距离
给你两个单词 word1
和 word2
, 请返回将 word1
转换成 word2
所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例 1:
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
回溯
考虑将word1匹配至word2所需的操作数
-
状态定义 dfs(i,j) = word1 中前 i 个字符与 word2 中前 j 个字符的「匹配」最少操作数
-
转移方程,考虑「匹配过程中的因素」
-
二者当前考虑的字符 i j 相等时,该字符无需任何操作即匹配完成,只需匹配前 i-1 j-1 个字符即可
因此word1[i]==word2[j] dfs(i,j) = dfs(i-1,j-1)(无需任何操作数,操作数与前i-1,j-1字符匹配相同)
-
二者字符不等时,考虑对word1三种操作后的状态
-
word1插入word2[j]字符后,word2[j]匹配完成,j往前一位变为j-1,原word1[i]尚未匹配,因此dfs(i,j-1)
-
word1[i]字符删除后,i往前一位变为i-1,原word2[j]尚未匹配,因此dfs(i-1,j)
-
word1[i]替换为word2[j]后,变成了上面的字符i,j相等的情况,即dfs(i-1,j-1)
-
以上三种情况取最小,+1操作数
-
因此dfs(i,j) = min ( dfs(i,j-1), dfs(i-1,j), dfs(i-1,j-1) ) + 1
-
-
初始化:
- i<0时,表示word1没有字符要匹配,将word2中[0,j]共j+1个字符插入word1,因此操作数为j+1
- j<0时,表示word2没有字符要匹配,将word1中[0,i]共i+1个字符删除,因此操作数为i+1
class Solution {
public:
int minDistance(string word1, string word2) {
function<int(int,int)> dfs = [&](int i,int j){
if(i<0) return j+1;
if(j<0) return i+1;
if(word1[i]==word2[j]) return dfs(i-1,j-1);
else return min(dfs(i,j-1),min(dfs(i-1,j),dfs(i-1,j-1)))+1;
};
return dfs(word1.size()-1,word2.size()-1);
}
};
记忆化搜索
class Solution {
public:
int minDistance(string word1, string word2) {
vector<vector<int>> cache(word1.size(),vector<int>(word2.size(),-1));
function<int(int,int)> dfs = [&](int i,int j){
if(i<0) return j+1;
if(j<0) return i+1;
int &cur = cache[i][j];
if(cur==-1){
if(word1[i]==word2[j]) cur = dfs(i-1,j-1);
else cur = min(dfs(i,j-1),min(dfs(i-1,j),dfs(i-1,j-1)))+1;
}
return cur;
};
return dfs(word1.size()-1,word2.size()-1);
}
};
递推
注意数组f [0] [0] 表示word1和word2都没有字符要匹配,因此操作数为0
class Solution {
public:
int minDistance(string word1, string word2) {
//f[0][0] = 0 word1和word2都**没有字符要匹配**,因此操作数为0
vector<vector<int>> f(word1.size()+1,vector<int>(word2.size()+1,0));
for(int j=1;j<=word2.size();j++) f[0][j] = j;
for(int i=1;i<=word1.size();i++) f[i][0] = i;
for(int i=1;i<=word1.size();i++){
for(int j=1;j<=word2.size();j++){
if(word1[i-1]==word2[j-1]) f[i][j] = f[i-1][j-1];
else f[i][j] = min(f[i-1][j],min(f[i][j-1],f[i-1][j-1]))+1;
}
}
return f.back().back();
}
};
空间优化
除了上题的用pre记录斜上方的f [i-1] [j-1]状态外
这题还需要每轮 i 循环开始时对f [i] [0]也就是f[0]初始化
class Solution {
public:
int minDistance(string word1, string word2) {
vector<int> f(word2.size()+1,0);
/*将下面二者的初始化等价翻译
for(int j=1;j<=word2.size();j++) f[0][j] = j;
for(int i=1;i<=word1.size();i++) f[i][0] = i;
*/
for(int j=1;j<=word2.size();j++) f[j] = j;//j在这里初始化了,i的初始化呢?
int pre;
for(int i=1;i<=word1.size();i++){
pre = f[0];//用pre记录f[i-1][j-1](j从1开始,因此第一个j的f[i-1][j-1]就是f[i-1][0])
f[0] = i;//每轮 i 循环开始时对f [i] [0]也就是f[0]初始化
for(int j=1;j<=word2.size();j++){
int tmp = f[j];
if(word1[i-1]==word2[j-1]) f[j] = pre;
else f[j] = min(f[j],min(f[j-1],pre))+1;//如果不每轮对f[0]初始化,其始终为f[0][0]也就是[0],那么每轮中j=1时,这里的f[i][j-1]即f[i][0]和pre(f[i-1][j-1]即f[i-1][0])都是错误的数值0
pre = tmp;
}
}
return f.back();
}
};
516. 最长回文子序列
给你一个字符串 s
,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
示例 1:
输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。
示例 2:
输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。
回溯/区间dp
子序列的本质是「选或不选」,如果我们考虑头尾双指针的「选或不选」,就变成了与上题最长公共子序列一样的思路
注意边界条件,i == j即一个字母时,其本身就是回文序列,返回1
i == j+1时,也即从i==j-1转移过来时,回文子序列长度返回0
class Solution {
public:
int longestPalindromeSubseq(string s) {
function<int(int,int)> dfs = [&](int i,int j){
if(i==j) return 1;
else if(i == j+1) return 0;
if(s[i]==s[j]) return dfs(i+1,j-1)+2;
else return max(dfs(i+1,j),dfs(i,j-1));
};
return dfs(0,s.length()-1);
}
};
记忆化搜索
class Solution {
public:
int longestPalindromeSubseq(string s) {
vector<vector<int>> cache(s.length(),vector<int>(s.length(),-1));
function<int(int,int)> dfs = [&](int i,int j){
if(i==j) return 1;
else if(i == j+1) return 0;
if(cache[i][j]==-1){
if(s[i]==s[j]) cache[i][j] = dfs(i+1,j-1)+2;
else cache[i][j] = max(dfs(i+1,j),dfs(i,j-1));
}
return cache[i][j];
};
return dfs(0,s.length()-1);
}
};
递推
注意 i 需要倒序遍历,i+1才是i的上一轮
- 之前的dp中,i 由 i-1 转移而来,因此 i -1 是 i 的上一轮,正序遍历
注意边界条件怎么写:一开始f数组均初始化为0,对于每轮的 i ,手动初始化f [i] [i] = 1,j只需要从i+1开始即可
class Solution {
public:
int longestPalindromeSubseq(string s) {
vector<vector<int>> f(s.length(),vector<int>(s.length(),0));
for(int i = f.size()-1;i>=0;i--){//注意 i 需要倒序遍历,i+1才是i的上一轮
f[i][i] = 1;//对于每轮的i,手动初始化f [i] [i] = 1
for(int j = i+1;j<f.size();j++){//j只需要从i+1开始即可
if(s[i]==s[j]) f[i][j] = f[i+1][j-1]+2;
else f[i][j] = max(f[i+1][j],f[i][j-1]);
}
}
return f[0][s.length()-1];
}
};
空间优化
在每轮 i 中给 f[i] 赋值1的基础上,设置 pre 保存 i,j 均为上一轮 i+1,j-1 时候的状态量
- 相当于 i-1 , j-1是 i,j的「左上角」上一轮状态的保存,只不过这里 i 由 i+1转移而来
- 因为对于每轮 i ,j 由 i+1 开始, [i,i+1] 的上一轮 为 [i+1,i] ,因此pre初始化为0
class Solution {
public:
int longestPalindromeSubseq(string s) {
vector<int> f(s.length(),0);
for(int i = f.size()-1;i>=0;i--){
f[i] = 1;//每轮 i 中给 f[i][i] 赋值1
int pre = 0;//f[i+1][i]为每轮第一个计算的f[i][i+1]的「左上角」状态,根据定义赋值0
for(int j = i+1;j<f.size();j++){
int tmp = f[j];//每轮保存f[i][j]作为下一轮的f[i+1][j-1]
if(s[i]==s[j]) f[j] = pre+2;
else f[j] = max(f[j],f[j-1]);
pre = tmp;
}
}
return f[s.length()-1];
}
};
312. 戳气球
有 n
个气球,编号为0
到 n - 1
,每个气球上都标有一个数字,这些数字存在数组 nums
中。
现在要求你戳破所有的气球。戳破第 i
个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1]
枚硬币。 这里的 i - 1
和 i + 1
代表和 i
相邻的两个气球的序号。如果 i - 1
或 i + 1
超出了数组的边界,那么就当它是一个数字为 1
的气球。
求所能获得硬币的最大数量。
示例 1:
输入:nums = [3,1,5,8]
输出:167
解释:
nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167
区间dp
对 nums 数组稍作处理,将其两边各加上题目中假设存在的 nums[−1] 和 nums[n] = 1作为头尾两个不存在的气球
我们观察戳气球的操作,发现这会导致两个气球从不相邻变成相邻,使得后续操作难以处理。于是我们倒过来看这些操作,将全过程看作是每次添加一个气球。
令 dfs(i,j) 表示将开区间 (i,j) 内的位置全部填满气球能够得到的最多硬币数。由于是开区间,因此开区间两端的气球的编号就是 i 和 j,对应着 nums[i] 和 nums[j],也即添加气球时左右两气球的值
-
当 i≥j−1 时,开区间中没有气球,dfs(i,j) 的值为 0;
-
当 i<j−1 时,我们枚举开区间 (i,j) 内的全部位置 mid(区间dp的含义),令 mid 为当前区间第一个添加的气球,该操作能得到的硬币数为 nums[i]×nums[mid]×nums[j]。同时我们递归地计算分割出的两区间对 dfs(i,j) 的贡献,这三项之和的最大值,即为 dfs(i,j) 的值。这样问题就转化为求 dfs(i,mid) 和 dfs(mid,j),可以写出方程:
记忆化搜索
class Solution {
public:
int maxCoins(vector<int>& nums) {
nums.push_back(1);
nums.insert(nums.begin(),1);
vector<vector<int>> cache(nums.size(),vector<int>(nums.size(),-1));
function<int(int,int)> dfs = [&](int left,int right){
if(left == right-1) return 0;
if(cache[left][right]==-1){
for(int i = left+1;i<right;i++){//在(i,j)区间内dp
cache[left][right] = max(cache[left][right],\
nums[i]*nums[left]*nums[right]+dfs(left,i)+dfs(i,right));
}
}
return cache[left][right];
};
return dfs(0,nums.size()-1);
}
};
递推
最终答案即为 dp[0] [n+1]。实现时要注意动态规划的次序
- 由于枚举 (i,j) 中的 k 时,k > i 且 k < j,而 f[i] [j] 由 f[i] [k], f[k] [j] 转移而来,因此 i 为倒序枚举(先求出「大于」当前 i 的 f[k] […]值),j 为正序枚举(先求出「小于」当前 j 的 f[…] [k]值)
class Solution {
public:
int maxCoins(vector<int>& nums) {
nums.push_back(1);
nums.insert(nums.begin(),1);
vector<vector<int>> f(nums.size(),vector<int>(nums.size(),0));//自带边界条件0
for(int i = nums.size()-1;i>=0;i--){//注意枚举顺序
for(int j = i+2;j<nums.size();j++){
for(int k = i+1;k<j;k++){
f[i][j] = max(f[i][j],nums[k]*nums[i]*nums[j]+f[k][j]+f[i][k]);
}
}
}
return f[0][nums.size()-1];
}
};
132. 分割回文串 II
给你一个字符串 s
,请你将 s
分割成一些子串,使每个子串都是回文串
返回符合要求的 最少分割次数 。
示例 1:
输入:s = "aab"
输出:1
解释:只需一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。
区间dp
这题同131不同在于数据量,131可以双指针中心扩散判断子串是否回文串,这题不行,需要在O(1)内判断是否回文串,因此第一遍dp判断回文串
f[i] [j] 表示 s中 [i,j] 子串是否为回文串,则转移方程为f[i] [j] = s[i] == s[j] && f[i+1] [j-1]
第二遍dp
f1[i] 表示 以 [0,i] 子串的最少分割次数,则转移:在 [0,i] 区间中枚举 j ,判断 [j,i] 是否回文串,即f[j] [i] == true
这样 f1[i] = f[j-1] + 1,在枚举中取最小值即得到 [0,i] 子串的最少分割次数
以上即为区间dp,即枚举区间的for循环内转移为子问题
class Solution {
public:
int minCut(string s) {
vector<vector<bool>> f(s.length(),vector<bool>(s.length(),true));
//第一遍dp:任意[i,j]子串是否回文串的预处理
for(int i = s.length()-1;i>=0;i--){
for(int j = i+1;j<s.length();j++){
f[i][j] = s[i]==s[j]&&f[i+1][j-1];
}
}
vector<int> f1(s.length()+1,INT_MAX);
f1[0] = -1;//注意初始化,如果[0,i]为一个回文串,分割次数为0,那么边界条件设为-1
for(int i = 1;i<f1.size();i++){
for(int j = i;j>0;j--){
if(f[j-1][i-1]==true) f1[i] = min(f1[j-1]+1,f1[i]);
}
}
return f1.back();
}
};
375. 猜数字大小 II
我们正在玩一个猜数游戏,游戏规则如下:
- 我从
1
到n
之间选择一个数字。 - 你来猜我选了哪个数字。
- 如果你猜到正确的数字,就会 赢得游戏 。
- 如果你猜错了,那么我会告诉你,我选的数字比你的 更大或者更小 ,并且你需要继续猜数。
- 每当你猜了数字
x
并且猜错了的时候,你需要支付金额为x
的现金。如果你花光了钱,就会 输掉游戏 。
给你一个特定的数字 n
,返回能够 确保你获胜 的最小现金数,不管我选择那个数字 。
示例 1:
输入:n = 10
输出:16
解释:制胜策略如下:
- 数字范围是 [1,10] 。你先猜测数字为 7 。
- 如果这是我选中的数字,你的总费用为 $0 。否则,你需要支付 $7 。
- 如果我的数字更大,则下一步需要猜测的数字范围是 [8,10] 。你可以猜测数字为 9 。
- 如果这是我选中的数字,你的总费用为 $7 。否则,你需要支付 $9 。
- 如果我的数字更大,那么这个数字一定是 10 。你猜测数字为 10 并赢得游戏,总费用为 $7 + $9 = $16 。
- 如果我的数字更小,那么这个数字一定是 8 。你猜测数字为 8 并赢得游戏,总费用为 $7 + $9 = $16 。
- 如果我的数字更小,则下一步需要猜测的数字范围是 [1,6] 。你可以猜测数字为 3 。
- 如果这是我选中的数字,你的总费用为 $7 。否则,你需要支付 $3 。
- 如果我的数字更大,则下一步需要猜测的数字范围是 [4,6] 。你可以猜测数字为 5 。
- 如果这是我选中的数字,你的总费用为 $7 + $3 = $10 。否则,你需要支付 $5 。
- 如果我的数字更大,那么这个数字一定是 6 。你猜测数字为 6 并赢得游戏,总费用为 $7 + $3 + $5 = $15 。
- 如果我的数字更小,那么这个数字一定是 4 。你猜测数字为 4 并赢得游戏,总费用为 $7 + $3 + $5 = $15 。
- 如果我的数字更小,则下一步需要猜测的数字范围是 [1,2] 。你可以猜测数字为 1 。
- 如果这是我选中的数字,你的总费用为 $7 + $3 = $10 。否则,你需要支付 $1 。
- 如果我的数字更大,那么这个数字一定是 2 。你猜测数字为 2 并赢得游戏,总费用为 $7 + $3 + $1 = $11 。
在最糟糕的情况下,你需要支付 $16 。因此,你只需要 $16 就可以确保自己赢得游戏。
示例 2:
输入:n = 1
输出:0
解释:只有一个可能的数字,所以你可以直接猜 1 并赢得游戏,无需支付任何费用。
示例 3:
输入:n = 2
输出:1
解释:有两个可能的数字 1 和 2 。
- 你可以先猜 1 。
- 如果这是我选中的数字,你的总费用为 $0 。否则,你需要支付 $1 。
- 如果我的数字更大,那么这个数字一定是 2 。你猜测数字为 2 并赢得游戏,总费用为 $1 。
最糟糕的情况下,你需要支付 $1 。
区间dp
假设dp[l][r]
为区间[l,r]
内的猜数字的最小成本。(这里并不是说猜对的最小成本,毕竟如果一下就猜中了,就不花钱了。而是说最少花多少钱,才可以让我稳赢。)
因此我们假设区间[l,r]
内某一个i
值,假设i
不是答案时,就要支付i
元,之后就要猜i
的左右两侧,i
的左右两侧各需要花dp[l][i-1]
以及dp[i+1][r]
。
由于猜i
错误,我们需要在左右两侧只选择一侧,直到猜对答案为止。根据这个规则dp[l][r]
应该由i+dp[l][i-1]
或者i+dp[i+1][r]
转移过来。此时,我们取左侧以及右侧成本最高的那个加上i
,就得到了当前稳赢的成本为:cur=Math.max(dp[l][i-1],dp[i+1][r])+i
Q:为什么要选一个成本最高的加上i
?
A:这是因为最高的那个成本确保了我可以稳赢。
最后在我能稳赢的基础上,选取最小的稳赢成本,即dp[l][r]=Math.min(dp[l][r],cur)
。
- 计算最坏反馈情况下的最少花费金额(选了x之后, 正确数字落在花费更高的那侧)
注意边界条件:递归中为 left >= right 时return 0
递推中怎么翻译?在下标原 [1,n] 范围上加上前后两个哨兵变成 [0,n+1],同时递推数组初始化为0,这样当left = 1,right = n,枚举变量 k = left 或者 k = right 时不会越界
class Solution {
public:
int getMoneyAmount(int n) {
// function<int(int,int)> dfs = [&](int left,int right){
// if(left>=right) return 0;
// int ret = INT_MAX,cur = INT_MIN;
// for(int i = left;i<=right;i++){
// cur = max(dfs(left,i-1),dfs(i+1,right))+i;
// ret = min(ret,cur);
// }
// return ret;
// };
// return dfs(1,n);
vector<vector<int>> f(n+2,vector<int>(n+2,0));
for(int i = n;i>=1;i--){
for(int j = i+1;j<=n;j++){
f[i][j] = INT_MAX/2;//除了边界条件外,每个单元格需要初始化
for(int k = i;k<=j;k++){
int cur = max(f[i][k-1],f[k+1][j]) + k;//k = i = 1时,如果没有下标0对应单元格,会越界,k = j = n时同理
f[i][j] = min(f[i][j],cur);
}
}
}
return f[1][n];
}
};
1039. 多边形三角剖分的最低得分
你有一个凸的 n
边形,其每个顶点都有一个整数值。给定一个整数数组 values
,其中 values[i]
是第 i
个顶点的值(即 顺时针顺序 )。
假设将多边形 剖分 为 n - 2
个三角形。对于每个三角形,该三角形的值是顶点标记的乘积,三角剖分的分数是进行三角剖分后所有 n - 2
个三角形的值之和。
返回 多边形进行三角剖分后可以得到的最低分 。
记忆化搜索
class Solution {
public:
int minScoreTriangulation(vector<int> &v) {
int n = v.size(), memo[n][n];
memset(memo, -1, sizeof(memo)); // -1 表示还没有计算过
function<int(int, int)> dfs = [&](int i, int j) -> int {
if (i + 1 == j) return 0; // 只有两个点,无法组成三角形
int &res = memo[i][j]; // 注意这里是引用,下面会直接修改 memo[i][j]
if (res != -1) return res;
res = INT_MAX;
for (int k = i + 1; k < j; ++k) // 枚举顶点 k
res = min(res, dfs(i, k) + dfs(k, j) + v[i] * v[j] * v[k]);
return res;
};
return dfs(0, n - 1);
}
};
递推
class Solution {
public:
int minScoreTriangulation(vector<int> &v) {
int n = v.size(), f[n][n];
memset(f, 0, sizeof(f));
for (int i = n - 3; i >= 0; --i)//因为j至少为i+2,因此i从(n-1)-2开始倒序枚举
for (int j = i + 2; j < n; ++j) {
f[i][j] = INT_MAX;
for (int k = i + 1; k < j; ++k)
f[i][j] = min(f[i][j], f[i][k] + f[k][j] + v[i] * v[j] * v[k]);
}
return f[0][n - 1];
}
};
1911. 最大子序列交替和
一个下标从 0 开始的数组的 交替和 定义为 偶数 下标处元素之 和 减去 奇数 下标处元素之 和 。
- 比方说,数组
[4,2,5,3]
的交替和为(4 + 5) - (2 + 3) = 4
。
给你一个数组 nums
,请你返回 nums
中任意子序列的 最大交替和 (子序列的下标 重新 从 0 开始编号)
一个数组的 子序列 是从原数组中删除一些元素后(也可能一个也不删除)剩余元素不改变顺序组成的数组。比方说,[2,7,4]
是 [4,**2**,3,**7**,2,1,**4**]
的一个子序列(加粗元素),但是 [2,4,2]
不是。
示例 1:
输入:nums = [4,2,5,3]
输出:7
解释:最优子序列为 [4,2,5] ,交替和为 (4 + 5) - 2 = 7 。
子序列&奇偶状态机dp
定义 f[i] [0] 表示前 i 个数中「长」为偶数的子序列的最大交替和,f[i] [1] 表示前 i 个数中长为奇数的子序列的最大交替和。
初始时有 f[0] [0]=0,f[0] [1]=−∞。
子序列:对于第 i 个数,有选或不选两种决策。
-
对于 f[i+1] [0],若不选第 i 个数,则从 f[i] [0] 转移过来,否则从 f[i] [1]−nums[i] 转移过来,取二者最大值。
-
对于 f[i+1] [1],若不选第 i 个数,则从 f[i] [1] 转移过来,否则从 f[i] [0]+nums[i] 转移过来,取二者最大值。
因此得到如下状态转移方程:
- f[i+1] [0]=max(f[i] [0],f[i] [1]−nums[i])
- f[i+1] [1]=max(f[i] [1],f[i] [0]+nums[i])
记 nums 的长度为 n,nums 子序列的最大交替和为 max(f[n] [0],f[n] [1])
注意到,由于长度为偶数的子序列的最后一个元素在交替和中需要取负号,在 nums 的元素均为正数的情况下,那不如不计入该元素。
因此 f[n] [1]>f[n] [0] 必然成立,于是返回 f[n] [1] 即可。
注意:正常下标从1开始时,「长」为偶数的子序列最后一个元素下标为偶数,比如1,2,那么新加入的元素就为奇数下标,转移方程为newodd = oldeven - nums[i],长为奇数子序列最后下标为奇数,比如1,2,3,转移方程为neweven = oldodd + nums[i]
但是下标从0开始,一切都相反,长度为偶数子序列,新加入的元素为偶数,比如0,1,新加入2为偶数下标,应该是+nums[2],转移方程:newodd = oldeven + nums[i],neweven = oldodd - nums[i]
class Solution {
public:
long long maxAlternatingSum(vector<int>& nums) {
// vector<pair<long long,long long>> f(nums.size()+1);
// f[0] = pair(0,INT_MIN/2);
// for(int i=0;i<nums.size();i++){
// auto [even,odd] = f[i];
// auto &[neweven,newodd] = f[i+1];
// neweven = max(even,odd-nums[i]);
// newodd = max(odd,even+nums[i]);
// }
// return f.back().second;
long long odd = INT_MIN, even = 0;
for(int i=0;i<nums.size();i++){
long long oldodd = odd;
odd = max(odd,even+nums[i]);
even = max(even,oldodd-nums[i]);
}
return odd;
}
};
300. 最长递增子序列
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的子序列
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
回溯(超时)
回溯有两种思路
-
选或不选思路,倒序思考,先选择3,那么选或不选3前面的数字时,还需要记录上一个选择的数字3下标,与其比较大小,麻烦
-
枚举选哪个,就可以在选完3以后,枚举选比3小的元素,只需要知道当前选择的 数字下标,方便
状态定义:dfs( i ) 表示以 nums[i] 结尾的数组的最长递增子序列(LIS)长度
转移方程:dfs(i) = max(dfs(j)) + 1 其中 0 <= j < i 也就是枚举 i 之前的比 nums[i] 小的元素
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
function<int(int)> dfs = [&](int index){
int res = 0;//这里自带初始化,因为index取到不可能的情况时,LIS长度即0
for(int j = index-1;j>=0;j--){
if(nums[j]<nums[index]) res = max(res,dfs(j));//取前面数中最大的LIS长度
}
return res+1;//+1返回当前Nums[i]结尾的LIS长度
};
int ans = 0;
for(int i=nums.size()-1;i>=0;i--){//对每个nums[i]都计算其结尾的LIS长度,取最大作为ans
ans = max(ans,dfs(i));
}
return ans;
}
};
记忆化搜索
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int> cache(nums.size(),-1);
function<int(int)> dfs = [&](int index){
int res = 0;
for(int j = index-1;j>=0;j--){
if(nums[j]<nums[index]){
if(cache[j]==-1) cache[j] = dfs(j);
res = max(cache[j],res);
}
}
return res+1;
};
int ans = 0;
for(int i=nums.size()-1;i>=0;i--){
ans = max(ans,dfs(i));
}
return ans;
}
};
递推
这里不用多一个数组头部元素作为j<0边界条件,只需要每次进入内层循环之前赋LIS默认值为0即可
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int> f(nums.size(),0);//正常创建f数组大小
int ans = 0;
for(int i=0;i<f.size();i++){
int maxLIS = 0;//边界条件在这里
for(int j=i-1;j>=0;j--){
if(nums[j]<nums[i]) maxLIS = max(f[j],maxLIS);
}
f[i] = maxLIS+1;
ans = max(ans,f[i]);
}
return ans;
}
};
贪心&二分
以上做法都是O(n^2)的,下面为O(nlogn)
因为g是严格递增的,可以用二分法,因此遍历nums数组,依次添加数字,二分找nums[i]的lower_bound
也就是g中第一个 >= nums[i]的数,将其替换为nums[i],替换保证了严格递增而非不严格递增
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int> g;
auto lower_bound = [&](int num){
int left = 0,right = g.size()-1;
while(left<=right){
int mid = (right-left)/2+left;
if(g[mid]>=num) right = mid-1;//找nums[i]的lower_bound,染成蓝色
else left = mid+1;
}
return left;
};
for(int num:nums){
int pos = lower_bound(num);
if(pos==g.size()) g.push_back(num);
else g[pos] = num;//替换
}
return g.size();
}
};
如果要求非严格递增子序列
对每个nums[i]找g中第一个 > 的数即可,也就是upper_bound
122. 买卖股票的最佳时机 II
给你一个整数数组 prices
,其中 prices[i]
表示某支股票第 i
天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
示例 1:
输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
总利润为 4 + 3 = 7 。
示例 2:
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
总利润为 4 。
示例 3:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0 。
lambda表达式赋值给函数对象
Lambda 表达式 lambda和函数对象语法 lambda表达式示例
函数调用重载 运算符重载 operator() 是 一整个 重载运算符
可以使用默认捕获模式来指示如何捕获 Lambda 体中引用的任何外部变量:
[&]
表示通过引用捕获所有外部变量,而[=]
表示通过值捕获它们返回类型必须跟在参数列表的后面,你必须在返回类型前面包含 trailing-return-type 关键字
->
由于 lambda 表达式已类型化,所以你可以将其指派给
auto
变量或function
对象,如下所示:// declaring_lambda_expressions1.cpp // compile with: /EHsc /W4 #include <functional> #include <iostream> int main() { using namespace std; // Assign the lambda expression that adds two numbers to an auto variable. auto f1 = [](int x, int y) { return x + y; }; cout << f1(2, 3) << endl;//5 // Assign the same lambda expression to a function object. function<int(int, int)> f2 = [](int x, int y) { return x + y; }; cout << f2(3, 4) << endl;//7 }
chatgpt
解释function<int(int, bool)> dfs = [&](int i, bool hold) -> int {}; 这段代码定义了一个 C++11 及以后版本的 lambda 表达式,并将其赋值给一个名为 dfs 的函数对象,其类型是 function<int(int, bool)>。这个 lambda 表达式表示一个深度优先搜索(DFS)函数。 #include <functional> int main() { std::function<int(int, bool)> dfs = [&](int i, bool hold) -> int { // 在这里实现 DFS 逻辑 // ... return 0; // 为了示例,此处返回一个整数 }; // 调用 DFS 函数 int result = dfs(0, true); return 0; } 这个 lambda 表达式接受两参数:一个整数 i 和一个布尔值 hold。lambda 表达式通过 & 捕获外部变量,此处使用的是引用捕获([&]),它表示捕获所有外部变量,并通过引用访问。捕获的外部变量在 lambda 表达式中可以直接使用。 返回类型被指定为 int,这是因为该 DFS 函数预期返回一个整数。在实际的 DFS 逻辑中,你可以根据问题的具体情况进行递归调用,更新状态等操作。 总的来说,这是一个典型的使用 lambda 表达式创建函数对象的例子,用于实现深度优先搜索的功能。
总之记住lambda模板
function<retype(para1,para2)> func = [&](type para1,type2 para) -> retype{
func(p1,p2)
};
状态机dp
学习状态机dp思想
递归
递归边界也可以设成day 0
flag为0表示第0天结束时,没持有股票,因此dfs(0,0)返回0
flag为1表示第0天结束时,持有股票,因此dfs(0,1)返回-prices[0]
从后往前递归,dfs(n-1,1)相当于最后一天手上还持有股票,没有意义,因此直接dfs(n-1,0)
灵茶山艾府的递归边界可以处理空数组
//递归边界设成第0天
if(day==0) return flag?-prices[0]:0;
class Solution {
public:
int maxProfit(vector<int> &prices) {
int n = prices.size();
function<int(int,int)> dfs = [&](int day,int flag) -> int{
if(day<0) return flag?INT_MIN:0;//递归边界,见上图,flag为1的情况,设为一个不可能的最小值,被下面的max抵了
if(flag == 0) return max(dfs(day-1,1)+prices[day],dfs(day-1,0));
else return max(dfs(day-1,0)-prices[day],dfs(day-1,1));
};
return dfs(n-1,0);
}
};
记忆化搜索(memset)
递归+记忆化 = 记忆化搜索
见 198.打家劫舍 记忆化搜索
memset(数组地址(无论一维二维), 初始值, sizeof(数组地址,无论一维二维))
memset是按照字节对待初始化空间进行初始化的,也就是说,函数里面的第二个参数的那个初值(一般为0/-1)是按照一个一个字节往第一个参数所指区域赋值的
也就是说memset只能用来初始化为 0/-1
sizeof(dp)给出dp的字节大小
看是否能用数组记忆,返回**「当前递归层」已经记忆的待求值**
class Solution {
public:
int maxProfit(vector<int> &prices) {
int n = prices.size();
int dp[n][2];
memset(dp,-1,sizeof(dp));//记住memset和sizeof对二维数组初始化
function<int(int,int)> dfs = [&](int day,int flag) -> int{
//if (i < 0) return flag ? INT_MIN : 0;
if(day==0) return flag?-prices[0]:0;
int &cur = dp[day][flag];//如果「当前递归」的值已经记忆,直接返回,否则继续往深处递归,并记忆返回值
if(cur!=-1) return cur;
if(flag == 0) cur = max(dfs(day-1,1)+prices[day],dfs(day-1,0));
else cur = max(dfs(day-1,0)-prices[day],dfs(day-1,1));
return cur;
};
return dfs(n-1,0);
}
};
递推
class Solution {
public:
int maxProfit(vector<int> &prices) {
int n = prices.size();
int dp[n][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for(int i=1;i<n;i++){
dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i]);
dp[i][1] = max(dp[i-1][1],dp[i-1][0]-prices[i]);
}
return dp[n-1][0];
}
};
也可以把全部状态往后挪一位,使得f[0]表示实际上f[-1]的状态
那么循环体还是从 [ 0 , n − 1 ] [0,n-1] [0,n−1],但是循环对象变成了 f [ i + 1 ] [ 0 ] f[i+1][0] f[i+1][0],也就是更新下标+1的元素,也即更新下标往后挪1的元素
class Solution {
public:
int maxProfit(vector<int> &prices) {
int n = prices.size(), f[n + 1][2];
memset(f, 0, sizeof(f));
f[0][1] = INT_MIN;
for (int i = 0; i < n; i++) {
f[i + 1][0] = max(f[i][0], f[i][1] + prices[i]);
f[i + 1][1] = max(f[i][1], f[i][0] - prices[i]);
}
return f[n][0];
}
};
空间优化
由于递推中只有用到了 昨天结束时 持有/未持有 两个状态量
可以变成滚动数组,两个变量表示昨天结束时 持有/未持有的手上的钱
class Solution {
public:
int maxProfit(vector<int> &prices) {
int n = prices.size();
int hold = -prices[0],nothold = 0;
// int dp[n][2];
// dp[0][0] = 0;
// dp[0][1] = -prices[0];
for(int i=1;i<n;i++){
// dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i]);
// dp[i][1] = max(dp[i-1][1],dp[i-1][0]-prices[i]);
int todaynothold = max(tmpnothold,hold+prices[i]);
int todayhold = max(hold,tmpnothold-prices[i]);
nothold = todaynothold;
hold = todayhold;
}
return nothold;
}
};
如上文,哨兵初始化为表示为-1天的状态
class Solution {
public:
int maxProfit(vector<int> &prices) {
int nothold = 0, hold = INT_MIN;
for (int p: prices) {
int todaynothold = max(tmpnothold,hold+p);
int todayhold = max(hold,tmpnothold-p);
nothold = todaynothold;
hold = todayhold;
}
return nothold;
}
};
贪心
找增量的部分
找到所有涨幅并相加,或者想象股票价格折线图的上坡,全部加起来
不用关心具体什么时候买卖,只需要关心今天与昨天的股票价格之差,如果为正数则获取利润,也就是将判断每一天的获利情况
class Solution {
public:
int maxProfit(vector<int>& prices) {
int ret = 0;
for(int i=1;i<prices.size();++i)
{
ret+=max((prices[i]-prices[i-1]),0);
}
return ret;
}
};
309. 买卖股票的最佳时机含冷冻期
给定一个整数数组prices
,其中第 prices[i]
表示第 *i*
天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
- 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: prices = [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
示例 2:
输入: prices = [1]
输出: 0
状态机dp
相较于上一题买股票II,多加一个冷冻期的状态,也就是变成 d p [ n ] [ 3 ] dp[n][3] dp[n][3] 的数组
我们用 f [ i ] f[i] f[i] 表示第 i i i 天结束时的「累计最大收益」。根据题目描述,由于我们最多只能同时买入(持有)一支股票,并且卖出股票后有冷冻期的限制,因此我们会有三种不同的状态:
我们目前持有一支股票,对应的「累计最大收益」记为 f [ i ] [ 0 ] f[i][0] f[i][0];
我们目前不持有任何股票,并且处于冷冻期中,对应的「累计最大收益」记为 f [ i ] [ 1 ] f[i][1] f[i][1];
我们目前不持有任何股票,并且不处于冷冻期中,对应的「累计最大收益」记为 f [ i ] [ 2 ] f[i][2] f[i][2]。
这里的「处于冷冻期」指的是在第 i i i 天结束之后的状态。也就是说:如果第 i i i 天结束之后处于冷冻期,那么第 i + 1 i+1 i+1 天无法买入股票。
如何进行状态转移呢?在第 i i i 天时,我们可以在不违反规则的前提下进行「买入」或者「卖出」操作,此时第 i i i 天的状态会从第 i − 1 i−1 i−1 天的状态转移而来;我们也可以不进行任何操作,此时第 i i i 天的状态就等同于第 i − 1 i−1 i−1 天的状态。那么我们分别对这三种状态进行分析:
对于 f [ i ] [ 0 ] f[i][0] f[i][0],我们目前持有的这一支股票可以是在第 i − 1 i−1 i−1 天就已经持有的,对应的状态为 f [ i − 1 ] [ 0 ] f[i−1][0] f[i−1][0];或者是第 i i i 天买入的,那么第 i − 1 i−1 i−1 天就不能持有股票并且不处于冷冻期中,对应的状态为 f [ i − 1 ] [ 2 ] f[i−1][2] f[i−1][2] 加上买入股票的负收益 p r i c e s [ i ] prices[i] prices[i]。因此状态转移方程为:
f [ i ] [ 0 ] = m a x ( f [ i − 1 ] [ 0 ] , f [ i − 1 ] [ 2 ] − p r i c e s [ i ] ) f[i][0]=max(f[i−1][0],f[i−1][2]−prices[i]) f[i][0]=max(f[i−1][0],f[i−1][2]−prices[i])
对于 f [ i ] [ 1 ] f[i][1] f[i][1],我们在第 i i i 天结束之后处于冷冻期的原因必定是在当天卖出了股票,那么说明在第 i − 1 i−1 i−1 天时我们必须持有一支股票,对应的状态为 f [ i − 1 ] [ 0 ] f[i−1][0] f[i−1][0] 加上卖出股票的正收益 p r i c e s [ i ] prices[i] prices[i]。因此状态转移方程为:f [ i ] [ 1 ] = f [ i − 1 ] [ 0 ] + p r i c e s [ i ] f[i][1]=f[i−1][0]+prices[i] f[i][1]=f[i−1][0]+prices[i]
对于 f [ i ] [ 2 ] f[i][2] f[i][2],我们在第 i i i 天结束之后不持有任何股票并且不处于冷冻期,说明当天没有进行任何操作,即第 i − 1 i−1 i−1 天时不持有任何股票:如果处于冷冻期,对应的状态为 f [ i − 1 ] [ 1 ] f[i−1][1] f[i−1][1];如果不处于冷冻期,对应的状态为 f [ i − 1 ] [ 2 ] f[i−1][2] f[i−1][2]。因此状态转移方程为:
f [ i ] [ 2 ] = m a x ( f [ i − 1 ] [ 1 ] , f [ i − 1 ] [ 2 ] ) f[i][2]=max(f[i−1][1],f[i−1][2]) f[i][2]=max(f[i−1][1],f[i−1][2])
这样我们就得到了所有的状态转移方程。如果一共有 n n n 天,那么最终的答案即为:
m a x ( f [ n − 1 ] [ 0 ] , f [ n − 1 ] [ 1 ] , f [ n − 1 ] [ 2 ] ) max(f[n−1][0],f[n−1][1],f[n−1][2]) max(f[n−1][0],f[n−1][1],f[n−1][2])
注意到如果在最后一天(第 n − 1 n−1 n−1 天)结束之后,手上仍然持有股票,那么显然是没有任何意义的。因此更加精确地,最终的答案实际上是 f [ n − 1 ] [ 1 ] f[n−1][1] f[n−1][1] 和 f [ n − 1 ] [ 2 ] f[n−1][2] f[n−1][2] 中的较大值,即:
m a x ( f [ n − 1 ] [ 1 ] , f [ n − 1 ] [ 2 ] ) max(f[n−1][1],f[n−1][2]) max(f[n−1][1],f[n−1][2])
将第 0 0 0 天的情况作为动态规划中的边界条件:
{ f [ 0 ] [ 0 ] = − p r i c e s [ 0 ] f [ 0 ] [ 1 ] = 0 f [ 0 ] [ 2 ] = 0 \begin{cases} f[0][0]=−prices[0]\\ f[0][1]=0\\ f[0][2]=0 \end{cases} ⎩ ⎨ ⎧f[0][0]=−prices[0]f[0][1]=0f[0][2]=0
在第 0 0 0 天时,如果持有股票,那么只能是在第 0 0 0 天买入的,对应负收益 − p r i c e s [ 0 ] −prices[0] −prices[0]如果不持有股票,那么收益为零
如果为冷冻期,实际上是不可能的,可以想象成当天买入卖出,因此当天结束时为冷冻期,因此「累计最大收益」为 0 0 0
记住:从买入(持有)结束日子状态,是不能直接变成卖出(空闲)结束日子状态的,必须先变成冷冻期状态
用状态机视角去看
class Solution {
public:
int maxProfit(vector<int>& prices) {
int hold = -prices[0], nothold = 0, freeze = 0;
for(int i=1;i<prices.size();i++){
int todayhold = max(hold,nothold-prices[i]);//不能从冷冻期变成持有股票
int todaynothold = max(freeze,nothold);//当天不是冷冻期,可能是冷冻期的下一天,也可能一直没持有
int todayfreeze = hold+prices[i];//当天卖出了股票,因此当天结束为冷冻期
freeze = todayfreeze, hold = todayhold, nothold = todaynothold;
}
return max(freeze,nothold);
}
};
哨兵初始化边界
class Solution {
public:
int maxProfit(vector<int>& prices) {
int hold = INT_MIN, nothold = 0, freeze = 0;//-1天持有是不可能的,因此根据下面的max,设为极小值,其余设为0
for(int price:prices){//从下标0开始,不用从1开始
int todayhold = max(hold,nothold-price);
int todaynothold = max(freeze,nothold);
int todayfreeze = hold+price;
freeze = todayfreeze, hold = todayhold, nothold = todaynothold;
}
return max(freeze,nothold);
}
};
少冷冻期状态的写法
相当于把上面两题 198打家劫舍 和 122买股票II 结合起来
只管今天是否买入股票的情况,不用管卖出,因为卖出没有冷冻期限制,只要手上有股票就可以卖,而买入有冷冻期限制
如果今天结束时持有股票,则昨天持有股票,或者今天买了股票,由于昨天不能卖出,因此「昨天开始时」必不持有股票,
也就是「前天结束时」必不持有股票
类比为 198打家劫舍 中偷房子,如果这个房子要偷,那么上个房子就不能偷,只能从上上个偷房子的最大钱算
并且考虑到本题特殊限制:「前天结束时」即为「昨天开始时」,而状态表示为「每天结束时」是否持有股票的收益
或者直接想成:前天结束时没持有,今天开始也没持有,那么昨天就什么也没干,正好符合昨天是“冷冻期”
用于理解的递归
class Solution {
public:
int maxProfit(vector<int> &prices) {
int n = prices.size();
function<int(int,int)> dfs = [&](int day,int flag) -> int{
if(day<0) return flag?INT_MIN:0;//递归边界,见上图,flag为1的情况,设为一个不可能的最小值,被下面的max抵了
if(flag == 0) return max(dfs(day-1,1)+prices[day],dfs(day-1,0));
else return max(dfs(day-2,0)-prices[day],dfs(day-1,1));//这里表示今天买入的改成dfs(day-2)即可,表示前天结束时不持有股票的最大收益
};
return dfs(n-1,0);
}
};
递推
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if(n<2) return 0;
int dp[n][2];
dp[0][0] = 0, dp[0][1] = -prices[0];
dp[1][0] = max(dp[0][0],dp[0][1]+prices[1]), dp[1][1] = max(dp[0][1],dp[0][0]-prices[1]);
for(int i=2;i<n;i++){
dp[i][0] = max(dp[i-1][0],dp[i-1][1]+prices[i]);
dp[i][1] = max(dp[i-1][1],dp[i-2][0]-prices[i]);
}
return dp[n-1][0];
}
};
或者按照灵茶山艾府,哨兵初始化
整体往后挪两位,dp[0],dp[1]表示-2,-1天的状态
那么循环体还是从 [ 0 , n − 1 ] [0,n-1] [0,n−1],但是循环对象变成了 f [ i + 2 ] [ 0 ] f[i+2][0] f[i+2][0],也就是更新下标+2的元素,也即更新整体往后挪2的元素
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if(n<2) return 0;
int dp[n+2][2];//往后挪2天
dp[0][0] = 0;//-2天
dp[1][0] = 0, dp[1][1] = INT_MIN;//-1天
for(int i=0;i<n;i++){//从第0(下标为2)天开始算,相当于往后挪2天
dp[i+2][0] = max(dp[i+1][0],dp[i+1][1]+prices[i]);//要计算的日子从0--n-1 --> 2--n+1
dp[i+2][1] = max(dp[i+1][1],dp[i][0]-prices[i]);
}
return dp[n+1][0];
}
};
空间优化
class Solution {
public:
int maxProfit(vector<int>& prices) {
//pre0 == dp[i-2][0] f0 == dp[i-1][0] f1 == dp[i-1][1]
int pre0 = 0, f0 = 0, f1 = INT_MIN;
for(int price:prices){
int newf0 = max(f1+price,f0);//今天「结束」时的未持有利润
f1 = max(pre0-price,f1);//今天「结束」时的持有利润
pre0 = f0;//天数往前推进,因此前天结束的未持有转移为昨天结束的未持有(随着prices的遍历)
f0 = newf0;
}
return f0;
}
};
特判初始化的版本,从下标2开始遍历
class Solution {
public:
int maxProfit(vector<int>& prices) {
if(prices.size()<2) return 0;
int pre0 = 0, f0 = max(prices[1]-prices[0],0), f1 = max(-prices[0],-prices[1]);
for(int i=2;i<prices.size();i++){
int newf0 = max(f1+prices[i],f0);
f1 = max(pre0-prices[i],f1);
pre0 = f0;
f0 = newf0;
}
return f0;
}
};
188. 买卖股票的最佳时机 IV
给你一个整数数组 prices
和一个整数 k
,其中 prices[i]
是某支给定的股票在第 i
天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k
笔交易。也就是说,你最多可以买 k
次,卖 k
次。
**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
示例 2:
输入:k = 2, prices = [3,2,6,5,0,3]
输出:7
解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。
递归
买+卖 算作一次交易,实际计算次数的时候,只在 买/卖 一个地方处加减次数即可
请注意:由于开始时与最后未持有股票,手上的股票一定会卖掉,所以代码中的
j-1
可以是在买股票的时候,也可以是在卖股票的时候,这两种写法都是可以的。
注意交易次数是 [ 0 , k ] [0,k] [0,k] ,与prices的下标取值 [ 0 , n − 1 ] [0,n-1] [0,n−1] 区分,交易次数为0-k均是合法的
卖出时记录交易次数
class Solution {
public:
int maxProfit(int k, vector<int> &prices) {
int n = prices.size();
function<int(int,int,int)> dfs = [&](int day, int times, int flag) -> int{
if(times<0) return INT_MIN;//递归边界,设为不可能的值
if(day<0) return flag?INT_MIN:0;//递归边界,flag为1的情况不可能发生,设为不可能的最小值,被下面的max抵了
if(flag == 0) return max(dfs(day-1,times-1,1)+prices[day],dfs(day-1,times,0));
else return max(dfs(day-1,times,0)-prices[day],dfs(day-1,times,1));
};
return dfs(n-1,k,0);
}
};
买入时记录交易次数
class Solution {
public:
int maxProfit(int k, vector<int> &prices) {
int n = prices.size();
function<int(int,int,int)> dfs = [&](int day, int times, int flag) -> int{
//递归边界,设为不可能的值,这里注意不能设为INT_MIN,因为下面会-prices[day],INT负溢出
if(times<0) return INT_MIN/2;
if(day<0) return flag?INT_MIN:0;
if(flag == 0) return max(dfs(day-1,times,1)+prices[day],dfs(day-1,times,0));
else return max(dfs(day-1,times-1,0)-prices[day],dfs(day-1,times,1));
};
return dfs(n-1,k,0);
}
};
记忆化搜索
买+卖 算作一次交易,实际计算次数的时候,只在 买/卖 一个地方处加减次数即可
注意先判断times是否不合理,再判断days,否则在买的时候算交易次数会出错
比如离散数学里面的**|**,这里的times和day的不合理性是|关系,只要有一个不合理,就返回极小值,
比如 d f s ( 0 , 0 , 1 ) = m a x ( d f s ( − 1 , − 1 , 0 ) − p 0 , d f s ( − 1 , 0 , 1 ) ) dfs(0,0,1) = max(dfs(-1,-1,0)-p0, dfs(-1,0,1)) dfs(0,0,1)=max(dfs(−1,−1,0)−p0,dfs(−1,0,1))
如果先判断 i f ( d a y < 0 ) if(day<0) if(day<0) 那么 d f s ( − 1 , − 1 , 0 ) − p 0 dfs(-1,-1,0)-p0 dfs(−1,−1,0)−p0 返回的是 0 − p 0 0-p0 0−p0 而非 − i n f − p 0 -inf-p0 −inf−p0
注意交易次数是 [ 0 , k ] [0,k] [0,k] 也即是 k + 1 k+1 k+1 次交易 ,与prices的下标取值 [ 0 , n − 1 ] [0,n-1] [0,n−1] 也即是 n n n 天 区分,交易次数为0-k均是合法的
class Solution {
public:
int maxProfit(int k, vector<int> &prices) {
int n = prices.size();
int dp[n][k+1][2];//注意交易次数有k+1种可能
memset(dp,-1,sizeof(dp));//注意这里的memset只能初始化为-1,而不可以其他值,因为memset是按照字节来初始化的
function<int(int,int,int)> dfs = [&](int day, int times, int flag) -> int{
if(times<0) return INT_MIN/2;
if(day<0) return flag?INT_MIN:0;
if(dp[day][times][flag]!=-1) return dp[day][times][flag];//判断当层是否已记忆化
//记住赋值表达式的值就是等号右值,可直接return
if(flag == 0)
return dp[day][times][flag] = max(dfs(day-1,times-1,1)+prices[day],dfs(day-1,times,0));
else
return dp[day][times][flag] = max(dfs(day-1,times,0)-prices[day],dfs(day-1,times,1));
};
return dfs(n-1,k,0);
}
};
递推
有 n n n 种可能的日子, k + 1 k+1 k+1 种可能的交易次数,因此三维数组设为 d p [ n + 1 ] [ k + 2 ] [ 2 ] dp[n+1][k+2][2] dp[n+1][k+2][2]
凡是交易次数为 -1 的全部元素以及 -1 天但是持有股票,均初始化为不合法,因此一开始全局设为不合法值
也就是要对i=0的全部元素和j=0的全部元素初始化
注意这里初始化为 INT_MIN/2 不能用memset,因为该数值不是 0/-1,学会vector的「俄罗斯套娃」初始化方法
然后单独把合法的-1天不持有股票情况初始化为0
然后 「对三维dp数组进行二维循环」,其中 i i i 从 [ 0 , n − 1 ] [0,n-1] [0,n−1], j j j 从 [ 0 , k ] [0,k] [0,k],但是更新的元素往后挪了一位,因此是 d p [ i + 1 ] [ j + 1 ] [ 0 / 1 ] dp[i+1][j+1][0/1] dp[i+1][j+1][0/1]
因为有ij两个循环维度,因此 单独把合法的-1天不持有股票 且交易次数>=0 的情况初始化为0 的初始化中
也要对i=0下整个j([1,k+1])维度进行0初始化,而不是只初始化一个点 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0]
也即是对所有-1天不持有股票0初始化时,需要考虑到所有的j值(即无论j取任何合法值都无所谓,只关心i维度上的初始化)
买+卖 算作一次交易,实际计算次数的时候,只在 买/卖 一个地方处加减次数即可
class Solution {
public:
int maxProfit(int k, vector<int> &prices) {
int n = prices.size();
//像「俄罗斯套娃」般初始化三维vector,INT_MIN/2是防止接下来买入时算作交易次数发生「负溢出」,如果卖出时算,就无负溢出
vector<vector<vector<int>>> dp(n+1,vector<vector<int>>(k+2,vector<int>(2,INT_MIN/2)));
for(int j=1;j<=k+1;j++){//单独把合法的-1天不持有股票 且交易次数>=0 的情况初始化为0
dp[0][j][0] = 0;
}
for(int i=0;i<n;i++){
for(int j=0;j<=k;j++){
dp[i+1][j+1][0] = max(dp[i][j+1][0],dp[i][j+1][1]+prices[i]);
dp[i+1][j+1][1] = max(dp[i][j+1][1],dp[i][j][0]-prices[i]);//买入时算作交易次数
}
}
/*或者j从[1,k+1]也可以,能更加看清 操作次数更替的状态转移
for(int i=0;i<n;i++){
for(int j=1;j<=k+1;j++){
dp[i+1][j][0] = max(dp[i][j][0],dp[i][j-1][1]+prices[i]);
dp[i+1][j][1] = max(dp[i][j][1],dp[i][j][0]-prices[i]);
}
}
*/
return dp[n][k+1][0];
}
};
官解写法,两个数组代替第三维,清晰一点
class Solution {
public:
int maxProfit(int k, vector<int> &prices) {
int n = prices.size();
vector<vector<int>> hold(n+1,vector<int>(k+2,INT_MIN/2));
vector<vector<int>> unhold(n+1,vector<int>(k+2,INT_MIN/2));
for(int j=1;j<=k+1;j++){//单独把合法的-1天 且交易次数>=0 不持有股票情况初始化为0
unhold[0][j] = 0;
}
for(int i=0;i<n;i++){
for(int j=1;j<=k+1;j++){
hold[i+1][j] = max(unhold[i][j]-prices[i],hold[i][j]);//更新的是挪后的元素(un)hold[i+1]
unhold[i+1][j] = max(unhold[i][j],hold[i][j-1]+prices[i]);
}
}
return unhold[n][k+1];
}
};
空间优化
由于上面dp[i+1]只用到dp[i] 的状态,也就是其上一轮的状态,因此这个「上一轮」可以滚动化
去掉i维度,相当于循环的时候就是在滚动了,上一轮的状态存储在尚未更新的元素格子中
由于 d p [ i + 1 ] [ j ] dp[i+1][j] dp[i+1][j] 需要从 d p [ i ] [ j − 1 ] dp[i][j-1] dp[i][j−1] 转移过来,因此这里倒序遍历,就可以使得上一轮的 d p [ i ] [ j − 1 ] dp[i][j-1] dp[i][j−1] 还没有更新成 d p [ i + 1 ] [ j − 1 ] dp[i+1][j-1] dp[i+1][j−1]
由于 d p [ j ] [ 0 ] dp[j][0] dp[j][0] 更新需要依赖 d p [ j ] [ 1 ] dp[j][1] dp[j][1],因此被依赖的那个元素放在后面更新(或者更改买/卖加减次数的地方)
正序也是对的,见官解
class Solution {
public:
int maxProfit(int k, vector<int> &prices) {
//直接把上面递推中除了循环体外的i空间删除即可
int n = prices.size();
vector<vector<int>> dp(k+2,vector<int>(2,INT_MIN/2));
for(int j=1;j<=k+1;j++){
dp[j][0] = 0;
}
for(int i=0;i<n;i++){
for(int j=k+1;j>=1;j--){//倒序更新,使得dp[i+1][j]依赖的dp[i][j/j-1]还没被更新为dp[i+1][j/j-1]
dp[j][0] = max(dp[j][0],dp[j][1]+prices[i]);//dp[j][0]依赖dp[j][1],因此前者先更新
dp[j][1] = max(dp[j][1],dp[j-1][0]-prices[i]);
}
}
return dp[k+1][0];
}
};
官解
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
vector hold(k+2, INT_MIN/2), unhold(k+2, 0);
unhold[0] = INT_MIN/2;
for (int i : prices) {
for (int j = k+1; j >=1 ; j--) {
unhold[j] = max(unhold[j], hold[j] + i);
hold[j] = max(hold[j], unhold[j - 1] - i);
}
}
return unhold.back();
}
};
213. 打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 3:
输入:nums = [1,2,3]
输出:3
动态规划的第一步问题分解
从第一个房子或者最后一个房子开始思考,因为他们的约束最少
比如考虑最后一个房子选还是不选
如果选了最后一个房子,就不能选第一个房子,那么相当于从将问题分解为 [1,n-1] 房子的最大金额
如果不选最后一个房子,问题问题分解为 [0,n-2] 房子的最大金额
class Solution {
public:
int rob(vector<int>& nums) {
int n =nums.size();
if(n==1) return nums[0];
if(n<=2) return max(nums[0],nums[1]);
//由于首元素和尾元素不能同时相连,故比较0到n-1以及1到n之间的动态规划最大值
int pre1 = nums[0],post1 = max(nums[0],nums[1]);//0到n-1的滚动数组
int pre2 = nums[1],post2 = max(nums[1],nums[2]);//1到n的滚动数组
for(int i=2;i<n-1;i++){
int cur1 = max(nums[i]+pre1,post1);
pre1 = post1;
post1 = cur1;
//为了简洁,放到一轮循环里面,但是遍历范围是不一样的,前后差1,因此这里为nums[i+1]
int cur2 = max(nums[i+1]+pre2,post2);
pre2 = post2;
post2 = cur2;
}
return max(post1,post2);
}
};
其实直接对滚动数组初始化为0,for范围从头开始即可
class Solution {
public:
int rob(vector<int>& nums) {
int n =nums.size();
if (nums.size() == 1) return nums[0];
int pre1 = 0,post1 = 0;//0到n-1的滚动数组
int pre2 = 0,post2 = 0;//1到n的滚动数组
for(int i=0;i<n-1;i++){
int cur1 = max(nums[i]+pre1,post1);
pre1 = post1;
post1 = cur1;
//为了简洁,放到一轮循环里面,但是遍历范围是不一样的,前后差1,因此这里为nums[i+1]
int cur2 = max(nums[i+1]+pre2,post2);
pre2 = post2;
post2 = cur2;
}
return max(post1,post2);
}
};
别人的,pop_back()和erase(nums2.begin()) 构造两个不同的遍历范围
class Solution {
public:
int rob(vector<int>& nums) {
int pre1 = 0;
int cur1 = 0;
int pre2 = 0;
int cur2 = 0;
if (nums.size() == 1) return nums[0];
//由于首元素和尾元素不能同时相连,故比较0到n-1以及1到n之间的动态规划最大值,再求出两者的最大值
vector<int> num1 = nums;
vector<int> num2 = nums;
num1.pop_back();
num2.erase(num2.begin());
//利用滚动数组,每个时刻只需存储前两间房屋最高金额,将空间复杂度降为O(1)
for (int i : num1) {
int temp1 = max(cur1, pre1 + i);
pre1 = cur1;
cur1 = temp1;
}
for (int i : num2) {
int temp2 = max(cur2, pre2 + i);
pre2 = cur2;
cur2 = temp2;
}
return max(cur1, cur2);
}
};
1388. 3n 块披萨
给你一个披萨,它由 3n 块不同大小的部分组成,现在你和你的朋友们需要按照如下规则来分披萨:
- 你挑选 任意 一块披萨。
- Alice 将会挑选你所选择的披萨逆时针方向的下一块披萨。
- Bob 将会挑选你所选择的披萨顺时针方向的下一块披萨。
- 重复上述过程直到没有披萨剩下。
每一块披萨的大小按顺时针方向由循环数组 slices
表示。
请你返回你可以获得的披萨大小总和的最大值。
示例 1:
输入:slices = [1,2,3,4,5,6]
输出:10
解释:选择大小为 4 的披萨,Alice 和 Bob 分别挑选大小为 3 和 5 的披萨。然后你选择大小为 6 的披萨,Alice 和 Bob 分别挑选大小为 2 和 1 的披萨。你获得的披萨总大小为 4 + 6 = 10 。
环形状态机dp
打家劫舍II的变形:拿了一块披萨,就不能拿相邻的披萨,那我们只从左往右遍历,只需考虑下标在前面的披萨有没有被拿,就变成了打家劫舍
加上了强制拿取次数的状态,f 数组多加一维表示状态即可,见买卖股票IV
class Solution {
public:
int maxSizeSlices(vector<int>& slices) {
int n = slices.size()/3;
//因为环形,slices[0]和slices[n-1]只能选一个,列两种情况
vector<vector<int>> f0(slices.size()+1,vector<int>(n+2,0));
vector<vector<int>> f1(slices.size()+1,vector<int>(n+2,0));
for(int i = 0;i<f0.size();i++){
f0[i][0] = f1[i][0] = INT_MIN/2;
}
for(int i = 0;i<slices.size()-1;i++){
for(int j = 1;j<n+2;j++){
f0[i+2][j] = max(f0[i+1][j],f0[i][j-1]+slices[i]);
f1[i+2][j] = max(f1[i+1][j],f1[i][j-1]+slices[i+1]);
}
}
return max(f0.back().back(),f1.back().back());
}
};
2466. 统计构造好字符串的方案数
给你整数 zero
,one
,low
和 high
,我们从空字符串开始构造一个字符串,每一步执行下面操作中的一种:
- 将
'0'
在字符串末尾添加zero
次。 - 将
'1'
在字符串末尾添加one
次。
以上操作可以执行任意次。
如果通过以上过程得到一个 长度 在 low
和 high
之间(包含上下边界)的字符串,那么这个字符串我们称为 好 字符串。
请你返回满足以上要求的 不同 好字符串数目。由于答案可能很大,请将结果对 10^9 + 7
取余 后返回。
递归边界
爬楼梯中,dfs(0)=1, dfs(1)=1。从 0 爬到 0 有「一种」方法,即原地不动。从 0 爬到 1 有一种方法,即爬 1 个台阶
因此这题中,递归边界:
// 构造空串的方案数为 1,不可能构造不合理的串
if(n<0) return 0;
if(n==0) return 1;
不需要对 n=zero 或者 n=one 初始化
- n<min(zero,one) 时,cache[n] = recur(negative)+recur(negative) = 0
- min(zero,one)<n<max(zero,one) 时,cache[n] = recur(n-min(zero,one))+recur(negative) = recur(n-min(zero,one))
- n>max(zero,one) 时,cache[n] = recur(n-zero)+recur(n-one)
记忆化递归
最后直接返回记忆化后的数组元素
class Solution {
public:
const int MOD = 1e9+7;//指数写法:10^9 --> 1e9
int ans = 0;
vector<int> cache;
int glozero,gloone;
int recur(int n){
if(n<0) return 0;//注意递归边界
if(n==0) return 1;
if(cache[n]!=-1) return cache[n];// 之前计算过
cache[n] = (recur(n-glozero)+recur(n-gloone))%MOD;//记忆化
return cache[n];//最后直接返回记忆化后的数组元素
}
int countGoodStrings(int low, int high, int zero, int one) {
glozero = zero;
gloone = one;
cache.resize(high+1,-1);
for(int h = low;h<=high;h++){
ans+=recur(h);
ans%=MOD;
}
return ans;
}
};