題目:
輸出最長遞增子序列的長度,如輸入 4 2 3 1 5 6,輸出 4 (因為 2 3 5 6組成了最長遞增子序列)。
暴力破解法:這種方法很簡單,兩層for循環搞定,時間復雜度是O(N2)。
動態規划:之前我們使用動態規划去解決一般是創建一維數組或者二維數組來構建出dp表,利用之前的歷史上dp表中的值進行相關的處理求解出這個過程中的幾個最大值,最小值,然后相加減來得出dp表的當前元素的值,所以我們會想,先創建一個一維數組,因為數組中選擇的元素的范圍在進行變化,所以dp表表示的值為截取到當前范圍內最長的遞增子序列是多少,但是在填表的過程中發現這種一般方法是不行的。但是這作為一道經典的題目,思路本身是很難的,有人想出來了它的dp解法,我們可以通過借鑒它的思路來幫助我們擴展理解dp算法。
那么它的思路是:dp[i]表示的意思是必須包含以dp[i]結尾的最長遞增子序列,那么它的值就是以dp[i]結尾的最長遞增子序列長度。依次去掃描dp[i]這個字符之前的字符,假如發現當前字符大於之前的字符那么比較當前的最長子序列的長度與當前dp[i] + 1的值的大小來更新當前最長遞增子序列的長度,當掃描完當前字符i之前的字符那么這個時候說明找到了以當前字符結尾的最長遞增子序列的長度,把這個值賦值給dp[i]。
需要注意的是最后還需要掃描一下dp數組,看一下哪一個dp[i]最大,因為我們不知道以哪個字符結尾的字符序列為最長的遞增子序列,所以需要掃描一下,最后找到dp[i]的最大值返回,這是與之前的dp數組不同的一個點,以前是返回dp數組的最后一個元素就可以了,因為最后一個元素就是我們需要求解的最優解。這個解法時間復雜度也是O(N2)。
那解法三的思路呢:dp[i]表示的是長度為i的最長遞增子序列的末尾的那個數。先初始化dp[0]為數組中第一個元素的值,即dp[0] = 4,定義一個指針p用來記錄當前遞增子序列的位置,從數組的第二個元素開始比較當前arr[i]與當前指針指向的dp數組的位置的值的大小,假如arr[i] > dp[p]那么說明當前的字符可以構成更長的遞增子序列那么我們應該更新dp數組,指針往下移動,然后把當前的arr[i]的值賦值給dp[++p],假如arr[i] < dp[p]說明當前字符不能夠構成更長的遞增子序列,此時需要進行元素的替換,為什么進行替換呢?因為當前更小的元素更有利於最長遞增子序列的貢獻,所以掃描dp數組指向位置以及之前的位置假如arr[i] 大於dp[p]那么我們將其dp[p] 替換為arr[i]
最后返回的是指針p指向的位置,注意返回最長遞增子序列的長度是p指向的下標,這也是與之前動態規划的一般解法不同的點。更進一步的優化就是這種解法在掃描dp數組的過程中可以通過二分查找來降低時間復雜度,整體從O(N2)降低到O(NlgN)。
代碼:
package chapter_08_貪心策略與動態規划;
public class 最長遞增子序列 {
static int[] arr = { 4, 2, 3, 1, 5, 6, 4, 8, 5, 9 };
public static void main(String[] args) {
System.out.println(f(arr)); // 輸出 6
System.out.println(dp(arr)); // 輸出 6
System.out.println(dp1(arr)); // 輸出 6
}
// 暴力破解法
private static int f(int[] arr) {
int maxCnt = 0;
for (int i = 0; i < arr.length; i++) {
int p = i;
int cnt = 1;
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] > arr[p]) {
cnt++;
p = j;
}
}
// if (cnt>maxCnt){
// maxCnt=cnt;
// }
maxCnt = Math.max(maxCnt, cnt);
}
return maxCnt;
}
static int[] dp = new int[arr.length];
// 解法二
private static int dp(int[] arr) {
dp[0] = 1;
for (int i = 1; i < arr.length; i++) {
int cnt = 1;
for (int j = i - 1; j >= 0; j--) {
if (arr[i] > arr[j]) {
cnt = Math.max(cnt, dp[j] + 1);
}
}
dp[i] = cnt;
}
int ans = -1;
for (int i = 0; i < dp.length; i++) {
ans = Math.max(ans, dp[i]);
}
return ans;
}
// 解法三
/* 在優化之后,可以達到O(NlgN) */
private static int dp1(int[] arr) {
dp = new int[arr.length + 1];
dp[1] = arr[0];// 長度為1的最長遞增子序列,初始化為第一個元素
int p = 1;// 記錄dp更新的最后位置
for (int i = 1; i < arr.length; i++) {
if (arr[i] > dp[p]) {
dp[p + 1] = arr[i];
p++;
} else {
//掃描dp數組,替換第一個比arr[i]大的dp
//for (int j = 0; j <= p; j++) {
// if (dp[j] > arr[i]) {
// dp[j] = arr[i];
// }
//}
// 二分查找,比上面掃描dp數組耗時少
int indexOfFirstBigger = indexOfFirstBigger(dp, arr[i], 0, p);
if (indexOfFirstBigger != -1)
dp[indexOfFirstBigger] = arr[i];
}
}
return p;
}
/**
* 在遞增數組中,從左查找第一個比v大的元素的下標
*/
public static int indexOfFirstBigger(int[] dp, int v, int l, int r) {
while (l <= r) {
int mid = (l + r) >> 1;
if (dp[mid] > v) {
r = mid; // 保留大於v的下標以防這是第一個
} else {
l = mid + 1;
}
if (l == r && dp[l] > v)
return l;
}
return -1;
}
}
