数据结构与算法总结7(个人原创,带详细注释代码)

1143. 最长公共子序列

给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 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. 编辑距离

给你两个单词 word1word2请返回将 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] = 1j只需要从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 个气球,编号为0n - 1,每个气球上都标有一个数字,这些数字存在数组 nums 中。

现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 这里的 i - 1i + 1 代表和 i 相邻的两个气球的序号。如果 i - 1i + 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. 我从 1n 之间选择一个数字。
  2. 你来猜我选了哪个数字。
  3. 如果你猜到正确的数字,就会 赢得游戏
  4. 如果你猜错了,那么我会告诉你,我选的数字比你的 更大或者更小 ,并且你需要继续猜数。
  5. 每当你猜了数字 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

买卖股票的最佳时机-bilibili

学习状态机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,n1],但是循环对象变成了 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 i1 天的状态转移而来;我们也可以不进行任何操作,此时第 i i i 天的状态就等同于第 i − 1 i−1 i1 天的状态。那么我们分别对这三种状态进行分析:

对于 f [ i ] [ 0 ] f[i][0] f[i][0],我们目前持有的这一支股票可以是在第 i − 1 i−1 i1 天就已经持有的,对应的状态为 f [ i − 1 ] [ 0 ] f[i−1][0] f[i1][0];或者是第 i i i 天买入的,那么第 i − 1 i−1 i1 天就不能持有股票并且不处于冷冻期中,对应的状态为 f [ i − 1 ] [ 2 ] f[i−1][2] f[i1][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[i1][0],f[i1][2]prices[i])
对于 f [ i ] [ 1 ] f[i][1] f[i][1],我们在 i i i 天结束之后处于冷冻期的原因必定在当天卖出了股票,那么说明在第 i − 1 i−1 i1 天时我们必须持有一支股票,对应的状态为 f [ i − 1 ] [ 0 ] f[i−1][0] f[i1][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[i1][0]+prices[i]
对于 f [ i ] [ 2 ] f[i][2] f[i][2],我们在第 i i i 天结束之后不持有任何股票并且不处于冷冻期,说明当天没有进行任何操作,即第 i − 1 i−1 i1 天时不持有任何股票:如果处于冷冻期,对应的状态为 f [ i − 1 ] [ 1 ] f[i−1][1] f[i1][1];如果不处于冷冻期,对应的状态为 f [ i − 1 ] [ 2 ] f[i−1][2] f[i1][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[i1][1],f[i1][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[n1][0],f[n1][1],f[n1][2])
注意到如果在最后一天(第 n − 1 n−1 n1 天)结束之后,手上仍然持有股票,那么显然是没有任何意义的。因此更加精确地,最终的答案实际上是 f [ n − 1 ] [ 1 ] f[n−1][1] f[n1][1] f [ n − 1 ] [ 2 ] f[n−1][2] f[n1][2] 中的较大值,即:
m a x ( f [ n − 1 ] [ 1 ] , f [ n − 1 ] [ 2 ] ) max(f[n−1][1],f[n−1][2]) max(f[n1][1],f[n1][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,n1],但是循环对象变成了 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,n1] 区分,交易次数为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 0p0 而非 − i n f − p 0 -inf-p0 infp0

注意交易次数是 [ 0 , k ] [0,k] [0,k] 也即是 k + 1 k+1 k+1 次交易 ,与prices的下标取值 [ 0 , n − 1 ] [0,n-1] [0,n1] 也即是 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,n1], 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][j1] 转移过来,因此这里倒序遍历,就可以使得上一轮的 d p [ i ] [ j − 1 ] dp[i][j-1] dp[i][j1] 还没有更新成 d p [ i + 1 ] [ j − 1 ] dp[i+1][j-1] dp[i+1][j1]

由于 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. 统计构造好字符串的方案数

给你整数 zeroonelowhigh ,我们从空字符串开始构造一个字符串,每一步执行下面操作中的一种:

  • '0' 在字符串末尾添加 zero 次。
  • '1' 在字符串末尾添加 one 次。

以上操作可以执行任意次。

如果通过以上过程得到一个 长度lowhigh 之间(包含上下边界)的字符串,那么这个字符串我们称为 字符串。

请你返回满足以上要求的 不同 好字符串数目。由于答案可能很大,请将结果对 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;
    }
};
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值