最长公共上升子序列

  最长公共上升子序列

问题:

给定两个字符串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)中的求最大值,而不是在整个二维数组中找最大值,那样的话就浪费时间了!

练习:

参考资料:


<think>嗯,用户问的是C++的最长公共上升子序列。首先,我得确认他们需要的是这个问题的解决方法,还是想了解概念。最长公共上升子序列(LCIS)结合了最长公共子序列(LCS)和最长上升子序列(LIS)的特点,所以可能需要先解释一下这个概念。 然后,我得回忆一下LCIS的动态规划解法。通常,这类问题会用一个二维数组dp[i][j]来表示处理到第一个数组的第i个元素和第二个数组的第j个元素时的状态。但可能需要优化空间复杂度,或者有没有更高效的方法? 比如,可能有一个优化的方法,遍历第二个数组的同时维护一些状态,比如当前的最大长度,或者某种条件下的最大值。这样可以将时间复杂度降到O(n^2)而不是更高的复杂度。比如,原版的LCS是O(n^2),而LIS如果使用优化方法可以到O(n log n),但这里两者结合可能需要不同的处理。 举个例子,假设有两个数组a和b。我们需要找到既是两者的公共子序列,又是严格递增的。这个时候,动态规划的状态设计可能需要考虑到当前元素的值,以确保递增的条件。 可能的状态转移方程是怎样的?比如,当a[i] == b[j]的时候,我们需要找到一个k,使得b[j] > 当前某个值,然后更新dp[j]的值。或者,可以维护一个数组,其中dp[j]表示以b[j]结尾的LCIS的长度。这样的话,每次处理a中的元素时,遍历b数组,找到符合条件的j的位置,并更新dp值。 比如,当处理到a的第i个元素时,对于每个b中的元素j,如果a[i] == b[j],那么我们需要找到在b中比b[j]小的所有位置k,然后dp[j] = max(dp[j], dp[k]+1)。或者,可以维护一个变量来记录当前的最大值,从而避免重复遍历。 或者,另一种方法是,对于每一个元素在数组a中,遍历数组b中的元素,同时记录当前的最大长度。比如,对于数组b中的每个元素,如果它等于当前a中的元素,并且之前的元素满足递增条件,则更新对应的dp值。 这个时候可能需要一个双重循环,外层遍历数组a的元素,内层遍历数组b的元素,并维护一个变量来记录在当前位置之前的最大可行长度。这样时间复杂度是O(nm),其中n和m是两个数组的长度。 比如,初始化一个数组dp,长度为m,初始为0。然后对于每个a中的元素a[i],维护一个变量current_max,初始为0。然后遍历b数组中的每个元素b[j],如果a[i] > b[j],则current_max = max(current_max, dp[j]),因为这样当后面遇到a[i]等于某个b[j]时,可以继承这个current_max加1。如果a[i]等于b[j],则dp[j] = current_max + 1,这样保证递增的条件。 这样处理的话,当a[i]等于b[j]时,dp[j]的值取决于之前所有比b[j]小的元素的dp值中的最大值加1,这样确保了上升的特性。同时,因为a和b的公共元素必须按顺序出现,所以这样处理可以保证公共子序列的顺序。 现在需要将这个过程用C++代码实现的话,可能需要两个数组,或者优化空间使用一个一维数组。例如,用一个一维数组dp,其中dp[j]表示以b[j]结尾的最长公共上升子序列的长度。 具体的代码结构可能是这样的: 初始化dp数组为0。 遍历数组a中的每个元素x: 初始化current_max为0。 遍历数组b中的每个元素y及其索引j: 如果y < x,则current_max = max(current_max, dp[j]) 否则如果y == x,则dp[j] = max(dp[j], current_max + 1) 最后,dp数组中的最大值就是LCIS的长度。 这样处理的话,时间复杂度是O(nm),空间复杂度是O(m),如果数组b的长度是m的话。 然后需要考虑边界情况,比如两个数组中有重复元素,或者其中一个数组为空的情况。另外,是否需要严格递增还是非严格递增?题目中的“上升”通常指严格递增,所以在判断条件时要注意。 例如,如果a数组是[1,3,2,4],b数组是[1,2,3,4],那么LCIS应该是[1,2,4]或者[1,3,4],长度3。 测试这样的例子是否能被正确处理。 现在,在代码实现时,需要注意循环的顺序。外层是a的每个元素,内层是遍历b数组。每次处理a中的一个元素时,先遍历b数组,记录比当前a元素小的位置的最大dp值,然后在遇到相等的元素时更新对应的dp[j]。 这样,每次处理a的元素时,可以正确累积前面的信息。例如,当a[i]等于b[j]时,此时current_max保存的是所有在b中位置j之前且元素值小于a[i]的位置的最大dp值,因此将dp[j]更新为current_max+1是正确的。 最后,遍历dp数组找到最大值即可。 这样的算法应该能正确处理问题。那现在需要给出一个C++的示例代码。例如: #include <iostream> #include <vector> #include <algorithm> using namespace std; int LCIS(vector<int>& a, vector<int>& b) { int m = b.size(); vector<int> dp(m, 0); for (int x : a) { int current_max = 0; for (int j = 0; j < m; ++j) { if (b[j] < x) { current_max = max(current_max, dp[j]); } else if (b[j] == x) { dp[j] = max(dp[j], current_max + 1); } } } return *max_element(dp.begin(), dp.end()); } int main() { vector<int> a = {1, 3, 2, 4}; vector<int> b = {1, 2, 3, 4}; cout << LCIS(a, b) << endl; // 输出3 return 0; } 这样应该可以得到正确的结果。当然,需要处理其他情况,比如数组为空的情况。但在这个示例中,假设输入的数组都是非空的。 总结来说,回答用户的问题时,应该先解释LCIS的定义,然后介绍动态规划的解法,说明状态转移的思路,再给出代码示例,并解释其原理。同时需要注意时间复杂度和空间复杂度,以及可能的优化点。最长</think>公共上升子序列(Longest Common Increasing Subsequence,LCIS)是结合了最长公共子序列(LCS)和最长上升子序列(LIS)的问题,要求找到两个序列中既是公共子序列又是严格递增的最长子序列。以下是详细解释和C++实现: --- ### **问题描述** 给定两个整数序列 `a` 和 `b`,找到它们的最长公共上升子序列的长度。 --- ### **动态规划解法思路** 1. **状态定义**: 使用一维数组 `dp[j]`,表示以 `b[j]` 结尾的最长公共上升子序列的长度。 2. **状态转移**: - 遍历 `a` 的每个元素 `x`,同时在遍历 `b` 时维护一个变量 `current_max`。 - 当 `b[j] < x` 时,更新 `current_max` 为当前已知的最大可能长度。 - 当 `b[j] == x` 时,将 `dp[j]` 更新为 `current_max + 1`,确保子序列严格递增。 3. **时间复杂度**:O(n*m),其中 `n` 和 `m` 是两序列的长度。 --- ### **C++代码实现** ```cpp #include <iostream> #include <vector> #include <algorithm> using namespace std; int LCIS(const vector<int>& a, const vector<int>& b) { int m = b.size(); vector<int> dp(m, 0); // dp[j] 表示以 b[j] 结尾的LCIS长度 for (int x : a) { int current_max = 0; // 维护当前x之前的最大可行长度 for (int j = 0; j < m; ++j) { if (b[j] < x) { current_max = max(current_max, dp[j]); // 更新current_max } else if (b[j] == x) { dp[j] = max(dp[j], current_max + 1); // 更新以b[j]结尾的LCIS } } } return *max_element(dp.begin(), dp.end()); // 返回dp数组中的最大值 } int main() { vector<int> a = {1, 3, 2, 4}; vector<int> b = {1, 2, 3, 4}; cout << LCIS(a, b) << endl; // 输出3(序列为1,2,4 或 1,3,4) return 0; } ``` --- ### **关键点解释** - **双循环遍历**:外层遍历 `a` 的元素,内层遍历 `b`,确保公共子序列的顺序。 - **current_max 的作用**:记录当前 `a` 元素之前所有小于 `b[j]` 的元素能构成的最大长度,保证严格递增。 - **空间优化**:使用一维数组而非二维数组,空间复杂度降至 O(m)。 --- ### **示例分析** 对于输入 `a = [1,3,2,4]` 和 `b = [1,2,3,4]`: - 公共上升子序列包括 `[1,2,4]` 和 `[1,3,4]`,长度均为3。 - 代码通过动态规划正确计算得到结果。 该算法高效且直观,适用于中等规模的输入。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值