最长递增子序列(LIS)和最长公共子序列(LCS)

我怕爱的太早我们不能终老 提交于 2020-02-10 19:04:30

最长递增子序列(LIS)

在计算机科学中,最长递增子序列(longest increasing subsequence)问题是指,在一个给定的数值序列中,找到一个子序列,使得这个子序列元素的数值依次递增,并且这个子序列的长度尽可能地大。最长递增子序列中的元素在原序列中不一定是连续的。许多与数学、算法、随机矩阵理论、表示论相关的研究都会涉及最长递增子序列。解决最长递增子序列问题的算法最低要求O(n log n)的时间复杂度,这里n表示输入序列的规模。——百度百科


LIS的一种做法是 O(n2)O(n^2) 的复杂度,显然效率不高。这里我记录一下 O(nlogn)O(n\log n) 的做法。
f[i]f[i] 表示所有数 ( a[i]a[i]a[n]a[n] ) 组成的LIS中,所有长度为 ii 的LIS的末尾元素中最小值。我们需要从 a[1]a[1] 遍历到 a[n]a[n] ,用每个数组元素更新我们的 ff 数组。用一个变量 lenlen ,来记录我们当前最长LIS的长度。
如果 f[len] < a[i],那么 a[i]a[i] 就可以接在当前最长的(长度为 lenlen)的LIS之后,就得到一个新的更长的LIS,相应的操作就是 f[++len] = a[i]
如果 a[i]a[i] 不能使LIS的长度增加,那么它一定相对比较小,那么,我们是否可以用它来优化 ff 数组呢?他总是替换 ff 数组从左往右第一个大于它的数。换句话说,它总是接在从左往右最后一个小于它的数后面。为什么?因为如果接在大于他的数后面,就不符合LIS的定义了(LIS要递增)。但如果替换一个比它小的数,又违反了 ff 数组的定义(ff 数组要最小)。综上,应该替换 ff 数组从左往右第一个大于它的数。换句话说,它总是接在从左往右最后一个小于它的数后面,这样才能对 ff 数组进行优化,那么这里就用到了二分思想
接着,可以发现,f 数组是单调递增的!为什么?因为首先一开始他是单调递增的 ,更新时,如果发现更长LIS,单调性不变;如果没有,那么设 xx 替换了 f[i]f[i] ,则 f[i1]<xf[i-1] < x(见上面粗体第二句话),于是单调性质不变。
所以,在单调有序的东西上,我们就可以进行:二分!(可以使用STLlower_bound()

//兼用动态规划和二分搜索求最长递增子序列(LIS),时间复杂度O(nlogn)
//求LIS长度
#include <bits/stdc++.h>
using namespace std;
const int N = 100000 + 10;
int n;
int a[N]; //第一个元素为a[0]
int f[N]; //f[i]表示长度为i+1的递增子序列的尾元素最小值

int lis() {
    f[0] = a[0];
    int length = 1; //length 表示前i个元素构成的最长递增子序列的长度

    for (int i = 1; i < n; i++) {
        if (f[length - 1] < a[i]) {
            f[length++] = a[i];
        } else {
            *lower_bound(f, f + length, a[i]) = a[i]; //找f[j](j=0,1,...,length-1)中第一个大于等于a[i]的元素
        }
    }
    return length;
}

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; ++i) {
        scanf("%d", &a[i]);
    }
    printf("%d\n", lis()); //lis()只求最长递增子序列的 长度
    return 0;
}

当然,O(nlogn)O(n\log n) 的做法也可以求出具体的LIS序列。
方法:把 ff 数组中存的具体值改成在原数组中的下标,用一个 prepre 数组记录原数列中的每一项是由原数列中的哪一项转移过来的就行了。说得再清楚一点,就是它接在了哪一个数字的后面。但是如果 pre[i]=f[j]=xpre[i] = f[j] = x,而 f[j]f[j] 被改成了比 xx 小的 yy ,那么不要改 pre[i]pre[i] ,因为 a[i]a[i] 是不能接在 yy 后面的(因为 yy 在原数列中的位置在 a[i]a[i] 之后)。
这种方法求出来的LIS,是LIS中总和最小的(这是因为 ff 数组的定义)。

//求LIS的长度和具体的一个序列
#include <bits/stdc++.h>
using namespace std;
const int N = 10000 + 10;
int n, len = 0;
int a[N];
int f[N];	//用f[i]表示所有数 (a[i]到a[n]) 组成的LIS中,所有长度为i的LIS的末尾元素中最小值
int pre[N];	//pre[i]记录在LIS中a[i]的前一个数的下标
int t[N];

int bound(int x) {
    int L = 1, R = len;
    while (L <= R) {
        int mid = (L + R) >> 1;
        if (a[f[mid]] >= x)
            R = mid - 1;
        else
            L = mid + 1;
    }
    return L;
}

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    for (int i = 1; i <= n; i++) {
        if (a[f[len]] < a[i]) {
            f[++len] = i;
            pre[i] = f[len - 1];
        } else {
            int b = bound(a[i]);
            f[b] = i;
            pre[i] = f[b - 1];
        }
    }
    int x = f[len];
    for (int i = 1; i <= len; i++) {	//找出这个长为len的序列
        t[i] = x;
        x = pre[x];
    }
    cout << len << endl; //len是LIS的长度
    for (int i = len; i >= 1; i--) {
        if (i != len) cout << ' ';
        cout << a[t[i]];
    }
    return 0;
}

以上内容参考博客:https://www.luogu.org/blog/ztyluogucpp/lis-di-onlogn-suan-fa-ge-ren-li-xie

最长公共子序列(LCS)

等填坑

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!