Luogu P2215 【[HAOI2007]上升序列】


  • P2215 【[HAOI2007]上升序列】

这是一道比较基础的dp题,可以往LIS的思路上靠,但其实并不是完全套用LIS。

题解区有一位大佬的思路与我很是类似,但是他的解释貌似有点少,这里我再详细说一下吧。


  • Prelude

虽然我要说的是dp思路,但是其他大佬的做法真的很好,于是我在这里顺便说下。

为什么要放在Prelude里面,因为在想到dp做法前我甚至已经想到其余做法,只是觉得dp更好实现。

对于这题我目前已知2种思路(准确来说还可细分):

  • dp思路

  • 树状数组 & 线段树

对于后两者,其实很好想,但是也有缺点,这题万幸中保证了\(a_i\)并不大,如果\(a_i\)范围很大,离散化就不可实现。但是即便如此,想到离散化想必在考场上可以骗到不少分吧(

而且其实线段树和树状数组用的也就是dp思路,只不过线段树和树状数组是把点值离散化区间维护后直接用自身数据结构具备的方法找\(LIS\)

我们再来思考dp,因为题目要求我们输出这个子序列,而不是序列和或者其他信息。

所以我们想必在输出的时候会扫一遍整个数组,这样最坏复杂度就是\(O(NM)\)已经比较小慢了。也不能在接受更高一级的复杂度了。

但是我们在扫的时候只需要考虑当前这个数是否输出。

因为题意中的字典序最小其实是对于序列中那些\(a_i\)下标\(i\)的字典序最小,所以遇到一个能输出就一定输出。

于是我们可以得出dp思路,如下。


  • Solution

对于dp,我先讲下最朴素的思路,也就是并没有到\(O(N^2)\)的预处理和\(O(NM)\)的输出,这个题数据再加强一下就可能被卡的那种

不过我们先考虑下Impossible的情况,我们只需要求出整个序列的那个最长子序列长度,对于查询长度小于等于最长长度的必然有解,因为可以从中随意挑选数个组成\(LIS\),如果大于最长长度必然没解,这个很显然吧。而这个长度我们可以在dp时就求出来了。

我们设dp状态\(f_i\)为从\(i\)这个位置开始那段区间的最长上升子序列长度

显然f[n]=1;

如何求呢,我们逆序扫描\(i\)n-1~1,然后扫\(j\)i+1~n,当\(a_i这就是一个长度为2的上升子序列了,那么

\[f_i=max\left\{f_j+1\right\}(a_i

我们可以理解为把\(a_i\)塞到这个数后面的那些上升子序列中的第一位,这样后面的最长的上升子序列长度\(+1\) 就是\(f_i\)最长。(当然要保证\(a_i\)是可以放的)

可能会有疑问:为什么要求这个。

这样我们在后面输出就可以\(O(1)\)判断这个数是否可以输出了。

我们设\(l\)为剩下的还没输出的数的个数,那么一个数可以输出当且仅当:

  • \(a_i > pre\) 这个数比上一个输出的那个数大,这个用一个变量(\(pre\))记录就可以了。

  • \(f_i \geq l\),说明输出这个数的话至少可以保证后面一直可以输出到\(L\)个数。

因为要求的是下标字典序最小,所以这个数可以输出就输出即可,输出到\(L\)个自动退出。


  • Code

#include
#include
#include
#include
#include

#define N 10005

using namespace std;

int a[N],f[N];

int main()
{
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
	}
	int maxl=1;
	f[n]=1;
	for(int i=n-1;i>0;i--)
	{
		f[i]=1; 
		for(int j=i+1;j<=n;j++)
		{
			if(a[j]>a[i]&&f[j]+1>le)f[i]=f[j]+1;//转移方程
		}
		maxl=max(maxl,f[i]);//maxl就相当于f[1],这步好像是多余的/xk
	}
	int m;
	scanf("%d",&m);
	while(m--)
	{
		int q;
		scanf("%d",&q);
		if(q>maxl)puts("Impossible");//不合法的情况
		else 
		{
			int pre=-(1<<30);
			bool ok=0;
			for(int i=1;i<=n;i++)
			{
				if(f[i]>=q&&a[i]>pre)//如上
				{
					printf("%d ",a[i]);
					pre=a[i];//记录下输出过的数中最大的
					q--;
					if(q<=0){putchar('\n');ok=1;break;}
                  /*一旦输出了q个*/
				}
			}
			if(!ok)putchar('\n');//这个貌似也无所谓
		}
	}
	return 0;
}

虽然我给出的是最朴素的算法,但是二分速度明显要比这个代码快,所以我在这里还是再说一下:

二分的思路大致与我们相似,但是他要有一个数组来存当前位置之前的\(LD(decreasing)S\),然后每次dp二分查找这个序列中最后严格大于\(a_i\)的数的位置\(j\),然后转移方程。最后记得每次用\(a_i\)更新区间内符合\(a_i\)大小的那个地方的值。

扫描还是倒序的,每次\(O(logN)\)查询,所以预处理总复杂度\(O(NlogN)\),输出同理。

到了最后,那个记录的数组最后存储的就是整个序列的\(LDS\)

可能说的有点糊,大家可以参考下题解区其他大佬的代码,虽然他们解释的也不多