从递归到三维动态规划,包含多维费用背包,严格位置依赖的三维动态规划,三维动态规划的空间压缩
注意:多维费用背包问题就是很普通的动态规划。但是后面文章里还会安排背包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];
}
};