【CF486E】LIS of Sequence题解

时间:2022-12-30 11:43:47

【CF486E】LIS of Sequence题解

题目链接

题意:

给你一个长度为n的序列a1,a2,...,an,你需要把这n个元素分成三类:1,2,3:

1:所有的最长上升子序列都不包含这个元素

2:有但非所有的最长上升子序列包含这个元素

3:所有的最长上升子序列都包含这个元素

输入格式:

第一行包含一个正整数n,表示序列的长度。第二行包含n个正整数a1,a2,...,an,表示序列中的元素。

输出格式:

一行,包含一个长度为n的、由1,2,3三种数字组成的字符串,第i个数字表示ai所属类别。

样例输入1:

1
4

样例输出1:

3

样例输入2:

4
1 3 2 5

样例输出2:

3223

样例输入3:

4
1 5 2 3

样例输出3:

3133

时空限制:

每个测试点2s,256MB

数据范围:

1≤n≤105

1≤ai≤105

题解:

首先,O(nlogn)求LIS大家应该都会吧……这里就不阐述了。

我们可以通过DP来算出以每个点i结尾的最长上升子序列的长度,记为length[i],把整个序列的LIS长度记为len。

然后,大家要想明白一个命题:

如果元素i可能出现在LIS中,那么它在LIS中的位置一定刚好等于length[i]。

这个自证不难。

其次,我们可以用一个“分层数组”,其实所谓的“分层数组”就是一个二维数组,只不过我习惯把第一维叫做“层”罢了,第二维因为节省空间,我们开一个vector。

这个“分层数组”相当重要,它是我们解决问题的核心。我们在DP时就已经算出每个元素的length[i]了,因此我们把这个元素的编号添加到层数为length[i]的vector中去,因此“分层数组”中每一层的每个点就和序列中的元素一一对应。

这就是“分层数组”的建立过程,可参见代码注释。

然后呢?

大家又需要想明白两个命题:

在每一层中,随着编号的递增,与点对应的元素的值单调不递增。

每一层(除第一层外)中的每个点一定在前一层中有编号小于它且序列元素值小于它的节点。

考虑我们DP的过程,我们在更新dp数组中下标为length[i]的最小值时,如果已经通过二分查找算出该元素的length[i],元素可以更新dp[length[i]]的条件是元素的值一定不能比dp[length[i]]大,而dp[length[i]]已经通过前面的元素算出,因此在“分层数组”层数为length[i]的层中,该元素的值一定小于等于在该层中编号比它小的元素的值。

另外,我们在DP时,依据dp数组单调递增的特性可以理解dp[length[i]-1]的值一定比a[i]小,因此在“分层数组”的前一层中,一定有编号小于它且序列元素值小于它的点,这样保证我们在逐层往前推的时候不会出现“一对空”的情况,而是在前一层中是有一个区间(包含编号、序列元素值两方面的限制)与之对应。这个区间中的点就是当前点在前一层中可以扩展到的节点。

大家可能有疑问:为什么要倒着从后往前推呢?

因为倒着推能够保证要扩展的当前点一定有LIS经过它,因此它扩展到的前一层的节点也一定有LIS经过,等到该层所有点扩展完毕后,前一层没有被扩展到的点一定没有LIS经过,打上“1”类标记,下次扩展就不要扩展这些点了。而其他的点都打上“2”类标记,如果只有一个“2”类标记,那么把“2”改成“3”,说明该节点是所有LIS的必经节点。

参考代码如下(若有不理解的参看注释):

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 #include<vector>
 5 #define maxn 100005
 6 using namespace std;
 7 int n,a[maxn];//整个序列
 8 int dp[maxn];//dp[i]代表当前长度为i的上升子序列末尾元素的最小值
 9 int len=0;//整个序列LIS的长度
10 int sign[maxn];//序列元素所属类别
11 vector<int>layer[maxn];//分层数组,一共有len层。layer[i]中的点表示这些元素:以这些元素结尾的最长上升子序列长度为i
12 inline int bins(int i,int val){
13     int l=0,r=layer[i].size()-1;
14     while(l<r){
15         int m=(l+r)>>1;
16         if(a[layer[i][m]]>=val)l=m+1;
17         else r=m;
18     }
19     return l;
20 }
21 int main(){
22     scanf("%d",&n);
23     memset(dp,0x3f,sizeof(dp));
24     dp[0]=0;
25     for(int i=1;i<=n;i++){
26         scanf("%d",&a[i]);
27         int length=lower_bound(dp,dp+n+1,a[i])-dp;//O(nlogn)求LIS
28         dp[length]=min(dp[length],a[i]);
29         //length表示以该元素结尾的最长上升子序列的长度
30         layer[length].push_back(i);//加入到分层数组
31         len=max(len,length);//更新整个序列的LIS长度
32     }
33     for(int i=1;i<=n;i++)sign[i]=1;//初始化全都为第1类,即任何LIS都不经过它们
34     if(layer[len].size()==1)sign[layer[len][0]]=3;
35     else for(int i=0;i<layer[len].size();i++)sign[layer[len][i]]=2;
36     for(int i=len;i>=2;i--){//倒序处理分层数组,一层一层往前推
37         for(int j=0;j<layer[i].size();j++){//枚举当前层的所有点
38             int bh=layer[i][j];//点的编号
39             if(sign[bh]>1){//如果当前节点可以向前扩展(存在LIS经过当前点)
40                 int l=bins(i-1,a[bh]);//二分查找,扩展的节点在序列中的值必须小于当前节点,才能保证LIS严格递增
41                 int r=lower_bound(layer[i-1].begin(),layer[i-1].end(),bh)-layer[i-1].begin()-1;//二分查找,扩展的点编号必须小于当前点编号,才能是“序列”
42                 //当前点可扩展到的前一层的点的范围是区间[l,r]
43                 for(int k=l;k<=r;k++)sign[layer[i-1][k]]=2;//打上标记,该节点能够被扩展到说明一定在整个序列中有某个LIS包含该点
44             }
45         }
46         //当前层能够扩展到的前一层的点是当前层所有点能扩展到的前一层的节点的并集
47         int cnt=0,pos=0;
48         for(int j=0;j<layer[i-1].size();j++)if(sign[layer[i-1][j]]==2){
49             cnt++;
50             pos=j;
51         }
52         if(cnt==1)sign[layer[i-1][pos]]=3;//如果该层所有可扩展的点只能在前一层中扩展出一个节点,说明这个节点是所有LIS的必经节点。
53     }
54     for(int i=1;i<=n;i++)printf("%d",sign[i]);//不留空格打印
55     return 0;
56 }