給定數組arr,返回arr的最長遞增子序列。算法
arr={2,1,5,3,6,4,8,9,7},返回的最長遞增子序列爲{1,3,4,8,9}。數組
本期主要從動態規劃和二分法兩個方向來求解最長遞增子序列問題。ide
先介紹時間複雜度爲O(N^2^)的方法,具體過程以下:學習
按照步驟1~3能夠計算出dp數組,具體過程請參看以下代碼中的方法,參考代碼以下:優化
#include<stdio.h> #define MAXN 1000 int arr[MAXN + 10]; int dp[MAXN + 10]; int main() { int N, i, j; scanf("%d", &N); for (i = 0; i < N; ++i) { scanf("%d", &arr[i]); } dp[0] = 1; for (i = 1; i < N; ++i) { /* * 每次求以第i個數爲終點的最長上升子序列的長度 */ int tmp = 0;/* 記錄知足條件的、第i個數左邊的上升子序列的最大長度 */ for (j = 0; j < i; ++j) { /* 查看以第j個數爲終點的最長上升子序列 */ if (arr[i] > arr[j]) { if (tmp < dp[j]) tmp = dp[j]; } } dp[i] = tmp + 1; } int ans = -1; for (i = 0; i < N; ++i) { if (ans < dp[i]) ans = dp[i]; } printf("%d\n", ans); return 0; }
程序執行完後,數組arr[]和狀態數組dp[]以下:設計
最長上升子序列有6個:(1,5,6,8,9)、(2,5,6,8,9)、(2,3,6,8,9)、(2,3,4,8,9)、(1,3,4,8,9)和(1,3,6,8,9),長度都是5。3d
問題:若是還要輸出最長的子序列呢?例如,除了輸出5以外,還要輸出(1,3,4,8,9)這個序列。code
接下來解釋如何根據求出的dp數組獲得最長遞增子序列。以題目的例子來講明,arr={2,1,5,3,6,4,8,9,7},求出的數組dp={1,1,2,2,3,3,4,5,4}。具體求解步驟以下:blog
dp數組包含每一步決策的信息,其實根據dp數組找出最長遞增子序列的過程就是從某一個位置開始逆序還原出決策路徑的過程。具體過程請參看以下代碼:遊戲
#include<stdio.h> #include<stdlib.h> /* 動態內存分配 */ #define MAXN 1000 int arr[MAXN + 10]; int dp[MAXN + 10]; int main() { int N, i, j; scanf("%d", &N); for (i = 0; i < N; ++i) { scanf("%d", &arr[i]); } dp[0] = 1; for (i = 1; i < N; ++i) { /* * 每次求以第i個數爲終點的最長上升子序列的長度 */ int tmp = 0;/* 記錄知足條件的、第i個數左邊的上升子序列的最大長度 */ for (j = 0; j < i; ++j) { /* 查看以第j個數爲終點的最長上升子序列 */ if (arr[i] > arr[j]) { if (tmp < dp[j]) tmp = dp[j]; } } dp[i] = tmp + 1; } int ans = -1; for (i = 0; i < N; ++i) { if (ans < dp[i]) ans = dp[i]; } printf("%d\n", ans); /* 輸出最長遞增子序列的長度 */ /* * 下面根據dp數組還原出最長遞增子序列。 * len中記錄了最長遞增子序列的長度,固然有len=ans。 * index記錄最長遞增子序列中最後一個數在arr數組中的位置。 */ int len = 0; int index = 0; for (i = 0; i < N; ++i) { if (dp[i] > len) { len = dp[i]; index = i; } } /* * lis數組用來存放最長遞增子序列。 */ int* lis = (int*)malloc(sizeof(int) * len); lis[--len] = arr[index]; /* 最長遞增子序列中最後一個數爲arr[index] */ for (i = index; i >= 0; i--) { /* 從index位置開始從右往左掃描數組arr */ if (arr[i] < arr[index] && dp[i] == dp[index] - 1) { lis[--len] = arr[i]; index = i; } } /* 打印最長遞增子序列 */ for (i = 0; i < ans; ++i) { printf("%d", lis[i]); if (i < ans - 1)printf(" "); } printf("\n"); free(lis); return 0; }
輸入:
9
2 1 5 3 6 4 8 9 7
輸出:
5
1 3 4 8 9
運行結果:
計算dp數組過程的時間複雜度爲O(N^2^),根據dp數組獲得最長遞增子序列過程的時間複雜度爲O(N),因此整個過程的時間複雜度爲O(N^2^)。
問題:若是把序列的長度增長到N=10^4^,10^5^,10^6^ 呢?如何將計算dp數組的時間複雜度降到O(Nlog N)?
時間複雜度O(Nlog N)生成dp數組的過程是利用二分查找來進行的優化。先生成一個長度爲N的數組ends,初始時ends[0]=arr[0],其餘位置上的值爲0。生成整型變量right, 初始時right=0。在從左到右遍歷arr數組的過程當中,求解dp[i]的過程須要使用ends數組和 right變量,因此這裏解釋一下其含義。遍歷的過程當中,ends[0..right]爲有效區, ends[right+1..N-1]爲無效區。對有效區上的位置b若是有ends[b]=c,則表示遍歷到目前爲止,在全部長度爲b+1的遞增序列中,最小的結尾數是c。無效區的位置則沒有意義。
好比,arr=[2,1,5,3,6,4,8,9,7],初始時 dp[0]=1,ends[0]=2, right=0。ends[0..0]爲有效區, ends[0]=2的含義是,在遍歷過arr[0]以後,全部長度爲1的遞增序列中(此時只有[2]),最小的結尾數是2。以後的遍歷繼續用這個例子來講明求解過程。
具體過程請參看以下代碼:
#include<stdio.h> #include<stdlib.h> /* 動態內存分配 */ #define MAXN 100000 int arr[MAXN + 10]; int dp[MAXN + 10]; int ends[MAXN + 10]; int max(int x, int y) { return x > y ? x : y; } int main() { int N, i; scanf("%d", &N); for (i = 0; i < N; ++i) { scanf("%d", &arr[i]); } dp[0] = 1; ends[0] = arr[0]; int right = 0; int ll = 0; int rr = 0; int mm = 0; for (i = 1; i < N; ++i) { ll = 0; rr = right; while (ll <= rr) { mm = (ll + rr) / 2; if (arr[i] > ends[mm]) { ll = mm + 1; } else { rr = mm - 1; } } right = max(right, ll); ends[ll] = arr[i]; dp[i] = ll + 1; } int ans = -1; for (i = 0; i < N; ++i) { if (ans < dp[i]) ans = dp[i]; } printf("%d\n", ans); /* 輸出最長遞增子序列的長度 */ /* * 下面根據dp數組還原出最長遞增子序列。 * len中記錄了最長遞增子序列的長度,固然有len=ans。 * index記錄最長遞增子序列中最後一個數在arr數組中的位置。 */ int len = 0; int index = 0; for (i = 0; i < N; ++i) { if (dp[i] > len) { len = dp[i]; index = i; } } /* * lis數組用來存放最長遞增子序列。 */ int* lis = (int*) malloc(sizeof(int) * len); lis[--len] = arr[index]; /* 最長遞增子序列中最後一個數爲arr[index] */ for (i = index; i >= 0; i--) { /* 從index位置開始從右往左掃描數組arr */ if (arr[i] < arr[index] && dp[i] == dp[index] - 1) { lis[--len] = arr[i]; index = i; } } /* 打印最長遞增子序列 */ for (i = 0; i < ans; ++i) { printf("%d", lis[i]); if (i < ans - 1) printf(" "); } printf("\n"); free(lis); return 0; }
運行結果:
推薦一:《用x種方式求第n項斐波那契數,99%的人只會第一種》,文章內容:斐波那契數列及其求法,動態規劃,數組的巧妙使用--滾動數組。
推薦二:《深刻淺出理解動態規劃(二) | 最優子結構》,文章內容:經典例題---數字三角形求解。
推薦三:《深刻淺出理解動態規劃(一) | 交疊子問題》,文章內容:記憶化搜索算法、打表法求解第n個斐波那契數。
做者: C you again,從事軟件開發 努力在IT搬磚路上的技術小白
公衆號: 【C you again】,分享計算機類畢業設計源碼、IT技術文章、遊戲源碼、網頁模板、程序人生等等。公衆號回覆 【粉絲】進博主技術羣,與大佬交流,領取乾貨學習資料
關於轉載:歡迎轉載博主文章,轉載時代表出處
求贊環節:創做不易,記得 點贊+評論+轉發 謝謝你一路支持