动态规划之最长公共子序列(LCS)原理及C++与Python实现

一、基本概念

 

二、穷举算法的时间复杂度

 

三、分析子问题

 

四、递推方程和标记函数

 

 

五、算法LCS

1. 伪代码

算法:LCS(X,Y,m,n)

输入:序列X及其长度m,序列Y及其长度n

输出:最长公共子序列的长度

for iß0 to m do

  C[i,0]ß0

for jß0 to n do

  C[0,j]

for iß1 to m do

  for jß1 to n do

if X[i]=Y[j]

then C[i,j]=C[i-1,j-1]+1

    B[i,j]=’↖’

else if C[i-1,j]>=C[i,j-1]

    then C[i,j]ßC[i-1,j]

        B[i,j]ß’’

    else C[i,j]ßC[i,j-1]

        B[i,j]ß’’

 

2. C/C++实现

/********************************************************
Function:LCS
Description:计算两个字符串的最长公共子序列.
Input:字符串s1,s2
Output:最长公共子序列(LCS)的长度
*********************************************************/
int LCS(string s1,string s2){
    int m = s1.size()+1;
    int n = s2.size()+1;

    /*申请二维数组B和C*/
    /*
    C是优化函数值矩阵
    C[i][j]表示s1的前i个元素和s2的前j个元素的最长公共子序列的长度
    */
    int** C = new int* [m];
    for(int i=0;i<m;i++)
        C[i] = new int [n];
    /*
    B是标记函数值矩阵,记录当前优化函数值是从那个方向来的
    B中元素取值解释--0:左,1:上,2:左上
    */
    int** B = new int* [m];
    for(int i=0;i<m;i++)
        B[i] = new int [n];

    /*
    初始化B和C矩阵的第0行与第0列
    C[i][0]表示s1的前i个字符与s2的前0个字符的
    公共子序列的长度,显然其值为0;C[0][j]与C[i][j]相同;
    */
    for(int i=0;i<m;i++){
        C[i][0] = 0;
        B[i][0] = 0;
    }
    for(int j=0;j<n;j++){
        C[0][j] = 0;
        B[0][j] = 0;
    }
    /*
    动态规划过程,逐步填表
    由小到大逐步计算C[i][j]和B[i][j]
    */
    for(int i=1;i<m;i++){
        for(int j=1;j<n;j++){
            if(s1[i-1]==s2[j-1]){
                C[i][j] = C[i-1][j-1]+1;
                B[i][j] = 2;
            }else{
                if(C[i-1][j]>=C[i][j-1]){
                    C[i][j] = C[i-1][j];
                    B[i][j] = 0;
                }else{
                    C[i][j] = C[i][j-1];
                    B[i][j] = 1;
                }
            }
        }
    }
    return C[m-1][n-1];
}

 

3. Python实现 

def LCS(s1,s2):
    """
    计算两个字符串的最长公共子序列.
    
    Parameters
    ----------
    s1:字符串
    
    s2:字符串
    
    Returns
    -------
    B:二维Numpy数组
        标记函数值组成的矩阵
        
    C:二维Numpy数组
        优化函数值组成的矩阵
    """
    m = len(s1)+1
    n = len(s2)+1;
    # 优化函数值矩阵
    # C[i][j]表示s1的前i个元素和s2的前j个元素的最长公共子序列的长度
    C = np.zeros((m,n))
    # 标记函数值矩阵
    # 记录当前优化函数值是从那个方向来的
    # B中元素取值解释--0:左,1:上,2:左上
    B = np.zeros((m,n))
    for i in range(1,m):
        for j in range(1,n):
            if s1[i-1]==s2[j-1]:
                C[i][j] = C[i-1][j-1]+1
                B[i][j] = 2
            else:
                if C[i-1][j]>=C[i][j-1]:
                    C[i][j] = C[i-1][j]
                    B[i][j] = 0
                else:
                    C[i][j] = C[i][j-1]
                    B[i][j] = 1
    return B,C

4. 时间复杂度与空间复杂度 

算法完成时需要将m*n矩阵C和B填充完,而每填充一项是时间是O(1),因此该算法的时间复杂度是O(mn)。该算法需要的额外空间主要是矩阵B和C,其大小均为m*n,因此空间复杂度为O(mn).

 

六、反向构造LCS

1. 伪代码

算法:struct_sequence

输入:标记矩阵B,B最后一个元素的坐标(i,j)

输出:最长公共子序列

if i=0 or j=0 return

if B[i,j]=’↖’

then struct_sequence(B,i-1,j-1)

    输出X[i]

else if B[i,j]=’’

then struct_sequence(B,i-1,j)

else struct_sequence(B,i,j-1)

 

2. C/C++实现

 

/**********************************************************************
Function:struct_sequece
Description:通过给定标记矩阵B,反向求出LCS
Input:标记矩阵B,B右下角坐标(i,j),字符串s1和s2
Output:LCS
***********************************************************************/
void struct_sequence(int ** B,int i,int j,string s1,string s2){
    if(i==0||j==0)
        return;
    switch(B[i][j]){
        case 0://左
            struct_sequence(B,i-1,j,s1,s2);
            break;
        case 1://上
            struct_sequence(B,i,j-1,s1,s2);
            break;
        case 2://左上
            struct_sequence(B,i-1,j-1,s1,s2);
            cout<<s1[i-1]<<" ";
            break;
    }
}

3.Python实现 

def struct_sequence(B,i,j,s1,s2):
    """
    根据标记函数值矩阵输出最长公共子序列
    
    Parameters:
    -----------
    B:二维Numpy数组
        标记函数值组成的矩阵
        
    i,j:B矩阵中当前需要判断值的坐标
    
    s1:字符串
    
    s2:字符串
    """
    if i==0 or j==0: #base case
        return 0
    if B[i,j] == 2:
        struct_sequence(B,i-1,j-1,s1,s2)
        print(s1[i-1],end=' ')
    elif B[i,j] == 1:
        struct_sequence(B,i,j-1,s1,s2)
    elif B[i,j] == 0:
        struct_sequence(B,i-1,j,s1,s2)

 

 

 

4.时间复杂度

该算法会沿着某条路径从B矩阵的右下角走到左边界或上边界,最坏情况是从右下角到左上角,时间复杂度为O(m+n).

 

七、完整代码

1.C/C++代码

 

#include <iostream>
#include <string>
using namespace std;
/**********************************************************************
Function:struct_sequece
Description:通过给定标记矩阵B,反向求出LCS
Input:标记矩阵B,B右下角坐标(i,j),字符串s1和s2
Output:LCS
***********************************************************************/
void struct_sequence(int ** B,int i,int j,string s1,string s2){
    if(i==0||j==0)
        return;
    switch(B[i][j]){
        case 0://左
            struct_sequence(B,i-1,j,s1,s2);
            break;
        case 1://上
            struct_sequence(B,i,j-1,s1,s2);
            break;
        case 2://左上
            struct_sequence(B,i-1,j-1,s1,s2);
            cout<<s1[i-1]<<" ";
            break;
    }
}

/********************************************************
Function:LCS
Description:计算两个字符串的最长公共子序列.
Input:字符串s1,s2
Output:最长公共子序列(LCS)的长度
*********************************************************/
int LCS(string s1,string s2){
    int m = s1.size()+1;
    int n = s2.size()+1;

    /*申请二维数组B和C*/
    /*
    C是优化函数值矩阵
    C[i][j]表示s1的前i个元素和s2的前j个元素的最长公共子序列的长度
    */
    int** C = new int* [m];
    for(int i=0;i<m;i++)
        C[i] = new int [n];
    /*
    B是标记函数值矩阵,记录当前优化函数值是从那个方向来的
    B中元素取值解释--0:左,1:上,2:左上
    */
    int** B = new int* [m];
    for(int i=0;i<m;i++)
        B[i] = new int [n];

    /*
    初始化B和C矩阵的第0行与第0列
    C[i][0]表示s1的前i个字符与s2的前0个字符的
    公共子序列的长度,显然其值为0;C[0][j]与C[i][j]相同;
    */
    for(int i=0;i<m;i++){
        C[i][0] = 0;
        B[i][0] = 0;
    }
    for(int j=0;j<n;j++){
        C[0][j] = 0;
        B[0][j] = 0;
    }
    /*
    动态规划过程,逐步填表
    由小到大逐步计算C[i][j]和B[i][j]
    */
    for(int i=1;i<m;i++){
        for(int j=1;j<n;j++){
            if(s1[i-1]==s2[j-1]){
                C[i][j] = C[i-1][j-1]+1;
                B[i][j] = 2;
            }else{
                if(C[i-1][j]>=C[i][j-1]){
                    C[i][j] = C[i-1][j];
                    B[i][j] = 0;
                }else{
                    C[i][j] = C[i][j-1];
                    B[i][j] = 1;
                }
            }
        }
    }
    cout<<"最长公共子序列为:";
    struct_sequence(B,m-1,n-1,s1,s2);
    return C[m-1][n-1];
}

int main()
{
    /*测试*/
    string s1 = "ABCBDAB";
    string s2 = "BDCABA";
    cout<<endl<<"最长公共子序列长度为:"<<LCS(s1,s2);
    return 0;
}

 

2.Python代码 

import numpy as np
def LCS(s1,s2):
    """
    计算两个字符串的最长公共子序列.
    
    Parameters
    ----------
    s1:字符串
    
    s2:字符串
    
    Returns
    -------
    B:二维Numpy数组
        标记函数值组成的矩阵
        
    C:二维Numpy数组
        优化函数值组成的矩阵
    """
    m = len(s1)+1
    n = len(s2)+1;
    # 优化函数值矩阵
    # C[i][j]表示s1的前i个元素和s2的前j个元素的最长公共子序列的长度
    C = np.zeros((m,n))
    # 标记函数值矩阵
    # 记录当前优化函数值是从那个方向来的
    # B中元素取值解释--0:左,1:上,2:左上
    B = np.zeros((m,n))
    for i in range(1,m):
        for j in range(1,n):
            if s1[i-1]==s2[j-1]:
                C[i][j] = C[i-1][j-1]+1
                B[i][j] = 2
            else:
                if C[i-1][j]>=C[i][j-1]:
                    C[i][j] = C[i-1][j]
                    B[i][j] = 0
                else:
                    C[i][j] = C[i][j-1]
                    B[i][j] = 1
    return B,C

def struct_sequence(B,i,j,s1,s2):
    """
    根据标记函数值矩阵输出最长公共子序列
    
    Parameters:
    -----------
    B:二维Numpy数组
        标记函数值组成的矩阵
        
    i,j:B矩阵中当前需要判断值的坐标
    
    s1:字符串
    
    s2:字符串
    """
    if i==0 or j==0: #base case
        return 0
    if B[i,j] == 2:
        struct_sequence(B,i-1,j-1,s1,s2)
        print(s1[i-1],end=' ')
    elif B[i,j] == 1:
        struct_sequence(B,i,j-1,s1,s2)
    elif B[i,j] == 0:
        struct_sequence(B,i-1,j,s1,s2)
        

'''测试代码'''
s1 = "ABCBDAB"
s2 = "BDCABA"
B,C = LCS(s1,s2)
print("LCS的长度为:%d"%C[-1,-1])
struct_sequence(B,B.shape[0]-1,B.shape[1]-1,s1,s2)

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BQW_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值