动态规划初步--最长上升子序列(LIS)

一、问题

有一个长为n的数列 a0,a1,a2...,an-1a。请求出这个序列中最长的上升子序列的长度和对应的子序列。上升子序列指的是对任意的i < j都满足ai < aj的子序列。

二、思路

如果i < j且ai < aj则认为ai到aj存在有向边,由于一个数不可能直接或间接的指向自己,所以是一个有向无环图。但是,在这里我们并不需要真正的建立图。我们可以用动态规划来做,对于状态的设定有多种方式。

三、代码实现

1、将dp[i]表示以ai结束的最长上升子序列,当j < i且a[j] < a[i],dp[i] = max(dp[i],dp[j] + 1),初始化dp[i] = 1。由于定义的是结束位置,所以记录的是逆序,用栈倒过来再输出就行。

代码一:

#include<stdio.h>
#include<iostream>
#include<stack>
#include<algorithm>
using namespace std;

const int INF = 0x3f3f3f3f;
const int maxn = 1000 + 10;
int n;
int a[maxn], dp[maxn];    //dp[i]表示以i结束的最长上升子序列
int nextp[maxn];        //记录路径
int res = 0;
int first = 0;

void print_ans()
{
    stack<int>s;
    for (int i = 0; i < res; i++)
    {
        s.push(first + 1);
        first = nextp[first];
    }
    for (int i = 0; i < res; i++)
    {
        int tmp = s.top(); s.pop();
        printf("%d ", tmp);
    }
    printf("\n%d\n", res);
}

void slove()
{
    memset(dp, 0, sizeof(dp));
    memset(nextp, 0, sizeof(nextp));
    res = 0; first = 0;                //记得清零
    for (int i = 0; i < n; i++)
    {
        dp[i] = 1;
        for (int j = 0; j < i; j++)
        {
            if (a[j] < a[i])
            {
                if (dp[j] + 1 > dp[i])
                {
                    dp[i] = dp[j] + 1;
                    nextp[i] = j;
                }
            }
        }
        if (dp[i] > res)
        {
            res = dp[i];
            first = i;
        }
    }
    print_ans();
}

int main()
{
    while (scanf("%d", &n) == 1 && n)
    {
        for (int i = 0; i < n; i++)
            scanf("%d", &a[i]);
        slove();
    }
    return 0;
}

代码二:两者只是输出的处理不同,用逆向寻路,不需要额外的空间,但要消耗一些时间。

点击查看代码
#include<stdio.h>
#include<iostream>
#include<stack>
#include<vector>
#include<algorithm>
using namespace std;

const int INF = 0x3f3f3f3f;
const int maxn = 1000 + 10;
int n;
int a[maxn], dp[maxn];    //dp[i]表示以i结束的最长上升子序列
stack<int>sta;

void print_ans(int s)
{
    sta.push(s + 1);
    if (dp[s] == 1)
    {
        while (!sta.empty())
        {
            int tmp = sta.top(); sta.pop();
            printf("%d ", tmp);
        }
        return;
    }
    for (int i = 0; i < n; i++)
    {
        if (a[i] < a[s] && dp[s] == dp[i] + 1)
        {
            print_ans(i);
            break;
        }
    }
}
void slove()
{
    memset(dp, 0, sizeof(dp));
    int res = 0;
    int first = 0;
    stack<int>s;
    for (int i = 0; i < n; i++)
    {
        dp[i] = 1;
        for (int j = 0; j < i; j++)
        {
            if (a[j] < a[i])
            {
                dp[i] = max(dp[i],dp[j] + 1);
            }
        }
        if (dp[i] > res)
        {
            res = dp[i];
            first = i;            //first记录最长上升子序列的最后元素的位置
        }
    }

    print_ans(first);
    printf("\n%d\n", res);
}

int main()
{
    while (scanf("%d", &n) == 1 && n)
    {
        for (int i = 0; i < n; i++)
            scanf("%d", &a[i]);
        slove();
    }
    return 0;
}

2、用dp[i]表示以i开始的最长上升子序列,初始化同样是dp[i] = 0。同时由于定义的是开始位置,可以直接输出最长上升子序列。

点击查看代码
void print_ans()
{
    for (int i = 0; i < res; i++)
    {
        printf("%d ", first + 1);
        first = nextp[first];
    }
    printf("\n%d\n", res);
}

void slove()
{
    res = 0;        //定义成全局变量时,注意清零
    for (int i = n - 1;i >= 0; i--)
    {
        dp[i] = 1;
        for (int j = i + 1; j < n; j++)
        {
            if (a[i] < a[j])
            {
                if (dp[j] + 1 > dp[i])
                {
                    dp[i] = dp[j] + 1;
                    nextp[i] = j;
                }
            }
        }
        if (dp[i] > res)
        {
            res = dp[i];
            first = i;
        }
    }
    print_ans();
}

3、把dp[i]表示长度为i + 1的上升子序列中末尾元素的最小值(不存在的话就是INF)。

贪心 + 二分查找,利用贪心思想,对于一个一个上升子序列,显然最后一个元素越小,越有利于添加新的元素,这样序列就越长。

类似的我们也可以定义对称的状态,即把dp[i]表示长度为i + 1的上升子序列中开始元素的最大值

相比前面的状态定义,有两个好处:如果子序列长度相同,可以使最末尾元素的值最小;时间复杂度由O(n^2)降至O(nlogn)。

开始全部初始化为INF,如果i == 0或者dp[i - 1] < aj,dp[i] = min(dp[i],aj),最终dp[i] < INF,最大的i + 1就是结果。如何找到aj呢,由于dp[i]记录的长度为i + 1的最末尾元素的最小值,而最末尾元素是这个长度为i+1中的最大值,所以aj大于dp[i]就能更新。我们还能发现,dp单增,每个aj只需用来更来一次。

完整的dp是,dp[i][j] 表示前i个、长度为j的最长上升子序列的末尾元素的最小值,对于每个i,只会更新某一个j且只需要logn的时间、
n个元素,每次查找logn,总的时间复杂度为O(nlogn).

void slove()
{
    fill(dp, dp + n, INF);
    for (int i = 0; i < n; i++)
    {
        *lower_bound(dp, dp + n, a[i]) = a[i];
        // *upper_bound(dp, dp + n, a[i]) = a[i];  // 非严格递增用upper_bound
    }
    int first = lower_bound(dp, dp + n, INF) - dp;
    printf("%d\n", dp[first - 1]);
    printf("%d\n", first);
}

打印结果路径似乎不太方便,以后再补上吧。

一个例题:leetcode 354. 俄罗斯套娃信封问题
只需要先将第一维升序排序,第二维降序,然后对第二维按照上面的方法处理

因为最终的最长上升子序列肯定是两维都是有序的,将第一维排序不会影响最终答案,并达到降维的效果
而将第二维降序,防止这种情况 {2, 2}, {3,4}, {5, 6}, {5,7} 正确的答案应该是3而不是4

class Solution {
public:
    // https://leetcode-cn.com/problems/russian-doll-envelopes/solution/liang-ge-wei-du-de-zui-chang-di-zeng-zi-ctbmd/813562
    // 排序完就当一维的做了
    int maxEnvelopes(vector<vector<int>>& envelopes) {
        sort(envelopes.begin(), envelopes.end(), [](vector<int>& a, vector<int>& b) {
            if (a[0] == b[0])
                return a[1] > b[1];
            return a[0] < b[0];
        });
        vector<int> dp(envelopes.size(), INT_MAX);
        for (int i = 0; i < envelopes.size(); i++) {
            *lower_bound(dp.begin(), dp.begin() + i, envelopes[i][1]) = envelopes[i][1];
        }
        // return max_element(dp.begin(), dp.end())-dp.begin(); // {1,1}
        return lower_bound(dp.begin(), dp.end(), INT_MAX)-dp.begin();
    }
};  
posted @ 2018-08-08 15:35  Rogn  阅读(346)  评论(0编辑  收藏  举报