动态规划全总结(涵盖所有题型,左神思路全讲解+LeetCode)


动态规划是对暴力递归算法的优化,主要是通过数组记录的方法,优化掉一些重复计算的过程。总结下动态规划的过程:

(1) 抽象出一种“试法”,递归解决问题的方法,很重要

(2) 找到“试法”中的可变参数,规划成数组表,可变参数一般是0维的,有几个可变参数就是几维的表

(3) 找到base case,问题最基础的解,填入数组表中

(4) 根据“试法”中的递归过程,和base case已经填到数组表的值,继续填表

(5) 根据问题给定的参数,找到数组中对应的位置,就是最终的解

然后通过几个例子具体看一下动态规划是怎么玩的。

 设计模式总结:

递归函数的可变参数不能是数组类型,一个可变参数就是一维表,两个可变参数就是二维表。

常用总结:

//记住,只要递归的return参数复制,则dp就需要赋值。

        //记住,要将所有的递归return的参数全部换成dp。

        //此题中,递归返回的参数是min。那么在递归中最上面min = Integer.MaxValue, 就需要给dp数组都赋值为此。而不是继续使用min。

           //递归参数是减,那么dp的for循环就是从小到大,从初始数据先写0也能看出来是从小到大循环。

写递归方法时,判定停止条件可以是index == nums.length 也可以是index == nums.length - 1:

index == nums.length - 1  

这个停止条件适合那种小人移动问题,比如说一个m*n 数组最短路径,爬楼梯,打家劫舍。

其余的都用index == nums.length

模型二: 多样本位置全对应的尝试模型

【例1】机器人达到指定位置方法数

假设有排成一行的 N 个位置,记为 1~N,N 一定大于或等于 2。开始时机器人在其中的 M 位置上(M 一定是 1~N 中的一个),机器人可以往左走或者往右走,如果机器人来到 1 位置, 那么下一步只能往右来到 2 位置;如果机器人来到 N 位置,那么下一步只能往左来到 N-1 位置。规定机器人必须走 K 步,最终能来到 P 位置(P 也一定是 1~N 中的一个)的方法有多少种。给定四个参数 N、M、K、P,返回方法数。

【举例】N=5,M=2,K=3,P=3

上面的参数代表所有位置为 1 2 3 4 5。机器人最开始在 2 位置上,必须经过 3 步,最后到达 3 位置。走的方法只有如下 3 种: 1)从2到1,从1到2,从2到3 2)从2到3,从3到2,从2到3 3)从2到3,从3到4,从4到3
所以返回方法数 3。 N=3,M=1,K=3,P=3

上面的参数代表所有位置为 1 2 3。机器人最开始在 1 位置上,必须经过 3 步,最后到达 3
位置。怎么走也不可能,所以返回方法数 0。

一. 基本的递归方法

【分析】先抽象出“试法”。机器人当前在i位置,剩余步数为n。i:[1,N] n:[0,K]

机器人左移一步的方法数加上机器人在右边位置的方法数。

递归公式:f(i,n)=f(i-1,n-1)+f(i+1,n-1)

base case: n==0时,f(i,0)=i==P?1:0,即只有在P位置,才算是一种解法

特殊情况:题目中已经说了,1位置只能往右走,N位置只能往左位置走。

f(1,n)=f(2,n-1) f(N,n)=f(N-1,n-1)

所以写出暴力递归的解法:

下面看重复解:

第一个数表示人所在的位置,第二个数表示还剩下几步可以走。

由图中可见,(7,8)是一个重复解。把(7,8)的返回结果存起来可以减少一步的运算。

如果出现重复的递归节点,则可以考虑用动态规划进行优化。设置缓存表dp

因为每次递归只有当前位置cur和剩余的步骤rest是变化的,所以dp数组是二元的dp[cur][rest].

二. 从顶向下的动态规划

接下来就考虑如何优化成动态规划。

1. 主函数中初始化dp数组。   dp数组的初始值都为-1。 将dp作为参数传给process2.

2. 根据基本的递归函数, 修改此辅助函数。 辅助函数中,首先加入缓存命中判断

3. 加入ans变量,用于储存计算出来的值,为了最后将此值加入到缓存dp中。

三. 二次优化(最终版本)

二次优化是直接通过观察dp表的写规则,直接对dp表进行写操作。

假设:

有1 2 3 4 5 这五个位置,机器人在位置2, 目标是4, 需要走6步。

此表的横坐标是rest 剩余需要走的步数, 纵坐标是cur 机器人目前所在的位置。

通过基本递归方法来填写这个dp表!! 

 1. 如果rest为0时, 只有cur==aim的位置是1, 剩下的都是0。

2. 当cur为1时,根据递归函数, 依赖 cur为2,并且rest-1的 位置。即在第一行,只依赖左下角的值。

3. 当cur 为N时, 根据递归函数,依赖 N-1,rest-1 的位置,即左上角的位置。

4. 当cur处于中间位置时, 依赖于 左上角加上左下角的值。

以上探究清楚了dp表的所有填入规则。此规则其实就是状态转移方程。下面根据此规则来写入dp表。

根据依赖规则,写dp表时按列写。

 此题我们需要(6,2)位置,所以结果为13.

public static int ways3(int N, int start, int aim, int K) {
    // 创建一个二维数组dp,用来存储从每个位置i到目标位置aim的在剩余步数为j的情况下的方法数。
    int[][] dp = new int[N + 1][K + 1]; 
    // 初始化目标位置aim,在0步情况下到达自身的方法数为1。
    dp[aim][0] = 1; 
    // 从列开始写入,遍历每一个剩余步数,从1到K。 
    for (int rest = 1; rest <= K; rest++) { 
        // 第一行 : 在第一位时,只能从2走到1,因此方法数与从2到1剩余步数减一的情况相同。
        dp[1][rest] = dp[2][rest - 1]; 
        // 中间位置: 遍历每一个位置,从2到N-1。
        for (int cur = 2; cur < N; cur++) { 
            // 当前位置的方法数是从左边来和从右边来的方法数之和。
            dp[cur][rest] = dp[cur - 1][rest - 1] + dp[cur + 1][rest - 1]; 
        }
        // 最后一行: 在最后一位时,只能从N-1走到N,因此方法数与从N-1到N剩余步数减一的情况相同。
        dp[N][rest] = dp[N - 1][rest - 1]; 
    }
    // 返回从起始位置start,在K步情况下到达目标位置aim的方法数。
    return dp[start][K]; 
}

64. 最小路径和 (规律)

规律:

这类在一个范围内移动的模型都有通用的规律:

首先:

在写递归方法中:

1. 考虑考虑递归停止条件: 就是到达了终点。 此时考虑当前位置是终点的返回值情况。

2.考虑越界条件: 这里的越界条件一般是到达了数组的边界。if条件中是==,而非>

3. 考虑一般条件。

在dp方法中:

1.首先初始化递归停止条件的赋值。

2.再用单层for循环,将越界条件的初始值加入到dp数组中。

3.用两层for循环 考虑普通情况。

class Solution {
    public int minPathSum(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        // return process1(grid, m, n, 0, 0);
        return process2(grid);
    }

    public int process1(int[][] grid, int m, int n, int x, int y) {
        if(x == m - 1 && y == n - 1) {
            return grid[x][y];
        }

        // if(x == m || y == n) {
        //     return 201;
        // }

        if(x == m - 1) {
            return grid[x][y] + process1(grid, m, n, x, y + 1);
        }

        if(y == n - 1) {
            return grid[x][y] + process1(grid, m, n, x + 1, y);
        }


        return Math.min(grid[x][y] + process1(grid, m, n, x + 1, y), grid[x][y] + process1(grid, m, n, x, y + 1));
    }

    public int process2(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;

        int[][] dp = new int[m][n];
        dp[m - 1][n - 1] = grid[m - 1][n - 1];
        for(int x = m - 2; x >= 0; x --) {
            dp[x][n - 1] = grid[x][n - 1] + dp[x + 1][n - 1];
        }
        for(int y = n - 2; y >= 0; y --) {
            dp[m - 1][y] = grid[m - 1][y] + dp[m - 1][y + 1];
        }

        for(int x = m - 2; x >= 0; x --) {
            dp[x][n - 1] = grid[x][n - 1] + dp[x + 1][n - 1];
        }
        for(int x = m -2; x >= 0; x --) {
            for(int y = n - 2; y >= 0; y --){
                dp[x][y] = Math.min(grid[x][y] + dp[x + 1][y], grid[x][y] + dp[x][y + 1]);
            }
        }

        return dp[0][0];
    }
}

【例2】排成一条线的纸牌博弈问题

【题目】
给定一个整型数组 arr,代表数值不同的纸牌排成一条线。玩家 A 和玩家 B 依次拿走每张纸 牌,
规定玩家 A 先拿,玩家 B 后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家 A 和 玩
家 B 都绝顶聪明。请返回最后获胜者的分数。

【举例】
arr=[1,2,100,4]。
开始时,玩家 A 只能拿走 1 或 4。如果玩家 A 拿走 1,则排列变为[2,100,4],接下来玩家 B
可以拿走 2 或 4,然后继续轮到玩家 A。如果开始时玩家 A 拿走 4,则排列变为[1,2,100],接
下 来玩家 B 可以拿走 1 或 100,然后继续轮到玩家 A。玩家 A 作为绝顶聪明的人不会先拿 4,
因为 拿 4 之后,玩家 B 将拿走 100。所以玩家 A 会先拿 1,让排列变为[2,100,4],接下来玩
家 B 不管 怎么选,100 都会被玩家 A 拿走。玩家 A 会获胜,分数为 101。所以返回 101。
arr=[1,100,2]。
开始时,玩家 A 不管拿 1 还是 2,玩家 B 作为绝顶聪明的人,都会把 100 拿走。玩家 B 会
获胜,分数为 100。所以返回 100。

1. 递归解法


【分析】博弈问题,抽象出“先手”和“后手”的概念,但是并不是指游戏玩家中的某一方,而是指一种状态的变化。某一方如果当前是先手,拿走纸牌之后就变成了后手。这样,对于游戏玩家任一方,先手操作和后手操作都是一样的。(这里需要琢磨,琢磨,在琢磨……)

“试法”:

(1) 当前为先手,那么比较拿走左边和拿走右边的分数,取较大的;

L是arr的最左边元素的下标,R是arr的最右边元素的下标。

(2) 当前为后手,那么比较拿走左边和拿走右边的分数,取较小的。(先手会把较大的拿走,因为此步不是后手能决定的,对手只能给你留下最小的。!!!!!!!!!!!!!!!)

注意理解代码的先手后手交替进行。

f函数用来求先手拿牌的最好分数

g函数用来求后手拿牌的最好分数

注意先手函数中,拿了牌之后就变成了g。同样g拿牌之后就变成了f。

重点:为什么g后手函数是math.min?

因为g函数是代表后手,即会全力以赴得到后面牌的最优解,但是先手的对手的f也是全力以赴不会让你有最优解,所以就变成了min。后手就是拿到min之后的全力以赴拿最优解。

2. 优化

第二次优化就是考虑加入什么缓存dp,怎么在递归方法中加入缓存dp。

(1)首先判断dp中应该存什么 -> 找递归中有什么是相同重复计算的。

找到了相同的项 f(1,6)。

(2)加入dp用来存相同的项。

需要两个dp表,一个f,一个g。

如果fmap的值!= -1, 就是缓存命中,则直接取,如果没有,就正常求解,最后将答案存到缓存表中。

3. 二次优化

构造严格表依赖

假设arr为: [7, 4, 16, 15, 1]  这5个数组成。

根据递归函数

(1)先看 L= R, f函数直接返回 当前位置的数,g函数返回0

主函数要(0,n-1)这个格子的值,这是我们最后的目标值,用星标好。

两张表中如果L > R, 则不可能有这种情况,所以L > R的位置都是叉。

(2)。 f表中正常点依赖g表中三角的位置。

g表中正常点依赖f表的三角的位置。

现在我们已经知道两个表中对角线的数,那么根据递归函数,f表中的7和14,可以推断出g表中(1,0)的位置是7(取最小值)。以此类推,一个表的第一条对角线,可以推出另一个表的第二条对角线。

1. 首先初始化fmap和gmap。N代表长度,声明长度为N,正好能一一对应0到N-1 的每一个数组下标。

2. 再将fmap和gmap的对角线填上。

3. 按对角线的顺序填值。  startCol代表第二列,也就是下标为1的列开始移动。

根据递归函数填上每一个fmap和gmap。并且不断移动对角线。

4. 最后返回两个目标点的最大值来判断输赢。

模型一:从左到右模型

【例3】背包问题 1.9/

1. 递归写法,尝试函数(从左到右)

index表示从左到右开始一个一个的选择物品,来到的当前位置。 bag表示背包剩余的空间。

去尝试,按照正常的思维去想,首先来到了index位置,那么就会有两种选择,把index位置的货物放进背包,或者不选此货物。

递归函数解决将对当前位置index的最佳选择。

base情况: 当index == 数组长度时,表明越界了,所以会返回0.

注意此题还需要考虑一个无效解。!!!!!此无效解的解法是通用的。

如果当前货物weight是7,bag剩余承重是6,那么如果选择此物品,会导致背包超重,所以不能去选这个物品。

2. 优化(二维表,从下到上遍历)

1.先看有没有重复利用,就随便写几个例子。

2. 通过递归函数,直接改成动态规划。

首先 当index==物品个数(4)时,表明越界了,所以都是0. 根据递归公式,当前节点依赖列数+1的节点。所以可以根据下面的列将上一列全部都推测出来,dp的填写顺序是从下到上。

直接改递归函数,将递归都变成向dp存值即可,函数的逻辑顺序全部都不变。!!!!

416. 分割等和子集(背包问题)

思路:

此题的关键是识别到这是一个背包问题,而不是一个字串的问题。重量是数组总和的一半。

当数组总和是奇数的时候,肯定不行,直接返回false。

注意此题中,返回的是布尔,而不是最少数量,所以此题可以重点关注一下布尔的递归逻辑,以及如何初始化一个布尔类型的dp。(此题初始化dp是一个坑,需要特别关注一下初始化赋值顺序)。

class Solution {
    public boolean canPartition(int[] nums) {
        // int sum = 0;
        // for(int i : nums){
        //     sum += i;
        // }

        // if(sum % 2 != 0) {
        //     return false;
        // }
        // return process1(nums, 0, sum/2);

        return process2(nums);
    }

    public boolean process1(int[] nums, int index, int rest) {
        if (rest == 0) {
            return true;
        }

        if (index == nums.length) {
            return false;
        }


        //注意这个在dp中一定要有一个if判断来实现
        if(rest < 0){
            return false;
        }

        boolean token = process1(nums, index + 1, rest - nums[index]);
        boolean not_token = process1(nums, index + 1, rest);
        
        return token || not_token;
    }



    public boolean process2(int[] nums) {
        int sum = 0;
        for(int i : nums){
            sum += i;
        }

        if(sum % 2 != 0) {
            return false;
        }

        //注意数组类型是boolean不是int
        boolean[][] dp = new boolean[nums.length + 1][sum/2 + 1];
        
        //一定注意这两个初始化的顺序,要是先放入true,答案就错了。
        //这个顺序根据具体题目去理解好理解。
        Arrays.fill(dp[nums.length], false);
        for(int i = 0; i < dp.length; i++){
            dp[i][0] = true;
        }


        for(int i = nums.length - 1; i >= 0; i --){
            for(int j = 0; j <= sum/2; j ++){
                boolean token = false;

                //注意此处要加入判断,去实现递归中rest<0 的判断。
                if(j - nums[i] >= 0){
                    token = dp[i + 1][j - nums[i]];
                }
                boolean not_token = dp[i + 1][j];
                dp[i][j] = token || not_token;
            }
        }

        return dp[0][sum/2];
    }
}

【例四】数字字符串的不同组成的个数。 1.9/

1. 递归尝试(从左到右)

(1)注意当i == str.length的时候,返回的是1. 

(2)当前位置是0 时,不能返回任何有效的方法,所以表示前一种方法做了错误的决定,返回0.

(3)如果将当前位置转换成字母的话,那么ways就是index + 1时的返回值。如果满足将当前位置跳过的时候,则再将当前的ways总次数再加上跳过index + 1时的返回值。

2. DP(一维表,从下到上遍历)

1143. 最长公共子序列

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        // return process1(text1, text2, 0, 0);
        return process2(text1,text2);
    }

    public int process1(String text1, String text2, int index1, int index2) {
        //注意这个判停条件是 || !!!!!!
        if(index1 == text1.length() || index2 == text2.length()) {
            return 0;
        }

        if(text1.charAt(index1) == text2.charAt(index2)) {
            return 1 + process1(text1, text2, index1 + 1, index2 + 1);
        }

        // 如果字符不匹配,尝试两种情况
        int skipText1 = process1(text1, text2, index1 + 1, index2);
        int skipText2 = process1(text1, text2, index1, index2 + 1);

        // 返回两种情况中的最大值
        return Math.max(skipText1, skipText2);
    }

    public int process2(String text1, String text2) {
        int[][] dp = new int[text1.length() + 1][text2.length() + 1];
        for(int index1 = text1.length() - 1; index1 >= 0; index1 --) {
            for(int index2 = text2.length() - 1; index2 >= 0; index2 --) {
                if(text1.charAt(index1) == text2.charAt(index2)) {
                    dp[index1][index2] = 1 + dp[index1 + 1][index2 + 1];
                }else{
                    int p1 = dp[index1 + 1][index2];
                    int p2 = dp[index1][index2 + 1];
                    dp[index1][index2] = Math.max(p1, p2);
                }

            }
        }

        return dp[0][0];
    }
}

设计模式总结:

递归函数的可变参数不能是数组类型,一个可变参数就是一维表,两个可变参数就是二维表。

模型三:多样本位置全对应的尝试模型

322. 零钱兑换    1.14/

322. 零钱兑换

以下为此类型的通用模板:

class Solution {
    public int coinChange(int[] coins, int amount) {
        
        return process1(coins, amount);
    }

    //多位置全对应模型的统一步骤:

    public int process1(int[] coins, int rest) {
        if (rest == 0) {
            return 0;
        }

        //1. 固定: 将min赋值为Integer.MAX_VALUE
        int min = Integer.MAX_VALUE;

        //2. 遍历每一个位置
        for (int coin : coins) {
            
            //!!!!!!!!!!!!!注意: 这部if是关键 !!!!!!!!
            //3. 用if来判断 筛选有效的位置, 同时 要保证下面的process(coins, rest - coin) 是有效的,也就是不是Integer.MAX_VALUE。
            //注意,此处为什么要用 !=-1 而不是 Integer.MAX_VALUE ?   因为 函数的最后如果min的值是Integer.MAX_VALUE返回了 -1 , 所以此处是 -1. 这个判断是固定的,是不变的。!!!!!!
            if (coin <= rest && process1(coins, rest - coin) != -1) {
                

                //4. 固定: 为min取最小值,此步也是固定不变的, 注意最后别忘了加1 !!!!! 。 
                min = Math.min(min, process1(coins, rest - coin) + 1 );
            }
        }

        //5. 将结果返回,并进行判断,如果是Integer.MAX_VALUE, 需要返回-1 .
        return min == Integer.MAX_VALUE ? -1 : min; 
    }


    public int process2(int [] coins, int rest) {
        //1. 固定: 初始化dp数组,因为我们想要dp[rest], 所以长度是rest + 1
        int[] dp = new int[rest + 1];

        //2. 固定
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0;


        for (int coin : coins) {
            for (int i = 1; i <= rest; i ++) {
                if (coin <= i) {

                    //一定注意此处!!!!!!!!!!!!
                    //此处递归是 != -1, 而此时是 != Integer.MAX_VALUE, 此处是唯一一处dp和递归不一样的地方
                    //他们的作用是相同的,都是防止dp[i-coin]处的值无效,但是因为递归是在最后将maxvalue变成-1了,而dp不需要回调函数,所以这里还是Maxvalue
                    if (dp[i - coin] != Integer.MAX_VALUE){
                        dp[i] = Math.min(dp[i], dp[i - coin] + 1);
                    }
                }
            }
        }

        return dp[rest] == Integer.MAX_VALUE ? -1 : dp[rest];
    }
}

【例5】贴纸问题   1.8/

 1. 递归方法

思路:

三种情况:我就第一张贴纸选”ba”看最后能用几张,2. 我第一张贴纸选“c”看最后能用几张, 3. 我就第一张选“abcd”看最后能用几张。

返回 -1: 怎么贴都不能得到

minus作用是将target减去当前贴纸所能提供的字符,所剩下的字符串。(最后返回排好序的字符串)

结尾返回值,还得加上当前的一个贴纸(如果接下来的组成不了,就返回0) .

2. DP(HashTable做表)

因为此题中不断变化的是字符串

279. 完全平方数  1.8/

279. 完全平方数

class Solution {
    public int numSquares(int n) {
        return process2(n, n);
    }

   public int process1(int n, int rest) {
       if (rest == 0) {
//注意是返回0, 只有在从左到右模型中,才会返回1 !。
           return 0;
       }
        //最后return结果时,如果是min,那么这个就是Integer.MAX_VALUE. 如果是max,那么此时这个就是0(背包问题).
       int min = Integer.MAX_VALUE;

        //此处进行范围内全尝试,将所有可能都尝试一遍。
        //此题: 假设1(4或9或.....n^2)要了
       for (int i = 1; i < n; i ++) {
           //既然是全尝试,那么就需要判断哪些尝试是没用的,此题中就是i ^ 2 比剩余的数还大,就是不可能的。
           if ((rest - i * i) >= 0) {
               min = Math.min(min, process4(n, rest - i * i) + 1);
           }
       }
       return min == Integer.MAX_VALUE ? -1 : min;
   }

   public int process2(int n, int rest) {
       int[] dp = new int[rest + 1];
        //***********************************************
        //记住,只要递归的return参数复制,则dp就需要赋值。
        //记住,要将所有的递归return的参数全部换成dp。
        //此题中,递归返回的参数是min。那么在递归中最上面min = Integer.MaxValue, 就需要给dp数组都赋值为此。而不是继续使用min。
       Arrays.fill(dp, Integer.MAX_VALUE);
       dp[0] = 0;

       for (int i = 1; i <= rest; i ++) {

           //递归参数是减,那么dp的for循环就是从小到大,从初始数据先写0也能看出来是从小到大循环。
           for (int j = 1; j  <= rest; j ++) {
               if(j >= i * i) {
                   //注意此处,所有的递归中的返回值min,都需要替换成dp。一个都别落下!!!!!!!!!!!!!!!
                    // 注意一定要加1 !!!!!。 因为此处是将index位置选上了,所以要加1.
                   dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
               }
           }
       }
       return dp[rest] == Integer.MAX_VALUE ? -1 : dp[rest];
   }
}

139. 单词拆分   1.14/

139. 单词拆分

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        // return process1(s, wordDict);
        HashMap<String, Boolean> dp = new HashMap<>();
        //此处一定是“”, 而不是‘’ 注意
        dp.put("", true);
        return process2(s, wordDict, dp);
    }

   public boolean process1(String s, List<String> wordDict) {
        if (s.length() == 0) {
           return true;
        }
        for(String word : wordDict) {
            //注意startsWith  是starts, 有s!!
            if (s.startsWith(word) ){
                //注意此处不能写在上面的if判断条件中用 && 去判断,因为没办法保证是否substring时是否越界。 而分开写则可以保证不越界。
                //注意substring string是小写,不是subString
                if (process1(s.substring(word.length()), wordDict)){
                     return true;
                }
            }
        }
        return false;
   }


// 注意,! 定义HashMap时,一定要用装箱类Boolean,(因为是泛型), 而不能用boolean基本数据类型。
   public boolean process2(String s, List<String> wordDict, HashMap<String, Boolean> dp) {
        //缓存命中
        if (dp.containsKey(s)) {
            return dp.get(s);
        }

        boolean ans = false;
        //缓存未命中
        for (String word : wordDict) {
            if (s.startsWith(word)) {
                if (process2(s.substring(word.length()), wordDict, dp)) {
                    ans = true;
                    dp.put(s, ans);
                    return ans;
                }
            }
        }
        dp.put(s, ans);
        return ans;
   }
}

模型四:子串问题

总结:

1. 所有字串问题,函数的参数index 的含义: 字串以index位置为结尾 !!!!!!!

2. 所有题统一的套路是:

2.1 在写递归函数时,要在原始函数中进行for循环,循环index。

2.2 初始化max或者min的时候,不能再是Integer.MAXVALUE或MINVALUE了。此处赋值多少考虑的是当只有nums[0]的时候,此处应该是什么。也就是考虑返回的字串只有一个元素,为nums[0],此时应该返回的答案。

2.3 注意: 如果递归函数中再有循环的话,那么用j来写,不用i(因为主函数的循环用i了)方便为后续改dp好区分。

class Solution {
    public int lengthOfLIS(int[] nums) {
        int max = ....;
        //或者是min = ...;
        for (int i = 0; i < nums.length; i++) {
            //或者是min
            maxRes = Math.max(maxRes, 递归方法(nums, i));
        }
        return maxRes;

    }

    public int 递归方法(int[] nums, int index) {
        //固定的,因为我们为index赋予的意义是以index为结束,所以就需要判定index是0,也就是nums的第一个元素。
        if (index == 0) {
            return ...;
        }

        int max = ....;
        //或者是min = ...;
    }

3.  将递归改dp:

3.1 注意哪里需要替换成dp(所有模型适用): 

将所有递归函数替换成dp,将所有递归函数返回的变量替换成dp(注意此处的dp不一定是dp[i] ,递归函数中是什么,就原封不动的抄什么)。比如说递归函数返回max,那么将所有递归函数中的max替换成dp。 

举例:

public int 递归(int[] nums, int index) {
        ......
        for(int j = 0; j < index; j ++) {
            if (......) {
                max = Math.max(max, 递归(nums, j) + 1);
            }
        }
        return max;

    }

    public int 动态规划(int[] nums) {
        .....
        for (int i = 1; i < nums.length; i ++) {
            for(int j = 0; j < i; j++) {
                if (........) {
              // 注意此时就是需要照抄原递归,递归是j,此处也应该是dp[j]
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
        }
        return Arrays.stream(dp).max().getAsInt();
    }

3.2 dp的初始化: 拿一维dp举例: dp[i] 需要都赋值为字串只有nums[i]的结果。因为字串问题就是长度可变,只是以index为结尾。那么最基本的情况就是字串只有第i个元素。

for (int i = 1; i < nums.length; i ++) {
            dp[i] = 1;
}

3.3 注意此时在index的循环中,只需要照抄递归中的函数即可,而不需要多加一个循环了。递归中有循环就写,没有就不用。

3.4 在写dp表达式的时候,注意理解含义去写,因为毕竟此模型和前面的几个模型不同的地方在于递归函数把for循环卸载了主函数中,此时dp[i]表示的是: 当字串以index结尾的时候的最好情况。

3.5 最后返回也很固定, 一般主函数会有Math.max/min 的判断,此时转换成dp就挑出dp数组中最大或最小值即可。

Arrays.stream(dp).max()/min().getAsInt();

[例1]. 300. 最长递增子序列

思路:

以index为结尾的字串, 找出所有index之前的比nums[index]小的数,比如在j位置的数比index小,那么只需要用 以i位置为结尾的最长递增子序列长度,再加上1(index位置的数), 就是最后的结果。    遍历所有这样的i,来取最大值,当作以i为结尾的最大值。

class Solution {
    public int lengthOfLIS(int[] nums) {
        // 如果只有0位置,那么最长字串长度为1.
        int maxRes = 1;
        //固定
        //注意虽然i表示以i为结尾,但仍是从前往后遍历。
        for (int i = 0; i < nums.length; i++) {
            //固定
            maxRes = Math.max(maxRes, process1(nums, i));
        }
        return maxRes;

        //dp方法
        // return process2(nums);
    }

    //子串以index位置的数为结尾
    public int process1(int[] nums, int index) {
        //如果index在开始,则最小的子串长度就是1
        if (index == 0) {
            return 1;
        }
        //最差情况就是只有index位置的一个数,所以是1.
        int max = 1;

        //遍历所有index之前的数,看看有没有比index小的
        //注意j也是从前往后遍历,因为最开始只是直到index=0的情况。
        for(int j = 0; j < index; j ++) {
            //判断是否比index小,如果小:
            if (nums[j] < nums[index]) {
                //以j结尾的最大的递增子序列长度,再加上index自己。来更新以index结尾的最长长度。
                max = Math.max(max, process1(nums, j) + 1);
            }
        }
        return max;

    }

//改dp
    public int process2(int[] nums) {
        int[] dp = new int[nums.length];
        
        dp[0] = 1;
        //主函数的for
        for (int i = 1; i < nums.length; i ++) {
            //注意此初始化手段:是固定的,都在for循环里面
            dp[i] = 1;

            //以下照抄递归,注意别多加了for
            for(int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    //固定: max都替换成dp[i], 注意: process1(j)替换成了dp[j]
                    //此处用实际情况理解,dp[i]表示以i为结尾的最长长度,要通过dp[j]来不断的更新。
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
        }

        //固定: 此步是表示原来主函数的Math.max的作用。
        return Arrays.stream(dp).max().getAsInt();
    }
}

[例2] 152. 乘积最大子数组(两个递归函数)

思路:

递归方法中注意要考虑到两种情况,这是难点

如果子串以index结尾,那么此时max的更新策略是:

1.   nums[index]是正数,那么应该乘上 process(index -1)的最大的子数组乘积。

2. nums[index] 是负数, 那么应该乘上process(index -1)的最小的子数组乘积。

所以要定义两个递归函数

class Solution {
    public int maxProduct(int[] nums) {
        // int max = nums[0];
        // for(int i = 0; i < nums.length; i++) {
        //     max = Math.max(max, process1(nums, i));
        // }
        // return max;
        return process3(nums);
    }


    //返回子串以index结尾的最大乘积
    public int process1(int[] nums, int index){
        if (index == 0) {
            return nums[0];
        }
        int max = nums[index];
        //注意Math.max/min方法只能具有两个参数。
        //一个是乘上最大的,一个是乘上最小的。用于全部考虑nums[i]正负情况。
        int res1 = Math.max(nums[index] * process1(nums, index - 1), nums[index] * process2(nums, index - 1));
        max = Math.max(max, res1);
        return max;
    }

    //返回子串以index结尾的最小乘积。
    public int process2(int[] nums, int index){
        if (index == 0) {
            return nums[0];
        }
        int min = nums[index];
        int res2 = Math.min(nums[index] * process2(nums, index - 1), nums[index] * process1(nums, index - 1));
        min = Math.min(min, res2);
        return min;
    }

    public int process3(int[] nums) {

        //此处更新两个dp数组,一个记录i结尾最大值,一个是最小
        int[] dp_max = new int[nums.length];
        int[] dp_min = new int[nums.length];
        dp_max[0] = nums[0];
        dp_min[0] = nums[0];

        for (int i = 1; i < nums.length; i++){
            dp_max[i] = nums[i];
            dp_min[i] = nums[i];

            if(nums[i] >= 0) {
                dp_max[i] = Math.max(dp_max[i], nums[i] * dp_max[i - 1]);
                dp_min[i] = Math.min(dp_min[i], nums[i] * dp_min[i - 1]);
            } else{
                dp_max[i] = Math.max(dp_max[i], nums[i] * dp_min[i - 1]);
                dp_min[i] = Math.min(dp_min[i], nums[i] * dp_max[i - 1]);
            }
            // int res1 = Math.max(nums[i] * dp_max[i - 1], nums[i] * dp_min[i - 1]);
            // int res2 = Math.min(nums[i] * dp_max[i - 1], nums[i] * dp_min[i - 1]);

            // //子数组的问题dp不用for循环
            // dp_max[i] = Math.max(res1, dp_max[i]);
            // dp_min[i] = Math.min(res2, dp_min[i]);
        }

        return Arrays.stream(dp_max).max().getAsInt();
    }
}

32. 最长有效括号(hard)

此题难点在于思路:

出错在了少考虑一种情况: “(())”

  1. dp[i] 表示以索引 i 结尾的最长有效括号子串的长度。
  2. s.charAt(i) 为 '(' 时,dp[i] 显然为 0,因为以 '(' 结尾的字符串不可能是有效的括号子串。
  3. s.charAt(i) 为 ')' 时,我们需要查看 i - dp[i - 1] - 1 的位置,即当前 ')' 之前有效子串的前一个字符。
    • 如果该字符是 '(',则 dp[i] = dp[i - 1] + 2。此外,还需要加上之前的有效子串的长度,即 dp[i - dp[i - 1] - 2]

class Solution {
    // 主方法:寻找最长有效括号子串的长度
    public int longestValidParentheses(String s) {
        int max = 0;
        // 遍历每个字符,对每个字符调用 process1 方法
        for(int i = 0; i < s.length(); i++){
            max = Math.max(max, process1(s, i));
        }
        return max; // 返回最大长度

        // 可选方法:使用动态规划
        // return process2(s);
    }

    // process1 方法:使用递归来找到以 index 为结束点的最长有效括号子串
    public int process1(String s, int index) {
        // 递归基础情况:如果索引小于等于0,返回0
        if(index <= 0) {
            return 0;
        }
        int max = 0;
        
        // 如果当前字符是右括号 ')'
        if(s.charAt(index) == ')'){
            // 检查是否形成有效的括号对
            if(index - process1(s,index - 1) - 1 >= 0 && s.charAt(index - process1(s,index - 1) - 1) == '(') {
                // 累加当前有效括号的长度
                max = process1(s,index - 1) + 2;
                // 如果存在嵌套的有效括号,则将它们的长度也加上
                if(index - process1(s,index - 1) - 1 >= 2 ){
                    max = max + process1(s,index - process1(s,index - 1) - 2);
                }
            }
        }

        return max; // 返回以当前索引为结尾的最长有效括号子串的长度
    }

    // process2 方法:使用动态规划来找出最长有效括号子串的长度
    public int process2(String s) {
        // dp 数组用于存储到当前索引为止的最长有效括号子串的长度
        int[] dp = new int[s.length()];
        int maxAns = 0;
        // 从第二个字符开始遍历
        for (int i = 1; i < s.length(); i++) {
            // 如果当前字符是右括号 ')'
            if (s.charAt(i) == ')') {
                // 检查是否形成有效的括号对
                if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') {
                    // 累加当前有效括号的长度
                    dp[i] = dp[i - 1] + 2;
                    // 如果在之前还有效括号,则将它们的长度也加上
                    if (i - dp[i - 1] >= 2)
                    {
                    dp[i] += dp[i - dp[i - 1] - 2];
                }
            }
            // 更新最大有效括号子串长度
            maxAns = Math.max(maxAns, dp[i]);
        }
    }
    // 返回最长有效括号子串的长度
    return maxAns;
}

 模型五: 范围扩散模型

此模型适用于双字符串,index表示字符串前index位的子串。 index表示的是长度!!!!而不是String的下标位数!!!!一定要注意。

讨论的if条件是根据index位子串的末尾来确定的。



[例1] 72. 编辑距离

思路:

先考虑初始化情况:

如果index1 = 0, 那么就是String1长度为0,如果想变成String2 , 则String2 长度为多少就得加上多少。

如果index2 = 0, 那么就是String2长度为0,如果想变成String2, 则String1有几个字母就得减去几。

根据index位子串的末尾来考虑分组:

情况1: String1 = “abcd” , String2 = “abe”。    index1 = 4, index2 = 3.   考虑将index1 - 1 子串变成String2, 然后将index1 位(d)删掉。 dp[i][j] = dp[i - 1][j]  + 1

情况2: String1 = “abc”,String2 = “abcd” 。 index1 = 3, index2 = 4, 考虑将index1 变成index2 - 1, 然后  将idnex2位(d)加上。   dp[i][j] = dp[i][j - 1] + 1;

情况3: 如果index1 - 1位和index2 - 1位相等, 则此位不需要操作,只需要考虑index1-1和index2-1的匹配情况。   dp[i][j] = dp[i-1][j-1]

情况4: 如果index1位和index2位不相等,则让index1-1和index-2匹配后,再将此位变成一样的。

dp[i][j] = dp[i - 1][j - 1] + 1;

class Solution {
    public int minDistance(String word1, String word2) {
        
        int[][] dp = new int[word1.length() + 1][word2.length() + 1];

        //注意一定要加上=,因为i和j代表前index个长度。最后要返回word1的长度。
        for(int i = 0; i <= word1.length(); i++){
            dp[i][0] = i;
        }

        for(int j = 0; j <= word2.length(); j++){
            dp[0][j] = j;
        }

        for(int i = 1; i <= word1.length(); i ++){
            for(int j = 1; j <= word2.length(); j++){

                //注意!!!!一定是i-1和j-1, 因为i和j表示的是字符串长度。
                if(word1.charAt(i - 1) == word2.charAt(j - 1)){
                    dp[i][j] = dp[i - 1][j - 1];
                } else{
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }

                dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j]);
                dp[i][j] = Math.min(dp[i][j - 1] + 1, dp[i][j]);
            }
        }
        return dp[word1.length()][word2.length()];
    }
}

1143. 最长公共子序列

​​class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int m = text1.length();
        int n = text2.length();
        int[][] dp = new int[m + 1][n + 1];
        for(int i = 1; i <= m; i++){
            for(int j = 1; j <= n; j++){

//如果最后面的字符相等,那么考虑前面的一位情况再加上1
                if(text1.charAt(i - 1) == text2.charAt(j - 1)){
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else{

//如果不相等,那么只能选择要text1的或者是text2的。取最小值。
                    dp[i][j] = Math.max(dp[i - 1][j] , dp[i][j - 1]);
                }
            }
        }

        return dp[m][n];
    }
}

  • 20
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值