动态规划解决最长公共子序列

惯例,先上美图:
在这里插入图片描述

什么是最长公共子序列?
在这里插入图片描述
   1. 官方定义:最长公共子序列也称作最长公共子串(不要求连续,但要求次序),英文缩写为 LCS(Longest Common Subsequence)。其定义是,一个序列 S ,如果分别是两个或多个已知序列的子序列,且是所有符合此条件序列中最长的,则 S 称为已知序列的最长公共子序列。
  
   2. 通俗的讲:就是给定两个字符串,求出这样一个最长的公共子序列的长度:子序列中的每个字符都能在两个原字符串中找到,并且每个字符的先后顺序和原串中的先后顺序一致。
 
例如:字符串S1为{a,s,d,f,g,h,q,e},S2为{b,s,f,h,q,w,p}。他们的最长公共子序列的长度就是4,即:sfhq。


为了统一,我们假定字符串本身并不算是其自身的子序列。比如:
S1 = “ASDF”
S2 = “AS”
它的最长子序列就是1 ,因为我们假定“AS”不算其自身的子序列。

如果它本身也算其自己的子序列,只需要在代码中做稍微的修改即可。


【解题思路】:
  既然是使用动态规划来解决这个问题,我们就需要一个东西来保存当前的最优状态,这里,我们可以设MaxLen(i,j)表示字符串S1从下标为 0 到下标为 i-1 的字符形成的子串,与字符串S2从下标为 0 到下标为 j-1 的字符形成的子串的最长公共子序列的长度。即:MaxLen(i,j)为本题的“状态”。
  假定 len_1 是字符串S1的长度,len_2 是字符串S2的长度,所以,最后求的东西就是MaxLen(len_1,len_2)。我们先分析题目写出递归方程,再根据递归方程总结出动态规划的方法。

  1. 递归出口:
    { M a x L e n ( n , 0 ) = 0 M a x L e n ( 0 , n ) = 0 \begin{cases} MaxLen(n,0)=0\\MaxLen(0,n)=0 \end{cases}
    上面公式中的n并不是特指字符串长度,它是一个小于字符串长度的一个任意数。

即:当两个字符串有任意一个只有一个字符时(下标从0 开始),他们的公共子序列就是0。因为我们假定自己不算自己的子序列。

  1. 递推公式:

M a x L e n ( i j ) = { M a x L e n ( i 1 j 1 ) + 1 , S1[i] == S2[j] m a x { M a x L e n ( i 1 , j ) , M a x L e n ( i , j 1 ) } S1[i] != S2[j] MaxLen(i,j) =\begin{cases}MaxLen(i-1,j-1)+1, & \text{S1[i] == S2[j]} \\max\{MaxLen(i-1,j),MaxLen(i,j-1)\}&\text{S1[i] != S2[j]}\end{cases}

即:
  (1)如果当前S1和S2的第i和第j位字符相等,这时的最优解就是字符串中该字符之前的字符的最优解加上1。
  (2)如果当前S1和S2的第i和第j位字符不相等,这时的最优解就是S1中前i-1个字符和S2中前 j 个字符的最优解,与S1中前 i 个字符和S2中前 j-1 个字符的最优解的较大者。


这里的第一个递推式毫无疑问是正确的,但是第二个可能就有点迷糊了,咱们证明一下。
  
  要证明 M a x L e n ( i j ) = m a x { M a x L e n ( i 1 , j ) , M a x L e n ( i , j 1 ) } MaxLen(i,j) =max\{MaxLen(i-1,j),MaxLen(i,j-1)\} 是正确的,只需证明两个方面。
  一:证明 MaxLen(i,j) 不会比后面 MaxLen(i-1,j) 和 MaxLen(i,j-1) 任意一个小。
  二:证明 MaxLen(i,j) 不会比后面 MaxLen(i-1,j) 和 MaxLen(i,j-1) 都大。

在这里插入图片描述
证明第一个:如果字符S1[i-1]和S2[j-1]不相同, 拿MaxLen(i,j) 和 MaxLen(i-1,j) 来比,两个j相等,i 大于 i-1 ,那么,肯定 MaxLen(i,j) 不会比 MaxLen(i-1,j) 要小。另外一个同理。

证明第二个:如果字符S1[i-1]和S2[j-1]不相同,可以使用反证法。
  (1)我们假设 MaxLen(i,j) 比 MaxLen(i-1,j) 要大,为什么 MaxLen(i,j) 会比 MaxLen(i-1,j) 要大,就因为求 MaxLen(i,j) 时,S1比求 MaxLen(i-1,j) 的S1多一位字符,所以才导致了 MaxLen(i,j) 比 MaxLen(i-1,j) ,所以,这个S1中的最后一个字符一定是最长公共子序列的一位。
  (2)同理,我们假设 MaxLen(i,j) 比 MaxLen(i,j-1) 要大,也可得到结论,S2中的最后一个字符一定时最长公共子序列的一位。
  (3)由此可知,S1的最后一位字符和S2 的最后一位字符都是最长公共子序列的一位,那他们就必定相等。这与我们的题意相违背,即:我们的假设不成立。
  (4)所以,得出结论:证明 MaxLen(i,j) 不会比后面 MaxLen(i-1,j) 和 MaxLen(i,j-1) 都大。

根据递推方程,我们很容易写出来递归函数,但是不可避免有重复的冗余计算问题。如下表:这只是S1和S2只有三个字符的情况下的递归数,如果字符很多,那么它的冗余计算将是很庞大的。
在这里插入图片描述
所以,我们不能使用递归的方法做,那就牺牲空间换取时间,创建一个二维数组MaxLen[M][N],这里M和N的大小是S1和S2的字符串的长度,MaxLen[i][j] 长度为 i+1 的 S1 和长度为 j+1 的S2拥有的最长公共子序列的长度。(这里规定,自己并不算自己的子序列,如果算,代码上给循环条件加一,最后的返回值的下标加一就行了)。

采用先给出已知,然后根据已知推未知的方法进行填表。

填出已知:如下表
在这里插入图片描述

根据已知推未知:
(1)如果当前字符对应相同,就在它的左上方继续寻找,这时最长公共子序列加一,如下:
在这里插入图片描述
(2)如果当前对应的两个字符不相同,就在它该行的前一个元素和该列的上一个元素比较出较大的一个公共子序列。如下:
在这里插入图片描述

根据以上分析,写出代码:

C++实现:

#include<iostream>
#include<cstring>
using namespace std;
const int N = 100;
int max(int a, int b)
{
	int m = a > b ?  a: b;
	return m;
}
int fun_f(char *str1,char *str2)
{
	int len1 = strlen(str1);
	int len2 = strlen(str2);
	int MaxLen[N][N];
	int i, j;
	for (i = 0; i < len1 ; i++)//行首都置0,表示str1中只有一个字符时,和str2的最长公共子序列为0
	{
		MaxLen[i][0] = 0;
	}
	for (j = 0; j < len2; j++)
	{
		MaxLen[0][j] = 0;

	}
	for (i = 1; i < len1; i++)
	{
		for (j = 1; j < len2; j++)
		{
			if (str1[i] == str2[j])//如果当前字符相同,就取前面串的最优解+1
			{
				MaxLen[i][j] = MaxLen[i - 1][j - 1] + 1;
			}
			else
			{
				MaxLen[i][j] = max(MaxLen[i - 1][j], MaxLen[i][j-1]);
			}
		}
	}
	return MaxLen[len1-1][len2-1];

}
int main()
{
	char s1[10], s2[10];
	cin >> s1 >> s2;
	int max = fun_f(s1, s2);
	cout << max << endl;
	return 0;
}

java实现:

package abcd;

import java.util.Scanner;

public class Demo1
{
    public static void main(String[] args) {
        String s1,s2 ;
        Scanner in = new Scanner(System.in);
        s1=in.nextLine();
        s2=in.nextLine();
        int m=fun(s1,s2);
        System.out.println(m);

    }
    public static int fun(String s1,String s2)
    {
        int[][] MaxLen =new int[s1.length()+1][s2.length()+1];
        int i, j;
        for (i = 0; i < s1.length() ; i++)//行首都置0,表示str1中只有一个字符时,和str2的最长公共子序列为0
        {
            MaxLen[i][0] = 0;
        }
        for (j = 0; j < s2.length(); j++)
        {
            MaxLen[0][j] = 0;

        }
        for (i = 1; i < s1.length(); i++)
        {
            for (j = 1; j < s2.length(); j++)
            {
                if (s1.charAt(i) ==s2.charAt(j))//如果当前字符相同,就取前面串的最优解+1
                {
                    MaxLen[i][j] = MaxLen[i - 1][j - 1] + 1;
                }
                else
                {
                    MaxLen[i][j] = max(MaxLen[i - 1][j], MaxLen[i][j-1]);
                }
            }
        }
        return MaxLen[s1.length()-1][s2.length()-1];
    }
    public static int max(int a,int b)
    {
        if(a>b)
        {
            return a;
        }
        else
            {
            return b;
        }
    }
}

以上两个算法,字符串本身并不算是其自身的子序列。比如:
S1 = “ASDF”
S2 = “AS”
它的最长子序列就是1 ,因为我们假定“AS”不算其自身的子序列。

  • 如果它本身也算自己的子序列,只需要在代码中做一点修改即可:如下:
#include<iostream>
#include<cstring>
using namespace std;
const int N = 100;
int max(int a, int b)
{
	int m = a > b ?  a: b;
	return m;
}
int fun_f(char *str1,char *str2)
{
	int len1 = strlen(str1);
	int len2 = strlen(str2);
	int MaxLen[N][N];
	int i, j;
	for (i = 0; i <=len1 ; i++)//行首都置0,表示str1中只有一个字符时,和str2的最长公共子序列为0
	{
		MaxLen[i][0] = 0;
	}
	for (j = 0; j <=len2; j++)
	{
		MaxLen[0][j] = 0;

	}
	for (i = 1; i <= len1; i++)
	{
		for (j = 1; j <= len2; j++)
		{
			if (str1[i] == str2[j])//如果当前字符相同,就取前面串的最优解+1
			{
				MaxLen[i][j] = MaxLen[i - 1][j - 1] + 1;
			}
			else
			{
				MaxLen[i][j] = max(MaxLen[i - 1][j], MaxLen[i][j-1]);
			}
		}
	}
	return MaxLen[len1][len2];

}
int main()
{
	char s1[100], s2[100];
	cin >> s1 >> s2;
	int max = fun_f(s1, s2);
	cout << max << endl;
	return 0;
}
发布了59 篇原创文章 · 获赞 47 · 访问量 5481

猜你喜欢

转载自blog.csdn.net/qq_44755403/article/details/104932806