【基礎模型】LIS 最長上升子序列

問題:
給你一個數列,讓你找出最長上升子序列。(子序列大概就是說從前往後按順序取數,可以不連續取)
例如: 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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章