1. 程式人生 > >三種演算法求一個數字序列的最長遞增子序列

三種演算法求一個數字序列的最長遞增子序列

也有很多部落格寫如何實現最長遞增子序列的演算法,自己查閱了一些資料總結出三種實現的演算法,兩種是常見的處理思路,還有一種是本人自己想出來的演算法,很好理解,但是效率不是特別高。

演算法一:

將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;
}