你还在为 “动态规划” 发愁吗?看完本秘籍,带你斩杀这类题~

news2024/10/7 4:29:12

目录

前言

一、动态规划——解题思路

二、动态规划——模板以及题目

2.1、Fibonacci

2.2、字符串分割(Word Break)

2.3、三角矩阵(Triangle)

2.4、路径总数(Unique Paths)

2.5、最小路径和(Minimum Path Sum)

2.6、背包问题

2.7、回文串分割(Palindrome Partitioning)

2.8、编辑距离(Edit Distance)

2.9、不同子序列(Distinct Subsequences)

三、总结


前言

        前段时间,本人也是之前被动态规划的题目pua过,但是题量上来了以后,理解了你会发现实际上会对应一些模板和套路,需要发挥一点想象力,问题就可以迎刃而解;那么这里呢,就专门针对于一些对类似动态规划的题目无从下手,亦或是怎么想也做不出来的“胖友们”,给你们讲解思路,以及解题方法,希望能够帮到你们!

注意:一定要按文章顺序浏览,并自己动手实践,效果才会显著!


一、动态规划——解题思路

        动态规划类的题目本质上来讲,就是需要我们能够大事化小,将大问题分解成一个一个子问题来解决;话不多说,直入正题!

适用场景:

        简单概括就是:最大值/最小值,是否可行,是不是,方案个数;

解题思路:

1.状态的定义(这里就是需要我们针对题目中问到的问题,抽象出子问题;状态定义的是否正确,就要看他是否能对应到最终问题的解);

2.列出状态转移方程——难点(例如通过一个什么样的公式,可以从第“i-1”这个状态得到第“i”个状态,有时还需要我们去用一个数组保存这个状态);

3.状态初始化(可以让转移方程继续递推下去);

4.返回结果;

这样讲还是比较笼统,具体的,我会在下方给出栗子(由浅入深),再进行讲解~


二、动态规划——模板以及题目

2.1、Fibonacci

题目描述:

大家都知道斐波那契数列,现在要求输入一个正整数 n ,请你输出斐波那契数列的第 n 项。

 题目来源:斐波那契数列_牛客题霸_牛客网

解题思路:

        提示:这道题可以用递归来做,但是不适合,因为一旦数字n过大,时间复杂度就是一个指数级别的O(2^N),所以这里我们可以考虑用动态规划来实现~

1.状态F(i):第i项的一个值;(子问题)

2.状态转移方程:F(i) = F(i - 1) + F(i - 2);

        这个方程是怎么得来的呢?前面我们提到说,将大问题化成一个一个相同的子问题,这里的F(i)便是子问题(第i项的一个值),那么思考一下这个值该怎么得来呢?由斐波那契数列的性质我们可以知道,第i项的值,是可以通过第i - 1项的值加上第i - 2项的值得来,因此这个式子就不难理解啦!

3.初始状态:F(0) = 0; F(1) = 1;

        怎么得出的初始状态?这里就需要看状态转移方程,需要几个初始值,才可以得出F(i),使得这个递推公式可以不断的运转下去;

4.返回结果:F(n) 表示最终问题的解;

具体代码和注释:

public class Solution {
    public int Fibonacci(int n) {
        //定义一个数组,用来保存中间状态
        int[] dp = new int[n + 1];
        //初始化,保证顺利递推
        dp[0] = 0;
        dp[1] = 1;
        //由初始化可以得出第三项,因此i从下标2开始,通过递推公式得到第n项
        for(int i = 2; i <= n; i++) {
            //列出状态转移方程
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        //返回结果
        return dp[n];
    }
}

优化:

        这里,空间上是可以优化的,为什么呢?我们得出第i项的值,实际上只需要前两项的值,即可,再往前面的值就用不上了,也没必要去创建一个数组去保存中间状态;

        因此我们可以通过两个变量来进行递推,并且动态的更新这两个变量即可;

public class Solution {
    public int Fibonacci(int n) {
        if(n == 0 || n == 1) {
            return n;
        }
        int fn = 0;
        int f1 = 1;//这里就是f(n-1)
        int f2 = 0;//这里就是f(n-2)
        for(int i = 2; i <= n; i++) {
            fn = f1 + f2;
            //更新状态
            f2 = f1;
            f1 = fn; 
        }
        return fn;
    }
}

2.2、字符串分割(Word Break)

题目描述:

给定一个字符串s和一组单词dict,判断s是否可以用空格分割成一个单词序列,使得单词序列中所有的单词都是dict中的单词(序列可以包含一个或多个单词)。

例如:

s = "leetcode", dict = ["leet", "code"]


返回true,因为"leetcode"可以被分割成"leet code".

题目来源:拆分词句_牛客题霸_牛客网

解题思路:

1.状态F(i):字符串的前i个字符是否可以被分割;

        怎么理解呢?首先来看问题:字符串整体是否可以被分割?那么我们可以从整体转换成局部,就是某个子串是否可以被分割,剩下未被分割的部分在字典中是否能够找到,这样一个子问题;(状态定义的是否正确,要看你定义的这个状态或多个这样的状态,最终能否对应上问题的某一个解)

2.状态转移方程:j < i && F(j) && [j + 1, i]是否能在词典中找到;

        解释:我们可以先列出几个栗子,比如字符串"leetcode",F(4)表示什么?他表示字符串的前4个字符可以被分割,那么如果他为true(这里显然为true),我们就需要关心的是[5,8]这个区间的字符串能否在字典中找到;回到题目中的示例我们的最终问题是,前8个字符能否被分割?那么以此类推,F(8)子问题(F(8)可以由那些状态得出结果?)就变成了如下状态:

  • F(1) && [2, 8]是否能在词典中找到;
  • F(2) && [3, 8]是否能在词典中找到;
  • F(3) && [4, 8]是否能在词典中找到;
  • F(4) && [5, 8]是否能在词典中找到;
  • F(5) && [6, 8]是否能在词典中找到;
  • F(6) && [7, 8]是否能在词典中找到;
  • F(7) && [8, 8]是否能在词典中找到;(这里的[8, 8]就是最终的结果)
  • F(0) && [1, 8];这里F(0)表示空字符串(并不表示实际状态),表示整体;
  • 这里不能出现F(8),因为他不能根据自身去推导自身

所以这里状态方程,F(8) = F(j) + F(j + 1, 8);  这里的8就是i,是其中的一个状态(注意根据上面推到,j 是小于 i 的)!

3.初始状态:F(0) = true;

        空串为什么被定义成在字典中找得到呢?因为这里不是说空串就能被找到,而是F(0)没有实际意义,只是一个辅助的状态! 我们可以想象一下,如果i = 4,也就是我们需要判断F(0) && [1, 4]能否在字典中找到?显然[1, 4]是可以找到的,所以这里的F(0)因该是个true才合理;

4.返回结果:F(字符串的长度) ;

代码如下:

import java.util.*;
public class Solution {
    public boolean wordBreak(String s, Set<String> dict) {
        //定义一个数组,用来表示每个i的状态
        boolean[] dp = new boolean[s.length() + 1];
        //初始化
        dp[0] = true;
        for(int i = 1; i <= s.length(); i++) {
            for(int j = 0; j < i; j++) {
                //表示在前j个字符可以拆分的前提下,j + 1 ~ i个字符可以在字典中找到
                if(dp[j] && dict.contains(s.substring(j, i))) {
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[s.length()];
    }
}

2.3、三角矩阵(Triangle)

题目描述

给定一个三角形 triangle ,找出自顶向下的最小路径和。

每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。如下:

[

[2],

[3,4],

[6,5,7],

[4,1,8,3]

]

题目链接:剑指 Offer II 100. 三角形中最小路径之和 - 力扣(LeetCode)

解题思路一:(自顶而下,不是最优解)

1.状态F(i, j):从[0,0]出发,到[i, j]点的最小路径和

2.状态转移方程:(0 < j < i)

        当j == 0的时候(最左边的边界):F(i, j) = F(i - 1, 0) + array[i][j];

        当j == i的时候(最右边的边界):F(i, j) = F(i - 1, j - 1) + array[i][j];

        剩余的其他情况:F(i, j) = min(F(i - 1, j - 1), F(i - 1, j)) + array[i][j];

        解释:中间某一结点[i, j]的最短路径,就是上一节点(正上方,或左上方的最小值路径和) 加上当前结点的路径和(array[i][j]);

        注意:边上的点都只有唯一一条路径可以到达(j == 0 || j == i),如下图

3.初始状态:F(0, 0) = array[0][0];

        解释:[0, 0]这个点无法在从上一个点的来,也就是一个固定值;

4.返回结果:min(F(最后一行));

        解释:这里便是要返回最后一行的最小值;

代码如下:

class Solution {
    // [2],
    // [3,4],
    // [6,5,7],
    // [4,1,8,3]
    public int minimumTotal(List<List<Integer>> triangle) {
        int row = triangle.size();//行
        //这里是否创建新的数组?可以不用创建的,题目中所给数组以及满足需求,我们
        //可以直接根据题目中所给的数据继续递推下去
        for(int i = 1; i < row; i++) {
            for(int j = 0; j <= i; j++) {
                if(j == 0) {//左边边界:只能有上面的一个元素得出
                    triangle.get(i).set(j, triangle.get(i - 1).get(j) + triangle.get(i).get(j));
                } else if(i == j) {//右边边界:只能由左上方的元素得出
                    triangle.get(i).set(j, triangle.get(i - 1).get(j - 1) + triangle.get(i).get(j));
                } else {//其他情况
                    triangle.get(i).set(j, Math.min(
                        triangle.get(i - 1).get(j), triangle.get(i - 1).get(j - 1)
                    ) + triangle.get(i).get(j));
                }
            }
        }
        //返回最底层的最小值
        int min = Integer.MAX_VALUE;
        for(int i = 0; i < row; i++) {
            min = Math.min(min, triangle.get(row - 1).get(i));
        }
        return min;
    }
}

解题思路二:(自底而上 —— 最优解,更简洁

        解法一介绍的是一个从上往下推到的结果,但是实际上还有更容易的办法,就是从最后一行(最底层)向上推到,推到顶层;来看看怎么做吧!

1.状态F(i, j):从(i, j)到达最后一行的最短路径(注意这里变化,不是从(0, 0)出发了);

2.状态转移方程:

        F(i, j) = min(F(i + 1, j), F(i + 1, j + 1)) + array[i][j];

        解释:就是从下方的两条路到达选择最小的一条路径到达(i, j);

        这样定义的好处:不用处理边界条件啦,因为自底向上,每一个解都有两条路径,并且最终的解就是[0, 0]这个点,不用再遍历最底层找出最小值啦!

3.初始状态:就是最后一行值(用题目中所给的数组即可)

4.返回结果:就是[0, 0]点;

代码如下:

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        //自底而上
        int row = triangle.size();
        for(int i = row - 2; i >= 0; i--) {//这里最后一行就是初始值,所以要从倒数第二行开始
            for(int j = 0; j <= i; j++) {
                triangle.get(i).set(j, Math.min(
                    triangle.get(i + 1).get(j), triangle.get(i + 1).get(j + 1)
                ) + triangle.get(i).get(j));
            }
        }
        return triangle.get(0).get(0);
    }
}

2.4、路径总数(Unique Paths)

题目描述:

一个机器人在m×n大小的地图的左上角(起点)。

机器人每次可以向下或向右移动。机器人要到达地图的右下角(终点)。

可以有多少种不同的路径从起点走到终点?

题目来源:不同路径的数目(一)_牛客题霸_牛客网

解题思路:

1.状态F(i, j):从(0, 0)到达(i, j)点的路径个数

2.状态转移方程:

        最顶层或最右侧第一列(i == 0 || j == 0): F(i, j) = 1;

        其他情况:F(i, j) = F(i - 1, j) + F(i, j + 1);

        解释:到达(i, j)的路径个数,实际上就是上方结点的路径个数加上左方结点的路径个数之和(这里一定是一个一步的操作,想一想,那些点可以一步到达这个状态);

3.初始状态:F(0, j) = 1,F(i, 0) = 1;

        解释:这里可以想象一下通过转移方程能够得出的第一个状态是谁呢?也就是(1, 1)这个点,因为这个点就可以从(1, 0)加上(0, 1)得到,那么,这两个点怎么初始化呢? 因为机器人只有两种路径可以走,一种是向右,一种是向下,想象一下,从(0, 0)出发,是不是对于最顶层,只有一条路径能到达(只能向右走)?是不是对于最左侧的第一列,只有一条路径能到达(只能向下走)?

4.返回结果:F(m - 1, n - 1); 最右下方星星表示的格子;

代码如下:

import java.util.*;
public class Solution {
    public int uniquePaths (int m, int n) {
        int[][] dp = new int[m][n];
        for(int i = 0; i < m; i++) {
            for(int j = 0; j < n; j++) {
                if(i == 0 || j == 0) {//最左侧和最上层
                    dp[i][j] = 1;
                } else {
                    dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
                }
            }
        }
        return dp[m - 1][n - 1];
    }
}

2.5、最小路径和(Minimum Path Sum)

题目描述:

给定一个由非负整数填充的m x n的二维数组,现在要从二维数组的左上角走到右下角,请找出路径上的所有数字之和最小的路径。
注意:你每次只能向下或向右移动。

题目来源:带权值的最小路径和_牛客题霸_牛客网

解题思路:

        与路径总数那道题类似,只是这里的每个格子赋值了,并求最小和;

1.状态F(i, j):从(0, 0)点到(i, j)的最小路径和;

2.状态转移方程:

        第一层(i == 0 && j > 0):F(0, j) = F(0, j - 1) + array[0][j];

        第一列(j == 0 && i > 0):F(i, 0) = F(i - 1, 0) + array[i][0];

        其他(i > 0 && j > 0):F(i, j) = min(F(i - 1, j), F(i, j - 1)) + array[i][j];

        解释:这里实际上和“路径总数” ,“三角矩阵”题目类似,每个格子被赋值了,并且是求不同路径能达到的最小和,方法是一样滴!

3.初始状态:F(0, 0) = array[0][0];

4.返回结果:F(m - 1, n - 1); 最右下方星星表示的格子;

代码如下:

import java.util.*;
public class Solution {
    public int minPathSum (int[][] grid) {
        int row = grid.length;
        for(int i = 0; i < row; i++) {
            for(int j = 0; j < grid[i].length; j++) {
                if(i == 0 && j > 0) {//第一层
                    grid[i][j] = grid[i][j - 1] + grid[i][j];
                } else if(j == 0 && i > 0) {//第一列
                    grid[i][j] = grid[i - 1][j] + grid[i][j];
                } else if(i > 0 && j > 0){
                    grid[i][j] = Math.min(
                        grid[i - 1][j], grid[i][j - 1]) + grid[i][j];
                }
            }
        }
        return grid[row - 1][grid[row - 1].length - 1];
    }
}

2.6、背包问题

题目描述:

有 n 个物品和一个大小为 m 的背包. 给定数组 A 表示每个物品的大小和数组 V 表示每个物品的价值.

问最多能装入背包的总价值是多大?

题目来源:125 · 背包问题(二) - LintCode

解题思路(不是最优解,二维数组):

        分析:这里有很多种情况,比如拿到的物品可能是:(体积大,价值大)、(体积大,价值小)......那么这里,需要考虑的因素就不止一种了,即需要考虑价值,还需要考虑体积(例如:包装不下,需要从包取出几个元素,再放入,又或是直接抛弃...)所以这里定义状态时,一维数组时满足不了的,这里需要一个二维数组;

1.状态F(i, j):(重点理解这里的状态,否在后面看不懂!)从前i个商品中选择,包的大小为j(j是包的总大小)时,所能放下的最大价值;

2.状态转移方程:

        第i个商品可以放入(虽然可以,但是不一定放入)大小为j的包中:

  •         A(i - 1) <= j && F(i, j) = max(F(i - 1, j), F(i - 1, j - A[i - 1]) + V[i - 1]);

        第i个商品不可以放入大小为j的包中(商品本身以及超出包的大小,怎么都放不进去):

  •         A(i - 1) > j && F(i, j) = F(i - 1, j);

        解释:

        F(i - 1, j - A[i - 1]) + V[i - 1])中的“j - A[i - 1]”表示为第i个商品预留出来的空间,也就是说(这里请重点理解!!!)第i个商品先不放入!只是预留出来的空间!因为这里通过从前i - 1个商品中做选择,在大小为j - A[i - 1]的包中已经选择出了存放最大价值的商品的方案,最后,由于已经预留好了空间,直接加上第i个商品的价值即可!!!

        对F(i - 1, j - A[i - 1]) + V(i - 1)) 整体的解释:两种情况,一是放入第i个商品 ---> F(i - 1, j - A[i - 1]) + V[i - 1]) ---> F(i, j)表示了 “不够放入第i个商品,需要从背包中取出某些商品,再放入” 和 “包的大小足够,直接放入第i个商品” ;二是不放入第i个商品 ---> F(i - 1, j) ---> F(i, j);通过这两种情况比较最大值可以决定是否把第i个商品放入;

        举个栗子:如下图

        这里如果选择放入第二个商品,则F(i - 1, j - A[i - 1]) + V(i - 1)) = F(2 - 1, 11 - 10) + 1 = F(1,1) + 1; 那么这里的F(1, 1)实际上就是一个空包,因为从前i个商品中做选择,放入大小为1的包中,但是第一个商品的大小为3,显然放不下,所以这里就表示放入第二个商品后最大价值为1;这合理吗?显然不合理,因为要去比较如果不放入第i个商品的价格F(1, 11) = 3 ,看第二个商品是否值得被放入,所有就有了F(i, j) = max(F(i - 1, j), F(i - 1, j - A[i - 1]) + V(i - 1));

3.初始化:F(i, 0) = 0, F(0, j) = 0;

4.返回结果:F(n, m);

还不情况,可以看看这个栗子:(如下图)

 代码如下:

public class Solution {
    public int backPackII(int m, int[] a, int[] v) {
        int n = a.length;//表示商品总个数
        int[][] dp = new int[n + 1][m + 1];
        //初始化(默认是0,就不用了)
        // for(int i = 0; i <= m; i++) {
        //     dp[0][i] = 0;
        // }
        // for(int i = 0; i <= n; i++) {
        //     dp[i][0] = 0;
        // }
        //递推
        for(int i = 1; i <= n; i++) {
            for(int j = 1; j <= m; j++) {
                if(a[i - 1] <= j) {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - a[i - 1]] + v[i - 1]);
                }else {
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        return dp[n][m];
    }
}

优化(最优解,一维数组):

        分析:上面解法是将其抽象成了二维数组的形式,实际上,一个一维数组就可以搞定,因为每次更新元素的时候,只需要用到上一行当前列的值和上一行某一列的值,所以可以将当前行未更新的值作为上一行的值, 并且要注意,更新值的时候需要倒序更新(原因就是上句话),这样空间上就得到了一个优化;

代码如下:

public class Solution {
    public int backPackII(int m, int[] a, int[] v) {
        int n = a.length;//表示商品总个数
        int[] dp = new int[m + 1];
        //递推
        for(int i = 1; i <= n; i++) {
            //注意这里要从后向前打印,因为这一行未更新的值就是上一行的值,而我们需要的就是
            //上一行当前列和上一行前面某一列的值;
            for(int j = m; j > 0; j--) {
                if(a[i - 1] <= j) {
                    dp[j] = Math.max(dp[j], dp[j - a[i - 1]] + v[i - 1]);
                }
                //else的部分也就不需要了,因为从后往前更新,未更新的值就是上一行的值
                //也就是说,当a[i - 1]>j时,直接不用管就可以了;
                // else { 
                //     dp[i][j] = dp[i - 1][j];
                // }
            }
        }
        return dp[m];
    }
}

2.7、回文串分割(Palindrome Partitioning)

题目描述:

给出一个字符串s,分割s使得分割出的每一个子串都是回文串

计算将字符串s分割成回文分割结果的最小切割数

例如:给定字符串s="aab",

返回1,因为回文分割结果["aa","b"]是切割一次生成的。

题目来源:132. 分割回文串 II - 力扣(LeetCode)

解题思路:(不是最优解,以下解法时间复杂度为O(n^3))

1.状态F(i):s的前i个字符最小的分割次数

2.状态转移方程:

        若整体是回文串[1, i],则F(i) = 0;

        j < i && [j + 1, i]是回文串,则F(i) = min(F(j) + 1); (这里是遍历找出最小值);

        解释:怎么的出来这个方程呢?动态转移方程,就是需要通过中间的某一状态,来一步推出下一个状态; 

        第一步)、假设我们以"baa"为例,假设我们需要求出F(3),那么可以想想,可以利用他的前一个状态F(2)吗?F(2)就表示前俩个字符的最小分割次数,那么,想推出F(3)就需要保证F(2)到F(3)中多出来的字符串"a"必须是回文的(若不回文,就不用管,直接跳过),这样就可以通过在F(2)计算的结果上加1得出F(3),而F(2) ---> b|a 到 F(3) ---> b|aa 无非就是多了一个字符"a",这一个字符必然是回文的,所以根据以上方法就可以得出F(3) = F(2) + 1 = 1 + 1 = 2;

        第二步)、再想一想,光看F(2)这一个状态够吗?显然不够,因此我们可以再看看F(1)是否可用,F(1)就表示前1个字符的最小分割次数,根据上面所讲到的方法,我们只需要确保F(1) 到F(3)中多出来的字符"aa"是回文的,那么就可以通过F(1) + 1的方法推出F(3),显然,"aa"是回文的,那么F(3) = F(1) + 1 = 0 + 1 = 1;这里算出的F(3)要小于 第一步 中算出的F(3),根据题目要求,所以我们取这里的F(3);

        第三步)、综上,想要得出第i个状态的值,我们需要将[1, i]这个区间都遍历一遍,并用上述方法选举出一个最小值即可;

3.初始状态:F(i) = i - 1;

        解释:每一个字符串的最大分割次数都是字符串的长度-1次,也就是分成一个个单字符,这样写就方便了转移方程比较最小值

4.返回结果:F(s.length());

代码如下:

class Solution {
    public int minCut(String s) {
        int len = s.length();
        if(len == 0) {
            return 0;
        }
        if(judge(s, 0, len - 1)) {
            return 0;
        }
        int[] dp = new int[len + 1];
        //初始化
        //填充可以被分割的最大次数
        for(int i = 1; i <= len; i++) {
            dp[i] = i - 1;
        }
        for(int i = 2; i <= len; i++) {
            if(judge(s, 0, i - 1)){//若前i个字符串整体回文
                dp[i] = 0;
                continue;
            }
            for(int j = 1; j < i; j++) {
                if(judge(s, j, i - 1)) {//注意索引
                    dp[i] = Math.min(dp[i], dp[j] + 1);
                }
            }
        }
        return dp[len];
    }
    //判断回文
    private boolean judge(String s, int start, int end) {
        while(start < end) {
            if(s.charAt(start) == s.charAt(end)) {
                start++;
                end--;
            } else {
                break;
            }
        }
        return start < end ? false : true;
    }
}

优化:(最优解,时间复杂度为O(n^2))

        解释:想要在时间复杂度上得到优化,就需要用空间来换取时间;实际上,判断回文串这里也可以用动态规划来解,并用一个二维数组保存每一种状态是否回文;

1.状态F(i, j):[i, j]这个子区间为回文串;

2.状态转移方程F(i, j):

        若i + 1 == j  则 F(i, j) : s[i] == s[j];

        其他情况 F(i + 1, j - 1) && s[i] == s[j]; 

解释: 

        若区间[i + 1, j - 1]是一个回文串,那么只需要保证字符串的第 i 个字符和字符串的第 j 个字符相等,就可以推出[i, j]这个i区间是回文;

注意:根据方程写法,由于需要用到上一行的结果,所以需要从最后一行开始更新;由于是判断回文,所以只需要更新一半即可;

3.初始化:i == j ,则F(i, j) = true;

4.返回结果:F(0, s.length() - 1);

代码如下:

class Solution {
    //获取回文信息
    private boolean[][] getMat(String s) {
        int n = s.length();
        boolean[][] dp = new boolean[n][n];
        for(int i = n - 1; i >= 0; i--) {
            for(int j = i; j < n; j++) {
                if(i == j) {
                    dp[i][j] = true;
                } else if(i + 1 == j) {
                    dp[i][j] = s.charAt(i) == s.charAt(j);
                } else {
                    dp[i][j] = (dp[i + 1][j - 1]) && (s.charAt(i) == s.charAt(j)); 
                } 
            }
        }
        return dp;
    }

    public int minCut(String s) {
        int len = s.length();
        if(len == 0) {
            return 0;
        }
        if(judge(s, 0, len - 1)) {
            return 0;
        }
        int[] dp = new int[len + 1];
        //初始化
        //填充可以被分割的最大次数
        for(int i = 1; i <= len; i++) {
            dp[i] = i - 1;
        }
        boolean[][] mat = getMat(s);
        for(int i = 2; i <= len; i++) {
            if(judge(s, 0, i - 1)){//若前i个字符串整体回文
                dp[i] = 0;
                continue;
            }
            for(int j = 1; j < i; j++) {
                if(mat[j][i - 1]) {//注意索引
                    dp[i] = Math.min(dp[i], dp[j] + 1);
                }
            }
        }
        return dp[len];
    }    
    //判断回文
    private boolean judge(String s, int start, int end) {
        while(start < end) {
            if(s.charAt(start) == s.charAt(end)) {
                start++;
                end--;
            } else {
                break;
            }
        }
        return start < end ? false : true;
    }
}

2.8、编辑距离(Edit Distance)

题目描述:

给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数  。

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

题目来源:72. 编辑距离 - 力扣(LeetCode)

解题思路:

1.状态F(i, j):word1前i个字符到word2的前j个字符的编辑距离;

2.状态转移方程:

        F(i, j) = min(插入,删除,替换) = min(F(i, j - 1) + 1, F(i - 1, j) + 1, F(i - 1, j - 1) + (w1[i] == w2 ? 0 : 1))

        解释:状态转移一定是一个一步的操作,所以想要得到F(i, j),我们可以往前推,如下,

        F(i, j - 1):表示 w1 的前 i 个字符已经变成了 w2 中的前 j - 1 个字符,所以只需要在此基础上插入一个字符,就可以变换到 F(i, j);

        F(i - 1, j):表示 w1 的前 i - 1 个字符已经变成了 w2 中的前 j 个字符,所以只需要在此基础上删掉一个字符,就可以变换到 F(i, j);

        F(i - 1, j - 1):表示 w1 的前 i - 1 个字符已经变成了 w2 中的前 j - 1 个字符,所以只需要在此基础上 先判断第i个字符与第j个字符是否相等,若相等就直接是 F(i, j),若不相等,就需要替换一个字符,才能变换到F(i, j);

        例如:w1 = "bi",w2 = "ke";(具体表格如下图)

3、4.初始化和返回结果都在上图中体现;

代码如下:

class Solution {
    public int minDistance(String word1, String word2) {
        int row = word1.length();
        int col = word2.length();
        int[][] min = new int[row + 1][col + 1];
        //初始化
        for(int i = 0; i <= row; i++) {
            min[i][0] = i;
        }
        for(int i = 1; i <= col; i++) {
            min[0][i] = i;
        }
        //递推
        for(int i = 1; i <= row; i++) {
            for(int j = 1; j <= col; j++) {
                //先求插入和删除中的最小步骤
                min[i][j] = Math.min(min[i][j - 1], min[i - 1][j]) + 1;
                //替换
                if(word1.charAt(i - 1) == word2.charAt(j - 1)) {//相等则不需要替换(不用加1)
                    min[i][j] = Math.min(min[i][j], min[i - 1][j - 1]);
                } else {//不相等
                    min[i][j] = Math.min(min[i][j], min[i - 1][j - 1] + 1);
                }
            }
        } 
        return min[row][col];
    }
}

2.9、不同子序列(Distinct Subsequences)

题目描述:

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。

字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是)

题目数据保证答案符合 32 位带符号整数范围。

题目来源:115. 不同的子序列 - 力扣(LeetCode)

解题思路:(空间复杂度未优化)

问题:S中与T相等的子序列的个数;

1.状态F(i, j):S的前 i 个子串构成的子串中与T前 j 个字符相同的子序列的个数;

2.状态转移方程:

        if(S[i] == T[i])  F(i, j) = F(i - 1, j - 1) + F(i - 1, j);

        if(S[i] != T[i])  F(i, j) = F(i - 1, j);

解释:

        F(i - 1, j - 1)就表示使用第i个字符,并且只能为最后一个字符,相当于和T的第j个字符匹配;F(i - 1, j)就表示不使用第i个字符;这两种情况是完全互斥的;例如T:"rabbb",S:"rabb",那么

S[5] == T[4]: 如果使用第五个字符,那么就要向前找看是否还有最后一个字符匹配的;

即F(4, 3) = rab 或 ra b  + S[5];F(4, 4) = rabb;

3.初始状态:F(i, 0) = 1;j != 0 && F(0, j) = 0;

        解释:F(i, 0)相当于是从前i个字符中找空串,那是可以找到的;j != 0 && F(0, j) = 0相当于从空串中找非空字符串,肯定是找不到的,所以是0;

4.返回结果:F(s.length(), t.length());

代码如下:

class Solution {
    public int numDistinct(String s, String t) {
        int row = s.length();
        int col = t.length();
        int[][] dp = new int[row + 1][col + 1];
        //初始化
        dp[0][0] = 1;
        //递推
        for(int i = 1; i <= row; i++) {
            dp[i][0] = 1; //初始化第一列
            for(int j = 1; j <= col; j++) {
                if(s.charAt(i - 1) == t.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];//使用和不使用第i个字符
                } else { //不相等,就退化到上一级
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        return dp[row][col];
    }
}

优化:(空间复杂度优化)

        这里类似于之前的背包问题,通过转移方程可以看出,一直只用到的上一行的某一列的值,所以这个时候,我们可以用一个一维数组来代替,未被更新的值上一行的值,更新后的值就是当前行,所以这个时候也要注意同样的问题:每一行后需要从后向前更新~

class Solution {
    public int numDistinct(String s, String t) {
        int row = s.length();
        int col = t.length();
        int[] dp = new int[col + 1];
        //初始化
        dp[0] = 1;
        //递推
        for(int i = 1; i <= row; i++) {
            for(int j = col; j > 0; j--) {
                if(s.charAt(i - 1) == t.charAt(j - 1)) {
                    dp[j] = dp[j - 1] + dp[j];//使用和不使用第i个字符
                }
            }
        }
        return dp[col];
    }
}

三、总结

做完这些题,在回头看看开头的总结的东西,是不是感觉都会很不一样!

这里再做一个小的总结:

        状态来源:从问题中抽象状态;

        抽象状态:每一个状态对应一个子问题;

        状态定义(难点)有很多,如何验证状态的合理性:1.某个状态的解或者多个状态的解能否对应到最终问题的解;2.状态之间可以形成递推关系;

        定义一维状态还是二维状态?优先一维,当其不满足合理性时,再选择二维;

常见问题定义原则

        字符串问题:状态一般对应子串,状态中的递推一般会增加一个新的字符;

        矩阵:往往对应二维状态,通过优化可以使其转变为一维;


码字不易~

很肝~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/86341.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第08讲:使用脚手架创建vue项目

一、安装NodeJS 二、配置环境变量 2.1、软件安装完成之后配置npm的环境变量 第1步&#xff1a;获取npm安装位置 使用管理员身份打开CMD&#xff0c;用如下命令获取npm的安装位置&#xff1a; npm config list第2步&#xff1a;配置环境变量 将以上获取的路径保存到path变…

flask请求与响应、session执行流程

目录 请求对象 响应对象 session的使用和原理 闪现(flash) 请求扩展 蓝图 请求对象 请求对象request是全局的&#xff0c;需要导入这个全局的request&#xff0c;在哪个视图函数中就是当次的request对象 请求数据&#xff1a; request.method # 获取提交的方法 …

文件包含漏洞简介

今天继续给大家介绍渗透测试相关知识&#xff0c;本文主要内容是文件包含漏洞简介。 免责声明&#xff1a; 本文所介绍的内容仅做学习交流使用&#xff0c;严禁利用文中技术进行非法行为&#xff0c;否则造成一切严重后果自负&#xff01; 再次强调&#xff1a;严禁对未授权设备…

一些好玩的js小作品

今天小编给大家带来了一些很实用的js小作品&#xff0c;下面请一看究竟。 1、计算详细年龄工具js脚本 2、检测是否安装Flash插件及版本号js脚本 3、无法查看源码的页面 4、面积换算js脚本 5、体积和容积换算js脚本 6、长度换算js脚本 7、重量换算js脚本 8、只能输入汉字…

记一些女装数据分析

文章目录服装维度女装生命周期门店维度常见度量值衍生指标服装维度 尺码&#xff1a;XS、S、M、L、XL颜色&#xff1a;黑、红、蓝、白……一级分类&#xff1a;上半身、下半身、全身季节&#xff1a;春、夏、秋、冬价格类型&#xff1a;正价、特价、折扣价、降价、优惠券…价格…

Android自定义ViewGroup布局进阶,完整的九宫格实现

自定义ViewGroup九宫格 前言 在之前的文章我们复习了 ViewGroup 的测量与布局&#xff0c;那么我们这一篇效果就可以在之前的基础上实现一个灵活的九宫格布局。 那么一个九宫格的 ViewGroup 如何定义&#xff0c;我们分解为如下的几个步骤来实现&#xff1a; 先计算与测量九…

【Linux学习】进程信号

文章目录前言一、信号初识1. 信号的概念2. Linux中的普通信号3. 信号的处理二、信号产生1. 终端按键产生信号2. 系统调用发送信号2.1 kill函数2.2 raise函数2.3 abort函数3. 由软件条件产生信号3.1 SIGPIPE信号3.2 alarm函数4. 由硬件异常产生信号三、信号阻塞1. 信号阻塞即其他…

[前端面试题]flex上下布局

[前端面试题]flex上下布局 [万字长文]一文教你彻底搞懂flex布局 [CSS]一些flex的应用场景 页面中有两个元素。元素bottom固定在底部&#xff0c;靠内容来撑开&#xff1b;而元素top在上边&#xff0c;高度自适应&#xff0c;自动铺满除bottom剩下的空间&#xff0c;且top内容…

第十四届蓝桥杯集训——JavaC组第十篇——分支语句

第十四届蓝桥杯集训——JavaC组第十篇——分支语句 目录 第十四届蓝桥杯集训——JavaC组第十篇——分支语句 if单分支 if单分支语法 if单分支语句示例 单分支例题&#xff1a; 连续单分支示例 if简写语法 if双分支语句 if双分支语法 if双分支语法示例 if双分支简写法…

全栈jmeter接口测试教程之Jmeter+ant+jenkins实现持续集成

jmeterantjenkins持续集成 一、下载并配置jmeter 首先下载jmeter工具&#xff0c;并配置好环境变量&#xff1b;参考&#xff1a;https://www.cnblogs.com/YouJeffrey/p/16029894.html jmeter默认保存的是.jtl格式的文件&#xff0c;要设置一下bin/jmeter.properties,文件内容…

圣诞节快来了~用python做一个粒子烟花震撼众人赚个女孩回来吧~

前言 嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 又到了学Python时刻~ 准备 准备一下你运行效果的背景图 以及一首你喜欢或那你女朋友喜欢的音乐 效果 代码展示 导入模块 import random import pygame as py import tkinter as tk from time import time, sleep fr…

Fuzzing with Data Dependency Information阅读笔记

相关数据 论文&#xff1a;https://www.s3.eurecom.fr/docs/eurosp22_mantovani.pdf 开源代码&#xff1a;https://github.com/elManto/DDFuzz 论文背景 这篇论文是2022年发表在sp上的一篇论文&#xff0c;也是在afl的基础上进行改进的一篇论文。afl是在afl的基础上进行整合…

第二十六章 linux-输入子系统二

第二十六章 linux-输入子系统二 文章目录第二十六章 linux-输入子系统二框架三个重要结构体struct input_devstruct input_handlerstruct input_handle框架 Linux系统支持的输入设备繁多&#xff0c;例如键盘、鼠标、触摸屏、手柄或者是一些输入设备像体感输入等等&#xff0c…

[附源码]Python计算机毕业设计电子投票系统Django(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等…

30岁转行网络安全来得及吗?有发展空间吗?

30岁转行网络安全来得及吗?有发展空间吗? 现阶段&#xff0c;很多30岁左右的人群都面临就业难的问题&#xff0c;尤其是对于年龄已过30.没有一技之长的人。现阶段&#xff0c;网络安全行业已成了风口行业&#xff0c;也有很多30岁人群也想转行学习网络安全&#xff0c;但又担…

python之界面案例

目录 一、海龟绘图 二、图形化编程入门 窗口创建 三、表格控件的简单认知 四、综合案例 一、海龟绘图 海龟绘图作用&#xff1a;提升界面美观度&#xff0c;吸引用户使用 学习网址&#xff1a; turtle --- 海龟绘图 — Python 3.8.14 文档 二、图形化编程入门 窗口创建 …

【数据结构与算法】跳表

目录 一、什么是跳表 二、跳表的效率验证 三、跳表的实现 1、search 2、add 3、erase 四、跳表与其它搜索结构对比 总结 一、什么是跳表 跳表是一个随机化的数据结构&#xff0c;可以被看做二叉树的一个变种&#xff0c;它在性能上和红黑树&#xff0c;AVL树不相上下&am…

【高精度定位】RTK定位与RTD定位知识科普

高精度定位一般指亚米级别或厘米级别的定位&#xff0c;常见的室内有蓝牙AoA和UWB两种技术&#xff0c;室外有北斗地基增强技术&#xff0c;这些技术都是采用算法进行定位。 工业4.0时代&#xff0c;在资源和环境约束不断强化的背景下&#xff0c;创新驱动传统制造向智能制造转…

【MAUI】条形码,二维码扫描功能

前言 本系列文章面向移动开发小白&#xff0c;从零开始进行平台相关功能开发&#xff0c;演示如何参考平台的官方文档使用MAUI技术来开发相应功能。 介绍 移动端的扫描条形码、二维码的功能已经随处可见&#xff0c;已经很难找到一个不支持扫描的App了&#xff0c;但是微软的…

sync fsync fdatasync 三者的区别

传统的UNIX系统实现在内核中设有缓冲区高速缓存或页高速缓存&#xff0c;大多数磁盘I/O都通过缓区进行。当我们向文件写入数据时&#xff0c;内核通常先将数据复制到缓冲区中&#xff0c;然后排入队列&#xff0c;晚些时候再入磁盘。这种方式被称为延迟写 (delayed wrie)(Bach[…