目录
最长上升子序列
何谓最长上升的子序列呢?首先我们要明白,对于一串数字来说,子序列必须要有顺序的描述,例如54321,543是一个子序列,421同样也是,但是4231就不是子序列了,因为里面的顺序与原串已经不相等了。而要求的是上升的子序列,也就是从头到尾,序列的值依次增大,例如12345,它本身就是一串递增的序列,那么理所当然最长上升子序列的数量便为5。我们看下面的一个例子。
相信大家看完例子之后,便对最长上升子序列有了很深刻的了解,而了解这一个名词,并且知道如何求一个字串的最长上升子序列,对于后面要求公共上升子序列会有很大的帮助。
求解最长上升子序列
①暴力法
顾名思义,暴力法yyds,也就是说我们可以写循环来进行,让一个指针从最开头开始,一直不断地计算出每一个数为开头,然后上升的所有子序列,这一部分相信很多读者能够独立想出,最终的时间复杂度大概是o(),在此不加以赘述。
②dp动态规划+二分优化+贪心
1.dp数组的构建
很多人会想说,这题怎么可以用动态规划呢?而且我们所需要的dp数组到底是个啥子东西哦?其实刚开始接触这样的题目的时候我也会感到很疑惑?但是此dp非彼dp,我们构建的一个新dp数组确实可能会不断的更新,但是这样的更新实际上是一种贪心的思想。我们暂且把这个dp数组称为f数组用f【i】表示,坐标i下f数组内最小的末尾值。根据这样的定义,f数组存的数一定是单调的,这就为后续二分优化时间复杂度,提供了很好的解题思路。
2.贪心的思想
其实应该会有少部分神犇能够一眼看出这样的贪心,但是对于我这样一个蒟蒻来说,只能通过一些画图,去找到其中的奥妙。下面是实验图。
首先我们看到这一个图片,不同的颜色表示了f数组的更新过程。我们可以看到f【1】=a【1】,因为至少有一个数本身属于上升的所以我们把length(最后要求的最长上升子序列的个数)初始化为1。然后有一个指针开始往后移动,如果指针所处位置指向的a【i】>f【len】的话,我们就会让len++,然后继续移动指针,但是如果小于的话,我们则需要找到一个新的位置更新f数组。这是为什么呢?我们看到途中的红黄f数组,我们看到其中的f【2】从3变成了2,这样的改变就是贪心的关键所在。我们注意到f数组的那一次更新,并不会影响到在那一瞬间的len的改变,但是我们做了一件非常好的事情,当f数组在更新的那一时刻,我们让它变得更小了,为什么要变得更小?正如这题,f【2】变成2了之后,如果在判断接下来的f【3】的时候,我们是可以放3的,也就是这样的操作,可以让后续的所有过程都在贪心的给结尾添上一个很小的数,直到最后,将很小很小的数一个一个的累加,成为最长的子序列。再例如下一个例子。
我们可以看到,在f【1】变成1的那一刻起,代表下一次在更新f【2】的时候我们可以装2,3,4而不再是装比5还大的6,7了,这样的操作可惜的地方在于,它并不能够保留下最长子序列的各个值,因为我们在设立f数组的初衷也不是为了输出最终的最长子序列,而是需要输出其中的长度,这也就是f数组在此的完美之缺吧。
3.位置的插入
当我们了解完整个流程的时候,我们还要关注一个点就是,当出现需要更新f数组的时候,我们要不要更新,更新位置的下标是哪一个。结合上面的思路,当f已经存入三个数5,6,7的时候,这个时候指针来到了1,1应当放在哪里呢,应当放在5的位置。变成1,6,7的时候需要再次找2应当放在的位置,应当放在6上,我们不难发现,最终更新的位置其实就是它能放在数组中且保证f数组依然有序的最优解。而这样的过程我们是可以利用二分来进行的。下面是相关的代码。
void ef(int l,int r,int x) //x便是指针所指向的位置
{
while(l+1!=r)
{
int mid=l+((r-l)>>1); //相当于(l+r)/2
if(f[mid]>a[x]) r=mid;
else l=mid;
}
f[r]=min(f[r],a[x]);
}
而这里的二分应该是很多人都没有见过的版本,但本蒟蒻认为,这个二分应当会被更多人理解,这也是从b站一个视频上学的,感兴趣的同学点这里鸭:蒟蒻sheep。相信大家能够对二分有着更加深刻的理解以及后续的应用。至此我们便可以写出相关的代码啦!下面的代码记得敲黑板了,r一定是len+1,就因为这个地方WA了好多个点呢!也就是说,此处二分的区间左边和右边都各自加了一个1,这里的详细说明,大家可以点击“蒟蒻sheep”上方的连接看视频的讲解!
4.最终代码和调试
#include<bits/stdc++.h>
using namespace std;
int a[100005],b[100005],c[100005];
int f[100005]; //f为最小的末尾数字
void ef(int l,int r,int x) //x便是指针所指向的位置
{
while(l+1!=r)
{
int mid=l+((r-l)>>1); //相当于(l+r)/2
if(f[mid]>a[x]) r=mid;
else l=mid;
}
f[r]=min(f[r],a[x]);
}
int main()
{
int n;
int len=1;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
f[1]=a[1];
for(int i=2;i<=n;i++)
{
if(a[i]>f[len])
{
f[++len]=a[i];
}
else
{
ef(0,len+1,i);
}
}
cout<<len<<endl;
}
yes,终于求出最长上升子序列了呢!
最长公共上升子序列
回到正题,最长公共上升子序列是个什么东西贼?这就得用洛谷上的模板题给大家引入了。
大家看完题目,可以先独立思考一下这个应该怎么做,实话实说,本蒟蒻第一次看到这个题目的时候真的是一脸懵逼呢,哎,还是太弱了,继续加油鸭!
求解思路
因为刚开始就介绍了最长上升子序列,所以相信会有人很快能够想出一个思路来。当然仍然可以暴力解,但是大家可以看看本题的数据,暴力虽然出奇迹,但是仍难躲过TLE啊! 下面我先奉上一副图片吧,看看大家能不能能够有所想法。
好像这个图片已经把思路写上去了,没错我们可以看到,如果我们能够把第一个数组在第二数组中的位置表示成一个新的字列,新字串的最长上升子序列的个数其实就是最终我们需要求的答案呢。这个其实非常好理解,公共最长上升子序列首先,长度得最长,其次由于是公共的,所以数组一在数组二公共那一部分的位置也自然是上升的。因此,只要我们通过两个数组,把新数组搞出来就好了的,我们暂且把它称作c数组,最后求出c数组的最长上升子序列个数便是我们的答案啦!对于求新数组的方法有很多,相信大家也能够很好地求解,下面是本蒟蒻的做法仅供大家参考。(这里还构造了一个辅助数组d),如果大家有更好的解法也可以说一说!
#include<bits/stdc++.h>
using namespace std;
int a[100005],b[100005],c[100005],d[100005];
int f[100005]; //f为最小的末尾数字
void ef(int l,int r,int x) //x便是指针所指向的位置
{
while(l+1!=r)
{
int mid=l+((r-l)>>1); //相当于(l+r)/2
if(f[mid]>c[x]) r=mid;
else l=mid;
}
f[r]=min(f[r],c[x]);
}
int main()
{
int n;
int len=1;
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
for(int i=1;i<=n;i++)
{
cin>>b[i];
d[b[i]]=i;
}
for(int i=1;i<=n;i++)
{
c[i]=d[a[i]];
}
f[1]=c[1];
for(int i=2;i<=n;i++)
{
if(c[i]>f[len])
{
f[++len]=c[i];
}
else
{
ef(0,len+1,i);
}
}
cout<<len<<endl;
}
这里是完整的代码!
这里是蒟蒻本题的AC成果(比较绿hhh)。
总结
这个题目应该是一个对于大佬们入门级别难度的模板题吧,其实我感觉好像慢慢摸索这个题目,发现其实有很多很多地方可以学习,同时再次写这个题是以博客的形式,而且在写这次博客的时候,我发现掌握的那个二分我还是出错了,也就是边界两边都要扩大一格,所以不写不知道,一写才能发现自己的很多很多问题。写到这,可以聊一些这几天写博客的感受哈,就是写博客,好像一写就是两三个小时,而且这个还是之前学过的东西。其实写博客好像就是一个写日记加写代码的过程,自己做了挺多的算法题目,然后感觉也积累了不少的经验,但是在学校的新生程序设计大赛上还是没能够取得很好的成绩,当然有很大部分原因是因为自己审题的不细心,导致能够AC的题目都差了一点,但是其实还是得承认自己就是太弱了。嗯。然后最近就是感觉写博客一个能够分享一些题目经验,再者是因为自己也能记录下自己在写题犯下的错误和很多模板,然后写完一篇博客之后我觉得自己在某一个模块的代码熟练度也能够提升不少,因此,还是一句话,如果自己有时间,一定要坚持写下去博客啊!最后在这里特别想说一句:终于在大学找到了一个适合自己生活的方式,终于不用在担心很多很多人际上的关系了,因为:沉迷算法无法自拔哈哈哈哈!大家加油啊!蒟蒻的我也会继续努力的!!