由2019年字节跳动第二次笔试开始学习,第一部分参考:最长不下降子序列nlogn以及输出序列,作者Milky-Way
1、最长不下降子序列-复杂度为
对于普通的最长不下降子序列,每个数都要从头开始遍历,复杂度 ,只能处理
以内的数据。
代码为:
2、复杂度为
利用序列的单调性。
对于任意一个单调序列,如 1 2 3 4 51 2 3 4 5 (是单增的),若这时向序列尾部增添一个数 xx ,我们只会在意 xx 和 55 的大小,若 x>5x>5 ,增添成功,反之则失败。由于普通代码是从头开始比较,而 xx 和 1,2,3,41,2,3,4 的大小比较是没有用处的,这种操作只会造成时间的浪费,所以效率极低。对于单调序列,只需要记录每个序列的最后一个数,每增添一个数 xx ,直接比较 xx 和末尾数的大小。只有最后一个数才是有用的,它表示该序列的最大限度值。
实现方法就是新开一个数组 dd ,用它来记录每个序列的末尾元素,以求最长不下降为例,d[k]d[k] 表示长度为k的不下降子序列的最小末尾元素。
我们用 lenlen 表示当前凑出的最长序列长度,也就是当前 dd 中的最后那个位置。
这样就很 easyeasy 了,每读入一个数 xx ,如果 xx 大于等于 d[len]d[len] ,直接让 d[len+1]=xd[len+1]=x ,然后 len++len++ ,相当于把 xx 接到了最长的序列后面;
如果 xx 小于 d[len]d[len] ,说明 xx 不能接到最长的序列后面,那就找 d[1...len−1]d[1...len−1] 中末尾数小于等于 xx 的的序列,然后把 xx 接到它后面。举个例子,若当前 x==7,len==8x==7,len==8 :
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
2 | 3 | 4 | 7 | 7 | 10 | 12 | 29 |
d[1]⋯d[5]d[1]⋯d[5] 均小于等于 xx ,若在 d[1]d[1] 后接 xx ,则 d[2]d[2] 应换成 xx ,但 d[2]==3d[2]==3 ,比 xx 小,能接更多的数,用 77 去换 33 显然是不划算的,所以 xx 不能接 d[1]d[1] 后。同理,d[2]⋯d[4]d[2]⋯d[4] 均不能接 xx 。由于 d[5]≤xd[5]≤x 且 x<d[6]x<d[6] ,77 能比 1010 接更多的数,所以选择在 d[5]d[5] 后接 xx ,用 xx 替换 1010 。
根据这个操作过程,易知数组 dd 一定是单调的序列,所以查找的时候可以用二分!二分效率是 lognlogn 的,所以整个算法的效率就是 nlognnlogn 的啦~
对于最长不下降,可以用 stlstl 中的 upperbound()upperbound() 函数,比如上述操作可以写为:
for (int i=2;i<=n;i++)
{
if (a[i]>=d[len]) d[++len]=a[i]; //如果可以接在len后面就接上
else //否则就找一个最该替换的替换掉
{
int j=upper_bound(d+1,d+len+1,a[i])-d;//找到第一个大于它的d的下标
d[j]=a[i];
}
}
但是,对于其他的单调序列,比如最长不上升等等,需要缜密地考虑,根据情况来手写二分。
注意 upperboundupperbound 是找单增序列中第一个大于 xx 的,lowerboundlowerbound 是找单增序列中第一个大于等于 xx 的,只要不是这两个,都需要手写二分。
代码:
//最长不下降子序列nlogn Song
#include<cstdio>
#include<algorithm>
using namespace std;
int a[40005];
int d[40005];
int main()
{
int n;
scanf("%d",&n);
for (int i=1;i<=n;i++) scanf("%d",&a[i]);
if (n==0) //0个元素特判一下
{
printf("0\n");
return 0;
}
d[1]=a[1]; //初始化
int len=1;
for (int i=2;i<=n;i++)
{
if (a[i]>=d[len]) d[++len]=a[i]; //如果可以接在len后面就接上
else //否则就找一个最该替换的替换掉
{
int j=upper_bound(d+1,d+len+1,a[i])-d; //找到第一个大于它的d的下标
d[j]=a[i];
}
}
printf("%d\n",len);
return 0;
}
最长不下降子序列 - NlogN
3、最长序列的序列输出-复杂度为
这时候需要增加一个 cc 数组 用来记录每个元素在最长序列中的位置,即 c[i] 表示 a[i] 被放到了序列的第几个位置。
输出时,从 数组 a 的尾部开始,逆序依次找出 c 为 len, len-1, len-2 … 3, 2, 1 的元素,并且找到一个就接着寻找 c[i]-1,直到找到 c[i] 为 1 的数。
举个例子:
a: | 13 | 7 | 9 | 16 | 38 | 24 | 37 | 18 | 44 | 19 | 21 | 22 | 63 | 15 |
c: | 1 | 1 | 2 | 3 | 4 | 4 | 5 | 4 | 6 | 5 | 6 | 7 | 8 | 3 |
len = 8;
我们从 15 开始倒着找 c 为 8 的元素,找到 63,接着找 c 为 7 的,找到 22,再找 c 为 6 的,找到 21,再找 c 为 5 …… 以此类推。
从而,我们得出的序列为 63,22,21,19,18,16,9,7
逆序输出来,就是 7,9,16,18,19,21,22,63
为什么这个方法是对的呢?倒序查找保证了两个条件:
- 如果 c 中有多个相同的数,后面的一定是最新更新的;
- 在保证一条件的前提下,倒序找,后面的数一定可以接到前面数的后面。”
代码:
//From - Milky Way
#include <cstdio>
#include <algorithm>
#include <stack>
using namespace std;
int d[100], c[100], a[100], len = 1;
int main() {
int n; scanf("%d", &n);
for (int i = 1; i <= n; ++ i) {
scanf("%d", &a[i]);
}
d[1] = a[1], c[1] = 1;
for (int i = 2; i <= n; ++ i) {
if (d[len] <= a[i]) {
d[++ len] = a[i], c[i] = len;
} else {
int j = upper_bound(d + 1, d + len + 1, a[i]) - d;
d[j] = a[i], c[i] = j;
}
}
stack<int> sta;
for (int i = n, j = len; i >= 1; -- i) {
if (c[i] == j) {
sta.push(a[i]); --j;
}
if (j == 0) break;
}
printf("%d\n", len);
while (!sta.empty()) {
printf("%d ", sta.top());
sta.pop();
}
return 0;
}
最长不下降之输出子序列 - NlogN