最長遞增子序列

n1ce2cv發表於2024-10-14

作者:Grey

原文地址: 最長遞增子序列

問題描述

LeetCode 300. 最長遞增子序列

說明:這裡的遞增指的是嚴格遞增,相等的時候不算遞增

暴力解法

dp[i]表示:

必須以i位置結尾的最長遞增子序列是多少,如果求出了每個位置的dp[i]值,最大的dp[i]值,就是最長遞增子序列的長度

顯而易見

dp[0] = 1; // 必須以0位置結尾的最長遞增子序列顯然是1

針對一個普遍位置i,dp[i]至少是1,那dp[i]還能擴多少呢?取決於[0,i)之間的位置j,如果arr[j] < arr[i],那麼dp[i]可以是dp[j]+1。每次來到i位置,都遍歷一下[0,i)之間的j位置的值是否可以和arr[i]位置連成最長遞增子序列,整個暴力解法的時間複雜度是O(N^2)。暴力解法的完整程式碼如下:

public class LeetCode_0300_LongestIncreasingSubsequence {
    // 暴力解(O(N^2))
    public static int lengthOfLIS(int[] arr) {
        if (arr == null || arr.length == 0) {
            return 0;
        }
        if (arr.length == 1) {
            return 1;
        }
        int max = 1;
        int[] dp = new int[arr.length];
        dp[0] = 1;
        for (int i = 1; i < arr.length; i++) {
            // dp[i]至少是1
            dp[i] = 1; 
            for (int j = 0; j < i; j++) {
                dp[i] = Math.max(dp[i], arr[j] < arr[i] ? dp[j] + 1 : 1);
            }
            max = Math.max(dp[i], max);
        }
        return max;
    }
}

O(N*logN)解法

設定兩個陣列,含義如下

dp陣列

長度和原陣列長度一樣,dp[i]表示必須以i結尾的,最長遞增子序列有多長。

ends陣列

長度和原陣列長度一樣,ends[i]表示所有長度為i+1的遞增子序列中最小結尾是什麼。

舉例說明

以下示例圖採用Processon繪製,地址見:最長遞增子序列

image

其中i初始指向0位置,用-表示dpends陣列中都未填資料,開始遍歷原始陣列arr

當i來到0位置,arr[i]=3,顯然:dp[i]=1,此時,所有長度為0+1的遞增子序列中最小結尾是3。所以ends[i]=3,i來到下一個位置1。

image

當i來到1位置,arr[1] = 2,用arr[1]的值去ends陣列中做二分查詢,找大於等於arr[1]的最左位置,即ends陣列的0位置,因為ends[0] = 3表示:所有長度為1的遞增子序列中的最小結尾是3,所以arr[1]=2無法成長為長度為2的遞增子序列,arr[1] = 2只能成長為長度為1的最長遞增子序列,將ends[0]位置的值從3改成2。ends[0]左邊包含自己只有1個數,所以dp[1] = 1。i來到下一個2位置。

image

此時,arr[2] = 1,用arr[2]的值去ends陣列中做二分查詢,找大於等於arr[2]的最左位置,即ends陣列的0位置,因為ends[0] = 2表示:所有長度為1的遞增子序列中的最小結尾是2,所以arr[2]=1無法成長為長度為2的遞增子序列,arr[2] = 1只能成長為長度為1的最長遞增子序列,將ends[0]位置的值從2改成1。ends[0]左邊包含自己只有1個數,所以dp[2] = 1。i來到下一個3位置。

image

此時,arr[3] = 2,用arr[3]的值去ends陣列中做二分查詢,找大於等於arr[3]的最左位置,沒有找到,則可以擴充ends陣列,因為ends[0] = 1表示:所有長度為1的遞增子序列中的最小結尾是1,而現在的arr[3]=2,所以arr[3]=2有資格成長為長度為2的遞增子序列,設定ends[1]=2ends[1]左邊包含自己有2個比自己小的數,所以dp[3]=2,i來到下一個4位置。

image

此時,arr[4] = 3,用arr[4]的值去ends陣列中做二分查詢,找大於等於arr[4]的最左位置,沒有找到,則可以擴充ends陣列,因為ends[1] = 2表示:所有長度為2的遞增子序列中的最小結尾是2,而現在的arr[4]=3,所以arr[4]=3有資格成長為長度為3的遞增子序列,設定ends[2]=3ends[2]左邊包含自己有3個比自己小的數,所以dp[4]=3,i來到下一個5位置。

image

此時,arr[5] = 0,用arr[5]的值去ends陣列中做二分查詢,找大於等於arr[5]的最左位置,即ends陣列的0位置,因為ends[0] = 1表示:所有長度為1的遞增子序列中的最小結尾是2,所以arr[5]=0無法成長為長度為2的遞增子序列,arr[5] = 0只能成長為長度為1的最長遞增子序列,將ends[0]位置的值從1改成0。ends[0]左邊包含自己只有1個數,所以dp[5] = 1。i來到下一個6位置。

image

此時,arr[6] = 4,用arr[6]的值去ends陣列中做二分查詢,找大於等於arr[6]的最左位置,沒有找到,則可以擴充ends陣列,因為ends[2] = 3表示:所有長度為3的遞增子序列中的最小結尾是3,而現在的arr[6]=4,所以arr[6]=4有資格成長為長度為4的遞增子序列,設定ends[3]=4ends[3]左邊包含自己有4個比自己小的數,所以dp[6]=4,i來到下一個7位置。

image

此時,arr[7] = 6,用arr[7]的值去ends陣列中做二分查詢,找大於等於arr[7]的最左位置,沒有找到,則可以擴充ends陣列,因為ends[3] = 4表示:所有長度為4的遞增子序列中的最小結尾是4,而現在的arr[7]=6,所以arr[7]=6有資格成長為長度為5的遞增子序列,設定ends[4]=6ends[4]左邊包含自己有5個比自己小的數,所以dp[7]=5,i來到下一個8位置。

image

此時,arr[8] = 2,用arr[8]的值去ends陣列中做二分查詢,找大於等於arr[8]的最左位置,即ends陣列的1位置,因為ends[1] = 2表示:所有長度為2的遞增子序列中的最小結尾是2,所以arr[8]=2無法成長為長度為3的遞增子序列,arr[8] = 2只能成長為長度為2的最長遞增子序列,將ends[1]位置的值改成arr[8]中的這個2。ends[1]左邊包含自己只有2個數,所以dp[8] = 2。i來到最後一個9位置。

image

arr[9] = 7,用arr[9]的值去ends陣列中做二分查詢,找大於等於arr[9]的最左位置,沒有找到,則可以擴充ends陣列,因為ends[4] = 6表示:所有長度為5的遞增子序列中的最小結尾是6,而現在的arr[9]=7,所以arr[9]=7有資格成長為長度為6的遞增子序列,設定ends[5]=7ends[5]左邊包含自己有6個比自己小的數,所以dp[9]=6,終止。

image

原始陣列arr的最長遞增子序列長度為6。

暴力解法中,來到每個i位置需要找一遍[0,i),複雜度是O(N^2),現在有了ends陣列的輔助,用二分法加速了這一過程,所以,這個解法的複雜度是O(N*logN)

完整程式碼如下

public class LeetCode_0300_LongestIncreasingSubsequence {

    public static int lengthOfLIS(int[] arr) {
        if (null == arr || arr.length == 0) {
            return 0;
        }
        int N = arr.length;
        int[] dp = new int[N];
        int[] ends = new int[N];
        dp[0] = 1;
        ends[0] = arr[0];
        int l;
        int r;
        int right = 0;
        int max = 1;
        for (int i = 0; i < N; i++) {
            l = 0;
            r = right;
            while (l <= r) {
                int m = (l + r) / 2;
                if (arr[i] > ends[m]) {
                    l = m + 1;
                } else {
                    r = m - 1;
                }
            }
            right = Math.max(right, l);
            dp[i] = l + 1;
            ends[l] = arr[i];
            max = Math.max(max, dp[i]);
        }
        return max;
    }
}

類似問題

LeetCode 354. 俄羅斯套娃信封問題

主要思路

信封的兩個維度的資料分別依據如下規則排序:

一維從小到大,二維從大到小,然後把二維資料拿出來,最長遞增子序列就是巢狀層數。

舉例說明:

假設信封資料如下:[[5,4],[6,4],[6,7],[2,3]]

按照一維從小到大,二維從大到小排序後的信封如下:[[2,3],[5,4],[6,7],[6,4]]

拿出二維資料的陣列如下:[3,4,7,4]

這個陣列的最長遞增子序列是[3,4,7], 所以返回3層。

原理:

假設信封一維從小到大,二維從大到小,然後把二維資料拿出來後的陣列為[a,b,c,d,e,f,g],注:其中的字母只是抽象表示,不表示具體的數值大小。

我們調研任一位置,假設調研的位置是2號的c,根據排序策略,可以得到兩個結論:

結論1:一維資料和c表示的信封一樣的,二維資料不如c表示的信封的資料,一定在c的右邊。這樣的資料一定可以套入c裡面

結論2:一維資料不如c的,二維資料也不如c的,一定在c的左邊,c信封一定可以把這些資料套入

兩部分內容不會干擾,所以最長遞增子序列就是套的層數。

更多

演算法和資料結構筆記

參考資料

演算法和資料結構大廠刷題班-左程雲

相關文章