LIS(最长上升子序列)的 n ^ 2, nlogn解法以及路径标记

最长上升子序列(Longest Increasing Subsequence,LIS)是一个数列中最长的(非)严格上升的子序列, LIS的求解是DP思想的一个经典体现, 这里通过例题来介绍两种复杂度不同的解法和一个标记路径的问题。

一. n ^ 2 解法(双层循环递推关系式)

例:hdu 1160

链接戳我

题意是一只老鼠有体重跟速度两种属性, 每一行会都会输入一只老鼠的体重和速度,同时每只老鼠都在输入时按照顺序有了一个编号,  求一个最长的老鼠序列要求老鼠的体重严格上升而速度严格下降, 并将其按照原来输入顺序的编号输出。

题解:用结构体存下每个老鼠的属性及编号, 将其按体重升序排列, 体重相同按速度降序排列。排序之后在速度序列中求一个lis并且标记路径输出即可。

(1)求解lis:

DP的核心思想是将问题分成若干的最优子问题以及确定子问题无后效性, 简单来说就是把一个大的问题转化成小问题的堆积来计算到大问题。拿本题来说, 假设目标数列的长度是n, 则转化成的子问题就是通过对前n - 1, n - 2, n - 3..... 2, 1位数形成数列lis的求解来最终推出目标序列的lis。通过一个DP数组来存下当前位置的最长上升子序列, 通过一个判断关系可以很轻松的通过双层循环得到最终lis的长度。

实现代码如下:

​

for(i = 0;i < 10;i++){
    dp[i] = 1;//初始化为1
    for(j = 0;j < i;j++){
        if(s[j] < s[i])
            dp[i] = max(dp[i], dp[j] + 1);
    }
    ans = max(ans, dp[i]);//每一轮外层循环都得到一个长度ans取最大值
}
​

这样的最后就可以得到最长上升子序列的长度ans了, 然后就是对其根本的递推式变形来标记路径。 因为这样写的话每个数都可以清晰的找的它之前的数, 只需要用结构体设置一个变量pre将在执行递推式子的时候将s[i]的pre存成它的前置数字的下标j, 这样就可以通过dp数组的数值找到“一条”最长lis序列。

实现如下:

struct LIS
{
    int s, pre, dp = 1;
}arr[20];

for(i = 0;i < 20;i++)
{
    maxx = 0, pos;//pos用来记录最长递增子序列的末尾数字下表
    for(j = 0;j < i;j++){
        if(arr[j].s < arr[i].s && arr[j].dp + 1 > arr[i].dp){
            arr[i].dp = arr[j] + 1;
            arr[i].pre = j;
            if(maxx < arr[i].dp){
                maxx = arr[i].dp;
                pos = j;//更新pos
            }
        }
    }
}

这样就可以在递推完成后就可以得到很多条“数链”每一条都是上升的, 只需要根据pos来找到最长的那一条的结尾数字然后根据pre找回去就可以得到具体的下标了。

AC代码:

#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
struct mouse
{
    int w, f, pos;
    int pre = 0, dp = 1;
}arr[1005];
bool cmp(mouse a, mouse b)
{
    if(a.w != b.w)return a.w < b.w;
    else return a.f > b.f;
}
int main()
{
    int i = 1, j, n, k, len = 1, q;
    while(~scanf("%d %d", &arr[i].w, &arr[i].f)){
        arr[i].pos = i;
        i += 1;
    }
    n = i - 1;
    sort(arr + 1, arr + i, cmp);
    for(i = 1;i <= n;i++){
        for(j = 1;j < i;j++){
            if(arr[j].w < arr[i].w && arr[j].f > arr[i].f && arr[j].dp + 1 > arr[i].dp){
                arr[i].dp = arr[j].dp + 1;
                arr[i].pre = j;
                if(len < arr[i].dp){
                    q = i;
                    len = arr[i].dp;
                }
            }
        }
    }
    int ans[1005];
    for(i = len - 1;i >= 0;i--){
        ans[i] = arr[q].pos;
        q = arr[q].pre;
    }
    printf("%d\n", len);
    for(i = 0;i < len;i++)
        printf("%d\n", ans[i]);
    return 0;
}

二.n * logn解法(贪心思想 二分优化)

方法一递推关系简单, 代码实现也异常简洁, 唯一的问题是n ^ 2的复杂度在题目给的数据量较大时会超时。

而方法一也不算是最优的解决方法。实际上这个问题还可以用二分来优化。

做法是构造出一个新的有序的DP数列, 用原数列中的数从左到右维护更新新数列。

初始时DP[1] = s[1], 从i = 2时遍历原数列, 将每个遍历的数与DP数列的末尾进行比较, 如果大于末尾, 则把DP数列长度提1将s[i]放在DP数列的最后, 如果小于末尾那么替换掉DP数列中比s[i]大的第一个数。

结束后DP数列的长度就是LIS的长度。

从LIS的性质出发,要想得到一个更长的上升序列,该序列前面的数必须尽量的小

对于序列1,5,8,3,6,7来说,当子序列为1,5,8时,遇到3时,序列已经不能继续变长了。但是,我们可以通过替换,使“整个序列看上去更小,从而有更大的机会去变长。这样,当替换5-3和替换8-6完成后(此时序列为1,3,6),我们可以在序列末尾添加一个7了。

那为什么复杂度可以是O(NlogN)呢?

关键就在“替换”这一步上,若直接遍历序列替换,每次替换都要O(N)的时间。但是只要我们再次利用LIS的性质——序列是有序的(单调的),就可以用二分查找,在O(logN)的时间内完成一次替换,所以算法的复杂度是O(NlogN)的。

代码实现如下:

dp[1] = s[1];
len = 1;
for(i = 1;i < 20;i++){
    if(s[i] > dp[len])
        dp[++len] = s[i];
    else *lower_bound(dp + 1, dp + len + 1, s[i]) = s[i];//二分查找比s[i]大的第一个数并替换
}//len为LIS的长度

然后就是路径标记的问题了, 这个算法的路径标记也不复杂, 我们首先要知道, 在二分所得的DP数组中每一位存的都是LIS中第i位的最小值, 这里我们可以增加一个新数组pos, pos[i]用来记录原序列的第i个数在我们构造DP数组时出现的位置, 因为DP数组中对应位置都会出现正确的LIS序列数, 所以我们记录每个数在DP数组中的位置, 在原数列遍历结束后再从原数列从右向左遍历依次寻找pos数组中的最大值, 就可以得到一个倒着的LIS序列, 反向输出即可得到正确的LIS序列。

代码也很明了:

​

​

​dp[1] = s[1];
len = 1;
pos[1] = len;
for(i = 2;i <= 20;i++){
    if(s[i] > dp[len]){
        dp[++len] = s[i];
        pos[i] = len;//记录s[i]在dp数组中出现的位置
    }
    else{
        int m = lower_bound(dp + 1, dp + len + 1, s[i]) - dp;
        dp[m] = s[i];
        pos[i] = m;
    }
}
maxx = 99999999;
for(i = 20;i > 0;i--){
    if(!len)
        break;
    if(pos[i] == len && maxx > a[i]){
        ans[len] = i;//获得正确路径
        len -= 1;
        maxx = a[i];//确保正确性
    }
}
​

[点击并拖拽以移动]
​

[点击并拖拽以移动]
​

呼~好久之前学的了, 挺简单的东西写了这么多, 一直都没怎么写过博客, 学的东西越来越多, 再不写博客怕是要凉~

花一个小时整理的还专门找了例题, 希望也能对读者有帮助。

猜你喜欢

转载自blog.csdn.net/LxcXingC/article/details/81238008