今天看到“左神”有关动态规划的课程,瞬间觉得醍醐灌顶。

“左神”认为动态规划都可以从递归进行转移,无非动态规划是存储了递归过程中的中间变量,优化了递归用时。

以及大家都脑壳痛的动态规划转移方程如何得到:

  • 1 递归尝试
  • 2 见多识广

从递归->动态规划套路:

  • 1 手工进行尝试和推导,查看和总结规律
  • 2 尝试暴力递归
  • 3 从顶到底的动态规划:在暴力递归的基础上外挂缓存
    • dp数组中取数据
    • dp数组中存数据
  • 4 从底到顶的动态规划:抛弃暴力递归,直接从源头开始进行状态计算
    • 将ans和递归调用全部变成dp[i]
  • 5 空间压缩(从底到顶的动态规划基础上对递归数组进行空间压缩)
    • dp数组变成滚动的常数项个变量
    • dp[i] -> cur
    • next = dp[n], nextNext = dp[n+1] or prePre = dp[0], pre = dp[1]
    • return pre or return next

1 509. 斐波那契数

斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定 n ,请计算 F(n) 。

1.1 手工尝试

0 1 1 2 3 5 8 13 21 ……

规律 : f(n) = f(n-2) + f(n-1)

1.2 暴力递归

class Solution {
public:
    int fib(int n) {
        if (n == 0 || n == 1) return n;
        return fib(n-2) + fib(n-1);
    }
};

1.3 从顶到底的动态规划

class Solution {
public:
    int f1(int n, vector& dp){
        if (n == 0 || n == 1) return n;
        // 1 如果递归结果在递推数组中,则直接返回
        if (dp[n] != -1)  return dp[n];
        // 2 通过递归得到递推结果
        int ans = f1(n-2, dp) + f1(n-1, dp);
        // 3 将递归结果存储到递推数组
        dp[n] = ans;
        return dp[n];
    }

    int fib(int n) {
        vector dp(n+1, -1);
        return f1(n, dp);
    }
};

1.4 从底到顶的动态规划

class Solution {
public:
    int fib(int n) {
        if (n==0 || n==1) return n;
        vector dp(n+1, -1);
        dp[0] = 0;
        dp[1] = 1;
        for(int i=2; i<=n; i++){
            dp[i] = dp[i-2] + dp[i-1];
        }
        return dp[n];
    }
};

1.5 空间压缩

class Solution {
public:
    int fib(int n) {
        if (n==0 || n==1) return n;
        int prePre=0, pre=1;
        int cur;
        for(int i=2; i<=n; i++){
            cur = prePre + pre;
            prePre = pre;
            pre = cur;
        }
        return pre;
    }
};

2 983. 最低票价

在一个火车旅行很受欢迎的国度,你提前一年计划了一些火车旅行。在接下来的一年里,你要旅行的日子将以一个名为 days 的数组给出。每一项是一个从 1 到 365 的整数。

火车票有 三种不同的销售方式 :

  • 一张 为期一天 的通行证售价为 costs[0] 美元;
  • 一张 为期七天 的通行证售价为 costs[1] 美元;
  • 一张 为期三十天 的通行证售价为 costs[2] 美元。

通行证允许数天无限制的旅行。 例如,如果我们在第 2 天获得一张 为期 7 天 的通行证,那么我们可以连着旅行 7 天:第 2 天、第 3 天、第 4 天、第 5 天、第 6 天、第 7 天和第 8 天。

返回 你想要完成在给定的列表 days 中列出的每一天的旅行所需要的最低消费 

2.1 手工尝试

days  = [1, 4, 6, 7, 8, 20]

costs = [2, 7, 15]

f(x) : 从x天开始需要花费的最小金钱

f(x) = min({选择一天的cost+f(j) , 选择7天的cost+f(j), 选择30天的cost+f(j)})

f(0) = min({2 + f(1), 7+f(4), 15})

2.2 暴力递归

class Solution {
public:
    vector duration = {1, 7, 30};
    int f1(vector& days, vector& costs, int i){
        if (i == days.size()) return 0;
        int ans = INT_MAX;
        // i为当前day下标 j为下一个day下标 k为买什么票的下标
        for(int k=0, j=i; k<3; k++){
            while(j < days.size() && days[i]+duration[k] > days[j]) j++;
            ans = min(ans, costs[k] + f1(days, costs, j));
        }
        return ans;
    }

    int mincostTickets(vector& days, vector& costs) {
        int result = f1(days, costs, 0);
        return result;
    }
};

2.3 从顶到底的动态规划

class Solution {
public:
    vector duration = {1, 7, 30};
    int f1(vector& days, vector& costs, int i, vector& dp){
        if (i == days.size()) return 0;
        if (dp[i] != INT_MAX) return dp[i];
        int ans = INT_MAX;
        // i为当前day下标 j为下一个day下标 k为买什么票的下标
        for(int k=0, j=i; k<3; k++){
            while(j < days.size() && days[i]+duration[k] > days[j]) j++;
            ans = min(ans, costs[k] + f1(days, costs, j, dp));
        }
        dp[i] = ans;
        return ans;
    }

    int mincostTickets(vector& days, vector& costs) {
        vector dp(days.size()+1, INT_MAX);
        int result = f1(days, costs, 0, dp);
        return result;
    }
};

2.4 从底到顶的动态规划

class Solution {
public:
    vector duration = {1, 7, 30};

    int mincostTickets(vector& days, vector& costs) {
        vector dp(days.size()+1, INT_MAX);
        dp[days.size()] = 0;
        for(int i=days.size()-1; i>=0; i--){
            // i为当前day下标 j为下一个day下标 k为买什么票的下标
            for(int k=0, j=i; k<3; k++){
                while(j < days.size() && days[i]+duration[k] > days[j]) j++;
                dp[i] = min(dp[i], costs[k] + dp[j]);
            }
        }
        return dp[0];
    }
};

2.5 空间压缩

由于f(j)中的j下一个元素的索引不固定,所以无法使用空间压缩。

3 91. 解码方法

一条包含字母 A-Z 的消息通过以下映射进行了 编码 :

'A' -> "1"
'B' -> "2"
...
'Z' -> "26"

要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,"11106" 可以映射为:

  • "AAJF" ,将消息分组为 (1 1 10 6)
  • "KJF" ,将消息分组为 (11 10 6)

注意,消息不能分组为  (1 11 06) ,因为 "06" 不能映射为 "F" ,这是由于 "6" 和 "06" 在映射中并不等价。

给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数 。

题目数据保证答案肯定是一个 32 位 的整数。

3.1 手工尝试

“1101”: f(0) = f(1) + f(2)

  • 1  101
  • 11  01

3.2 暴力递归

class Solution {
public:
    int f1(string s, int i){
        if (i == s.size()) return 1;
        if (s[i] == '0') return 0;
        int ans = f1(s, i+1);
        if (i+1 < s.size() && (s[i]-'0')*10 + (s[i+1]-'0') <= 26){
            ans += f1(s, i+2);
        }
        return ans;
    }

    int numDecodings(string s) {
        int result = f1(s, 0);
        return result;
    }
};

3.3 从顶到底的动态规划

class Solution {
public:
    int f1(string s, int i, vector& dp){
        if (i == s.size()) return 1;
        if (dp[i] != -1) return dp[i];
        if (s[i] == '0') return 0;
        int ans = f1(s, i+1, dp);
        if (i+1 < s.size() && (s[i]-'0')*10 + (s[i+1]-'0') <= 26){
            ans += f1(s, i+2, dp);
        }
        dp[i] = ans;
        return ans;
    }

    int numDecodings(string s) {
        vector dp(s.size()+1, -1);
        return f1(s, 0, dp);
    }
};

3.4 从底到顶的动态规划

class Solution {
public:

    int numDecodings(string s) {
        vector dp(s.size()+1, 0);
        dp[s.size()] = 1;
        for(int i=s.size()-1; i>=0; i--){
            if (s[i] == '0'){
                dp[i] = 0; continue;
            }
            dp[i] = dp[i+1];
            if (i+1 < s.size() && (s[i]-'0')*10 + (s[i+1]-'0') <= 26){
                dp[i] += dp[i+2];
            }
        }
        return dp[0];
    }
};

3.5 空间压缩

class Solution {
public:

    int numDecodings(string s) {
        int next=1, nextNext=0;
        int cur;
        for(int i=s.size()-1; i>=0; i--){
            if (s[i] == '0') cur = 0;
            else{
                cur = next;
                if (i+1 < s.size() && (s[i]-'0')*10 + (s[i+1]-'0') <= 26){
                    cur += nextNext;
                }
            }
            nextNext = next;
            next = cur;
        }
        return next;
    }
};

4 639. 解码方法 II

一条包含字母 A-Z 的消息通过以下的方式进行了 编码 :

'A' -> "1"
'B' -> "2"
...
'Z' -> "26"

要 解码 一条已编码的消息,所有的数字都必须分组,然后按原来的编码方案反向映射回字母(可能存在多种方式)。例如,"11106" 可以映射为:

  • "AAJF" 对应分组 (1 1 10 6)
  • "KJF" 对应分组 (11 10 6)

注意,像 (1 11 06) 这样的分组是无效的,因为 "06" 不可以映射为 'F' ,因为 "6" 与 "06" 不同。

除了 上面描述的数字字母映射方案,编码消息中可能包含 '*' 字符,可以表示从 '1' 到 '9' 的任一数字(不包括 '0')。例如,编码字符串 "1*" 可以表示 "11""12""13""14""15""16""17""18" 或 "19" 中的任意一条消息。对 "1*" 进行解码,相当于解码该字符串可以表示的任何编码消息。

给你一个字符串 s ,由数字和 '*' 字符组成,返回 解码 该字符串的方法 数目 。

由于答案数目可能非常大,返回 109 + 7 的  。

4.1 手工尝试

4.2 暴力递归

4.3 从顶到底的动态规划

4.4 从底到顶的动态规划

4.5 空间压缩

5 264. 丑数 II

给你一个整数 n ,请你找出并返回第 n 个 丑数 。

丑数 就是质因子只包含 23 和 5 的正整数。

class Solution {
public:
    int nthUglyNumber(int n) {
        vector  dp(n+1, 0);
        dp[1] = 1;
        int i2 = 1, i3 = 1, i5 = 1;
        for(int i=2; i<=n; i++){
            dp[i] = min({2*dp[i2], 3*dp[i3], 5*dp[i5]});
            if (dp[i] == 2*dp[i2]) i2++;
            else if (dp[i] == 3*dp[i3]) i3++;
            else if (dp[i] == 5*dp[i5]) i5++;
            while (dp[i] == dp[i-1]){
                dp[i] = min({2*dp[i2], 3*dp[i3], 5*dp[i5]});
                if (dp[i] == 2*dp[i2]) i2++;
                else if (dp[i] == 3*dp[i3]) i3++;
                else if (dp[i] == 5*dp[i5]) i5++;
            }
        }
        return dp[n];
    }
};

6 32. 最长有效括号

给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

class Solution {
public:
    int longestValidParentheses(string s) {
        if (s=="") return 0;
        int ans = 0;
        vector dp(s.size(), 0);
        for(int i=1; i<s.size(); i++){ if (s[i] == ')'){ int pre = i - dp[i-1] - 1; // 匹配的左括号的位置 if (pre >= 0 && s[pre] == '('){
                    int temp = pre-1>=0?dp[pre-1] : 0;
                    dp[i] = 2 + dp[i-1] + temp; 
                }
            }
            ans = max(dp[i], ans);
        }
        return ans;
    }
};

7 467. 环绕字符串中唯一的子字符串

定义字符串 base 为一个 "abcdefghijklmnopqrstuvwxyz" 无限环绕的字符串,所以 base 看起来是这样的:

  • "...zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd....".

给你一个字符串 s ,请你统计并返回 s 中有多少 不同非空子串 也在 base 中出现。

该题不是传统动态规划使用下标位置作为dp数组的索引,而是使用字符作为dp数组的索引。

dp[‘a’] ……

8 940. 不同的子序列 II

给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数,结果需要对 109 + 7 取模。

s = "abc"
all : {}, {a}
all : {}, {a}, {b}, {ab}
all : {}, {a}, {b}, {ab}, {c}, {ac}, {bc}, {abc}

s = "aba"
all : {}, {a}
all : {}, {a}, {b}, {ab}
all : {}, {a}, {b}, {ab}, {a}, {aa}, {ba}, {aba} -> {}, {a}, {b}, {ab}, {aa}, {ba}, {aba}

s = "aaa"
all : {}, {a}
all : {}, {a}, {a}, {aa} -> {}, {a},{aa}
all : {}, {a}, {aa}, {a}, {aa}, {aaa} -> {}, {a}, {aa}, {aaa}

核心在于纯新增字符 = all – 当前上次记录

all中包含空集合, 字符集合中包含空集合。

s = "aba"
eg: "ab" -> "aba"
new add is : all({}, {a}, {b}, {ab}) - a("a") = 3 而不是4