動態規劃-最長上升子序列模型

Kocoder發表於2021-03-09

1. 題目描述

給定一個長度為N的數列,求數值嚴格單調遞增的子序列的長度最長是多少。

輸入格式

第一行包含整數N。

第二行包含N個整數,表示完整序列。

輸出格式

輸出一個整數,表示最大長度。

資料範圍

\(1≤N≤1000\)
\(−10^9≤數列中的數≤10^9\)

輸入樣例:

7
3 1 2 1 8 5 6

輸出樣例:

4

2. (DP)樸素解法\(O(n^2)\)

本題是一個簡單的DP問題。

\(a[i]\)表示陣列中第\(i\)個數,\(f[i]\)表示以陣列中第\(i\)個數結尾的最長上升子序列的長度。

\(f[i] = max(f[j]) + 1,j < i 且a[j] < a[i]\)
程式碼如下:

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;

int f[N], a[N];
int n;

int main()
{
    cin >> n;
    for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);
    int res = 0;
    for(int i = 1; i <= n; i ++)
    {
        // 注意初始化f[i] = 1.
        f[i] = 1;   
        for(int j = 1; j < i ; j ++)
            if(a[j] < a[i])
                f[i] = max(f[i],f[j] + 1);
        res = max(res, f[i]);
    }
    cout << res << endl;
	return 0;
}

3. (DP+貪心)優化版本\(O(nlgn)\)

通過對本題的觀察與思考。我們可以得到如下的事實:

  • 對於兩個長度相同的子序列,假設兩個子序列的最後一個值分別是\(x_1, x_2\),且\(x_1 > x_2\)。假設我們的\(a[i]\)可以放在最後一個值為\(x_1\)的序列的後面,則其一定能夠放在最後一個值為\(x_2\)的序列的後面。故,對於長度相同的序列,我們只需要儲存最後一個值小的序列即可。

當我們掃描到第\(i\)個數的時候,可以將其前面的數構成的子序列按長度進行分類,長度為\(1\)的最長上升子序列只儲存結尾值最小的,長度為\(2\)的最長上升子序列只儲存結尾值最小的,那麼我們可以證明:不同長度最長上升子序列最後一個數的值是隨長度嚴格單調遞增的

證明:

假設長度為\(5\)的最長上升子序列最後一個數為\(a\),長度為\(6\)的最長上升子序列最後一個數為\(b\),倒數第二個數為\(c\)

反證法:如果\(a>=b\),由於\(b > c\),則\(a>c\)。又由於對於長度相同的子序列我們只儲存最後一個數值較小值。故\(a< c\)。推出矛盾。

這樣的話,當我們掃描到第\(i\)個數的時候,就可以通過二分法,找到小於\(a[i]\),且最後一個數最大的子序列,將其該數新增到子序列後面。

程式碼如下:

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;

int n;
int a[N];
int q[N];

int main()
{
    cin >> n;
    for(int i = 0; i < n; i ++)cin >> a[i];

    int len = 0;
    q[0] = -2e9;
    for(int i = 0; i < n; i ++)
    {
        int l = 0, r =len ;
        while(l < r)  // 二分法找出小於等於a[i]的最大的子序列。
        {
            int mid = l + r + 1 >> 1;
            if(q[mid] < a[i]) l = mid;
            else r = mid - 1;
        }
        len = max(len, r + 1);  // 更新長度
        q[r + 1] = a[i];        // 更新末尾值

    }
    cout << len << endl;
    return 0;
}


注:程式碼中含有部分細節沒有詳細說明,供讀者思考

相關文章