DP 最长上升子序列(LIS)问题

最长上升子序列问题,也就是Longest increasing subsequence,缩写为LIS。是指在一个序列中求长度最长的一个上升子序列的问题,是动态规划中一个相当经典问题。上升子序列指的是对于任意的i<j都是满足ai<aj的子序列。

  定义dp[i]:=以ai为末尾的最长上升子序列的长度

以ai结尾的上升子序列是

  (1)只包含ai的子序列

  (2)在满足j<i并且aj<ai的以aj为结尾的上升子序列末尾,追加上ai后得到的子序列

满足二者之一。这样就能得到如下的递推关系:

  dp[i]=max{1,dp[j]+1 | j<i且aj<ai}

使用这一递推公式可以在O(n2)的时间内解决这个问题。

int n;
int a[maxn];
int dp[maxn];
void solve()
{
    int res=0;
    for(int i=0;i<n;i++)
    {
        res=dp[i];
        for(int j=0;j<i;j++)//每次添加a[i]时,不断对dp[i]更新
            if(a[j]<a[i])
                dp[i]=max(dp[i],dp[j]+1);//每次a[i]都应加入到最大的dp[j]中,保证局部性最优
        res=max(res,dp[i]);
    }
    cout<<res<<endl;
}

下面介绍时间复杂度为O(n*logn)的DP算法。上面的是利用DP求取针对最末位的元素的最长的子序列。如果子序列的长度相同,那么最末位的元素较小的在之后会更有优势,所以我们在反过来用DP针对相同长度情况下最小的末尾的元素进行求解。 (感觉很像贪心算法)

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

最开始全部dp[i]的值都初始化为INF(一个很大的数)。然后从前到后逐个考虑数列的元素,对于每个aj,如果i=0或者dp[i-1]<aj的话,就用dp[i]=min(dp[i],aj)进行更新。最终找出是的dp[i]<INF的最大的i+1就是结果了。这个DP直接实现的话,时间复杂度依然为O(n2)。我们可以对这进一步优化。首先DP数列中除了INF之外是单调递增的,所以可以知道对于每个aj最多只更新一次。对于这次更新的位置,我们不用逐个遍历,用二分搜索,二分查找的时间复杂度是O(logn),所以可在O(nlogn)时间内求出结果。

 假设a = [4, 2, 6, 3, 1, 5], 初始dp=[], 具体算法运行步骤如下:

  1. a[0]=4 => dp=[4];
  2. a[1]=2 => dp=[2];
  3. a[2]=6 => dp=[2, 6];
  4. a[3]=3 => dp=[2, 3];
  5. a[4]=1 => dp=[1, 3];
  6. a[5]=5 => dp=[1, 3, 5];
    所以这个a数组的LIS就是len(dp)=3。 从运行步骤里可以看出,如果一个数很小, 可以作为LIS的头部或者中部, 让后面的数字更容易接到它后面,以此增大LIS长度;而一个数非常大, 则可以很容易接到LIS的尾部, 也一样能增大LIS长度; 所以让它们找准自己的定位还是非常重要的。

    int dp[maxn];
    const int INF=0x3f3f3f3f;
    void solve()
    {
        memset(dp,INF,sizeof(dp));
        for(int i=0;i<n;i++)
            *lower_bound(dp,dp+n,a[i])=a[i];
        cout<<lower_bound(dp,dp+n,INF)-dp<<endl;
    }

 lowee_bound这个STL函数。这个函数从已排好序的数列中利用二分查找找出指向满足ai>=k的ai的最小的指针。类似的函数还有upper_bound,这一函数求出的是指向满足ai>k的ai的最小的指针。

原文地址:https://www.cnblogs.com/orion7/p/7516035.html