最长升/降/不升/不降子序列:模型、解决及扩展应用

【最长升/降/不升/不降子序列:模型】
简单来说,标题中所说的最长升/降/不升/不降子序列这四种类型,都可以概括成一句话。就是对于这个被选出的子序列中的每一个元素,都大于/小于/不小于/不大于子序列中的前一个元素。如果设原来的序列为A,被选出的子序列为B,并且我们用compare(u,v)来表示子序列中的前后满足关系(即当compare(u,v)为真时,v可以在子序列中在u的后面),那么我们将关系概括成下面这样
最长升/降/不升/不降子序列:模型、解决及扩展应用 - wenjianwei1 - 算法的设计
  而最长升/降/不升/不降子序列就是将B的长度最大化,注意B可能不唯一,所以说一般只需要输出长度。
【最长升/降/不升/不降子序列:解决】
【解法1:搜索】
很容易想到,我们完全可以枚举,利用一个指数级的搜索,2^n地枚举某个元素是放还是不放,那么再加一个检查过程,就很容易证明其正确性。
【解法2:动态规划的暴力】
我们很容易发现解法1的计算有相当的冗余的计算,所以,怎样优化呢?答案应该从动态规划入手,利用子问题的划分,来减少冗余的计算。对于每一个A中的元素,我们都有两种策略:放和不放,所以容易看出DP的方程:
最长升/降/不升/不降子序列:模型、解决及扩展应用 - wenjianwei1 - 算法的设计
这个算法足够高效了。某种意义上来说,可能没有更好的算法了,或者说其他算法的时间复杂度和这个算法或者其的优化版相当。动态规划的高效之处,就在于将子问题大量重叠的情况全部归类,利用子问题,将重复的计算省去,这样我们就可以得到一个代码可能只有二三十行的短小精悍的高效程序。
回到原来的问题,我们下面来说明这个算法的正确性。以最长(上)升序列为例,首先,对于每一个f(i),我们都应该在其前面找一个f(j),使得f(i)可以“接上”f(j),那么如果没有呢?也就是说,当前的这个元素比前面所有元素都要小呢?那么我们就将这个元素设为1,或者我们更好的方法是加一个哨兵,比如在最前面加一个 最长升/降/不升/不降子序列:模型、解决及扩展应用 - wenjianwei1 - 算法的设计 即可,这样不管后面有多么小,都不可能比负无穷大还要小,所以必然会接到前面去,而这样算出来的长度值也一样。在加了哨兵之后,我们就很容易看出其的最优性了。对于每一个f(i),我们都在前面找一个最大的f(j),使得f(i)可以接在f(j)的后面,而且容易看出最优性(因为f(j)最大),所以是最长的。另外,上升的性质也可以满足(我们只考虑了那些符合要求的)。回到动态规划的本质,无后效性?当然满足了,因为我们是顺序考虑的,将i从1(哨兵位置+1)一直到n(序列长度)去考虑,这样不难发现其是正确的。
那么我们怎么实现呢?我们很容易想到暴力的方法,就是直接一个一个去寻找符合条件的j并且去比较就好了,时间复杂度为 最长升/降/不升/不降子序列:模型、解决及扩展应用 - wenjianwei1 - 算法的设计。属于高效的暴力……具体的程序实现起来很简单,也就二三十行就可以搞定了(或许没有,我实现过世行左右的程序)。
【解法3:动态规划的高数(高级数据结构)优化】
我们如果遇到一些苛刻的题目,数据量很大,用普通的算法过不了,那么我们应该怎么办?答案是:优化。在这里我们就可以使用线段树来优化,当然树状数组也可以。
动态规划的思想部分我就不讲了,主要的困难就在于怎样实现,使得在j考虑了之后才考虑i,而且找到f(j) ,使其最大。这就是我们要找的重点所在。要求的最大值很容易想到线段树,可是怎样使得 最长升/降/不升/不降子序列:模型、解决及扩展应用 - wenjianwei1 - 算法的设计为真呢?我们继续以最长(上)升序列考虑。
和树状数组来做逆序对类似,我们可以仍然用计数排序的思路!首先依次考虑每一个i,接着对于每一个i,在线段树中在它前面的所有数(就是比Ai小的)中找一个最大的f(j)。就是说,我们用计数排序的思路,但是排的是Ai,然后在计数排序中的数组中建线段树比如这个计数排序的数组是C,比如我们考虑一个i,那么1到(Ai)-1的区间必然每一项的A值都比Ai要小,也就是说,前面的Cj<Ai。但是考虑另外一个问题:Cj在A中的编号k有可能不能满足k<i,有可能还没有考虑过!不过这也不用担心:没考虑过,我们设为0不就是了!也就是说,首先全部的f(i)=0,接着逐次考虑每一个i(按照A中编号),那么我们很容易发现,Ai前面的都是可选的。就算没有考虑过也没有关系,因为我们要求C中1到Ai-1的最大值,没有考虑过的话f值就是0,那么很显然在非极端情况下,是不会选到它的(因为考虑过的会更优)。而极端情况下,也就是考虑过的都和没考虑过的一样的时候(就是说,前面只有没考虑过的元素,或者说干脆没有元素),那么没考虑过的元素就都相当于哨兵了,答案和原来一样。
但是注意到有可能Ai会很大,那么我们的一大工具离散化就又出场了。下面是整个算法的描述:先加个哨兵,再将A中数组排序后放到C中,并且将A中和C中元素都将其原来的和现在的编号全部打上去,将每一个元素的f值全部设为0,接着依次考虑每一个A中的元素,找到其在C中的元素,利用线段树或树状数组找到C中那个元素前面的所有元素的f的最大值,并且加1后就作为当前这个元素的f值。
忽略掉线段树之后代码真的不长……不过这个算法用树状数组或许更是个不错的选择,因为这里要求的区间类似于前缀和的模型,用树状数组去维护这种有限制的前缀类区间最值会比较方便。至于时间复杂度,容易看出来是 最长升/降/不升/不降子序列:模型、解决及扩展应用 - wenjianwei1 - 算法的设计
【解法4:动态规划的单调性优化】
下面再来介绍一个神奇的算法:利用单调性,使用二分查找优化的算法。虽然时间复杂度一样,但是容易写了很多,而且常数也小了。
刚才,我们是以高度作为下标,那么我们看一看用其他的东西作为下标怎样?比如说,子序列的长度?这就是我们要的这种神奇的算法。以f作为下标。我们设为D数组。但是我们考虑一下,可能会有很多的f相同的元素,那么我们选哪一个呢?答案是:那个越有利,就选哪个。继续以最长(上)升序列为例,我们就应该选择相应的A值最小的一个。因为这样才可能让更多的元素能够连接上这个D中的f。容易看出,D是单调不降的。
那么,算法也出来了:首先,同样是加个哨兵,然后对于所有A中的元素i,二分查找出D中最后一个(严格)小于Ai的元素,然后将这个元素连上i,也就是说,设这个元素为f(j),那么f(i)=f(j)+1。接着,我们就去更新相应的f即可。
要证明其的单调性,我们用类似递推的方式(我忘记这叫什么方法了),首先一开始高度必然是单调不降的,然后每一次我们放进去一个元素,如果这个元素i后面的一个u要比这个i的高度低,就是说原来的Ai<Au,那么i应该是连到u的才对,又因为u在i放进去的位置的后面,所以说i只要不是算法实现错误,那么我们的Ai必然是在Au后面的,因此反证得出后面没有这样的一个u比i的高度要低。所以,就这样证明了单调性。
【扩展应用】
由于暂时没有时间,马上就要过年了,我就只讲一个最长公共子序列的问题。
最长公共子序列的定义我就不讲了,就是说在两个序列A和B中分别选取若干个元素,使得这些若干个元素以在各自原来的序列中的位置排成的两个序列相同,并且长度最长。比如说“AAB”就是“ACAAB”的子序列。
那么,怎样高效地求这样的一个最长公共子序列呢?如果我们要求长度的话,我们还是可以用最长上升子序列来做。我们先假定A中没有重复的元素,那么我们顺序将A中元素标为1,2,3,4……n,然后将B中的元素依次按照这样的标号标出来。比如说A中有一个'D'标为了8,那么B中的所有‘D'都得标上8。接着我们对B这个经过处理的标号进行一次最长上升子序列就好了。想想看,我们可以找出来一个重大的发现,就是B中的一个上升子序列,对应着A中的一个子序列,而且这两个子序列的内容完全相同!想想也很容易理解,因为子序列的定义就是要下标递增,那么B中的所有子序列都是下标递增的;而由于每一个的标号都对应着A中的一个字符,那么我们应该想到,由于我们考虑的这个上升子序列的标号都是上升的,那么由于标号对应着A中的下标,那么其对应的A中下标也是递增的!接着,只有A和B中相同的字符有相同的标号,那么我们也可以发现,这两个对应的子序列完全相同!所以只需求一个最长上升子序列即可。
不过这里还有一点问题,那就是:如果有一个字符,只在B中有怎么办(就是说找不到对应的A中标号)?那么我们最简单的处理办法就是:不管它!因为这个字符值在B中有,所以说不可能在公共子序列中存在,因此忽略即可。
这就是最长升/降/不升/不降子序列,可扩展的其实还有很多,这里就不一一细讲了。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值