1. 程式人生 > >LIS演算法: 最長上升子序列

LIS演算法: 最長上升子序列

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)做法:dp動態規劃

狀態設計:dp[i]代表以a[i]結尾的LIS的長度
狀態轉移:dp[i]=max(dp[i], dp[j]+1) (0<=j< i, a[j]< a[i])
邊界處理:dp[i]=1 (0<=j< n)
時間複雜度:O(N^2)
舉例: 對於序列(1, 7, 3, 5, 9, 4, 8),dp的變化過程如下

dp[i] 初始值 j=0 j=1 j=2 j=3 j=4 j=5
dp[0] 1
dp[1] 1 2
dp[2] 1 2 2
dp[3] 1 2 2 3
dp[4] 1 2 3 3 4
dp[5] 1 2 2 3 3 3
dp[6] 1 2 3 3 4 4 4

求完dp陣列後,取其中的最大值就是LIS的長度。【注意答案不是dp[n-1],這個樣例只是巧合】

#include <bits/stdc++.h>
using namespace std;
const
int MAXX=100000+5; const int INF=INT_MAX; int a[MAXX],dp[MAXX]; // a陣列為資料,dp[i]表示以a[i]結尾的最長遞增子序列長度 int main() { int n; while(cin>>n) { for(int i=0; i<n; i++) { cin>>a[i]; dp[i]=1; // 初始化為1,長度最短為自身 } int ans=1; for(int i=1; i<n; i++) { for(int j=0; j<i; j++) { if(a[i]>a[j]) { dp[i]=max(dp[i],dp[j]+1); // 狀態轉移 } } ans=max(ans,dp[i]); // 比較每一個dp[i],最大值為答案 } cout<<ans<<endl; } return 0; }

O(NlogN)做法:貪心+二分

a[i]表示第i個數據。
dp[i]表示表示長度為i+1的LIS結尾元素的最小值。
利用貪心的思想,對於一個上升子序列,顯然當前最後一個元素越小,越有利於新增新的元素,這樣LIS長度自然更長。
因此,我們只需要維護dp陣列,其表示的就是長度為i+1的LIS結尾元素的最小值,保證每一位都是最小值,這樣子dp陣列的長度就是LIS的長度。

dp陣列具體維護過程同樣舉例講解更為清晰。
同樣對於序列 a(1, 7, 3, 5, 9, 4, 8),dp的變化過程如下:

  • dp[0] = a[0] = 1,長度為1的LIS結尾元素的最小值自然沒得挑,就是第一個數。 (dp = {1})
  • 對於a[1]=7,a[1]>dp[0],因此直接新增到dp尾,dp[1]=a[1]。(dp = {1, 7})
  • 對於a[2]=3,dp[0]< a[2]< dp[1],因此a[2]替換dp[1],令dp[1]=a[2],因為長度為2的LIS,結尾元素自然是3好過於7,因為越小這樣有利於後續新增新元素。 (dp = {1, 3})
  • 對於a[3]=5,a[3]>dp[1],因此直接新增到dp尾,dp[2]=a[3]。 (dp = {1, 3, 5})
  • 對於a[4]=9,a[4]>dp[2],因此同樣直接新增到dp尾,dp[3]=a[9]。 (dp = {1, 3, 5, 9})
  • 對於a[5]=4,dp[1]< a[5]< dp[2],因此a[5]替換值為5的dp[2],因此長度為3的LIS,結尾元素為4會比5好,越小越好嘛。(dp = {1, 3, 4, 9})
  • 對於a[6]=8,dp[2]< a[6]< dp[3],同理a[6]替換值為9的dp[3],道理你懂。 (dp = {1, 3, 5, 8})

ok,這樣子dp陣列就維護完畢,所求LIS長度就是dp陣列長度4。
通過上述求解,可以發現dp陣列是單調遞增的,因此對於每一個a[i],先判斷是否可以直接插入到dp陣列尾部,即比較其與dp陣列的最大值即最後一位;如果不可以,則找出dp中第一個大於等於a[i]的位置,用a[i]替換之。
這個過程可以利用二分查詢,因此查詢時間複雜度為O(logN),所以總的時間複雜度為O(NlogN)

#include <bits/stdc++.h>
using namespace std;
const int MAXX=100000+5;
const int INF=INT_MAX;

int a[MAXX],dp[MAXX]; // a陣列為資料,dp[i]表示長度為i+1的LIS結尾元素的最小值

int main()
{
    int n;
    while(cin>>n)
    {
        for(int i=0; i<n; i++)
        {
            cin>>a[i];
            dp[i]=INF; // 初始化為無限大
        }
        int pos=0;    // 記錄dp當前最後一位的下標
        dp[0]=a[0];   // dp[0]值顯然為a[0]
        for(int i=1; i<n; i++)
        {
            if(a[i]>dp[pos])    // 若a[i]大於dp陣列最大值,則直接新增
                dp[++pos] = a[i];
            else    // 否則找到dp中第一個大於等於a[i]的位置,用a[i]替換之。
                dp[lower_bound(dp,dp+pos+1,a[i])-dp]=a[i];  // 二分查詢
        }
        cout<<pos+1<<endl;
    }
    return 0;
}

一道例題

題目描述:
給出1-n的兩個排列P1和P2,求它們的最長公共子序列。

輸入格式:
第一行是一個數n,
接下來兩行,每行為n個數,為自然數1-n的一個排列。

輸出格式:
一個數,即最長公共子序列的長度

輸入樣例:
5
3 2 1 4 5
1 2 3 4 5
輸出樣例:
3

【資料規模】
對於50%的資料,n≤1000
對於100%的資料,n≤100000

求解:
顯然這是一道LCS最長公共子序列的問題,但是N特別大,對於LCS的O(N^2)做法,時間肯定得爆吧,由於題目中的兩個序列都是1~n的一個排列,噢很巧哦,由於這個條件LCS的問題可以用LIS來解決。
若其中一個排列是1,2,3…n,那麼他們的LCS(最長公共子序列)就是就是另一個序列的LIS(最長上升子序列)。如果兩個序列的排列都不是1,2,3…n,那麼我們可以認為其中一個序列是1,2,3..n,然後把第一個序列的a[1]對映到1,a[2]對映到2,a[n]對映到n,對b序列也按照a序列的對映規則處理,這樣再求b序列的LIS即可。

#include <bits/stdc++.h>
using namespace std;
const int MAXX=100000+5;
const int INF=INT_MAX;

int dp[MAXX];
int a[MAXX],b[MAXX];

int main()
{
    int n;
    while(cin>>n)
    {
        int v;
        for(int i=0; i<n; i++)
        {
            cin>>v;
            a[v]=i;//把a[i]對映到i
        }
        for(int i=0; i<n; i++)
        {
            cin>>v;
            b[i]=a[v];//把b陣列按照a陣列的對映規則進行對映
        }
        for(int i=0; i<n; i++)
            dp[i]=INF; //初始化

        int pos=0;    // 記錄dp當前最後一位的下標
        dp[0]=b[0];
        for(int i=1; i<n; i++)
        {
            if(b[i]>=dp[pos])
                dp[++pos]=b[i];
            else
                dp[lower_bound(dp,dp+pos+1,b[i])-dp]=b[i];
        }
        cout<<pos+1<<endl;
    }
    return 0;
}