【算法优选】 动态规划之两个数组dp——壹

简介: 【算法优选】 动态规划之两个数组dp——壹

🎋前言

动态规划相关题目都可以参考以下五个步骤进行解答:

  1. 状态表示
  2. 状态转移⽅程
  3. 初始化
  4. 填表顺序
  5. 返回值

后面题的解答思路也将按照这五个步骤进行讲解。

🌴最长公共子序列

🚩题目描述

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

  • 例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

  • 示例 1:
    输入:text1 = “abcde”, text2 = “ace”
    输出:3
    解释:最长公共子序列是 “ace” ,它的长度为 3 。
  • 示例 2:
    输入:text1 = “abc”, text2 = “abc”
    输出:3
    解释:最长公共子序列是 “abc” ,它的长度为 3 。
  • 示例 3:
    输入:text1 = “abc”, text2 = “def”
    输出:0
    解释:两个字符串没有公共子序列,返回 0 。
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
    }
}

🚩算法思路

  1. 状态表⽰:

对于两个数组的动态规划,我们的定义状态表⽰的经验就是:

  • 选取第⼀个数组 [0, i] 区间以及第⼆个数组 [0, j] 区间作为研究对象;
  • 结合题⽬要求,定义状态表⽰。

在这道题中,我们根据定义状态表⽰为:

dp[i][j] 表⽰: text1 的 [0, i] 区间以及 text2 的 [0, j] 区间内的所有的⼦序列中,最⻓公共⼦序列的⻓度。

  1. 状态转移⽅程:

分析状态转移⽅程的经验就是根据「最后⼀个位置」的状况,分情况讨论。

对于 dp[i][j] ,我们可以根据 text1[i] 与 text2[j] 的字符分情况讨论:

  • 两个字符相同, text1[i] = text2[j] :那么最⻓公共⼦序列就在 text1 的 [0, i - 1] 以及 text2 的 [0, j - 1] 区间上找到⼀个最⻓的,然后再加上 text1[i] 即可。因此dp[i][j] = dp[i - 1][j - 1] + 1 ;
  • 两个字符不相同, text1[i] != text2[j] :那么最⻓公共⼦序列⼀定不会同时以 text1[i] 和 text2[j] 结尾。那么我们找最⻓公共⼦序列时,有下⾯三种策略:
  • 去 text1 的 [0, i - 1] 以及text2 的 [0, j] 区间内找:此时最⼤⻓度为 dp[i- 1][j] ;
  • 去 text1 的 [0, i] 以及 text2 的 [0, j - 1] 区间内找:此时最⼤⻓度为 dp[i ][j - 1] ;
  • 去text1 的 [0, i - 1] 以及text2 的 [0, j - 1] 区间内找:此时最⼤⻓度为dp[i - 1][j - 1] 。

我们要三者的最⼤值即可。但是我们细细观察会发现,第三种包含在第⼀种和第⼆种情况⾥⾯,但是我们求的是最⼤值,并不影响最终结果。因此只需求前两种情况下的最⼤值即可。

综上,状态转移⽅程为:

if(text1[i] == text2[j]) dp[i][j] = dp[i - 1][j - 1] + 1 ;

if(text1[i] != text2[j]) dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) 。

  1. 初始化:

要注意「下标的映射关系」,以及⾥⾯的值要「保证后续填表是正确的」。

  1. 填表顺序:

根据「状态转移⽅程」得:从上往下填写每⼀⾏,每⼀⾏从左往右。

  1. 返回值:

根据「状态表⽰」得:返回 dp[m][n] 。

🚩代码实现

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++) {
                if(text1.charAt(i - 1) == text2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j],dp[i][j - 1]);
                }
            }
        }
        return dp[m][n];
    }
}

🎄不相交的线

🚩题目描述

在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。

现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足:

nums1[i] == nums2[j]

且绘制的直线不与任何其他连线(非水平线)相交。

请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。

以这种方法绘制线条,并返回可以绘制的最大连线数。

  • 示例 1:

    输入:nums1 = [1,4,2], nums2 = [1,2,4]
    输出:2
    解释:可以画出两条不交叉的线,如上图所示。
    但无法画出第三条不相交的直线,因为从 nums1[1]=4 到 nums2[2]=4 的直线将与从 nums1[2]=2 到 nums2[1]=2 的直线相交。
  • 示例 2:
    输入:nums1 = [2,5,1,2,5], nums2 = [10,5,2,1,5,2]
    输出:3
  • 示例 3:
    输入:nums1 = [1,3,7,1,7,5], nums2 = [1,9,2,5,1]
    输出:2
class Solution {
    public int maxUncrossedLines(int[] nums1, int[] nums2) {
    }
}

🚩算法思路

如果要保证两条直线不相交,那么我们「下⼀个连线」必须在「上⼀个连线」对应的两个元素的「后⾯」寻找相同的元素。这不就转化成「最⻓公共⼦序列」的模型了嘛。那就是在这两个数组中寻找「最⻓的公共⼦序列」。

只不过是在整数数组中做⼀次「最⻓的公共⼦序列」,代码⼏乎⼀模⼀样,这⾥就不再赘述算法原理啦~

🚩代码实现

class Solution {
    public int maxUncrossedLines(int[] nums1, int[] nums2) {
        // 1. 创建 dp 表
        // 2. 初始化
        // 3. 填表
        // 4. 返回值
        int m = nums1.length;
        int n = nums2.length;
        int[][] dp = new int[m + 1][n + 1];
        for(int i = 1; i <= m; i++) {
            for(int j = 1; j <= n; j++) {
                if(nums1[i - 1] == nums2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[m][n];
    }
}

🎍不同的子序列

🚩题目描述

给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数,结果需要对 109 + 7 取模。

  • 示例 1:
    输入:s = “rabbbit”, t = “rabbit”
    输出:3
  • 示例 2:
    输入:s = “babgbag”, t = “bag”
    输出:5
class Solution {
    public int numDistinct(String s, String t) {
    }
}

🚩算法思路

  1. 状态表⽰:

对于两个字符串之间的 dp 问题,我们⼀般的思考⽅式如下:

  • 选取第⼀个字符串的 [0, i] 区间以及第⼆个字符串的 [0, j] 区间当成研究对象,结合题⽬的要求来定义「状态表⽰」;
  • 然后根据两个区间上「最后⼀个位置的字符」,来进⾏「分类讨论」,从⽽确定「状态转移⽅程」。

我们可以根据上⾯的策略,解决⼤部分关于两个字符串之间的 dp 问题。

dp[i][j] 表⽰:在字符串 s 的 [0, j] 区间内的所有⼦序列中,有多少个 t 字符串 [0,i] 区间内的⼦串。

  1. 状态转移⽅程:

⽼规矩,根据「最后⼀个位置」的元素,结合题⽬要求,分情况讨论:

  • 当 t[i] == s[j] 的时候,此时的⼦序列有两种选择:
  • ⼀种选择是:⼦序列选择 s[j] 作为结尾,此时相当于在状态 dp[i - 1][j - 1]中的所有符合要求的⼦序列的后⾯,再加上⼀个字符 s[j] (请⼤家结合状态表⽰,好好理解这句话),此时 dp[i][j] = dp[i - 1][j - 1] ;
  • 另⼀种选择是:我就是任性,我就不选择 s[j] 作为结尾。此时相当于选择了状态 dp[i][j - 1] 中所有符合要求的⼦序列。我们也可以理解为继承了上个状态⾥⾯的求得的⼦序列。此时 dp[i][j] = dp[i][j - 1] ;
  • 两种情况加起来,就是 t[i] == s[j] 时的结果。
  • 当 t[i] != s[j] 的时候,此时的⼦序列只能从 dp[i][j - 1] 中选择所有符合要求的⼦序列。只能继承上个状态⾥⾯求得的⼦序列, dp[i][j] = dp[i][j - 1] ;

综上所述,状态转移⽅程为:

  • 所有情况下都可以继承上⼀次的结果: dp[i][j] = dp[i][j - 1] ;
  • 当 t[i] == s[j] 时,可以多选择⼀种情况: dp[i][j] += dp[i - 1][j - 1]
  1. 初始化:
  • 「空串」是有研究意义的,因此我们将原始 dp 表的规模多加上⼀⾏和⼀列,表⽰空串。
  • 引⼊空串后,⼤⼤的⽅便我们的初始化。
  • 但也要注意「下标的映射关系」,以及⾥⾯的值要「保证后续填表是正确的」。

当 s 为空时, t 的⼦串中有⼀个空串和它⼀样,因此初始化第⼀⾏全部为 1 。

  1. 填表顺序:

「从上往下」填每⼀⾏,每⼀⾏「从左往右」。

  1. 返回值:

根据「状态表⽰」,返回 dp[m][n] 的值,本题不用取模也可通过

🚩代码实现

class Solution {
    public int numDistinct(String s, String t) {
        // 1. 创建 dp 表
        // 2. 初始化
        // 3. 填表
        // 4. 返回值
        int m = t.length();
        int n = s.length();
        int[][] dp = new int[m + 1][n + 1];
        for(int j = 0; j <= n; j++) {
            dp[0][j] = 1;
        }
        for(int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                dp[i][j] = dp[i][j - 1];
                if (t.charAt(i - 1) == s.charAt(j - 1)) {
                    dp[i][j] += dp[i - 1][j - 1];
                }
            }
        }
        return dp[m][n];
    }
}

🍀通配符匹配

🚩题目描述

给你一个输入字符串 (s) 和一个字符模式 § ,请你实现一个支持 ‘?’ 和 ‘*’ 匹配规则的通配符匹配:

  • ‘?’ 可以匹配任何单个字符。
  • ‘*’ 可以匹配任意字符序列(包括空字符序列)。

判定匹配成功的充要条件是:字符模式必须能够 完全匹配 输入字符串(而不是部分匹配)。

  • 示例 1:
    输入:s = “aa”, p = “a”
    输出:false
    解释:“a” 无法匹配 “aa” 整个字符串。
  • 示例 2:
    输入:s = “aa”, p = ""
    输出:true
    解释:'
    ’ 可以匹配任意字符串。
  • 示例 3:
    输入:s = “cb”, p = “?a”
    输出:false
    解释:‘?’ 可以匹配 ‘c’, 但第二个 ‘a’ 无法匹配 ‘b’。
class Solution {
    public boolean isMatch(String ss, String pp) {
    }
}

🚩算法思路:

  1. 状态表⽰:

对于两个字符串之间的 dp 问题,我们⼀般的思考⽅式如下:

  • 选取第⼀个字符串的 [0, i] 区间以及第⼆个字符串的 [0, j] 区间当成研究对象,结合题⽬的要求来定义「状态表⽰」;
  • 然后根据两个区间上「最后⼀个位置的字符」,来进⾏「分类讨论」,从⽽确定「状态转移
    ⽅程」。

我们可以根据上⾯的策略,解决⼤部分关于两个字符串之间的 dp 问题。

因此,我们定义状态表⽰为:

dp[i][j] 表⽰: p 字符串 [0, j] 区间内的⼦串能否匹配字符串 s 的 [0, i] 区间内的

⼦串。

  1. 状态转移⽅程:

⽼规矩,根据最后⼀个位置的元素,结合题⽬要求,分情况讨论:

  • 当 s[i] == p[j] 或 p[j] == ‘?’ 的时候,此时两个字符串匹配上了当前的⼀个字符,只能从 dp[i - 1][j - 1] 中看当前字符前⾯的两个⼦串是否匹配。只能继承上个状态中的匹配结果, dp[i][j] = dp[i][j - 1] ;
  • 当 p[j] == ’ * ’ 的时候,此时匹配策略有两种选择:
  • ⼀种选择是: * 匹配空字符串,此时相当于它匹配了⼀个寂寞,直接继承状态 dp[i][j - 1] ,此时 dp[i][j] = dp[i][j - 1] ;
  • 另⼀种选择是: * 向前匹配 1 ~ n 个字符,直⾄匹配上整个 s1 串。此时相当于从 dp[k][j - 1] (0 <= k <= i) 中所有匹配情况中,选择性继承可以成功的情况。此时 dp[i][j] = dp[k][j - 1] (0 <= k <= i) ;
  • 当 p[j] 不是特殊字符,且不与 s[i] 相等时,⽆法匹配。

三种情况加起来,就是所有可能的匹配结果。

综上所述,状态转移⽅程为:

  • 当 s[i] == p[j] 或 p[j] == ‘?’ 时: dp[i][j] = dp[i][j - 1] ;
  • 当 p[j] == ‘*’ 时,有多种情况需要讨论: dp[i][j] = dp[k][j - 1] (0 <=k <= i) ;

优化:当我们发现,计算⼀个状态的时候,需要⼀个循环才能搞定的时候,我们要想到去优化。优化的⽅向就是⽤⼀个或者两个状态来表⽰这⼀堆的状态。通常就是把它写下来,然后⽤数学的⽅式做⼀下等价替换:

当 p[j] == ’ * ’ 时,状态转移⽅程为:

dp[i][j] = dp[i][j - 1] || dp[i - 1][j - 1] || dp[i - 2][j - 1]

我们发现 i 是有规律的减⼩的,因此我们去看看 dp[i - 1][j] :

dp[i - 1][j] = dp[i - 1][j - 1] || dp[i - 2][j - 1] || dp[i - 3][j - 1]

我们惊奇的发现, dp[i][j] 的状态转移⽅程⾥⾯除了第⼀项以外,其余的都可以⽤ dp[i -1][j] 替代。

因此,我们优化我们的状态转移⽅程为: dp[i][j] = dp[i - 1][j] ||dp[i][j - 1] 。

  1. 初始化:

由于 dp 数组的值设置为是否匹配,为了不与答案值混淆,我们需要将整个数组初始化为false 。

由于需要⽤到前⼀⾏和前⼀列的状态,我们初始化第⼀⾏、第⼀列即可。

  • dp[0][0] 表⽰两个空串能否匹配,答案是显然的,初始化为 true 。
  • 第⼀⾏表⽰ s 是⼀个空串, p 串和空串只有⼀种匹配可能,即 p 串表⽰为 “**" ,此时也相当于空串匹配上空串。所以,我们可以遍历 p 串,把所有前导为 "” 的 p ⼦串和空串的 dp 值设为 true 。
  • 第⼀列表⽰ p 是⼀个空串,不可能匹配上 s 串,跟随数组初始化即可。
  1. 填表顺序:

从上往下填每⼀⾏,每⼀⾏从左往右。

  1. 返回值:

根据状态表⽰,返回 dp[m][n] 的值。

🚩代码实现

class Solution {
    public boolean isMatch(String ss, String pp) {
        // 1. 创建 dp 表
        // 2. 初始化
        // 3. 填表
        // 4. 返回结果
        int m = ss.length();
        int n = pp.length();
        ss = " " + ss;
        pp = " " + pp;
        char[] s = ss.toCharArray();
        char[] p = pp.toCharArray();
        boolean[][] dp = new boolean[m + 1][n + 1];
        dp[0][0] = true;
        for(int j = 1; j <= n; j++) {
            if(p[j] == '*') {
                dp[0][j] = true;
            } else {
                break;
            }
        }
        for(int i = 1; i <= m; i++) {
            for(int j = 1; j <= n; j++) {
                if(p[j] == '*') {
                    dp[i][j] = dp[i - 1][j] || dp[i][j - 1];
                } else {
                    dp[i][j] = (p[j] == '?' || p[j] == s[i]) && dp[i - 1][j - 1];
                }
            }
        }
        return dp[m][n];
    }
}

⭕总结

关于《【算法优选】 动态规划之两个数组dp——壹》就讲解到这儿,感谢大家的支持,欢迎各位留言交流以及批评指正,如果文章对您有帮助或者觉得作者写的还不错可以点一下关注,点赞,收藏支持一下

相关文章
|
20天前
|
算法 Python
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果;贪心算法在每一步选择局部最优解,追求全局最优;动态规划通过保存子问题的解,避免重复计算,确保全局最优。这三种算法各具特色,适用于不同类型的问题,合理选择能显著提升编程效率。
36 2
|
2月前
|
算法
动态规划算法学习三:0-1背包问题
这篇文章是关于0-1背包问题的动态规划算法详解,包括问题描述、解决步骤、最优子结构性质、状态表示和递推方程、算法设计与分析、计算最优值、算法实现以及对算法缺点的思考。
69 2
动态规划算法学习三:0-1背包问题
|
2月前
|
算法 程序员 索引
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
栈的基本概念、应用场景以及如何使用数组和单链表模拟栈,并展示了如何利用栈和中缀表达式实现一个综合计算器。
33 1
数据结构与算法学习七:栈、数组模拟栈、单链表模拟栈、栈应用实例 实现 综合计算器
|
2月前
|
算法
动态规划算法学习四:最大上升子序列问题(LIS:Longest Increasing Subsequence)
这篇文章介绍了动态规划算法中解决最大上升子序列问题(LIS)的方法,包括问题的描述、动态规划的步骤、状态表示、递推方程、计算最优值以及优化方法,如非动态规划的二分法。
71 0
动态规划算法学习四:最大上升子序列问题(LIS:Longest Increasing Subsequence)
|
2月前
|
算法
动态规划算法学习二:最长公共子序列
这篇文章介绍了如何使用动态规划算法解决最长公共子序列(LCS)问题,包括问题描述、最优子结构性质、状态表示、状态递归方程、计算最优值的方法,以及具体的代码实现。
145 0
动态规划算法学习二:最长公共子序列
|
2月前
|
存储 算法 定位技术
数据结构与算法学习二、稀疏数组与队列,数组模拟队列,模拟环形队列
这篇文章主要介绍了稀疏数组和队列的概念、应用实例以及如何使用数组模拟队列和环形队列的实现方法。
23 0
数据结构与算法学习二、稀疏数组与队列,数组模拟队列,模拟环形队列
|
2月前
|
存储 算法
动态规划算法学习一:DP的重要知识点、矩阵连乘算法
这篇文章是关于动态规划算法中矩阵连乘问题的详解,包括问题描述、最优子结构、重叠子问题、递归方法、备忘录方法和动态规划算法设计的步骤。
111 0
|
2月前
|
算法 安全 数据安全/隐私保护
基于game-based算法的动态频谱访问matlab仿真
本算法展示了在认知无线电网络中,通过游戏理论优化动态频谱访问,提高频谱利用率和物理层安全性。程序运行效果包括负载因子、传输功率、信噪比对用户效用和保密率的影响分析。软件版本:Matlab 2022a。完整代码包含详细中文注释和操作视频。
|
13天前
|
算法 数据安全/隐私保护 索引
OFDM系统PAPR算法的MATLAB仿真,对比SLM,PTS以及CAF,对比不同傅里叶变换长度
本项目展示了在MATLAB 2022a环境下,通过选择映射(SLM)与相位截断星座图(PTS)技术有效降低OFDM系统中PAPR的算法实现。包括无水印的算法运行效果预览、核心程序及详尽的中文注释,附带操作步骤视频,适合研究与教学使用。
|
21天前
|
算法 数据挖掘 数据安全/隐私保护
基于FCM模糊聚类算法的图像分割matlab仿真
本项目展示了基于模糊C均值(FCM)算法的图像分割技术。算法运行效果良好,无水印。使用MATLAB 2022a开发,提供完整代码及中文注释,附带操作步骤视频。FCM算法通过隶属度矩阵和聚类中心矩阵实现图像分割,适用于灰度和彩色图像,广泛应用于医学影像、遥感图像等领域。