Bootstrap

算法【从递归入手三维动态规划】

从递归到三维动态规划,包含多维费用背包,严格位置依赖的三维动态规划,三维动态规划的空间压缩

注意:多维费用背包问题就是很普通的动态规划。但是后面文章里还会安排背包dp的内容,那时候会把其他几种背包问题做汇总讲述。

尝试函数有1个可变参数可以完全决定返回值,进而可以改出1维动态规划表的实现。同理,尝试函数有2个可变参数可以完全决定返回值,那么就可以改出2维动态规划的实现。同理,尝试函数有3个可变参数可以完全决定返回值,那么就可以改出3维动态规划的实现。

大体过程都是:写出尝试递归->记忆化搜索(从顶到底的动态规划)->严格位置依赖的动态规划(从底到顶的动态规划)->空间、时间的更多优化

下面通过一些题目来加深理解。

题目一

测试链接:https://leetcode.cn/problems/ones-and-zeroes/

分析:下面直接给出记忆化搜索和严格位置依赖以及空间压缩的版本。对于递归可以构造三维dp数组,dp[index][i][j]代表从下标index开始0的个数不超过i,1的个数不超过j的最大子集长度。代码如下。

class Solution {
public:
    int dp[601][101][101];
    int zeros, ones;
    void getZerosAndOnes(int index, vector<string>& strs){
        zeros = 0;
        ones = 0;
        int length = strs[index].size();
        for(int i = 0;i < length;++i){
            if(strs[index][i] == '0'){
                ++zeros;
            }else{
                ++ones;
            }
        }
    }
    int f(int index, int i, int j, vector<string>& strs){
        if(dp[index][i][j] != -1){
            return dp[index][i][j];
        }
        if(index == strs.size()){
            dp[index][i][j] = 0;
            return 0;
        }
        int ans1 = f(index+1, i, j, strs);
        int ans2 = 0;
        getZerosAndOnes(index, strs);
        if(i - zeros >= 0 && j - ones >= 0){
            ans2 = f(index+1, i-zeros, j-ones, strs) + 1;
        }
        dp[index][i][j] = ans1 > ans2 ? ans1 : ans2;
        return dp[index][i][j];
    }
    void build(){
        for(int i = 0;i < 601;++i){
            for(int j = 0;j < 101;++j){
                for(int k = 0;k < 101;++k){
                    dp[i][j][k] = -1;
                }
            }
        }
    }
    int findMaxForm(vector<string>& strs, int m, int n) {
        build();
        return f(0, m, n, strs);
    }
};

下面是严格位置依赖的版本。代码如下。

class Solution {
public:
    int dp[601][101][101] = {0};
    int zeros, ones;
    void getZerosAndOnes(int index, vector<string>& strs){
        zeros = 0;
        ones = 0;
        int length = strs[index].size();
        for(int i = 0;i < length;++i){
            if(strs[index][i] == '0'){
                ++zeros;
            }else{
                ++ones;
            }
        }
    }
    int findMaxForm(vector<string>& strs, int m, int n) {
        int length = strs.size();
        for(int i = length-1;i >= 0;--i){
            for(int j = 0;j <= m;++j){
                for(int k = 0;k <= n;++k){
                    dp[i][j][k] = dp[i+1][j][k];
                    getZerosAndOnes(i, strs);
                    if(j - zeros >= 0 && k - ones >= 0){
                        dp[i][j][k] = dp[i][j][k] > dp[i+1][j-zeros][k-ones] + 1 ?
                        dp[i][j][k] : dp[i+1][j-zeros][k-ones] + 1;
                    }
                }
            }
        }
        return dp[0][m][n];
    }
};

下面是在严格位置依赖版本的基础上进行空间压缩的版本。代码如下。

class Solution {
public:
    int dp[101][101] = {0};
    int zeros, ones;
    void getZerosAndOnes(int index, vector<string>& strs){
        zeros = 0;
        ones = 0;
        int length = strs[index].size();
        for(int i = 0;i < length;++i){
            if(strs[index][i] == '0'){
                ++zeros;
            }else{
                ++ones;
            }
        }
    }
    int findMaxForm(vector<string>& strs, int m, int n) {
        int length = strs.size();
        for(int i = length-1;i >= 0;--i){
            for(int j = m;j >= 0;--j){
                for(int k = n;k >= 0;--k){
                    getZerosAndOnes(i, strs);
                    if(j - zeros >= 0 && k - ones >= 0){
                        dp[j][k] = dp[j][k] > dp[j-zeros][k-ones] + 1 ?
                        dp[j][k] : dp[j-zeros][k-ones] + 1;
                    }
                }
            }
        }
        return dp[m][n];
    }
};

其中,getZerosAndOnes方法是得到对应字符串中0和1的个数。

题目二

测试链接:https://leetcode.cn/problems/profitable-schemes/

分析:这道题和上一道题一样,依然给出三个版本的解答。对于记忆化搜索仍然可以采用递归的思路。dp[index][n][minProfit]代表从工作下标为index之后的工作中,在工作成员总数最多为n产生至少minProfit利润的盈利计划个数。代码如下。

class Solution {
public:
    int MOD = 1000000007;
    int dp[101][101][101];
    void build(){
        for(int i = 0;i < 101;++i){
            for(int j = 0;j < 101;++j){
                for(int k = 0;k < 101;++k){
                    dp[i][j][k] = -1;
                }
            }
        }
    }
    int f(int index, int n, int minProfit, vector<int>& group, vector<int>& profit){
        if(index == group.size()){
            return minProfit == 0 ? 1 : 0;
        }
        if(dp[index][n][minProfit] != -1){
            return dp[index][n][minProfit];
        }
        int ans1 = f(index+1, n, minProfit, group, profit);
        int ans2 = 0;
        if(n - group[index] >= 0){
            ans2 = f(index+1, n-group[index], minProfit-profit[index] <= 0 ? 0 : minProfit-profit[index], group, profit);
        }
        long long temp = (long long)ans1 + (long long)ans2;
        dp[index][n][minProfit] = (int)(temp % MOD);
        return dp[index][n][minProfit];
    }
    int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {
        build();
        return f(0, n, minProfit, group, profit);
    }
};

下面是严格位置依赖的版本。代码如下。

class Solution {
public:
    int MOD = 1000000007;
    int dp[101][101][101] = {0};
    int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {
        for(int i = 0;i <= n;++i){
            dp[group.size()][i][0] = 1;
        }
        long long temp;
        int profit_index;
        for(int i = group.size()-1;i >= 0;--i){
            for(int j = 0;j <= n;++j){
                for(int k = 0;k <= minProfit;++k){
                    dp[i][j][k] = dp[i+1][j][k];
                    if(j - group[i] >= 0){
                        profit_index = k - profit[i] <= 0 ? 0 : k - profit[i];
                        temp = (long long)dp[i][j][k] + (long long)dp[i+1][j-group[i]][profit_index];
                        dp[i][j][k] = (int)(temp % MOD);
                    }
                }
            }
        }
        return dp[0][n][minProfit];
    }
};

下面是空间压缩的版本。代码如下。

class Solution {
public:
    int MOD = 1000000007;
    int dp[101][101] = {0};
    int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {
        for(int i = 0;i <= n;++i){
            dp[i][0] = 1;
        }
        long long temp;
        int profit_index;
        for(int i = group.size()-1;i >= 0;--i){
            for(int j = n;j >= 0;--j){
                for(int k = minProfit;k >= 0;--k){
                    if(j - group[i] >= 0){
                        profit_index = k - profit[i] <= 0 ? 0 : k - profit[i];
                        temp = (long long)dp[j][k] + (long long)dp[j-group[i]][profit_index];
                        dp[j][k] = (int)(temp % MOD);
                    }
                }
            }
        }
        return dp[n][minProfit];
    }
};

题目三

测试链接:https://leetcode.cn/problems/knight-probability-in-chessboard/

分析:这道题直接给出记忆化搜索的版本,对于严格位置依赖的版本比较难写出。依然设置一个三维dp数组,dp[k][row][column]代表从row,column开始走k步,仍然停留在棋盘上的概率。代码如下。

class Solution {
public:
    double dp[101][25][25];
    void build(){
        for(int i = 0;i < 101;++i){
            for(int j = 0;j < 25;++j){
                for(int k = 0;k < 25;++k){
                    dp[i][j][k] = -1;
                }
            }
        }
    }
    double f(int k, int row, int column, int n){
        if(row >= n || column >= n || row < 0 || column < 0){
            return 0;
        }
        if(k == 0){
            return 1;
        }
        if(dp[k][row][column] != -1){
            return dp[k][row][column];
        }
        double ans = 0;
        ans += (f(k-1, row-2, column-1, n) / 8.0);
        ans += (f(k-1, row-2, column+1, n) / 8.0);
        ans += (f(k-1, row+2, column-1, n) / 8.0);
        ans += (f(k-1, row+2, column+1, n) / 8.0);
        ans += (f(k-1, row+1, column-2, n) / 8.0);
        ans += (f(k-1, row-1, column-2, n) / 8.0);
        ans += (f(k-1, row+1, column+2, n) / 8.0);
        ans += (f(k-1, row-1, column+2, n) / 8.0);
        dp[k][row][column] = ans;
        return ans;
    }
    double knightProbability(int n, int k, int row, int column) {
        build();
        return f(k, row, column, n);
    }
};

题目四

测试链接:https://leetcode.cn/problems/paths-in-matrix-whose-sum-is-divisible-by-k/

分析:这道题自然而然的能够想到三维动态规划,下面给出记忆化搜索和严格位置依赖的解法。dp[r][i][j]代表从i,j到右下角路径累加和模k等于r的路径个数。代码如下。

class Solution {
public:
    vector<vector<vector<int>>> dp;
    int row, column;
    int MOD = 1000000007;
    void build(int k){
        vector<vector<int>> temp1;
        vector<int> temp2;
        temp2.assign(column, -1);
        temp1.assign(row, temp2);
        dp.assign(k, temp1);
    }
    int f(int i, int j, int r, vector<vector<int>>& grid, int k){
        if(dp[r][i][j] != -1){
            return dp[r][i][j];
        }
        if(i == row-1 && j == column-1){
            dp[r][i][j] = grid[i][j] % k == r ? 1 : 0;
            return dp[r][i][j];
        }
        int cur_r = grid[i][j] % k;
        int next_r = (k + r - cur_r) % k;
        int ans = 0;
        if(i+1 < row){
            ans = f(i+1, j, next_r, grid, k);
        }
        if(j+1 < column){
            ans = (ans + f(i, j+1, next_r, grid, k)) % MOD;
        }
        dp[r][i][j] = ans;
        return ans;
    }
    int numberOfPaths(vector<vector<int>>& grid, int k) {
        row = grid.size();
        column = grid[0].size();
        build(k);
        return f(0, 0, 0, grid, k);
    }
};

下面是严格位置依赖的版本。代码如下。

class Solution {
public:
    vector<vector<vector<int>>> dp;
    int row, column;
    int MOD = 1000000007;
    void build(int k){
        vector<vector<int>> temp1;
        vector<int> temp2;
        temp2.assign(column, 0);
        temp1.assign(row, temp2);
        dp.assign(k, temp1);
    }
    int f(int i, int j, int r, vector<vector<int>>& grid, int k){
        if(dp[r][i][j] != -1){
            return dp[r][i][j];
        }
        if(i == row-1 && j == column-1){
            dp[r][i][j] = grid[i][j] % k == r ? 1 : 0;
            return dp[r][i][j];
        }
        int cur_r = grid[i][j] % k;
        int next_r = (k + r - cur_r) % k;
        int ans = 0;
        if(i+1 < row){
            ans = f(i+1, j, next_r, grid, k);
        }
        if(j+1 < column){
            ans = (ans + f(i, j+1, next_r, grid, k)) % MOD;
        }
        dp[r][i][j] = ans;
        return ans;
    }
    int numberOfPaths(vector<vector<int>>& grid, int k) {
        row = grid.size();
        column = grid[0].size();
        build(k);
        dp[grid[row-1][column-1] % k][row-1][column-1] = 1;
        for(int i = column-2;i >= 0;--i){
            for(int j = 0;j < k;++j){
                dp[j][row-1][i] = dp[(k+j-(grid[row-1][i] % k)) % k][row-1][i+1];
            }
        }
        for(int i = row-2;i >= 0;--i){
            for(int j = 0;j < k;++j){
                dp[j][i][column-1] = dp[(k+j-(grid[i][column-1] % k)) % k][i+1][column-1];
            }
        }
        for(int i = row-2;i >= 0;--i){
            for(int j = column-2;j >= 0;--j){
                for(int m = 0;m < k;++m){
                    dp[m][i][j] = (dp[(k+m-(grid[i][j] % k)) % k][i+1][j] + dp[(k+m-(grid[i][j] % k)) % k][i][j+1]) % MOD;
                }
            }
        }
        return dp[0][0][0];
    }
};

题目五

测试链接:https://leetcode.cn/problems/scramble-string/

分析:对于这道题,下面给出记忆化搜索和严格位置依赖两种解法。三维数组dp[index1][index2] [len]代表S1从下标index1开始,S2从下标index2开始长度为eln的子串是否为扰乱串。因为扰乱串可以交换,可以不交换,所以对于每一种长度,有两种判断。代码如下。

class Solution {
public:
    int dp[30][30][31];
    void build(){
        for(int i = 0;i < 30;++i){
            for(int j = 0;j < 30;++j){
                for(int k = 0;k < 31;++k){
                    dp[i][j][k] = -1;
                }
            }
        }
    }
    bool f(int index1, int index2, int len, string s1, string s2){
        if(dp[index1][index2][len] != -1){
            return dp[index1][index2][len];
        }
        if(len == 1){
            dp[index1][index2][len] = s1[index1] == s2[index2] ? 1 : 0;
            return dp[index1][index2][len];
        }
        int ans = 0;
        for(int gap = 1;gap < len;++gap){
            if(f(index1, index2, gap, s1, s2) && f(index1+gap, index2+gap, len-gap, s1, s2)){
                ans = 1;
                break;
            }
            if(f(index1, index2+len-gap, gap, s1, s2) && f(index1+gap, index2, len-gap, s1, s2)){
                ans = 1;
                break;
            }
        }
        dp[index1][index2][len] = ans;
        return ans;
    }
    bool isScramble(string s1, string s2) {
        build();
        int len = s1.size();
        return f(0, 0, len, s1, s2);
    }
};

下面是严格位置依赖的版本。代码如下。

class Solution {
public:
    int dp[30][30][31] = {0};
    bool isScramble(string s1, string s2) {
        int length = s1.size();
        for(int i = 0;i < length;++i){
            for(int j = 0;j < length;++j){
                dp[i][j][1] = s1[i] == s2[j] ? 1 : 0;
            }
        }
        for(int i = length-1;i >= 0;--i){
            for(int j = length-1;j >= 0;--j){
                for(int k = 2;k <= length && i+k <= length && j+k <= length;++k){
                    for(int gap = 1;gap < k;++gap){
                        if(dp[i][j][gap] && dp[i+gap][j+gap][k-gap]){
                            dp[i][j][k] = 1;
                            break;
                        }
                        if(dp[i][j+k-gap][gap] && dp[i+gap][j][k-gap]){
                            dp[i][j][k] = 1;
                            break;
                        }
                    }
                }
            }
        }
        return dp[0][0][length];
    }
};

;