例如:在序列1,-1,2,-3,4,-5,6,-7中,其最长递增子序列为1,2,4,6。
分析与解法
根据题目要求,求一维数组中的最长递增子序列,也就是找一个标号的序列b[0],b[1],... b[m](0<=b[0]
解法一
根据无后效性的定义我们知道,将各阶段按照一定的次序排列好之后,对于某个给定阶段的状态来说,它以前各阶段的状态无法直接影响它未来的决策,而只能间接地通过当前状态来影响。换句话说,每个状态都是过去历史的一个完整总结。
同样地,仍以序列1,-1,2,-3,4,-5,6,-7中为例,我们在找到4之后,并不关心4之前的两个值具体是怎样,因为它对找到6并没有直接影响。因此,这个问题满足无后效性,可以使用动态规划来解决。
可以通过数字的规律来分析目标串:1,-1,2,-3,4,-5,6,-7。
使用i来表示当前遍历的位置:
当i=1时,显然,最长的递增序列为(1),序列长度为1.
当i=2时,由于-1<1。因此,必须丢弃第一个值然后重新建立串。当前的递增序列为(-1),长度为1.
当i=3时,由于2>1,2>-1。因此,最长的递增序列为(1,2),(-1,2),长度为2。在这里,2前面是1还是-1对求出后面的递增序列没有直接影响。
依次类推之后,可以得出如下的结论。
假设在目标数组array[]的前i个元素中,最长递增子序列的长度为LIS[i]。那么,
LIS[i+1]=max{1,LIS[k]+1},array[i+1]>array[k],for any k<=i
即如果array[i+1]大于array[k],那么第i+1个元素可以接在LIS[k]长的子序列后面构成一个更长的子序列。与此同时array[i+1]本身至少可以构成一个长度为1的子序列。
根据上面的分析,可以得到如下的代码:
int LIS(int[] array) { int *LIS = new int[array.Length]; for(int i = 0; i < array.Length; i++) { LIS[i] = 1; //初始化默认的长度 for(int j = 0; j < i; j++) //前面最长的序列 { if(array[i] > array[j] && LIS[j] + 1 > LIS[i]) { LIS[i] = LIS[j] + 1; } } } return Max(LIS); //取LIS的最大值 }这种方法的时间复杂度为O(N^2+N)= O(N^2)。
解法二
显然O(N^2)的算法只是一个比较基本的解法,我们须要想想看是否能够进一步提高效率。在前面的分析中,当考虑第i+1个元素的时候,我们是不考虑前面i个元素的分布情况的。现在我们从另一个角度分析,即当考虑第i+1个元素的时候考虑前面i个元素的情况。
对于前面i个元素的任何一个递增子序列,如果这个子序列的最大的元素比array[i+1]小,那么就可以将array[i+1]加在这个子序列后面,构成一个新的递增子序列。
比如当i=4的时候,目标序列为:1,-1,2,-3,4,-5,6,-7最长递增序列为:(1,2),(-1,2)。那么,只要4>2,就可以把4直接增加到前面的子序列形成一个新的递增子序列。
因此,我们希望找到前i个元素中的一个递增子序列,使得这个递增子序列的最大的元素比array[i+1]小,且长度尽量地长。这样将array[i+1]加在该递增子序列后,便可找到以array[i+1]为最大元素的最长递增子序列。
仍然假设在数组的前i个元素中,以array[i]为最大元素的最长递增子序列的长度为LIS[i]。
同时,假设:
长度为1的递增子序列最大元素的最小值为MaxV[1];
长度为2的递增子序列最大元素的最小值为MaxV[2];
......
长度为LIS[i]的递增子序列最大元素的最小值为MaxV[LIS[i]]。
假如维护了这些值,那么,在算法中就可以利用相关的信息来减少判断的次数。
具体算法实现如代码所示:
int LIS(int array[]) { //记录数组中的递增序列信息 int *MaxV = new int[array.Length + 1]; MaxV[1] = array[0]; //数组中的第一值,边界值 MaxV[0] = Min(array) - 1; //数组中最小值,边界值 int *LIS = new int[array.Length]; //初始化最长递增序列的信息 for(int i = 0;i < LIS.Length; i++) { LIS[i] = 1; } int nMaxLIS = 1; //数组最长递增子序列的长度 for(int i = 1; i < array.Length; i++) { //遍历历史最长递增序列信息 int j; for(j = nMaxLIS; j >=0; j--) { if(array[i] > MaxV[j]) { LIS[i] = j + 1; break; } } //如果当前最长序列大于最长递增序列长度,更新最长信息 if(LIS[i] > nMaxLIS) { nMaxLIS = LIS[i]; MaxV[LIS[i]] = array[i]; } else if(MaxV[j] < array[i] && array[i] < MaxV[j + 1]) { MaxV[j + 1] = array[i]; } } return nMaxLIS; }由于上述解法中的穷举遍历,时间复杂度仍然为O(N^2)。
解法三
解法二的结果似乎仍然不能让人满意。我们是否把递增序列中间的关系全部挖掘出来了呢?再分析一下临时存储下来的最长递增序列信息。
在递增序列中,如果i
因此,根据这样单调递增的关系,可以将上面方法中的穷举部分进行如下修改:
for(int j = LIS[i - 1]; j >= 1; j--) { if(array[i] > MaxV[j]) { LIS[i] = j + 1; break; } }如果把上述的查询部分利用二分搜索进行加速,那么就可以把时间复杂度降为O(N*log2N)。
小结
从上面的分析中可以看出我们先提出一个最直接(或者说最简单)的解法,然后从这个最简单解法来看是否有提升的空间,进而一步一步地挖掘解法中的潜力,从而减少解法的时间复杂度。
在实际的面试中,这样的方法同样有效。因为面试者更加看中的是应聘者是否有解决问题的思路,不会因为最后没有达到最优算法而简单地给予否定。应聘者也可以先提出简单的办法,以此投石问路,看看面试者是否会有进一步的提示。