Q:給定一個無序的整數數組,找到其中最長上升子序列的長度。
示例:
輸入: [10,9,2,5,3,7,101,18]
輸出: 4
解釋: 最長的上升子序列是 [2,3,7,101],它的長度是 4。
說明:
可能會有多種最長上升子序列的組合,你只需要輸出對應的長度即可。
你算法的時間復雜度應該為 O(n2) 。
進階: 你能將算法的時間復雜度降低到 O(n log n) 嗎?
A:找最長遞增子序列長度:(這個比較簡單,就找了一個現成的)
public int lengthOfLIS(int[] nums) {
int[] dp = new int[nums.length];
// dp 數組全都初始化為 1
Arrays.fill(dp, 1);
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j])
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
int res = 0;
for (int i = 0; i < dp.length; i++) {
res = Math.max(res, dp[i]);
}
return res;
}
但這樣的復雜度為O(N^2),可以對最長子序的數組進行維護
此處引用:一步一步推導出官方最優解法,詳細圖解
從一個例子開始。我們先考慮下面的數組的最長上升子序列:
[10, 9, 2, 5, 3, 7, 101, 4, 1]
用暴力法求解,可以得下圖:
假如我們要在里面新增一個元素 X,希望找出插入 X 之后的最長子序列。
結論一指出,我們需要在當前允許插入的 最長 子序列之后添加元素。
於是,我們可以依次檢查序列長度 = 1,2,3,4 的遞增子序列,然后找出最長的,尾數 < X 的序列。
我們發現,對每一個序列長度 l,只需要檢查圖中的每一列的最小值(綠色的元素)是否 < X 即可。如果綠色的元素 < X,表明長度為 l 的遞增子序列后可添加元素 X。
因此,我們有 結論二:我們只需要維護 長度為 l 的遞增子序列的 最小結尾數字。
我們應該如何實現這樣的算法呢?
最簡單的實現方式,當插入新元素 X 時,我們從 1 逐個枚舉現有遞增子序列的長度,直到找到最大可添加元素 X 的長度。與此同時,維護每個長度 l 的最小尾數:
比如前述序列 [10, 9, 2, 5, 3, 7, 101, 4, 1],已構造 dp 數組[1, 3, 4, 101],要添加 “6”。
長度 l = 1 時,長度為 1 的遞增子序列末尾的最小數字為 1,6 > 1,可以添加。
l = 2 時,6 > 3,可以添加。
l = 3 時,6 > 4,可以添加。
l = 4 時,6 < 101,不可添加。
因此,以 “6” 為結尾的遞增子序列最長為 3 + 1 = 4。
另外,此時,長度為 4 的遞增子序列的最小尾數變成了 “6”。因此修改 “101” -> “6”。數組變為 [1, 3, 4, 6]。
由於數組是有序的,當要添加數 “x” 時,可以用二分搜索找出數組中小於 x 的最大數字。
public int lengthOfLIS(int[] nums) {
int n = nums.length;
if (n <= 1)
return n;
ArrayList<Integer> array = new ArrayList<>();
array.add(nums[0]);
for (int i = 1; i < n; i++) {
int index = Binary(array, nums[i], 0, array.size());//二分法查找該替代的位置
if (index == array.size())
array.add(nums[i]);//如果比最大的還大,放到隊尾
else {
array.set(index, nums[i]);//取代當前位置
}
}
return array.size();
}
private int Binary(ArrayList<Integer> array, int num, int start, int end) {
int left = start, right = end;
while (left < right) {
int mid = (left + right) / 2;
if (array.get(mid) == num) {
right = mid;
} else if (array.get(mid) > num) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
Q:給定一個未排序的整數數組,找到最長遞增子序列的個數。
示例 1:
輸入: [1,3,5,4,7]
輸出: 2
解釋: 有兩個最長遞增子序列,分別是 [1, 3, 4, 7] 和[1, 3, 5, 7]。
示例 2:
輸入: [2,2,2,2,2]
輸出: 5
解釋: 最長遞增子序列的長度是1,並且存在5個子序列的長度為1,因此輸出5。
注意: 給定的數組長度不超過 2000 並且結果一定是32位有符號整數。
A:
1.基於上一題的第一種解法,同樣使用動態規划。找個數時,放一個count數組。
public int findNumberOfLIS(int[] nums) {
if (nums.length <= 1)
return nums.length;
int n = nums.length;
int[] length = new int[n];
int[] count = new int[n];
Arrays.fill(length, 1);
Arrays.fill(count, 1);
int maxLen = 0;
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
if (length[i] < length[j] + 1) {//更新最大長度子序列
length[i] = length[j] + 1;
count[i] = count[j];
} else if (length[i] == length[j] + 1) {//同樣以i結尾的最大長度的子序列
count[i] += count[j];
}
}
}
maxLen = Math.max(maxLen, length[i]);
}
int res = 0;
for (int i = 0; i < n; i++) {
if (maxLen == length[i])
res += count[i];
}
return res;
}
2.基於上一題的第二種寫法,用樹狀數組做
3.用CDQ分治
(這兩個找時間再看吧)O(nlogn)的兩種解法:樹狀數組和CDQ分治(新手慎入)