最长单调递减子序列

来源:互联网 发布:动易cms如何升级 编辑:程序博客网 时间:2024/05/21 17:35

问题描述:求一个数组的最长递减子序列 比如{9,4,3,2,5,4,3,2}的最长递减子序列为{9,5,4,3,2}。

思路:这是一个标准的动态规划的问题,在不理解算法的时候,最感觉可以使用递归的思想,其实也是正确的,在最后给出一个递归的方法,在知道是动态规划问题以后,就需要进行分析,我们需要一个辅助数组记录信息,假如源数组为src,辅助数组为table,table[i]数组中记录着到src[0]~src[i]这个子数组中包含src[i]构成的最长单调子序列的长度(一定要注意就是table[i]记录了包含src[i]构成的递减序列中最长的那个,也就是以src[i]为结尾的最长递减子序列),如果src[i]大于以前的所有元素,那么table[i]=1.

       最后的结果就是,table[i] =max{table[k]+1, src[i] > src[k] && 0<k<i},这是初始化了table数组,那么最长递减子序列为max{table[i],0<i<n}.

       但是需要打印最长子序列,我们已经记录了max{table[i],0<i<n}的下标,那么我们就知道了最长子序列的最后一个元素,同时知道了最长子序列的长度。下面就是递归调用,假如我们已经知道table[k]最大,那么src[k]就是最长递减子序列中的最后一个,如果table[i]+1 = table[k] && src[i] < table[k]时,那么就需要答应src[i]。

#include <stdio.h>#include <stdlib.h>void Find(int src[],int n,int table[]){table[0]= 1;int i,j,maxi = 0;for(i=0;i<n;i++){table[i]=1;for(j=0;j<i;j++){/* 这里可能是常有的错误if( src[j] > src[i]){table[i] = table[j] +1;if(src[j]>src[i])table[i] = table[j]+1;if(table[i] > table[maxi])maxi = i;}*/   if(src[j]> src[i] && table[j]+1 > table[i])   {   table[i] = table[j]+1;//这个的作用是为了打印存在的,maxi记录最长子序列中最后一个数组元素if(table[i] > table[maxi])maxi = i;} }}//printf("%d\r\n",maxi);}void print(int src[],int table[],int maxi){int i=maxi-1;for(;i>=0;i--){if(table[maxi] == table[i]+1 && src[i] > src[maxi]){print(src,table,i);break;}}printf("%d ",src[maxi]);}void FindLongestDSCArray2(int *arr, int n){int mark[9];int link[9];int i = 0;for(i=0;i<n;i++){mark[i] = 0;link[i] = -1;}//link[0] = 1;int j = 0, maxMark = 0;for(i=0;i<n;i++){maxMark = 0;for(j=0;j<i;j++){if(arr[j]>arr[i]){if(maxMark<mark[j]){maxMark = mark[j];link[i] = j;}}}mark[i] = maxMark+1;}for(i=0;i<9;i++)printf("%d ",mark[i]);printf("\r\n");for(i=0;i<9;i++)printf("%d ",link[i]);printf("\r\n");/*//Print()int node = 0;maxMark = 0;for(i=0;i<n;i++){if(mark[i]>maxMark)node = i;}while(node != -1){printf("%d ",arr[node]);node = link[node];}*/}int main(){int src[]={9,4,3,2,5,4,3,2,4};int table[9];Find(src,9,table);    FindLongestDSCArray2(src,9);int i=0;for(;i<9;i++)printf("%d ",table[i]);printf("\r\n");return 0;}

这里的事件复杂度是O(n*n),但是在有的问题中,使用DP算法解决问题,事件复杂度是O(N*lgN).在后续的一篇文章中可以发现一种使用二分查找的方法来解决这个问题。是的事件复杂度稍微低一些

扩展: 从一列数中筛除尽可能少的数使得从左往右看,这些数是从小到大再从大到小的。

假设是一个数组arr[n], 它的分段点是 i (0-i 递增, i 到 n-1 递减), 假设我们用方法LIS(i) 找到最长的从0到 i 的递增子序列,LDS(i) 找到从 i 到 n -1的最长递减子序列,那么它的总长度为 LIS(i) + LDS(i) -1, 所以我们扫描整个数组,即让 i 从0 到 n-1, 找出使 LIS(i) + LDS(i) -1 最大的即可。

后面部分讲解如何用二分查找解决LIS问题,下面部分为转载。

转自: http://www.felix021.com/blog/read.php?1587

假设存在一个序列d[1..9] = 2 1 5 3 6 4 8 9 7,可以看出来它的LIS长度为5。
下面一步一步试着找出它。
我们定义一个序列B,然后令 i = 1 to 9 逐个考察这个序列。
此外,我们用一个变量Len来记录现在最长算到多少了
首先,把d[1]有序地放到B里,令B[1] = 2,就是说当只有1一个数字2的时候,长度为1的LIS的最小末尾是2。这时Len=1
然后,把d[2]有序地放到B里,令B[1] = 1,就是说长度为1的LIS的最小末尾是1,d[1]=2已经没用了,很容易理解吧。这时Len=1
接着,d[3] = 5,d[3]>B[1],所以令B[1+1]=B[2]=d[3]=5,就是说长度为2的LIS的最小末尾是5,很容易理解吧。这时候B[1..2] = 1, 5,Len=2
再来,d[4] = 3,它正好加在1,5之间,放在1的位置显然不合适,因为1小于3,长度为1的LIS最小末尾应该是1,这样很容易推知,长度为2的LIS最小末尾是3,于是可以把5淘汰掉,这时候B[1..2] = 1, 3,Len = 2
继续,d[5] = 6,它在3后面,因为B[2] = 3, 而6在3后面,于是很容易可以推知B[3] = 6, 这时B[1..3] = 1, 3, 6,还是很容易理解吧? Len = 3 了噢。
第6个, d[6] = 4,你看它在3和6之间,于是我们就可以把6替换掉,得到B[3] = 4。B[1..3] = 1, 3, 4, Len继续等于3
第7个, d[7] = 8,它很大,比4大,嗯。于是B[4] = 8。Len变成4了
第8个, d[8] = 9,得到B[5] = 9,嗯。Len继续增大,到5了。
最后一个, d[9] = 7,它在B[3] = 4和B[4] = 8之间,所以我们知道,最新的B[4] =7,B[1..5] = 1, 3, 4, 7, 9,Len = 5。
于是我们知道了LIS的长度为5。
!!!!! 注意。这个1,3,4,7,9不是LIS,它只是存储的对应长度LIS的最小末尾。有了这个末尾,我们就可以一个一个地插入数据。虽然最后一个d[9] = 7更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9,那么就可以把8更新到d[5], 9更新到d[6],得出LIS的长度为6。
然后应该发现一件事情了:在B中插入数据是有序的,而且是进行替换而不需要挪动——也就是说,我们可以使用二分查找,将每一个数字的插入时间优化到O(logN)~~~~~于是算法的时间复杂度就降低到了O(NlogN)~!
代码如下:

[java] view plaincopy
  1. public static int lis(int[] array) {  
  2.     if (array == null || array.length == 0return -1;  
  3.     int[] B = new int[array.length];  
  4.     B[0] = array[0];  
  5.     int pointer = 0, left = 0, right = 0, mid = 0;  
  6.     for (int i = 1; i < array.length; i++) {  
  7.         left = 0;   
  8.         right = pointer;  
  9.         while (left <= right) {  
  10.             mid = left + (right - left)/2//二分查找array[i]的插入位置  
  11.             if (B[mid] < array[i]) {left = mid + 1;}  
  12.             else {right = mid - 1;}  
  13.         }  
  14.         B[left] = array[i]; //插入  
  15.         if (left > pointer) pointer = left; //d[i]比现有的所有数字都大,所以left 才会大于 pointer。  
  16.     }  
  17.     return pointer + 1;  
  18. }   
PS. 从上面代码可以得到一个结论。如果在一个数组里面查找一个数,而这个数在数组里不存在,那么通过二分查找时,退出while循环的时候,如果那个数比数组里最大的还大,那么,left会是数组长度+1,而如果这个数的大小在数组范围内,但该值并不存在于数组,那么,最后left会停在第一个比那个值大的位置。

起始下面的代码非常精炼:

    int LIS_new(int *a, int n)      {          if(n <= 0) return 0;          vector<int> maxV;          maxV.push_back(a[0]);          for(int i=0; i<n; ++i)          {              if(a[i] > *maxV.rbegin())                  maxV.push_back(a[i]);              else                  *lower_bound(maxV.begin(), maxV.end(), a[i]) = a[i];          }          return maxV.size();      }  




3 11
原创粉丝点击