刚入学时学的算法,已经忘的差不多了,回顾一下。
对于普通的最长不下降子序列,每个数都要从头开始遍历,复杂度 $O(n^2)$,只能处理 $10^4$ 以内的数据。
刚刚学弟问我,就写了一下普通版的,顺便贴一下,这是 $openjudge$ 上的最长上升序列。
废话不多说,$nlogn$ 的算法如何实现?
利用序列的单调性。
对于任意一个单调序列,如 $1\ 2\ 3\ 4\ 5$(是单增的),若这时向序列尾部增添一个数 $x$,我们只会在意 $x$ 和 $5$ 的大小,若 $x>5$,增添成功,反之则失败。由于普通代码是从头开始比较,而 $x$ 和 $1,2,3,4$ 的大小比较是没有用处的,这种操作只会造成时间的浪费,所以效率极低。对于单调序列,只需要记录每个序列的最后一个数,每增添一个数 $x$,直接比较 $x$ 和末尾数的大小。只有最后一个数才是有用的,它表示该序列的最大限度值。
实现方法就是新开一个数组 $d$,用它来记录每个序列的末尾元素,以求最长不下降为例,$d[k]$ 表示长度为k的不下降子序列的最小末尾元素。
我们用 $len$ 表示当前凑出的最长序列长度,也就是当前 $d$ 中的最后那个位置。
这样就很 $easy$ 了,每读入一个数 $x$,如果 $x$ 大于等于 $d[len]$,直接让 $d[len+1]=x$,然后 $len++$,相当于把 $x$ 接到了最长的序列后面;
如果 $x$ 小于 $d[len]$,说明 $x$ 不能接到最长的序列后面,那就找 $d[1...len-1]$ 中末尾数小于等于 $x$ 的的序列,然后把 $x$ 接到它后面。举个例子,若当前 $x==7,len==8$:
|
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
|
2 |
3 |
4 |
7 |
7 |
10 |
12 |
29 |
$d[1]\cdots d[5]$ 均小于等于 $x$,若在 $d[1]$ 后接 $x$,则 $d[2]$ 应换成 $x$,但 $d[2]==3$,比 $x$ 小,能接更多的数,用 $7$ 去换 $3$ 显然是不划算的,所以 $x$ 不能接 $d[1]$ 后。同理,$d[2]\cdots d[4]$ 均不能接 $x$。由于 $d[5]\le x$ 且 $x<d[6]$,$7$ 能比 $10$ 接更多的数,所以选择在 $d[5]$ 后接 $x$,用 $x$ 替换 $10$。
根据这个操作过程,易知数组 $d$ 一定是单调的序列,所以查找的时候可以用二分!二分效率是 $logn$ 的,所以整个算法的效率就是 $nlogn$ 的啦~
对于最长不下降,可以用 $stl$ 中的 $upperbound()$ 函数,比如上述操作可以写为:
1 for (int i=2;i<=n;i++) 2 { 3 if (a[i]>=d[len]) d[++len]=a[i]; //如果可以接在len后面就接上 4 else //否则就找一个最该替换的替换掉 5 { 6 int j=upper_bound(d+1,d+len+1,a[i])-d;//找到第一个大于它的d的下标 7 d[j]=a[i]; 8 } 9 }
但是,对于其他的单调序列,比如最长不上升等等,需要根据情况来手写二分。
注意 $upperbound$ 是找单增序列中第一个大于 $x$ 的,$lowerbound$ 是找单增序列中第一个大于等于 $x$ 的,只要不是这两个,都需要手写二分。
代码:
1 //最长不下降子序列nlogn Song 2 3 #include<cstdio> 4 #include<algorithm> 5 using namespace std; 6 7 int a[40005]; 8 int d[40005]; 9 10 int main() 11 { 12 int n; 13 scanf("%d",&n); 14 for (int i=1;i<=n;i++) scanf("%d",&a[i]); 15 if (n==0) //0个元素特判一下 16 { 17 printf("0\n"); 18 return 0; 19 } 20 d[1]=a[1]; //初始化 21 int len=1; 22 for (int i=2;i<=n;i++) 23 { 24 if (a[i]>=d[len]) d[++len]=a[i]; //如果可以接在len后面就接上 25 else //否则就找一个最该替换的替换掉 26 { 27 int j=upper_bound(d+1,d+len+1,a[i])-d; //找到第一个大于它的d的下标 28 d[j]=a[i]; 29 } 30 } 31 printf("%d\n",len); 32 return 0; 33 }