問題:
給你一個數列,讓你找出最長上升子序列。(子序列大概就是說從前往後按順序取數,可以不連續取)
例如: 1 2 5 8 9 1 2 6 7 8
最長上升子序列就是1 2 5 6 7 8 ,長度爲6.
如果現在我們只想求長度,那麼一個十分樸素的想法就是:
令\(f[i]\)表示以s[i]爲結尾的最長上升子序列長度,那麼易得\(f[i] = max(f[j])\)其中j滿足\(s[j] < s[i]\).
然後我們分別枚舉i和j,那麼我們就得到了一個\(n^2\)的算法。
但如果我們需要更優秀的時間複雜度呢?
可以考慮這樣一種思路:
維護一個單調上升的數組,不斷的加入新的數,用更優秀的數代替不那麼優的數,最後得到一個最優的長度。
那麼考慮如何維護。
我們從前往後一個一個的加入新數,如果這個新數比數組最後一個還要大,那麼加入到數組末尾。
否則二分查找出這個數組內最靠前的比新數大的數,然後用新數替換掉它。
最後數組長度就是最長上升子序列的長度.
我們來證明這樣爲什麼是正確的。
如果你直接把數塞到數組末尾,這個正確性是顯然的對吧。
那麼我們其實只需要證明爲什麼把數塞到數組中間(即替換掉一個前面的數),其實是不影響後續操作的正確性的。
假設我們用新數x替換掉了第t個數,那麼對於t之前的數而言,你只是在它們末尾塞了一個數,所以對它們沒有影響,而對於t位置本身,如果後面的數想要接在這個數後面成爲一個子序列的話,其實完全可以選擇接在這個新數x後面,畢竟x比t之前的所有數都要大,因此可以取代t數在原來子序列當中的地位,而x又比t小,因此能接在t後面的數肯定也能接在x後面,而且既然是“後面的數”,那肯定不用擔心順序問題。因此對於t位置也不會有影響。
那麼對於t後面的數呢?
我們可以發現,在這個數列裏面,我們其實是在用一個數去代表一個子序列,每個數就代表了以它爲末尾最長的那個子序列,它所在的位置其實就是這個子序列的長度。
因此對於後面來的任意一個數而言,如果我們想接到一個前面的子序列,實際上我們只需要用到那個子序列的末尾數字。因此在該數組中某個數y前面的數被修改了其實根本不會對這個數y產生影響,因爲後面的數要用到y的時候,只需要用到y本身就足以獲取長度和末尾信息。
小提醒:這個算法最後得到的數組並不一定是最優的最長上升子序列,因爲你修改數組中間的數,雖然不會影響後期長度更新的正確性,但還是改變了這個數組的組成的。
#include<bits/stdc++.h>
using namespace std;
#define Ri register int
#define AC 110000
int n, m, ans;
int s[AC];
int read()
{
int x = 0; char c = getchar();
while(c > '9' || c < '0') c = getchar();
while(c <= '9' && c >= '0') x = x * 10 + c - '0', c = getchar();
return x;
}
int half(int x)//find the first one which is bigger than x
{
int l = 1, r = m;
if(l > r) return l;
while(l < r)
{
int mid = (l + r) >> 1;
if(s[mid] < x) l = mid + 1;
else r = mid;
}
return l;
}
void work()
{
n = read();
for(Ri i = 1; i <= n; i ++)
{
int now = read();
if(!m || s[m] < now) s[++ m] = now;
else s[half(now)] = now;
}
printf("%d\n", m);
for(Ri i = 1; i <= m; i ++) printf("%d ", s[i]);
printf("\n");
}
int main()
{
freopen("1.in", "r", stdin);
work();
fclose(stdin);
return 0;
}