首页 > 编程语言 >算法【从递归入手二维动态规划】

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

时间:2024-10-07 22:47:25浏览次数:7  
标签:算法 递归 int 二维 column length size dp row

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

一维、二维、三维甚至多维动态规划问题,大体过程都是:

1.写出尝试递归。

2.记忆化搜索(从顶到底的动态规划)。

3.严格位置依赖的动态规划(从底到顶的动态规划)。

4.空间、时间的更多优化。

动态规划表的大小:每个可变参数的可能性数量相乘。

动态规划方法的时间复杂度:动态规划表的大小 * 每个格子的枚举代价。

二维动态规划依然需要去整理动态规划表的格子之间的依赖关系,找寻依赖关系,往往通过画图来建立空间感,使其更显而易见。然后依然是从简单格子填写到复杂格子的过程,即严格位置依赖的动态规划(从底到顶)。

二维动态规划的压缩空间技巧原理不难,会了之后千篇一律。但是不同题目依赖关系不一样,需要 很细心的画图来整理具体题目的依赖关系。最后进行空间压缩的实现。

能改成动态规划的递归,统一特征:决定返回值的可变参数类型往往都比较简单,一般不会比int类型更复杂。

从这个角度,可以解释带路径的递归(可变参数类型复杂),不适合或者说没有必要改成动态规化。题目2就是说明这一点的。

一定要写出可变参数类型简单(不比int类型更复杂),并且可以完全决定返回值的递归,保证做到 这些可变参数可以完全代表之前决策过程对后续过程的影响,再去改动态规划。

不管几维动态规划,经常从递归的定义出发,避免后续进行很多边界讨论,这需要一定的经验来预知。

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

题目一

测试链接:https://leetcode.cn/problems/minimum-path-sum/

分析:这道题依旧是从递归到记忆化搜索到严格位置依赖到空间压缩,不过递归会超时,所以直接从记忆化搜索开始了。f方法求从下标i,j位置到右下角的和最小为多少。递归调用并采用记忆化搜索即可得到答案。代码如下。

class Solution {
public:
    int row;
    int column;
    int dp[200][200];
    void build(){
        for(int i = 0;i < row;++i){
            for(int j = 0;j < column;++j){
                dp[i][j] = -1;
            }
        }
    }
    int f(int i, int j, vector<vector<int>>& grid){
        if(dp[i][j] != -1){
            return dp[i][j];
        }
        int ans = -((1 << 31) + 1);
        if(j + 1 < column){
            ans = ans < f(i, j+1, grid) ? ans : f(i, j+1, grid);
        }
        if(i + 1 < row){
            ans = ans < f(i+1, j, grid) ? ans : f(i+1, j, grid);
        }
        dp[i][j] = ans == -((1 << 31) + 1) ? grid[i][j] : ans+grid[i][j];
        return dp[i][j];
    }
    int minPathSum(vector<vector<int>>& grid) {
        row = grid.size();
        column = grid[0].size();
        build();
        return f(0, 0, grid);
    }
};

从代码中可以看出这个题目的遍历方向以及递推公式,故可以写出严格位置依赖的动态规划。代码如下。

class Solution {
public:
    int row;
    int column;
    int dp[200][200];
    int minPathSum(vector<vector<int>>& grid) {
        row = grid.size();
        column = grid[0].size();
        for(int i = row-1;i >= 0;--i){
            for(int j = column-1;j >= 0;--j){
                int ans = -((1 << 31) + 1);
                if(j + 1 < column){
                    ans = ans < dp[i][j+1] ? ans : dp[i][j+1];
                }
                if(i + 1 < row){
                    ans = ans < dp[i+1][j] ? ans : dp[i+1][j];
                }
                dp[i][j] = ans == -((1 << 31) + 1) ? grid[i][j] : ans+grid[i][j];
            }
        }
        return dp[0][0];
    }
};

从代码中可以看出,只需使用一个一维数组滚动遍历即可,这样就可以得到空间压缩的代码。代码如下。

class Solution {
public:
    int row;
    int column;
    int dp[200];
    int minPathSum(vector<vector<int>>& grid) {
        row = grid.size();
        column = grid[0].size();
        dp[column-1] = grid[row-1][column-1];
        for(int i = column-2;i >= 0;--i){
            dp[i] = dp[i+1] + grid[row-1][i];
        }
        for(int i = row-2;i >= 0;--i){
            for(int j = column-1;j >= 0;--j){
                if(j + 1 < column){
                    dp[j] = dp[j] < dp[j+1] ? dp[j] : dp[j+1];
                }
                dp[j] += grid[i][j];
            }
        }
        return dp[0];
    }
};

这个题目几种解法的递进和之前的一维递归很像,在后面的题目中就不会再出现完整的全部解法的代码了。

题目二

测试链接:https://leetcode.cn/problems/word-search/

分析:这个题一看就会想到深度优先搜索,也是一个递归。不过这种带路径的递归没有必要改成动态规划,所以当成深搜题目即可。代码如下。

class Solution {
public:
    int row;
    int column;
    bool f(int i, int j, int index, vector<vector<char>>& board, string word){
        if(index == word.size()){
            return true;
        }
        if(i < 0 || i >= row || j < 0 || j >= column || word[index] != board[i][j]){
            return false;
        }
        char temp = board[i][j];
        board[i][j] = 0;
        bool ans = f(i-1, j, index+1, board, word)
        || f(i+1, j, index+1, board, word)
        || f(i, j-1, index+1, board, word)
        || f(i, j+1, index+1, board, word);
        board[i][j] = temp;
        return ans;
    }
    bool exist(vector<vector<char>>& board, string word) {
        row = board.size();
        column = board[0].size();
        for(int i = 0;i < row;++i){
            for(int j = 0;j < column;++j){
                if(f(i, j, 0, board, word)){
                    return true;
                }
            }
        }
        return false;
    }
};

题目三

测试链接:https://leetcode.cn/problems/longest-common-subsequence/

分析:这道题目普通递归和记忆化搜索都会超时,所以给出严格位置依赖和空间压缩的解法。dp数组的含义是字符串text1前i个字符和字符串text2前j个字符的最长公共子序列长度。这里之所以用个数而不是下标,是避免多个边界讨论,用下标也可以求解,只是代码较为冗余。代码如下。

class Solution {
public:
    int length_1;
    int length_2;
    int dp[1001][1001] = {0};
    int longestCommonSubsequence(string text1, string text2) {
        length_1 = text1.size();
        length_2 = text2.size();
        for(int i = 1;i <= length_1;++i){
            for(int j = 1;j <= length_2;++j){
                if(text1[i-1] == text2[j-1]){
                    dp[i][j] = dp[i-1][j-1] + 1;
                }else{
                    dp[i][j] = dp[i-1][j] > dp[i][j-1] ?
                    dp[i-1][j] : dp[i][j-1];
                }
            }
        }
        return dp[length_1][length_2];
    }
};

从代码中可以看出遍历方向从而得到空间压缩的解法。代码如下。

class Solution {
public:
    int length_1;
    int length_2;
    int dp[1001] = {0};
    int leftup1, leftup2;
    int longestCommonSubsequence(string text1, string text2) {
        length_1 = text1.size();
        length_2 = text2.size();
        for(int i = 1;i <= length_1;++i){
            leftup1 = 0;
            for(int j = 1;j <= length_2;++j){
                leftup2 = dp[j];
                if(text1[i-1] == text2[j-1]){
                    dp[j] = leftup1 + 1;
                }else{
                    dp[j] = dp[j] > dp[j-1] ? dp[j] : dp[j-1];
                }
                leftup1 = leftup2;
            }
        }
        return dp[length_2];
    }
};

其中,leftup1是保存即将可能被用到的左上角的dp值,leftup2是保存下一个可能被用到的左上角的dp值。

题目四

测试链接:https://leetcode.cn/problems/longest-palindromic-subsequence/

分析:这道题普通递归和记忆化搜索也是会超时,所以给出严格位置依赖和空间压缩的解法。dp数组的含义是从下标i到下标j中最长回文字序列的长度。代码如下。

class Solution {
public:
    int length;
    int dp[1000][1000] = {0};
    int longestPalindromeSubseq(string s) {
        length = s.size();
        for(int i = 0;i < length;++i){
            dp[i][i] = 1;
        }
        for(int i = length-2;i >= 0;--i){
            for(int j = i+1;j < length;++j){
                if(j == i+1){
                    dp[i][j] = s[i] == s[j] ? 2 : 1;
                }else{
                    if(s[i] == s[j]){
                        dp[i][j] = 2 + dp[i+1][j-1];
                    }else{
                        dp[i][j] = dp[i+1][j] > dp[i][j-1] ?
                        dp[i+1][j] : dp[i][j-1];
                    }
                }
            }
        }
        return dp[0][length-1];
    }
};

从代码中可以看出遍历方向从而得到空间压缩的解法。代码如下。

class Solution {
public:
    int length;
    int leftdown1, leftdown2;
    int dp[1000] = {0};
    int longestPalindromeSubseq(string s) {
        length = s.size();
        if(length == 1){
            return 1;
        }
        if(length == 2){
            return s[0] == s[1] ? 2 : 1;
        }
        for(int i = length-1;i >= 0;--i){
            dp[i] = 1;
            leftdown1 = 0;
            for(int j = i+1;j < length;++j){
                leftdown2 = dp[j];
                if(j == i+1){
                    dp[j] = s[i] == s[j] ? 2 : 1;
                }else{
                    if(s[i] == s[j]){
                        dp[j] = 2 + leftdown1;
                    }else{
                        dp[j] = dp[j] > dp[j-1] ?
                        dp[j] : dp[j-1];
                    }
                }
                leftdown1 = leftdown2;
            }
        }
        return dp[length-1];
    }
};

其中,leftdown1以及leftdown2和上一题的leftup含义基本一致,leftdown1是保存即将可能被用到的左下角的dp值,leftup2是保存下一个可能被用到的左下角的dp值。

题目五

测试链接:https://www.nowcoder.com/practice/aaefe5896cce4204b276e213e725f3ea

分析:下面给出记忆化搜索和严格位置依的解法。dp数组代表i个节点高度不超过j时有多少种方案。代码如下。

#include <iostream>
#include <vector>
#define MOD 1000000007
using namespace std;
int n, m;
int dp[51][51];
void build(){
    for(int i = 0;i <= n;++i){
        for(int j = 0;j <= m;++j){
            dp[i][j] = -1;
        }
    }
    for(int i = 0;i <= m;++i){
        dp[0][i] = 1;
    }
    dp[1][0] = 0;
    for(int i = 1;i <= m;++i){
        dp[1][i] = 1;
    }
    dp[2][0] = 0;
    dp[2][1] = 0;
    for(int i = 2;i <= m;++i){
        dp[2][i] = 2;
    }
}
int f(int i, int j){
    if(dp[i][j] != -1){
        return dp[i][j];
    }
    int ans = 0;
    long long temp;
    for(int num = 0;num < i;++num){
        temp = (long long)f(num, j-1) * (long long)f(i-1-num, j-1);
        ans = (int)(((long long)ans + temp) % MOD);
    }
    dp[i][j] = ans;
    return ans;
}
int main(void){
    scanf("%d%d", &n, &m);
    build();
    printf("%d", f(n, m));
}

从代码中看出遍历方向,从而得到严格位置依赖的解法。代码如下。

#include <iostream>
#define MOD 1000000007
using namespace std;
int n, m;
int dp[51][51] = {0};
int main(void){
    scanf("%d%d", &n, &m);
    for(int i = 0;i <= m;++i){
        dp[0][i] = 1;
    }
    for(int i = 1;i <= m;++i){
        dp[1][i] = 1;
    }
    for(int i = 2;i <= m;++i){
        dp[2][i] = 2;
    }
    long long temp;
    for(int i = 3;i <= n;++i){
        for(int j = 2;j <= m;++j){
            for(int num = 0;num < i;++num){
                temp = (long long)dp[num][j-1] * (long long)dp[i-1-num][j-1];
                dp[i][j] = (int)(((long long)dp[i][j] + temp) % MOD);
            }
        }
    }
    printf("%d", dp[n][m]);
}

题目六

测试链接:https://leetcode.cn/problems/longest-increasing-path-in-a-matrix/

分析:这个题给出记忆化搜索的解法,这种带路径的题目没有改成严格位置依赖的必要。dp数组的含义为从ij下标位置一直递增的最大路径长度。代码如下。

class Solution {
public:
    int row;
    int column;
    int arr[5] = {0, -1, 0, 1, 0};
    int dp[200][200] = {0};
    int f(int i, int j, vector<vector<int>>& matrix){
        if(dp[i][j] != 0){
            return dp[i][j];
        }
        int len = 0;
        int x, y;
        for(int index = 0;index < 4;++index){
            x = i+arr[index];
            y = j+arr[index+1];
            if(x >= 0 && x < row && y >= 0 && y < column && matrix[i][j] < matrix[x][y]){
                len = len > f(x, y, matrix) ? len : f(x, y, matrix);
            }
        }
        dp[i][j] = len+1;
        return len+1;
    }
    int longestIncreasingPath(vector<vector<int>>& matrix) {
        row = matrix.size();
        column = matrix[0].size();
        int ans = 0;
        for(int i = 0;i < row;++i){
            for(int j = 0;j < column;++j){
                ans = ans > f(i, j, matrix) ? ans : f(i, j, matrix);
            }
        }
        return ans;
    }
};

标签:算法,递归,int,二维,column,length,size,dp,row
From: https://blog.csdn.net/W_O_Z/article/details/142615799

相关文章

  • 代码随想录算法训练营day8|344.反转字符串 ● 541. 反转字符串II ● 卡码网:54.替换数
    学习资料:https://programmercarl.com/0344.反转字符串.html#算法公开课在python中字符串不可变,所以要增加空间lst=list(str)344.反转字符串(reverse库函数的基本代码)点击查看代码classSolution(object):defreverseString(self,s):""":types:List......
  • 代码随想录算法训练营 | 动态规划,509. 斐波那契数,70. 爬楼梯, 746. 使用最小花费爬楼梯
    动态规划:1.动态规划中每一个状态一定是由上一个状态推导出来的2.确定dp数组(dptable)以及下标的含义,确定递推公式dp,数组如何初始化,确定遍历顺序,举例推导dp数组;3.Debug:dp数组打印509.斐波那契数题目链接:509.斐波那契数文档讲解︰代码随想录(programmercarl.com)视频讲解︰斐波那......
  • Unity 二维数组序列化
    unity中,二维以上的数量是不支持序列化的,如:usingSystem.Collections.Generic;usingUnityEngine;publicclassTestArray:MonoBehaviour{//不支持序列化(在Inspector面板无法显示)publicRect[][]rect2Ds;//不支持序列化(在Inspector面板无法显示)//pub......
  • 数据结构课程设计大项目————迷宫问题(邻接矩阵,prim生成算法,DFS寻路,BFS寻路,路径回溯
    一.前言迷宫问题是数据结构中最值得实践的大项目之一,本文主要讲解思路,提供的代码大部分都有注释(没有的就是太多了懒得写了QAQ)。为了更好的表现效果,该程序使用了easyx可视化,easyx简单易学(大概一天到两天就可以学会),上手简单。该程序由c语言实现,本人水平有限程序可优化空间很大。......
  • KNN算法
    KNN算法一KNN算法介绍二KNN算法API2.1KNeighborsClassifier分类算法2.2KNeighborsRegressor回归算法三两个经典案例3.1鸢尾花案例3.2手写数字识别案例一KNN算法介绍K-近邻算法(KNearestNeighbor,简称KNN).比如根据你的“邻居”来推断出你的类别.KNN算法......
  • React 中的 diff 算法
    Reactdiff为什么使用虚拟DOM?浏览器在处理DOM的时候会很慢,处理JavaScript会很快,页面复杂的时候,频繁操作DOM会有很大的性能开销(每次数据变化都会引起整个DOM树的重绘和重排)。为了避免频繁操作DOM,React会维护两个虚拟DOM,如果有数据更新,会借此计算出所有修改的状态......
  • Day 24 贪心算法part02| LeetCode 122.买卖股票的最佳时机II,55.跳跃游戏,45.跳跃游戏II
    122.买卖股票的最佳时机II局部最优:每天的正利润全局最优:每天的正利润之和121.买卖股票的最佳时机classSolution{publicintmaxProfit(int[]prices){intres=0;for(inti=1;i<prices.length;i++){i......
  • 代码随想录算法训练营day7|● 454.四数相加II ● 383. 赎金信 ● 15. 三数之和 ●
    学习资料:https://programmercarl.com/0015.三数之和.html#其他语言版本学习记录:454.四数相加(hash_dict,前两个数一组遍历a+b,后两个数一组遍历找0-(a+b))点击查看代码classSolution:deffourSumCount(self,nums1:List[int],nums2:List[int],nums3:List[int],nums4:......
  • 代码随想录算法训练营 | 56. 合并区间,738.单调递增的数字
    56.合并区间题目链接:56.合并区间文档讲解︰代码随想录(programmercarl.com)视频讲解︰合并区间日期:2024-10-06想法:重叠区间类似问题Java代码如下:classSolution{publicint[][]merge(int[][]intervals){List<int[]>res=newArrayList<>();Arra......
  • C++ 算法学习——1.8 悬线法
    1.问题引入:对于一个矩形图,图中放置着不少障碍,要求出最大的不含障碍的矩形。2.分析:显然一个极大矩形是左右上下都被障碍挡住,无法再扩大的矩形,此时障碍也包括边界。3.方法:悬线法考虑以当前点所在行为下界,以往上能达到的最大距离为高度,正上方所有点的往左最大距离的最小值和往右......