最长公共上升子序列

  最长公共上升子序列

问题:

给定两个字符串x, y, 求它们公共子序列s, 满足si < sj ( 0 <= i < j < |s|).要求S的长度是所有条件序列中长度最长的.

比较直观的做法(O(n^4))

可以仿照最长上升子序列用dp[i][j], 表示以xi, yj结束的公共字串的长度.

so, 我们可以得出递推公式

if xi != yj
    dp[i][j] = 0
else
    dp[i][j] = max(dp[ii][ij]) ( 0 <= ii < i, 0 <= ij < j, dp[ii][ij] != 0 && x[ii] < x[i]) + 1

时间复杂是O(n^4)

O(n^3)的算法

LICS是从LIS和LCS演变而来的.我们来看看LIS和LCS的动态规划解决方法.

在LIS中dp[i]表示以xi结束的最长上升子序列的长度.在LCS中dp[i][j]表示x[0…i]和y[0…j]的最长公共字串的长度.

为什么在LIS中dp[i]表示的不是x[0…i]中的最长子序列的长度?

因为在算LIS中dp[i]的时, 需要知道上一次字符的信息, 这样才能判断是否把x[i]加入.而在计算LCS中dp[i][j]是不需要知道上一字符的信息, 只考虑当前字符就可以.

在LICS中, 和LIS中一样, 我们需要知道上一字符的信息, dp[i][j], xi和yj就是上一字符信息, 如果xi, yi 相等, 则信息重复冗余, 我们可以试着消除冗余, 以得到一个更好的算法.

这样我们可以定义dp[i][j]表示x[0…i]和y[0…j]上的LICS, 并且在y中的结束位置为j.

so, 我们可以得到递归公式

if xi != yj
    dp[i][j] = dp[i-1][j]
else
    dp[i][j] = max(dp[i-1][k])(0 < k < j && y[k] < y[j]) + 1

证明:

设x[0...m]和y[0...n]上的, 以y[n]为结束字符的最长公共上升子序列为z[0...zn].

若x[m] != y[n], 则显然z[0...zn]为x[0...m-1]和y[0...n]上的, 以y[n]为结束的LICS.

若x[m] == y[n], 则z[0...zn-1]必为x[0...m-1]和y[0...k]上的, 以y[k]为结束的最长的LICS( 0 < k < j), 否则会得出矛盾.

    反证:
    设s, s[0...sn]为x[0...m-1]和y[0...k]上的, 以x[k]为结束的一个LICS, 并且sn > zn-1.

    那么,s[0...sn] 可以加上y[n], 得到长度sn+1的一个以y[n]为结束字符的最长公共上升子序列, sn+1 > zn, 与假设矛盾.

所以,上述的递推公式的是对的.

时间复杂度为O(n^3).

O(n^2)对O(n^3)的一个优化.

我们看到, dp[i][j]依赖于dp[k][j-1] (0 < k < i).

在计算的时候可以把i作为外层循环,也可以把i作为内层循环.

如果把i做为外层循环的, 可以做一个优化, 把时间复杂度将为O(n^2).

O(n^3)的算法

memset(dp, 0, sizeof(dp));
for (i = 1; i <= m; i++) {
    for(j = 1; j <= n; j++) {

        dp[i][j] = 0;
        if (x[i] != y[j]) {
            dp[i][j] = dp[i-1][j];
        } else {
            for (k = 1; k < j; ++k) {
                if (dp[i][j] < dp[i - 1][k] && y[k] < y[j]) {
                    dp[i][j] = dp[i - 1][k];
                }
            }
            dp[i][j] += 1;
        }

如果优化, 就只能优化当x[i] = y[j]的时, dp[i][j]的计算.

因为现在O(n^2)个子问题,这是怎么搞也搞不掉的.

看这段代码:

for (k = 1; k < j; ++k) {
    if (dp[i][j] < dp[i - 1][k] && y[k] < y[j]) {
        dp[i][j] = dp[i - 1][k];
    }
}

当y[j] = x[i]时, 就等于

for (k = 1; k < j; ++k) {
    if (dp[i][j] < dp[i - 1][k] && y[k] < x[i]) {
        dp[i][j] = dp[i - 1][k];
    }
}

这是在求dp[i-1][k] (0 < k < j)中的满足y[k]< x[i]最大值

因为i是不变的(外层循环), j在递增, 因此没有必要从头计算.

保存一个mlen变量保存dp[i-1][k] (0 < k < j)中的满足y[k]< x[i]最大值, 当j增加时只用化O(1)的时间更新mlen和计算dp[i][j].

代码如下:

for (i = 1; i <= m; i++) {

    mlen = 0;

    for(j = 1; j <= n; j++) {

        dp[i][j] = dp[i-1][j];

        //更新mlen
        if (y[j] < x[i] && dp[i - 1][j] > mlen) {
                mlen = dp[i - 1][j];
        }

        //计算dp[i][j]
        if (y[j] == x[i]) {
            dp[i][j] = mlen + 1;
        }
    }
}

时间复杂度O(n^2)

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

其实仔细想想,在最后的长度应该是:dp [m] [j] ( 0=<j<=n)中的求最大值,而不是在整个二维数组中找最大值,那样的话就浪费时间了!

练习:

参考资料:


  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值