最长递增子序列(LIS)——算法笔记

LIS(Longest Increasing Subsequence)最长上升子序列 :

  一个数的序列bi,当b1 < b2 < … < bS的时候,我们称这个序列是上升的。对于给定的一个序列(a1, a2, …, aN),我们可以得到一些上升的子序列(ai1, ai2, …, aiK),这里1 <= i1 < i2 < … < iK <= N。
  比如,对于序列(1, 7, 3, 5, 9, 4, 8),有它的一些上升子序列,如(1, 7), (3, 4, 8)等等。这些子序列中最长的长度是4,比如子序列(1, 3, 5, 8).
  而这里要求的就是 最长上升子序列的长度
下面介绍几种求解方法:

动态规划:时间复杂度 O(n^2)
int length_LIS(int *a, int n)
{
     
    for (int i = 1; i <= n; i++)    //初始化 dp[] 数组,初值为 1
        dp[i] = 1;

    int maxn = 1;  // 记录当前最长递增自序列的长度
    for (int i = 2; i <= n; i++)
    {
     
        for (int j = 1; j < i; j++) //遍历 i 前面的数
        {
     
            if (a[j] < a[i])
                dp[i] = max(dp[i], dp[j] + 1);  //在当前最长值 与 之前最长值加 1 之间选取最大值
        }
        maxn = max(dp[i], maxn);    // 刷新最长值
    }
    return maxn;
}

算法思路:
dp[i] 表示第 i 个位置处最长的递增子序列长度。所以,dp[i]的初始值都为 1 。对于每一个位置的数,都要去遍历一遍它之前的数,用来更新该位置的 dp[i] 的值。状态转移方程:dp[i] = max(dp[i], dp[j] + 1);
举个例子:设数组为 : 1    7   3   5   9   4   8.   来求它的最长递增子序列的长度。
  第一个数,dp[1] = 1,子序列为 1.
  第二个数,dp[2] = dp[1] + 1 = 2,子序列为 1,7.
  第三个数,dp[3] = dp[1] + 1 = 2,子序列为1, 3.
  第四个数,二层循环遍历到 1 时,dp[4] = dp[1] + 1 = 2;当遍历到 3 时,dp[4] = dp[3] + 1 = 3,;取最大值 dp[4] = 3,子序列为 1, 3, 5.
  第五个数,二层循环遍历到 1 时,dp[5] = dp[1] + 1 = 2;遍历到 7 时,dp[5] = dp[2] + 1 = 3;遍历到 3 时,dp[5] = dp[3] +1 = 3;遍历到 5 时,dp[5] = dp[4] + 1 = 4;最后取最大值 dp[5] = 4,子序列为 1, 3, 5, 9.
  第六个数,类似于前面的遍历,最后得到 dp[6] = dp [3] + 1 = 3,子序列为 1, 3, 4;
  第七个数,同理,dp[7] = dp[6] + 1 = 4,子序列为1, 3, 4, 8.
最后,在dp数组中寻找最大值就是解

由上面的理解可以衍生一种求最大增长子序列和的方法:

(稍微改变)最大增长子序列和:
//最大增长子序列和
int sum_LIS(int *a, int n)
{
     
    for (int i = 1; i <= n; i++)    // 初始化dp[]数组为 a[]中值
        dp[i] = a[i];
    a[0] = 0;
    int sum_maxn = -9999;       // 记录当前最大增长序列和
    for (int i = 1; i <= n; i++)
    {
     
        for (int j = 0; j < i; j++)
        {
     
            if (a[j] < a[i])
                dp[i] = max(dp[i], dp[j] + a[i]);   //这里变成了加上数组值
        }
        sum_maxn = max(dp[i], sum_maxn);
    }
    return sum_maxn;
}

简单的就是把 dp 数组初始值改成 a 数组中的对应值,状态转移时加的是a[i](对应总和),而不是 1(对应长度)。

贪心 + 二分:时间复杂度 O(n*log n)
//二分查找,寻找比 key 大的第一个元素的位置
int binary_search(int *a, int left, int right, int key)
{
     
    while (left <= right)
    {
     
        int mid = (left + right) >> 1;
        if (a[mid] <= key)
            left = mid + 1;
        else
            right = mid - 1;
    }
    return left;
}

//贪心 + 二分求解最长递增子序列
int greedy_length_LIS(int *a, int n)
{
     
    for (int i = 1; i <= n; i++)
        low[i] = INF;
    low[1] = a[1];
    int ans = 1;
    for (int i = 2; i <= n; i++)
    {
     
        if (a[i] > low[ans])        //大于low末尾值,则向后接
            low[++ans] = a[i];
        else {
           //否则,找到low中第一个 >=a[i] 的位置low[j],用a[i]更新low[j]
            int index = binary_search(low, 1, n, a[i]);
            low[index] = a[i];
        }
    }
    return ans;
}

  算法思路:定义一个low数组,用来表示长度为i的LIS结尾元素的最小值。对于一个上升子序列,显然其结尾元素越小,越有利于在后面接其他的元素,也就越可能变得更长。因此,只需要维护 low 数组,对于每一个a[ i ],如果a[ i ] > low [当前最长的LIS长度],就把 a [ i ]接到当前最长的LIS后面,即low [++当前最长的LIS长度] = a [ i ]。最后,low数组的长度就是最长递增子序列的长度。
  具体操作:对于每一个a [ i ],如果a [ i ]能接到 LIS 后面,就接上去;否则,就用 a [ i ] 取更新 low 数组。在low数组中找到第一个大于等于a [ i ]的元素low [ j ],用a [ i ]去更新 low [ j ]。
  对于low 数组,内部一定是单调不降的,所有可以二分 low 数组,找出第一个大于等于a[ i ]的元素。二分一次 low 数组的时间复杂度的O(lgn),所以总的时间复杂度是O(nlogn)。

参考来自:
https://blog.csdn.net/wbin233/article/details/77570070
https://blog.csdn.net/lxt_Lucia/article/details/81206439
(未完待续)

你可能感兴趣的:(算法笔记,LIS)