作者:Grey
原文地址: 最長遞增子序列
問題描述
說明:這裡的遞增指的是嚴格遞增,相等的時候不算遞增。
暴力解法
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繪製,地址見:最長遞增子序列
其中i初始指向0位置,用-
表示dp
和ends
陣列中都未填資料,開始遍歷原始陣列arr
。
當i來到0位置,arr[i]=3
,顯然:dp[i]=1
,此時,所有長度為0+1
的遞增子序列中最小結尾是3。所以ends[i]=3
,i來到下一個位置1。
當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位置。
此時,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位置。
此時,arr[3] = 2
,用arr[3]
的值去ends
陣列中做二分查詢,找大於等於arr[3]的最左位置,沒有找到,則可以擴充ends陣列,因為ends[0] = 1
表示:所有長度為1的遞增子序列中的最小結尾是1,而現在的arr[3]=2
,所以arr[3]=2
有資格成長為長度為2的遞增子序列,設定ends[1]=2
,ends[1]
左邊包含自己有2個比自己小的數,所以dp[3]=2
,i來到下一個4位置。
此時,arr[4] = 3
,用arr[4]
的值去ends
陣列中做二分查詢,找大於等於arr[4]的最左位置,沒有找到,則可以擴充ends陣列,因為ends[1] = 2
表示:所有長度為2的遞增子序列中的最小結尾是2,而現在的arr[4]=3
,所以arr[4]=3
有資格成長為長度為3的遞增子序列,設定ends[2]=3
,ends[2]
左邊包含自己有3個比自己小的數,所以dp[4]=3
,i來到下一個5位置。
此時,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位置。
此時,arr[6] = 4
,用arr[6]
的值去ends
陣列中做二分查詢,找大於等於arr[6]的最左位置,沒有找到,則可以擴充ends陣列,因為ends[2] = 3
表示:所有長度為3的遞增子序列中的最小結尾是3,而現在的arr[6]=4
,所以arr[6]=4
有資格成長為長度為4的遞增子序列,設定ends[3]=4
,ends[3]
左邊包含自己有4個比自己小的數,所以dp[6]=4
,i來到下一個7位置。
此時,arr[7] = 6
,用arr[7]
的值去ends
陣列中做二分查詢,找大於等於arr[7]的最左位置,沒有找到,則可以擴充ends陣列,因為ends[3] = 4
表示:所有長度為4的遞增子序列中的最小結尾是4,而現在的arr[7]=6
,所以arr[7]=6
有資格成長為長度為5的遞增子序列,設定ends[4]=6
,ends[4]
左邊包含自己有5個比自己小的數,所以dp[7]=5
,i來到下一個8位置。
此時,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位置。
arr[9] = 7
,用arr[9]
的值去ends
陣列中做二分查詢,找大於等於arr[9]的最左位置,沒有找到,則可以擴充ends陣列,因為ends[4] = 6
表示:所有長度為5的遞增子序列中的最小結尾是6,而現在的arr[9]=7
,所以arr[9]=7
有資格成長為長度為6的遞增子序列,設定ends[5]=7
,ends[5]
左邊包含自己有6個比自己小的數,所以dp[9]=6
,終止。
原始陣列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;
}
}
類似問題
主要思路
信封的兩個維度的資料分別依據如下規則排序:
一維從小到大,二維從大到小,然後把二維資料拿出來,最長遞增子序列就是巢狀層數。
舉例說明:
假設信封資料如下:[[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信封一定可以把這些資料套入
兩部分內容不會干擾,所以最長遞增子序列就是套的層數。