动态规划应用篇:详解最长公共子序列问题

这篇具有很好参考价值的文章主要介绍了动态规划应用篇:详解最长公共子序列问题。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

动态规划是一个强大的工具,将复杂问题分解为多个容易解决的子问题,并且会对中间结果进行存储从而避免重复计算,然后将它们的解组合起来,形成大问题的解,高效地得出全局最优解。前面我们已经了解了动态规划的基础知识及一维动态规划问题的求解,今天,我们将继续深入学习动态规划算法,通过讲解最长公共子序列(LCS)问题的例子来学习更为复杂的二维动态规划。

最长公共子序列(LCS)

问题描述

最长公共子序列(Longest Common Subsequence,LCS)问题是一个经典的计算机科学问题,它寻找两个序列共有的最长子序列。这里的“子序列”是指在不改变序列元素的相对顺序的情况下,通过删除一些元素(也可能一个都不删)形成的新序列。注意,子序列不同于子串,子串要求元素在原序列中是连续的,而子序列不要求连续。

举个例子,如果我们有两个字符串 A = "ABCD" 和 B = "ACBAD",它们的一个最长公共子序列是 "ABD"。可以看出,"ABD" 是通过从 A 中选择"A", "B", "D" 且从 B 中选择"A", "B", "D"形成的,保持了它们在各自序列中的相对位置,但并不要求连续。

问题分析

这里我们先简单回顾一下动态规划的相关知识。动态规划解决问题的关键是将大问题分解为容易解决的小问题,解决这些小问题,然后将它们的解组合起来,形成大问题的解。因为分解得到的小问题通常都会有重叠部分,我们需要对中间结果进行存储从而避免重复计算。要成功应用动态规划,我们需要识别出两个主要特性:最优子结构和重叠子问题。

  • 最优子结构:一个问题的最优解包含了其子问题的最优解。
  • 重叠子问题:在解决大问题的过程中,相同的小问题会被反复解决多次。

在LCS问题中,我们会发现,一个序列的LCS可以通过组合其子序列的LCS得到,这就利用了最优子结构的特性。同时,要找到两个序列的最长公共子序列,我们需要多次寻找它们的子序列之间的最长公共子序列,这正是重叠子问题的体现。

在深入探讨动态规划如何解决LCS问题之前,我们还需要理解二维动态规划是什么。二维动态规划是动态规划的一种特殊形式,它特别适用于处理涉及两个维度或两个序列的问题。在这种情况下,我们通常使用一个二维数组(或矩阵)来存储中间结果,数组的每个元素对应于输入序列的一个子问题的解。

LCS问题正是二维动态规划的一个典型例子。为什么这样说呢?因为在解决LCS问题时,我们需要同时考虑两个序列中的元素,这意味着我们需要同时在两个维度上操作:一个维度是序列A,另一个维度是序列B。我们利用二维数组来存储所有可能的子序列组合的LCS长度,从而避免了重复计算相同子问题的工作。

通过这种方式,我们可以有效地将LCS问题的解构建在之前解决的子问题上。二维数组的每个位置(i, j)代表了序列A的前i个元素和序列B的前j个元素的最长公共子序列的长度。通过逐步填充这个二维数组,我们最终能解决整个LCS问题。这个过程不仅体现了动态规划的核心原则,也展示了二维动态规划在处理此类问题时的强大能力。

问题求解

1. 创建、初始化二维数组(定义状态与确定边界条件):

在LCS问题的动态规划解法中,状态是通过大小为(m+1) x (n+1) 的二维数组 dp来定义的,其中m和n分别是序列A和序列B的长度,dp[i][j] 表示序列A的前i个字符和序列B的前j个字符的最长公共子序列的长度,我们要求的最终结果是dp[m][n]

边界条件是解决动态规划问题的起点,它定义了最基本子问题的解。LCS问题的边界条件当序列A或序列B的长度为0时,最长公共子序列的长度为0。这可以通过初始化二维数组 dp 的第一行和第一列为0来实现,即 dp[0][j] = 0 和 dp[i][0] = 0,表明一个空序列和任意序列的最长公共子序列长度为0。

2. 填充二维数组(建立状态转移方程):

状态转移方程定义了从一个状态如何转移到另一个状态,即它描述了问题的递推关系。对于LCS问题,状态转移方程如下:

对于每一对元素 (i, j)(分别是序列A和B的索引,注意索引是从0开始,而对dp数组的操作是从1开始)

  • 如果 A[i] == B[j],那么 dp[i + 1][j + 1] = dp[i][j] + 1。这表示了当两个序列的当前元素相匹配时,当前的最长公共子序列长度是不包含这个元素的子序列的最长公共子序列长度加一
  • 如果 A[i] != B[j],那么 dp[i + 1][j + 1] = max(dp[i+1][j], dp[i][j+1])。这表示当两个序列的当前元素不匹配时,当前的最长公共子序列长度是两个可能的子问题解中的较大值,即要么不包含A序列的当前元素,要么不包含B序列的当前元素

3. 找出最长公共子序列:

从二维数组的右下角开始回溯。根据 dp 数组的值来确定序列A和B的哪些元素构成了LCS。如果 dp[i][j] 是由 dp[i-1][j-1] + 1 更新来的,说明 A[i-1] 和 B[j-1] 是LCS的一部分。如果是从 dp[i-1][j] 或 dp[i][j-1] 更新来的,我们则向值较大的方向移动,直到到达数组的左边界或上边界。

举例说明

为了能够更好地理解这一过程,我们来举一个例子。假设我们有两个序列:①序列A-"ABCD"②序列B- "AEBD",我们希望找到这两个序列的最长公共子序列。

首先,我们创建一个二维数组 dp,其大小为 (len(A)+1) x (len(B)+1)。其中 len(A)=4 和 len(B)=4,则 dp 数组的大小为 5x5。然后进行初始化。

动态规划法最长公共子序列问题,算法,蓝桥杯,动态规划,算法,蓝桥杯

接下来我们比较序列A的第一个字符"A"和序列B的所有字符。我们发现与序列B的第一个字符"A"匹配,因此 dp[1][1] 更新为dp[0][0] + 1 即1。

动态规划法最长公共子序列问题,算法,蓝桥杯,动态规划,算法,蓝桥杯

继续比较,B中的"E"、"B"、"D"无法匹配"A",因此我们根据状态转移方程,选取左边(dp[i+1][j])和上面(dp[i][j+1])之中的最大值来填充。则填充1。

动态规划法最长公共子序列问题,算法,蓝桥杯,动态规划,算法,蓝桥杯

接下来我们比较序列A的第一个字符"B"和序列B的所有字符,我们发现与序列B的字符"A"、“E”不匹配,则选取左边(dp[i+1][j])和上面(dp[i][j+1])之中的最大值来填充,填充1。与序列B的第三个字符"B"匹配,因此 dp[2][3] 更新为dp[1][2] + 1即2。继续,与序列B的字符"D"不匹配,则选取左边(dp[i+1][j])和上面(dp[i][j+1])之中的最大值来填充,填充2。

动态规划法最长公共子序列问题,算法,蓝桥杯,动态规划,算法,蓝桥杯

以此类推,直到填完整个dp数组。

动态规划法最长公共子序列问题,算法,蓝桥杯,动态规划,算法,蓝桥杯

下面演示如何根据这个结果来构建最长的公共子序列。

  1. 我们从 dp[4][4] 开始,它的值是3。这表示 "ABCD" 和 "AEBD" 的最长公共子序列的长度为3。
  2. 我们比较序列A和B的最后一个字符,最后一个字符都是"D",匹配。因此,"D" 是LCS的一部分。我们将 "D" 添加到LCS末尾,并在dp数组中将关注点向左上移动到 dp[3][3]。
  3. 现在我们在 dp[3][3],其值为2。比较A的第三个字符“C”和B的第三个字符 "B" ,不匹配。这时候我们需要查看 dp[2][3] 和 dp[3][2] 的值来决定向左移动还是向上移动。2 = dp[2][3] > dp[3][2] = 1,所以得知是从 dp[2][3] 转移过来的,我们需要将关注点向上移动到 dp[2][3]。
  4. 现在我们在 dp[2][3],其值为2。比较A的第二个字符“B”和B的第三个字符 "B",匹配。 因此,"B" 是LCS的一部分。我们将 "B" 添加到LCS的“D”前面,形成“BD”,并在dp数组中将关注点向左上移动到 dp[1][2]。
  5. 现在我们在 dp[1][2],其值为1。比较A的第一个字符“A”和B的第二个字符 "E",不匹配。这时候我们需要查看 dp[0][2] 和 dp[1][1] 的值来决定向左移动还是向上移动。1 = dp[1][1] > dp[0][2] = 0,所以得知是从 dp[1][1] 转移过来的,我们需要将关注点向左移动到 dp[1][1]。
  6. 现在我们在 dp[1][1],其值为1。比较A的第一个字符“A”和B的第一个字符 "A",匹配。因此,"A" 是LCS的一部分。我们将 "A" 添加到LCS的“B”前面,形成“ABD”,并在dp数组中将关注点向左上移动到 dp[0][0]。构建结束,子序列为“ABD”。

动态规划法最长公共子序列问题,算法,蓝桥杯,动态规划,算法,蓝桥杯

代码实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_SIZE 10
// 二维数组dp
int dp[MAX_SIZE][MAX_SIZE];
// 函数原型声明
int max(int a, int b);
char* getLCS(char *A, char *B, int m, int n);
int main() {
    char A[] = "ABCD"; // 序列A
    char B[] = "AEBD"; // 序列B
    int m = strlen(A);
    int n = strlen(B);
	int i,j;
    // 初始化、填充dp数组
    for (i = 0; i <= m; i++) {
        for (j = 0; j <= n; j++) {
            if (i == 0 || j == 0) {
                dp[i][j] = 0;
            } else if (A[i-1] == B[j-1]) {
                dp[i][j] = dp[i-1][j-1] + 1;
            } else {
                dp[i][j] = max(dp[i][j-1], dp[i-1][j]);
            }
        }
    }
    // 打印LCS长度
    printf("最长公共子序列的长度为: %d\n", dp[m][n]);
    // 打印LCS
    char *lcs = getLCS(A, B, m, n);
    printf("%s 和 %s 的最长公共子序列为 %s\n", A, B, lcs);
    free(lcs); // 释放动态分配的内存
    return 0;
}
// 返回两个整数中的最大值
int max(int a, int b) {
    return (a > b) ? a : b;
}
// 返回LCS
char* getLCS(char *A, char *B, int m, int n) {
    int index = dp[m][n];
    // 动态分配足够的空间来存储LCS
    char* lcs = (char*)malloc((index+1) * sizeof(char));
    lcs[index] = '\0'; // 设置字符串结束符
    int i = m, j = n;
    while (i > 0 && j > 0) {
        if (A[i-1] == B[j-1]) {
            lcs[index-1] = A[i-1]; // 将匹配的字符加入到lcs中
            i--;
            j--;
            index--;
        } else if (dp[i-1][j] > dp[i][j-1]) {
            i--;
        } else {
            j--;
        }
    }
    // 返回LCS
    return lcs; 
}
// 运行结果:
// 最长公共子序列的长度为: 3
// ABCD 和 AEBD 的最长公共子序列为 ABD

写在最后

在本文中,我们深入探讨了最长公共子序列(LCS)问题,这是动态规划算法应用的一个经典例子。通过详细的步骤,我们不仅学习了如何定义状态、建立状态转移方程和确定边界条件,还通过图表解析和代码示例具体了解了动态规划如何逐步解决这一问题。在下一篇文章中,我们将继续探讨更多的动态规划经典问题,力求稳扎稳打学好动态规划。

敬请期待!文章来源地址https://www.toymoban.com/news/detail-852308.html

到了这里,关于动态规划应用篇:详解最长公共子序列问题的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 算法:动态规划——最长公共子序列

    动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。 与分治法不同的是,适合于用动态规划法求解的问题,经分解得到的子问题往往不是互相独立的。若用分治法解这类问题,则分解得到的

    2023年04月27日
    浏览(46)
  • 【算法-动态规划】最长公共子序列

    💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kuan 的首页,持续学习,不断总结,共同进步,活到老学到老 导航 檀越剑指大厂系列:全面总

    2024年01月23日
    浏览(38)
  • 【动态规划】最长公共子序列(Java)

    给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。 一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

    2024年01月18日
    浏览(57)
  • 动态规划之最长公共子序列模板

    夏令营:动态规划特训 - 【算法模板题】最长公共子序列 - 蓝桥云课 (lanqiao.cn) 我们来解释一下状态转移方程吧。 dp[i][j]的含义是第i个和第j个字符,i与j的下标从1开始,代表着原子符串的第一个字符。那么理所当然字符串a的第0个字符和字符串b的0个字符的公共长度为0.如果字

    2024年02月12日
    浏览(31)
  • 【动态规划】最长公共子序列Python实现

    个人主页:丷从心 系列专栏:动态规划算法 问题描述 给定两个序列 X = {   x 1 , x 2 , ⋯   , x m   } X = set{x_{1} , x_{2} , cdots , x_{m}} X = { x 1 ​ , x 2 ​ , ⋯ , x m ​ } 和 Y = {   y 1 , y 2 , ⋯   , y n   } Y = set{y_{1} , y_{2} , cdots , y_{n}} Y = { y 1 ​ , y 2 ​ , ⋯ , y n ​ } ,找出 X X X

    2024年02月20日
    浏览(33)
  • 动态规划-最长公共子序列(c语言)

    实验 3: 最长公共子序列 内容: 给定两个字符串str1和str2,输出两个字符串的最长公共子序列,如果最长公共子序列为空,则返回“-1”。目前给出的数据,仅仅会存在一个最长的公共子序列。 数据范围: 0 ≤|str1|,|str2|≤2000 要求: 空间复杂度O(n 2 ) 具体思路: step1:

    2024年02月04日
    浏览(32)
  • 【动态规划】最长公共子序列——算法设计与分析

    子序列是给定序列中在任意位置去掉任意多个字符后得到的结果。例如: 给定序列 X X X : X : A B C B D A B X:ABCBDAB X : A BCB D A B X X X 的子序列: X 1 : A B C B D A B X_1:ABCBDAB X 1 ​ : A BCB D A B X 2 : A B C B X_2:ABCB X 2 ​ : A BCB X 3 : A C B B X_3:ACBB X 3 ​ : A CBB 给定两个序列

    2024年02月05日
    浏览(36)
  • (Java) 算法——动态规划 最长公共子序列 图解

    遇到了用动态规划来求解最长公共子序列问题,算法这块儿比较薄弱,便想着在网上找现成的思路和代码,也算拾人牙慧,但有一点没想到,都已经22年了,关于LCS问题网上给出的答案如此一言难尽……,只有零散几篇对于 新手 来说比较友好,但也仅仅这样,好在自己花了点

    2023年04月08日
    浏览(33)
  • 两个数组的动态规划——最长公共子序列模型

    1.考虑空串,即dp表多出一行一列, 代表某个字符串为空。 2.考虑最后一个位置;是否相等; 3.可在字符串最前面加虚拟位置以对应映射关系; 4.一般横行是j,列是i。此时第一行代表第二个字符串不为空,即第一个字符串是空的 给你两个字符串  s   和  t  ,统计并返回在

    2024年03月10日
    浏览(55)
  • 【算法】力扣【动态规划,LCS】1143. 最长公共子序列

    1143. 最长公共子序列 本文是对 LCS 这一 动态规划 模型的整理,以力扣平台上的算法题1143:最长公共子序列为模板题进行解析。 该题目要求计算两个字符串的最长公共子序列(Longest Common Subsequence,简称LCS)的长度。字符串的子序列是指在不改变字符顺序的情况下,通过删去

    2024年01月17日
    浏览(43)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包