【基础模型】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;
}