zoukankan      html  css  js  c++  java
  • 最长公共子序列(计数问题)

    题意:
    字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后所形成的字符序列。令给定的字符序列X=“x0,x1,…,xm-1”,序列Y=“y0,y1,…,yk-1”是X的子序列,存在X的一个严格递增下标序列<i0,i1,…,ik-1>,使得对所有的j=0,1,…,k-1,有xij = yj。
    例如X=“ABCBDAB”,Y=“BCDB”是X的一个子序列。对给定的两个字符序列,求出他们最长的公共子序列长度,以及最长公共子序列个数。

    一.求最长公共子序列的长度(基础LCS问题)
    首先从动态规划的角度来考虑:
    所有问题被分解为子问题,子问题套子问题。
    1.确定研究状态:最长公共子序列的长度
    那么每一层的问题就转化求该层的***最长公共子序列的长度***。
    2.确定研究问题的划分层次(如何划分子问题)。(循环变量所代表的意义及状态边界问题):当s1和s2的长度均为1时,结果是很容易判断的,所有很自然的想到用长度来对问题进行划分。长度小的子问题被包含在长度大的子问题中作为数据被调用。——>用两层循环(i,j)来进行。
    3.确定状态转移方程:(重中之重,因为状态转移方程是否正确决定难题的攻克与否)同时,也是解决问题的核心。那么对于长度为i的s1和长度为j的s2。该状态下的子问题结果:dp[i][j]=?。
    可以知道的是:分情况讨论:
    (1).当S1[i]==S2[j]时,dp[i][j]可有三个来源:
    当第i个和第j个参与最长公共子序列①:dp[i-1][j-1]+1;
    当第i个和第j个不参与最长公共子序列②:dp[i-1][j]和dp[i][j-1]。
    因为:dp[i-1][j-1]<=dp[i][j-1]<=dp[i-1][j-1]+1 所以,最终的答案是:
    dp[i][j]=dp[i-1][j-1]+1。
    (2).当S1[i]!=S2[j]时,S1[i]与S2[j]不可能同时入选最长公共子序列,所以dp[i][j]只能从dp[i-1][j]和dp[i][j-1]中推得结果。根据 最长 的要求,和dp所储存的内容( s1的前i个字符组成的串和 s2的前j个字符组成的串所能形成的最长公共子序列的长度)得到:dp[i][j]=max(dp[i-1][j],dp[i][j-1]).
    4.构造关键代码:

    for (i=1;i<=s1.length();i++) 
    for (j=1;j<=s2.length();j++) 
    if (s1[i]==s2[j]) dp[i][j]=dp[i-1][j-1]+1; 
    else dp[i][j]=max(dp[i-1][j],dp[i][j-1])

    二.求最长公共子序列的方案数:
    依照动态规划的过程:
    1.选定分析状态——最长公共子序列的方案数
    2.分析研究问题的划分层次——(子问题和最终最初一步)
    显而易见:当长度为1时结果显而易见。
    所以依照长度去研究子问题得到的循环变量以及划分这阶段的依据。
    3.分析状态转移方程。
    当循环变量至s1[i]和s2[j]时,状态转移方程为:
    (首先我们能够得知,g[i][j]来源于g[i-1][j-1],g[i][j-1],g[i-1][j])
    (1) 当s1[i]==s2[j]时,
    (①即当s1[i]与s2[j]做出贡献时,方案数与g[i-1][j-1]一致) g[i][j]+=g[i-1][j-1] 。
    (②即当s1[i]与s2[j]不做出贡献时),需要判定dp[i-1][j],dp[i][j-1]与dp[i][j]是否相等(出现不相等则意味着最长公共子序列的长度改变,那么方案数就不能被记入)。
    而同时因为 dp[i][j]=dp[i-1][j-1]+1所以dp[i][j]不一定会等于dp[i-1][j]或dp[i][j-1]。
    所以需要用if语句判断,如果相等再将g[i-1][j]和g[i][j-1]的方案书加上。
    即关键代码:

    if (s1[i]==s2[j])
            {
                dp[i][j]=dp[i-1][j-1]+1;
                if ((dp[i][j]==1)&&(g[i][j]==0)) ++g[i][j];
                (因为方案开头需要新增方案数,即从0到1的改变)
                g[i][j]+=g[i-1][j-1];
                (即当s1[i]与s2[j]做出贡献时,这种情况下g[i-1][j-1]被记入g[i][j])
                if (dp[i][j]==dp[i][j-1]) g[i][j]+=g[i][j-1];
                (倘若长度相等则意味着s1[i]与s2[j]没有被同时算入子序列时,
                仍然有长度等于dp[i][j]的方案,数量等于g[i][j-1],此时g[i][j-1]也要被记入g[i][j])
                if (dp[i][j]==dp[i-1][j]) g[i][j]+=g[i-1][j];
                (倘若长度相等则意味着s1[i]与s2[j]没有被同时算入子序列时,
                仍然有长度等于dp[i][j]的方案,数量等于g[i-1][j],此时g[i-1][j]也要被记入g[i][j])
                g[i][j]=g[i][j]%p;(数量过大时,需要取模)
                /*
                此处需要解释,g[i][j-1]和g[i-1][j]的方案数指代的方案完全不重合
                (s1[i]或s2[j]必然参与构成最长公共子序列)。
                因为如果dp[i][j]==dp[i-1][j]==dp[i-1][j-1]+1。
                
                假设:存在s1[i]与s2[j]均不参与组成最长公共子序列同时有合法方案
                则有dp[i-1][j]==dp[i-1][j-1]或dp[i][j-1]==dp[i-1][j-1]。
                
                因为在s1[i]与s2[j]均不参与构成时仍有合法方案,
                则所有参与构成最长公共子序列的元素均来自于s1的前i-1个字符和s2的前j-1个字符
                此时又有在s1[i]与s2[j]均不参与构成时仍有合法方案,
                则有dp[i][j]==dp[i-1][j]==dp[i-1][j-1] ==>矛盾
                
                反证:g[i][j-1]和g[i-1][j]的方案数指代的方案完全不重合
                */
            }

    (2) 当s1[i]!=s2[j]时,g[i][j]只有两个来源①g[i-1][j]②g[i][j-1] (因为s1[i]与s2[j] 不能同时参与当前的最长公共子序列的组成)。
    ①s1[i]与s2[j]均不做出贡献时:(dp[i][j]==dp[i-1][j-1])
    dp[i][j]==dp[i-1][j-1];g[i][j]+=g[i-1][j-1]。(因为均不做出贡献,所有参与构成最长公共子序列的元 素均来自于s1的前i-1个字符和s2的前j-1个字符)
    ②s1[i]做出贡献,而s2[j]不做出贡献时:(dp[i][j]==dp[i][j-1])
    dp[i][j]==dp[i][j-1],同时确定g[i][j-1]包含 s1[i]做出贡献与s1[i]不做出贡献两种情况。
    所以g[i][j]+=(g[i-1][j]-g[i-1][j-1])。
    ③s2[j]做出贡献,而s1[i]不做出贡献时:(dp[i][j]==dp[i-1][j])
    dp[i][j]==dp[i][j-1],同时确定g[i-1][j]包含 s2[j]做出贡献与s2[j]不做出贡献两种情况。
    所以g[i][j]+=(g[i][j-1]-g[i-1][j-1])。
    则有关键代码:

    if (s1[i]!=s2[j])
    {
        dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
        if (dp[i][j-1]==dp[i][j]) g[i][j]+=g[i-1][j];
        if (dp[i-1][j]==dp[i][j]) g[i][j]+=g[i][j-1];
        if (dp[i-1][j-1]==dp[i][j]) g[i][j]-=g[i-1][j-1];//其实在s1[i]==s2[j]中也因当有这一步,
        //但是因为dp[i][j]==dp[i-1][j-1]+1;所以这个判断语句可以省略。 
    }

    附:当字符串的长度达到5000+时,内存需求非常大,这样我们需要使用滚动数组。
    我们发现,每一次dp[i]与g[i]的答案都只与第i层和第i-1层的结果有关,所以前i-2的数据都不可能在被使用,运用滚动数组可以优化空间。

    (确定状态(最后一步与子问题)=>递归调用=>保留递归结果优化解决方案,状态转移方程,初始条件和边界情况,计算顺序)

    原文链接:https://blog.csdn.net/qq_35851866/article/details/104102182

  • 相关阅读:
    Applied Nonparametric Statistics-lec2
    Applied Nonparametric Statistics-lec1
    pandas-Notes2
    pandas-Notes1
    软件项目-软件项目开发各阶段文档模板(参考)
    项目管理-一个项目的完整过程
    腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面
    从新手到架构师,一篇就够:从100到1000万高并发的架构演进之路
    新手入门:零基础理解大型分布式架构的演进历史、技术原理、最佳实践
    我的常用网站
  • 原文地址:https://www.cnblogs.com/kayiko/p/14846604.html
Copyright © 2011-2022 走看看