临街小站

DP介绍-最长递增子序列

最长递增子序列系列是DP动态规划中简单、经典的入门题目。类似最长非递减子序列、导弹拦截问题、最长公共子序列问题等等。这在我们逐渐了解、认知动态规划的过程中可以给我们提高很大的帮助。

设有由n个不相同的整数组成的数列,记为:a[0]、a[1]、……、a[n-1]且a(i)<>a(j) (i<>j)

例如{2,3,4,5,6,343,5,36,45,64,56,564}

若存在i1<i2<i3< … < ie 且有a[i1]<a[i2]< … <a[ie]则称为长度为e的递增子序列序列。如上例中3,18,23,24就是一个长度为4的递增序列,同时也有3,7,10,12,16,24长度为6的递增序列

问题的引出与思路

要解决这个问题,按我们正常人的习惯性思维没我们往往会通过在下标从小到大逐渐增加,一步步的计算前n个数值(子数组,子序列最后的一个数值必须是a[n-1])中的最长递增子序列,所以前n数值最长子序列未必会比前n-1最长子序列结果要大,这是需要我们注意的。

换句话说。我们需要在前n-1个数值中循环找出比第n个数值小、且成递增规律的序列。假如当前的数值比上个子序列的最后一个数值都要大,那么就可以直接在上个子问题结果的基础上加1。当然,这不是要我们局限于紧邻的上个子问题,而是前面所有子问题都要对比,然后将这些结果再取最大值。实际上,值已经引出了状态转移方程。

显然,这些子问题相互重叠、每次的结果都会影响下一个子数组结果,但是前面子数组的结果不会在改变。

现在想想上篇文章DP动态规划简介中提出的DP动态规划的三大条件:

  • 最优化原理(最优子结构性质)

最优化原理可这样阐述:一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。

  • 无后效性

将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。

  • 子问题的重叠性

动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。

在这个问题中都得到了完善的支撑,而且这个思想也很适合解决这个系列的问题,高效、简便。

解决过程

我们使用一些列变量记录前n个子序列的结果。再具体的实现中,我们通常使用数组数据结构记录。数组opt[length]opt[i]记录原始数据中前i+1个数值(子数组)的最长递增子序列长度数值。

就以前面seq={2,3,4,5,6,343,5,36,45,64,56,564}提过的数据来看:

  • opt[0]:

opt[0]代表的是原始数据中前1个数值的最长递增长度值,只有一个数值,显然seq[0]=2就是目前这个子问题的path,opt[0]=1。用上篇文章提到的术语,当前子问题的状态就是opt[0]=1

  • opt[1]:

opt[1]代表的是原始数据中前2个数值的最长递增长度值,seq[1]>seq[0],所以根据前面托出的状态转移方程,可以得出opt[1]=opt[0]+1=2.

  • opt[2]:

opt[2]代表的是原始数据中前3个数值的最长递增长度值,seq[2]>seq[1],由于前面序列的连续递增,所以根据前面托出的状态转移方程,可以得出opt[2]=opt[1]+1=3.

  • opt[3]:

opt[3]代表的是原始数据中前4个数值的最长递增长度值,seq[3]>seq[2],由于前面序列的连续递增,所以根据前面托出的状态转移方程,可以得出opt[3]=opt[2]+1=4.

  • opt[4]:

opt[4]代表的是原始数据中前5个数值的最长递增长度值,seq[4]>seq[3],由于前面序列的连续递增,所以根据前面托出的状态转移方程,可以得出opt[4]=opt[3]+1=5.

  • opt[5]:

opt[5]代表的是原始数据中前6个数值的最长递增长度值,seq[5]>seq[4],由于前面序列的连续递增,所以根据前面托出的状态转移方程,可以得出opt[5]=opt[4]+1=6.

  • opt[6]:

opt[6]代表的是原始数据中前7个数值的最长递增长度值,seq[6]<seq[5],突如其来的变化让我们突然没了思绪。跳过seq[5]怎么样?seq[6]<seq[4],不行,再跳过直接到seq[2],现在因为seq[6]>seq[2],由于前面序列的连续递增,根据前面托出的状态转移方程,可以得出opt[6]=opt[2]+1=4.这些逻辑很容易理解。但是如果前面的数据源并不是连续递增的,我们还是应该老老实实的遍历、比照大小得出结果。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int main()
{
int seq[] = {2,3,4,5,6,343,5,36,45,64,56,564};
const int length=sizeof(seq)/sizeof(int);
int opt[length]={0};
int path[length]={0};
int i, j, maxa = 0,maxb=0;

for(i=0; i<length; i++)
opt[i] = 1;

//计算子序列状态,path是为了还原最长子序列的辅助数组
for(i=1;i<length;i++)
for(j=0;j<i;j++)
if(seq[i]>seq[j]&&opt[j]+1>opt[i])
{
opt[i]=opt[j]+1;
path[i]=j;
}
for(i=0;i<length;i++)
if(maxa<opt[i]){
maxa=opt[i];
maxb=i;
}

//还原递增最长子序列
for(j=0,i=maxb;i>=0;){
opt[maxa-1-j]=seq[i];
i=path[i];
if(++j==maxa)break;
}
for(i=0;i<maxa;i++)
cout<<opt[i]<<' ';
return 0;
}
clinjie wechat
Think about u every day