最长递增子序列问题——动态规划

最长递增子序列问题

LIS问题描述:给出一个数列A,求A的一个长度最大的子数列B,使得B是一个递增数列。

例如:数列A:5,2,8,6,3,6,9,7
一个递增的子数列为5,8,9;
一个长度最大的递增子数列为2,3,6,9或者2,3,6,7,则其最大长度为4.


解法一:动态规划法(时间复杂度O(n^2))

设长度为n的数组为[a0, a1, …, an-1],假定以aj结尾的数组序列的最长递增子序列长度为L[j],则可以得出公式L[j]=1+{max(L[i]), i < j且a[i] < a[j]}.
也就是说,我们需要遍历在j之前的所有位置i(从0到j-1),找到满足条件a[i] < a[j]的L[i],求出max(L[i])+1即为L[j]的值。
最后,我们遍历数组L[j] (j从0到n-1),找出其最大值即为最大递增子序列的长度。
由此可见,需要两层循环,时间复杂度为O(n^2)

最长递增子序列问题——动态规划_第1张图片
我们根据上面所述算法描述求数列A:5,2,8,6,3,6,9,7的最大递增子序列的过程。
5=>L[0]=1
2=>L[1]=1
8=>L[2]=2
6=>L[3]=2
3=>L[4]=2
6=>L[5]=3
9=>L[6]=4
7=>L[7]=4


代码如下:

#include 
using namespace std;

int lis(int arr[], int len) {
    int longest[len];
    for (int i = 0; i < len; i++) {
        longest[i] = 1;
    }

    for (int j = 1; j < len; j++) {
        for (int i = 0; i < j; i++) {
            if (arr[j] > arr[i] && longest[j] < longest[i] + 1) {// 注意longest[j] 小于 longest[i]+1 不能省略
                longest[j] = longest[i] + 1;// 计算以arr[j]结尾的序列的最长递增子序列的长度
            }
        }
    }

    int max = 0;
    for (int j = 0; j < len; j++) {
        cout << "longest[" << j << "]=" << longest[j] << endl;
        if (longest[j] > max) max = longest[j];// 从longest[j]中找出最大值,即为最长长度
    }
    return max;
}
int main() {
    int arr[] = {5, 2, 8, 6, 3, 6, 9, 7};// 测试数组
    cout << "The Length of Longest Increasing Subsequence is " << lis(arr, sizeof(arr) / sizeof(arr[0])) << endl;
    return 0;
}

程序运行结果如下:
最长递增子序列问题——动态规划_第2张图片


解法二:时间复杂度O(nlogn)

算法求解过程:

同样假设存在待求解的数列A,我们定义一个数组B,和一个变量resLen表示当前最长长度,初始化为1。
然后令i=0到7考察数列A[i]和B[resLen-1]的大小:若A[i]>B[resLen-1],则更新B[resLen]的值为A[i],resLen加1;否则,采用二分法将A[i]的值插入B数组中。

例如针对上述数列A:5,2,8,6,3,6,9,7,根据算法过程可得:

  1. B[0]=A[0]=5,即长度为1的LIS的最小末尾为5
  2. 因为A[1]=2小于B[0]=5,所以B[0]的值更新为2,即长度为1的LIS的最小末尾为2
  3. 因为A[2]=8大于B[0]=5,因此B[1]的值更新为8,即长度为2的LIS的最小末尾为8
  4. 因为A[3]=6小于B[1]=8,因此B[1]的值更新为6,即长度为2的LIS的最小末尾为6
  5. 因为A[4]=3小于B[1]=6,因此B[1]的值更新为3,即长度为2的LIS的最小末尾为3
  6. 因为A[5]=6大于B[1]=3,因此B[2]的值更新为6,即长度为3的LIS的最小末尾为6
  7. 因为A[6]=9大于B[2]=6,因此B[3]的值更新为9,即长度为4的LIS的最小末尾为9
  8. 因为A[7]=7小于B[3]=9,因此B[3]的值更新为7,即长度为4的最小末尾为7

其实,从这个过程中,我们可以得知,数组B维护的是当前最长递增子序列的最小末尾值,从而保证后面不断检查的数依然能够正确地插入到数组B中,并得到正确的LIS的长度。

而且,在数组B中插入数据是有序的,是进行替换插入而非挪动,我们可以使用二分查找,将每一个数的插入时间优化到O(logn),因此算法的时间复杂度就降到O(nlogn)了。


代码如下:

#include 
using namespace std;

// 二分查找,返回数组元素需要插入的位置
int BiSearch(int b[], int len, int value) {
    int left = 0, right = len - 1;
    int mid;
    while (left <= right) {
        mid = left + (right - left) / 2;
        if (b[mid] > value) {
            right = mid - 1;
        }
        else if (b[mid] < value) {
            left = mid + 1;
        }
        else {
            return mid;
        }
    }
    return left;
}

int LIS(int arr[], int len) {
    int resLen = 1;// 用于记录B数组中的元素个数,最终结果即为最长长度
    int B[len];// 在动态规划中使用的数组,用于记录中间结果
    B[0] = arr[0];

    for (int i = 1; i < len; i++) {
        if (arr[i] > B[resLen - 1]) {// 如果大于B中最大的元素,则直接插入到B数组末尾
            B[resLen] = arr[i];
            ++resLen;
        }
        else {
            int pos = BiSearch(B, resLen, arr[i]);// 二分查找需要插入的位置
            B[pos] = arr[i];
        }
    }
    // 输出B数组的结果
    for (int i = 0; i < resLen; i++) {
        cout << "B[" << i << "]=" << B[i] << endl;
    }
    return resLen;
}

int main() {
    int array[] = {5, 2, 8, 6, 3, 6, 9, 7};// 测试数组
    cout << "LIS: " << LIS(array, sizeof(array) / sizeof(array[0]));
    return 0;
}

运行结果如下:
这里写图片描述


未完待续…(待我理解LCS:最长公子序列问题)


参考博客:最长递增子序列

你可能感兴趣的:(算法)