1. 程式人生 > >最長遞增子序列詳解(longest increasing subsequence)

最長遞增子序列詳解(longest increasing subsequence)

一個各公司都喜歡拿來做面試筆試題的經典動態規劃問題,網際網路上也有很多文章對該問題進行討論,但是我覺得對該問題的最關鍵的地方,這些討論似乎都解釋的不很清楚,讓人心中不快,所以自己想徹底的搞一搞這個問題,希望能夠將這個問題的細節之處都能夠說清楚。

對於動態規劃問題,往往存在遞推解決方法,這個問題也不例外。要求長度為i的序列的Ai{a1,a2,……,ai}最長遞增子序列,需要先求出序列Ai-1{a1,a2,……,ai-1}中以各元素(a1,a2,……,ai-1)作為最大元素的最長遞增序列,然後把所有這些遞增序列與ai比較,如果某個長度為m序列的末尾元素aj(j<i)比ai要小,則將元素ai加入這個遞增子序列,得到一個新的長度為m+1的新序列,否則其長度不變,將處理後的所有i個序列的長度進行比較,其中最長的序列就是所求的最長遞增子序列。舉例說明,對於序列A{35, 36, 39, 3, 15, 27, 6, 42}當處理到第九個元素(27)時,以35, 36, 39, 3, 15, 27, 6為最末元素的最長遞增序列分別為
    35
    35,36
    35,36,39
    3
    3,15
    3,15,27
    3,6
當新加入第10個元素42時,這些序列變為
    35,42
    35,36,42
    35,36,39,42,
    3,42
    3,15,42
    3,15,27,42
    3,6,42

這其中最長的遞增序列為(35,36,39,42)和(3,15,27,42),所以序列A的最長遞增子序列的長度為4,同時在A中長度為4的遞增子序列不止一個。

該演算法的思想十分簡單,如果要得出Ai序列的最長遞增子序列,就需要計算出Ai-1的所有元素作為最大元素的最長遞增序列,依次遞推Ai-2,Ai-3,……,將此過程倒過來,即可得到遞推演算法,依次推出A1,A2,……,直到推出Ai為止,

程式碼如下
unsigned int LISS(const int array[], size_t length, int result[])
{
    unsigned int i, j, k, max;

    //變長陣列引數,C99新特性,用於記錄當前各元素作為最大元素的最長遞增序列長度
    unsigned int liss[length];

    //前驅元素陣列,記錄當前以該元素作為最大元素的遞增序列中該元素的前驅節點,用於列印序列用
    unsigned int pre[length];

    for(i = 0; i < length; ++i)
    {
        liss[i] = 1;
        pre[i] = i;
    }

    for(i = 1, max = 1, k = 0; i < length; ++i)
    {
        //找到以array[i]為最末元素的最長遞增子序列
        for(j = 0; j < i; ++j)
        {
            //如果要求非遞減子序列只需將array[j] < array[i]改成<=,
            //如果要求遞減子序列只需改為>
            if(array[j] < array[i] && liss[j] + 1> liss[i])
            {
                liss[i] = liss[j] + 1;
                pre[i] = j;

                //得到當前最長遞增子序列的長度,以及該子序列的最末元素的位置
                if(max < liss[i])
                {
                    max = liss[i];
                    k = i;
                }
            }
        }
    }

    //輸出序列
    i = max - 1;

    while(pre[k] != k)
    {
        result[i--] = array[k];
        k = pre[k];
    }

    result[i] = array[k];

    return max;
}
該函式計算出長度為length的array的最長遞增子序列的長度,作為返回值返回,實際序列儲存在result陣列中,該函式中使用到了C99變長陣列引數特性(這個特性比較贊),不支援C99的同學們可以用malloc來申請函式裡面的兩個陣列變數。函式的時間複雜度為O(nn),下面我們來介紹可以將時間複雜度降為O(nlogn)改進演算法。

在基本演算法中,我們發現,當需要計算前i個元素的最長遞增子序列時,前i-1個元素作為最大元素的各遞增序列,無論是長度,還是最大元素值,都毫無規律可循,所以開始計算前i個元素的時候只能遍歷前i-1個元素,來找到滿足條件的j值,使得aj < ai,且在所有滿足條件的j中,以aj作為最大元素的遞增子序列最長。有沒有更高效的方法,找到這樣的元素aj呢,實際是有的,但是需要用到一個新概念。在之前我舉的序列例子中,我們會發現,當計算到第10個元素時,前9個元素所形成最長子序列分別為

    35
    35,36
    35,36,39
    3
    3,15
    3,15,27

    3,6

這其中長度為3的子序列有兩個,長度為2的子序列有3個,長度為1的子序列2個,所以一個序列,長度為n的遞增子序列可能不止一個,但是所有長度為n的子序列中,有一個子序列是比較特殊的,那就是最大元素最小的遞增子序列(挺拗口的概念),在上述例子中,序列(3),(3,6),(3,5,27)就滿足這樣的性質,他們分別是長度為1,2,3的遞增子序列中最大元素最小的(截止至處理第10個元素之前),隨著元素的不斷加入,滿足條件的子序列會不斷變化。如果將這些子序列按照長度由短到長排列,將他們的最大元素放在一起,形成新序列B{b1,b2,……bj},則序列B滿足b1 < b2 < …… <bj。這個關係比較容易說明,假設bxy表示序列A中長度為x的遞增序列中的第y個元素,顯然,如果在序列B中存在元素bmm > bnn,且m < n則說明子序列Bn的最大元素小於Bm的最大元素,因為序列是嚴格遞增的,所以在遞增序列Bn中存在元素bnm < bnn,且從bn0到bnm形成了一個新的長度為m的遞增序列,因為bmm > bnn,所以bmm > bnm,這就說明在序列B中還存在一個長度為m,最大元素為bnm < bmm的遞增子序列,這與序列的定義,bmm是所有長度為m的遞增序列中第m個元素最小的序列不符,所以序列B中的各元素嚴格遞增。發現瞭如此的一個嚴格遞增的序列,這讓我們柳暗花明,可以利用此序列的嚴格遞增性,利用二分查詢,找到最大元素剛好小於aj的元素bk,將aj加入這個序列尾部,形成長度為k+1但是最大元素又小於bk+1的新序列,取代之前的bk+1,如果aj比Bn中的所有元素都要大,說明發現了以aj為最大元素,長度為n+1的遞增序列,將aj做Bn+1的第n+1個元素。從b1依次遞推,就可以在O(nlogn)的時間內找出序列A的最長遞增子序列。

理論說明比較枯燥,來看一個例子,以序列{6,7,8,9,10,1,2,3,4,5,6}來說明改進演算法的步驟:

程式開始時,最長遞增序列長度為1(每個元素都是一個長度為1的遞增序列),當處理第2個元素時發現7比最長遞增序列6的最大元素還要大,所以將6,7結合生成長度為2的遞增序列,說明已經發現了長度為2的遞增序列,依次處理,到第5個元素(10),這一過程中B陣列的變化過程是

    6
    6,7
    6,7,8
    6,7,8,9
    6,7,8,9,10

開始處理第6個元素是1,查詢比1大的最小元素,發現是長度為1的子序列的最大元素6,說明1是最大元素更小的長度為1的遞增序列,用1替換6,形成新陣列1,7,8,9,10。然後查詢比第7個元素(2)大的最小元素,發現7,說明存在長度為2的序列,其末元素2,比7更小,用2替換7,依次執行,直到所有元素處理完畢,生成新的陣列1,2,3,4,5,最後將6加入B陣列,形成長度為6的最長遞增子序列.

這一過程中,B陣列的變化過程是

    1,7,8,9,10
    1,2,8,9,10
    1,2,3,9,10
    1,2,3,4,10
    1,2,3,4,5
    1,2,3,4,5,6

當處理第10個元素(5)時,傳統演算法需要檢視9個元素(6,7,8,9,10,1,2,3,4),而改進演算法只需要用二分查詢陣列B中的兩個元素(3, 4),可見改進演算法還是很陰霸的。

下面是該演算法的實現:
unsigned int LISSEx(const int array[], size_t length, int result[])
{
    unsigned int i, j, k, l, max;

    //棧陣列引數,C99新特性,這裡的liss陣列與上一個函式意義不同,liss[i]記錄長度為i + 1
    //遞增子序列中最大值最小的子序列的最後一個元素(最大元素)在array中的位置
    unsigned int liss[length];

    //前驅元素陣列,用於列印序列
    unsigned int pre[length];

    liss[0] = 0;

    for(i = 0; i < length; ++i)
    {
        pre[i] = i;
    }

    for(i = 1, max = 1; i < length; ++i)
    {
        //找到這樣的j使得在滿足array[liss[j]] > array[i]條件的所有j中,j最小
        j = 0, k = max - 1;

        while(k - j > 1)
        {
            l = (j + k) / 2;

            if(array[liss[l]] < array[i])
            {
                j = l;
            }
            else
            {
                k = l;
            }
        }

        if(array[liss[j]] < array[i])
        {
            j = k;
        }

        //array[liss[0]]的值也比array[i]大的情況
        if(j == 0)
        {
            //此處必須加等號,防止array中存在多個相等的最小值時,將最小值填充到liss[1]位置
            if(array[liss[0]] >= array[i])
            {
                liss[0] = i;
                continue;
            }
        }

                //array[liss[max -1]]的值比array[i]小的情況
                if(j == max - 1)
        {
            if(array[liss[j]] < array[i])
            {
                pre[i] = liss[j];
                liss[max++] = i;
                continue;
            }
        }

        pre[i] = liss[j - 1];
        liss[j] = i;
    }

    //輸出遞增子序列
    i = max - 1;
    k = liss[max - 1];

    while(pre[k] != k)
    {
        result[i--] = array[k];
        k = pre[k];
    }

    result[i] = array[k];

    return max;
}
這個演算法的思想可以算得上巧妙,在時間複雜度上提升明顯,但是同時在實現時也比通俗演算法多了好些坑,這裡說明一下:
  • 演算法中為了獲得實際的序列,陣列B中儲存的不是長度為j的遞增序列的最大元素的最小值,而是該值在輸入陣列A中的位置,如果只想求出最長遞增子序列的長度,則B陣列可以直接儲存滿足條件元素的值
  • 二分查詢的結果,我們的目的是找到這樣的一個j,使滿足A[B[j]] > A[i]的所有j中,j取得最小值,但是在二分查詢的時候可能會發生兩種特殊情況,B陣列的所有元素都不小於A[i],B陣列的所有元素都比A[i]小,對於這兩中情況需要專門處理
  • 對於B中所有元素都不小於A[i]的情況,要將A[i]更新到B[0]的位置
  • 對於B中所有元素都小於A[i]的情況,要將更新到B[max]的位置,同時將max值增加1,說明找到了比當前最長的遞增序列更長的結果
  • 對於其他情況,在更新新節點的前驅節點時,要注意,當前元素的前驅節點是B[j-1],而不是pre[B[j]],這點要格外留意,後者看似有道理,但實際上在之前的更新中可能已經被變更過。

效能比較:長度為5000的隨機陣列,在我的機器上,改進演算法的速度提升將近200倍,可見演算法改進在程式效能表現中的重要性。不過傳統演算法也並非毫無價值,

首先,傳統演算法可以用來驗證改進演算法的正確性。二分搜尋中的不確定性還是相當讓人頭痛的。其次,如果要求最長非遞減子序列,最長遞減子序列等等,傳統演算法改起來非常的直觀(已經註釋說明),而改進演算法,最起碼我沒有一眼看出來如何一下就能改好。

目前我搜到的網上的有關此改進演算法,在二分搜尋滿足條件的節點時,聊聊幾筆,就完成了功能,但是我按照那種寫法無一例外都遇到了某種型別的序列無法處理的情況,不知是否是我在理解演算法方面出現偏差。

後繼,研究完這個問題之後產生了兩個遺留問題,暫時沒有答案,和大家分享一下
  • 對於一個序列A,最長遞增子序列可能不止一個,傳統演算法找到的是所有遞增子序列中,最大值下標最小(最早出現)的遞增子序列,而改進演算法找到的是最大值最小的遞增子序列,那麼改進演算法所找到的遞增子序列,是不是所有最長遞增子序列中各元素合最小的一個呢,我感覺很可能是,但是還沒想出怎麼證明。
  • 對於元素互不相同的隨機數序列A,他的最長遞增子序列的數學期望是多少呢?

相關推薦

遞增序列longest increasing subsequenceby joylnwang

看了幾個講最長遞增子序列的部落格,都有點迷,不過看了這個之後瞬間就懂了,轉來分享 一個各公司都喜歡拿來做面試筆試題的經典動態規劃問題,網際網路上也有很多文章對該問題進行討論,但是我覺得對該問題的最關鍵的地方,這些討論似乎都解釋的不很清楚,讓人心中不快,所以自己想徹底的搞一

遞增序列longest increasing subsequence

一個各公司都喜歡拿來做面試筆試題的經典動態規劃問題,網際網路上也有很多文章對該問題進行討論,但是我覺得對該問題的最關鍵的地方,這些討論似乎都解釋的不很清楚,讓人心中不快,所以自己想徹底的搞一搞這個問題,希望能夠將這個問題的細節之處都能夠說清楚。 對於動態規劃問題,往往存在遞

51nod 1376 遞增序列的數量不是dp哦,線段樹 +  思維

sort 是個 can stream const 方便 long 序列 printf 題目鏈接:https://www.51nod.com/onlineJudge/questionCode.html#!problemId=1376 題解:顯然這題暴力的方法很容易想到

51nod 1376: 遞增序列的數量二維偏序+cdq分治

col pac def esp 調用 sha oid 題目 數字 1376 最長遞增子序列的數量   Time Limit: 1 Sec   Memory Limit: 128MB   分值: 160        難度:6級算法題 Description   數組A包

三維一邊推:公共序列加強版三串LCS CAIOJ - 1073 dp lcs

題解 與二位lcs類似 列舉三個串的每個位置 狀態轉移考慮5種情況 abc當前位置全相等則由3個串長度全-1的位置轉移過來 lcs+1 ab相等但不與c相等 則由ab長度都-1或c長度-1取max轉移過來 ac相等但不與b相等和bc相等但不與a相等類似 abc互不相等則由a、b或c長度-

遞增序列 時間複雜度:O(NlogN

假設存在一個序列d[1..9] = 2 1 5 3 6 4 8 9 7,可以看出來它的LIS長度為5。 下面一步一步試著找出它。 我們定義一個序列B,然後令 i = 1 to 9 逐個考察這個序列。 此外,我們用一個變數Len來記錄現在最長算到多少了 首先,把d[1]有序地放

LeetCode 遞增序列的O(nlogn)

/***************************************************************************** * * Given an unsorted array of integers, find t

遞增序列(longest increasing subsequence) 問題

最長遞增子序列的定義: 按照序列元素的下標號,抽取一部分元素組成子序列,子序列中的元素之間為遞增的關係(下標可以不連續)。其中長度最長的遞增子序列就是最長遞增子序列。 方法思想: 為了求出該陣列的最長遞增子序列,就需要先求出在以陣列中每個元素為結尾的情況下

[luoguP2766] 遞增序列問題大流

close spl 方法 emp 路徑 pid code display div 傳送門 題解來自網絡流24題: 【問題分析】 第一問時LIS,動態規劃求解,第二問和第三問用網絡最大流解決。 【建模方法】 首先動態規劃求出F[i],表示以第i位為開頭的最長上

51nod 1218 遞增序列 V2dp + 思維

ear www str tdi binsearch tor con bsp href 題目鏈接:https://www.51nod.com/onlineJudge/questionCode.html#!problemId=1218 題解:先要確定這些點是不是屬於最長

遞增序列只求大小模板

初始化 輸入 div 分法 下界 ive tdi color ostream #include<iostream> #include<cstdio> #include<cstring> #include<algorithm>

dp-遞增序列 LIS

一個數 bsp 註意 str 只有一個 自然 end alt ace 首先引出一個例子 問題 :   給你一個長度為 6 的數組 , 數組元素為 { 1 ,4,5,6,2,3,8 } , 則其最長單調遞增子序列為 { 1 , 4 , 5 , 6 , 8 } , 並且長度

動態規劃之遞增序列LIS

lib sca while -c -o 組成 describe log ret 在一個已知的序列{ a1,a2,……am}中,取出若幹數組成新的序列{ ai1, ai2,…… aim},其中下標 i1,i2, ……im保持遞增,即新數列中的各個數之間依舊保持原

51node 1134 遞增序列 數據結構

賦值 log 寫法 數字 name 內存 遞增 max scan 題意: 最長遞增子序列 思路: 普通的$O(n^2)$的會超時。。 然後在網上找到了另一種不是dp的寫法,膜拜一下,自己寫了一下解釋 來自:https://blog.csdn.net/Adusts/artic

PAT (Advanced Level) Practice 1045 Favorite Color Stripe 30 分 遞增序列

Eva is trying to make her own color stripe out of a given one. She would like to keep only her favorite colors in her favorite order by cutting off th

大子段和與遞增序列貪心與動態規劃

話不多說先上程式碼。。。。。  最大子段和 題目描述 給出一段序列,選出其中連續且非空的一段使得這段和最大。 輸入輸出格式 輸入格式:   第一行是一個正整數NNN,表示了序列的長度。 第二行包含NNN個絕對值不大於100001000010000的

LeetCode題解:longest-increasing-subsequence遞增序列

題目描述 Example: Input: [10,9,2,5,3,7,101,18] Output: 4 Explanation: The longest increasing subsequence is [2,3,7,101], the length is 4. Not

遞增序列LIS

本篇部落格主要講述什麼是最長公共子序列、求解最長公共子序列的思想,以及程式碼。 什麼是最長公共子序列?   給定一個長度為N的陣列,找出一個最長的單調自增子序列(不要求是連續的)。例如:6 5 7 8 4 3 9 1,這裡的最長遞增子序列是{ 6, 7, 8, 9}或者{

Longest Ordered Subsequence 遞增序列的長度序列型dp

A numeric sequence of ai is ordered if a1 < a2 < ... < aN. Let the subsequence of the given numeric sequen

51Nod1134 遞增序列動歸

這道題用動歸的思想寫,在所給的陣列中找到最長遞增子序列。定義一個新的陣列存最長子序列,第i項如果大於陣列的最後一項,就加入陣列,如果小於,就用二分查詢找到第一個大於第i項的數,然後取代之。 lower