三種演算法求一個數字序列的最長遞增子序列
阿新 • • 發佈:2019-02-10
也有很多部落格寫如何實現最長遞增子序列的演算法,自己查閱了一些資料總結出三種實現的演算法,兩種是常見的處理思路,還有一種是本人自己想出來的演算法,很好理解,但是效率不是特別高。
演算法一:
將n個數的原序列A[n]排序後得到遞增序列B[n],則把求A的最長單調遞增子序列問題轉化成求A、B序列的最長公共子序列(LCS)。
這個演算法是演算法導論書後面習題的標準答案解法了,有很多人寫過實現。時間複雜度O(n^2)。
#include<iostream> #include<stdio.h> #include<cmath> #include<string> #include<string.h> #include<set> #include<map> #include <algorithm> using namespace std; /*方法1:將這n個數的序列排序之後,將最長遞增子序列轉變為LCS*/ int main() { int n; int A[100], B[100], res[100], len[105][105]; while (scanf("%d", &n) == 1) { memset(res, 0, sizeof(res)); memset(len, 0, sizeof(len)); for (int i = 0; i < n; i++) { scanf("%d", &A[i]); B[i] = A[i]; } sort(B, B + n); int i, j, cnt = 0; for (i = 1; i <= n; i++) { for (j = 1; j <= n; j++) { if(A[i-1] == B[j-1]) len[i][j] = 1 + len[i-1][j-1]; else len[i][j] = max(len[i-1][j], len[i][j-1]); } } //輸出任意一個最長公共子序列,倒敘遍歷len陣列 for (i = n, j = n; i > 0 && j > 0;) { if (len[i][j] == len[i-1][j]) { i--; } else if(len[i][j] == len[i][j-1]) { j--; } else { res[cnt++] = A[i-1]; i--; j--; } }
printf("%d\n%d", cnt, res[cnt-1]);//輸出這個最長公共子序列。
for (i = cnt - 2; i >= 0; i--) printf(" %d", res[i]);
printf("\n");
}
return 0;
}
注意:這種求法在輸入序列中元素各不相同是可行的,一旦A中有重複元素,單純的將A sort一下是不可以的,這時候要先去重再排序得到B序列。
演算法二:
因為自己正在學DP,所以一開始是嘗試使用DP的思想做的,時間複雜度也是O(n^2)。以dp[i]表示以i位結尾的最長遞增子序列的長度。那麼dp[i] = max(dp[i], dp[j]+1), j = 1, 2, 3,...,i-1。對於每個j<i遍歷求出最大的dp[i],並用res[i] = j 來記錄以i位結尾的最長遞增子序列的前一位是誰,方便之後遍歷輸出子序列。
#include<iostream> #include<stdio.h> #include<cmath> #include<string> #include<string.h> #include<set> #include<map> #include <algorithm> using namespace std; /*輸出最長遞增子序列*/ /*方法2:n^2複雜度,dp*/ int main() { int n; int num[100], res[100],dp[100],ans[100]; while (scanf("%d", &n) == 1) { memset(res, 0, sizeof(res)); memset(dp, 0, sizeof(dp)); memset(ans, 0, sizeof(ans)); for (int i = 0; i < n; i++) { scanf("%d", &num[i]); } dp[0] = 1; res[0] = 0; for (int i = 1; i < n; i++) { dp[i] = 1; for (int j = 0; j < i; j++) { if(num[i] > num[j]) { dp[i] = max(dp[i], dp[j] + 1); if(dp[i] == (dp[j] + 1)) res[i] = j; } } if(dp[i] == 1) res[i] = i; } //遍歷得到最長的遞增子序列的長度以及結尾的位數 int maxlen = dp[0], maxloc = 0; for(int i = 1; i < n; i++) { if(dp[i] > maxlen) { maxlen = dp[i]; maxloc = i; } } //printf("%d\n",maxlen); //遍歷res陣列,將這個最長遞增子序列儲存並輸出 int cnt = 0; ans[cnt++] = num[maxloc]; while (res[maxloc] != maxloc) { maxloc = res[maxloc]; ans[cnt++] = num[maxloc]; } for (int i = cnt - 1; i > 0; i--) { printf("%d ",ans[i]); } printf("%d\n", ans[0]); } return 0; }
演算法三:
這個演算法是效率比較高的dp求解。時間複雜度是O(nlgn)。關鍵是要抓住一個長度位i的候選子序列的尾元素至少不比一個長度為i-1的候選子序列的尾元素小。這是因為長度為i的候選單調遞增子序列一定是從某個長度為i-1的候選單調遞增子序列再加一個尾元素得到的,以L[i]表示長度為i的候選子序列的尾元素,則有L[1]<=L[2]<=L[3]<=...<=L[m],m為最長候選子序列長度。
#include<iostream>
#include<stdio.h>
#include<cmath>
#include<string>
#include<string.h>
#include<set>
#include<map>
#include <algorithm>
using namespace std;
/*方法3:nlgn複雜度。一個長度為i的尾元素至少不比一個長度為i-1的尾元素小。*/
int main() {
int n;
int num[100], L[100], prel[100], M[100], res[100];
while (scanf("%d", &n) == 1) {
memset(L, 0, sizeof(L));//L[i]記錄的是在所有長度為i的子序列中尾元素最小的那個數值
memset(prel, 0, sizeof(prel));//記錄遞增序列尾元素的前一位元素在原序列中的位置。
memset(M, 0, sizeof(M));//記錄尾元素在原序列中的位置。
int len = 0, i, pos;
for (i = 0; i < n; i++) {
scanf("%d", &num[i]);
}
L[0] = num[0];
M[0] = 0;
prel[0] = -1;
len = 1;
for (i = 1; i < n; i++) {
pos = lower_bound(L, L + len, num[i]) - L;
L[pos] = num[i];
M[pos] = i;
if(pos > 0) prel[i] = M[pos-1];
else prel[i] = -1;
if (pos == len) len++;
}
//for (i = 0; i < len; i++) printf("%d ", L[i]);
pos = M[len-1];
for (i = len - 1; i >= 0 && pos != -1; i--) {
res[i] = num[pos];
pos = prel[pos];
}
//輸出
for (i = 0; i < len - 1; i++) {
printf("%d ",res[i]);
}
printf("%d\n", res[len-1]);
}
return 0;
}