1. 程式人生 > >最長上升子序列問題的幾種解法

最長上升子序列問題的幾種解法

拿POJ 2533來說。

Sample Input

7
1 7 3 5 9 4 8

Sample Output(最長上升/非降子序列的長度)

4

從輸入的序列中找出最長的上升子序列(LIS)。

這題一看,是一道典型的DP問題(就是動態規劃),可以用dfs,深度優先遍歷來解,如下程式碼:

複製程式碼
#include<iostream>
#include<stack>usingnamespace std;
 
int n;
int* a;
stack<int> s;
int count=0;
int best=0;

void dfs(int i)
{
  if(i==n)
  {
    
if(s.size()>best) best=s.size(); return; } if(s.empty()||a[i]>s.top()) { s.push(a[i]); dfs(i+1); s.pop(); } if(s.size()+n-i-1>best) dfs(i+1); } int main() { while(cin>>n) { int i; a=newint[n]; for(i=0;i<n;i++) { cin>>a[i]; } dfs(0); cout
<<best<<endl; delete [] a; } return0; }
複製程式碼

  其實為了簡化程式碼以及演算法效率,我們可以用資料組來代替遞迴。。。下面我就給出LIS的DP的資料組形式的解法:

(懶得寫了,就拿個別人的程式碼來展示吧)

複製程式碼
#include <iostream>#define SIZE 1001usingnamespace std;
 
int main()
{
    int i, j, n, max;
    /* a[i]表示輸入第i個元素 */int a[SIZE];
    /* d[i]表示以a[i]結尾的最長子序列長度 */int d[SIZE];
 
    
while(cin >> n) { for (i =1; i <= n; i++) { cin >> a[i]; } max =0; for (i =1; i <= n; i++) { d[i] =1; for (j =1; j <= i -1; j++) { if (a[j] < a[i] && d[i] < d[j] +1) //這邊,要注意 d[i] < d[j] + 1這個條件的限制,它是為了在 //連續幾個 d[i]相同時 只加一次 { d[i] = d[j] +1; } } /* 記錄最長子序列 */if (d[i] > max) max = d[i]; } cout << max << endl; } //system("pause");return0; }
複製程式碼

(作者的解釋:)令A[i]表示輸入第i個元素,D[i]表示從A[1]到A[i]中以A[i]結尾的最長子序列長度。對於任意的0 <  j <= i-1,如果A(j) < A(i),則A(i)可以接在A(j)後面形成一個以A(i)結尾的新的最長上升子序列。對於所有的 0 <  j <= i-1,我們需要找出其中的最大值。

DP狀態轉移方程:

D[i] = max{1, D[j] + 1} (j = 1, 2, 3, ..., i-1 且 A[j] < A[i])

解釋一下這個方程,i, j在範圍內:

如果 A[j] < A[i] ,則D[i] = D[j] + 1

如果 A[j] >= A[i] ,則D[i] = 1

其實上面的方法的複雜度都達到了O(n^2)的數量級,有沒有更好的解法呢?

有,用棧。

這個解法不是我想的,從網上學來的,這裡面與大家分享一下。這個演算法的複雜度只有O(nlogn),在有大量資料的情況下,這演算法效率極高。。。

(摘錄原作者的話)

這個演算法其實已經不是DP了,有點像貪心。至於複雜度降低其實是因為這個演算法裡面用到了二分搜尋。本來有N個數要處理是O(n),每次計算要查詢N次還是O(n),一共就是O(n^2);現在搜尋換成了O(logn)的二分搜尋,總的複雜度就變為O(nlogn)了。

這個演算法的具體操作如下(by RyanWang):

開一個棧,每次取棧頂元素top和讀到的元素temp做比較,如果temp > top 則將temp入棧;如果temp < top則二分查詢棧中的比temp大的第1個數,並用temp替換它。 最長序列長度即為棧的大小top。

這也是很好理解的,對於x和y,如果x < y且Stack[y] < Stack[x],用Stack[x]替換Stack[y],此時的最長序列長度沒有改變但序列Q的''潛力''增大了。

舉例:原序列為1,5,8,3,6,7

棧為1,5,8,此時讀到3,用3替換5,得到1,3,8; 再讀6,用6替換8,得到1,3,6;再讀7,得到最終棧為1,3,6,7。最長遞增子序列為長度4。

我想,當出現1,5,8,2這種情況時,棧內最後的數是1,2,8不是正確的序列啊?難道錯了?

分析一下,我們可以看出,雖然有些時候這樣得不到正確的序列了,但最後算出來的個數是沒錯的,為什麼呢?

想想,當temp>top時,總個數直接加1,這肯定沒錯;但當temp<top時呢? 這時temp肯定只是替換了棧裡面的某一個元素,所以大小不變,就是說一個小於棧頂的元素加入時,總個數不變。這兩種情況的分析可以看出,如果只求個數的話,這個演算法比較高效。但如果要求打印出序列時,就只能用DP了。

用該演算法完成POJ2533的具體程式碼如下:

複製程式碼
#include <iostream>#define SIZE 1001usingnamespace std;
 
int main()
{
    int i, j, n, top, temp;
    int stack[SIZE];
    while(cin >> n)
   {
    top =0;
    /* 第一個元素可能為0 */
    stack[0] =-1;
    for (i =0; i < n; i++)
    {
        cin >> temp;
        /* 比棧頂元素大數就入棧 */if (temp > stack[top])
        {
            stack[++top] = temp;
        }
        else
        {
            int low =1, high = top;
            int mid;
            /* 二分檢索棧中比temp大的第一個數 */while(low <= high)
            {
                mid = (low + high) /2;
                if (temp > stack[mid])
                {
                    low = mid +1;
                }
                else
                {
                    high = mid -1;
                }
            }
            /* 用temp替換 */
            stack[low] = temp;
        }
    }
 
    /* 最長序列數就是棧的大小 */
    cout << top << endl;
  } 
    return0;
}

文章出處:http://www.cnblogs.com/dartagnan/archive/2011/08/29/2158247.html