当前位置:   article > 正文

数据结构与算法随笔之------动态规划问题之------最长上升子序列(LIS)问题(一文搞懂最长上升子序列(LIS))_用两分法最长上升子序列

用两分法最长上升子序列

LIS

(最长上升子序列(计算机科学))

定义:最长上升子序列(Longest Increasing Subsequence,LIS),在计算机科学上是指一个序列中最长的单调递增的子序列。

这里借鉴一个大佬的博客:https://www.cnblogs.com/GodA/p/5180560.html

方法1:枚举法(也就是动态规划)

我们都知道,动态规划的一个特点就是当前解可以由上一个阶段的解推出, 由此,把我们要求的问题简化成一个更小的子问题。子问题具有相同的求解方式,只不过是规模小了而已。最长上升子序列就符合这一特性。我们要求n个数的最长上升子序列,可以求前n-1个数的最长上升子序列,再跟第n个数进行判断。求前n-1个数的最长上升子序列,可以通过求前n-2个数的最长上升子序列……直到求前1个数的最长上升子序列,此时LIS当然为1。

  让我们举个例子:求 2 7 1 5 6 4 3 8 9 的最长上升子序列。我们定义d(i) (i∈[1,n])来表示前i个数以A[i]结尾的最长上升子序列长度。

  前1个数 d(1)=1 子序列为2(A[i]=2)

  前2个数 7前面有2小于7 d(2)=d(1)+1=2 子序列为2 7(A[i]=7)

  前3个数 在1前面没有比1更小的,1自身组成长度为1的子序列 d(3)=1 子序列为1(A[i]=1)

  前4个数 5前面有2小于5 d(4)=d(1)+1=2 子序列为2 5(A[i]=5)

  前5个数 6前面有2 5小于6 d(5)=d(4)+1=3 子序列为2 5 6(A[i]=6)

  前6个数 4前面有2小于4 d(6)=d(1)+1=2 子序列为2 4(A[i]=4)

  前7个数 3前面有2小于3 d(3)=d(1)+1=2 子序列为2 3(A[i]=3)

  前8个数 8前面有2 5 6小于8 d(8)=d(5)+1=4 子序列为2 5 6 8(A[i]=8)

  前9个数 9前面有2 5 6 8小于9 d(9)=d(8)+1=5 子序列为2 5 6 8 9(A[i]=9)

  d(i)=max{d(1),d(2),……,d(i)} 我们可以看出这9个数的LIS为d(9)=5

  总结一下,d(i)就是找以A[i]结尾的,在A[i]之前的最长上升子序列+1,当A[i]之前没有比A[i]更小的数时,d(i)=1。所有的d(i)里面最大的那个就是最长上升子序列。

       给个图大概就是:

具体实现代码如下:

  1. #include<cstdio>
  2. const int MAX=1001;
  3. int a[MAX];
  4. int lis(int x)
  5. {
  6. int num[MAX];
  7. for(int i=0;i<x;i++)
  8. {
  9. num[i]=1;//用来子序列统计长度
  10. for(int j=0;j<i;j++)
  11. {
  12. if(a[j]<a[i]&&num[j]+1>num[i])//满足向后找且前面存的序列的长度加一比现在的大(也就是说可以往后找)(其实就是再找num[j]+1的最大值)
  13. num[i]=num[j]+1;//那就把num里最长的加一后赋值过来
  14. }
  15. }
  16. int maxx=0;
  17. for(int i=0;i<x;i++)
  18. if(maxx<num[i])
  19. maxx=num[i];
  20. return maxx;
  21. }
  22. int main()
  23. {
  24. int n;
  25. scanf("%d",&n);
  26. for(int i=0;i<n;i++)
  27. scanf("%d",&a[i]);
  28. printf("%d\n",lis(n));
  29. return 0;
  30. }

这个算法的时间复杂度为〇(n²),并不是最优的算法。在限制条件苛刻的情况下,这种方法行不通。那么怎么办呢!有没有时间复杂度更小的算法呢?说到这里了,当然是有的啦!还有一种时间复杂度为〇(nlogn)的算法,下面就来看看。

方法二:二分法

  我们再举一个例子:有以下序列A[]=3 1 2 6 4 5 10 7,求LIS长度。

  我们定义一个B[i]来储存可能的排序序列,len为LIS长度。我们依次把A[i]有序地放进B[i]里。(为了方便,i的范围就从1~n表示第i个数)

  A[1]=3,把3放进B[1],此时B[1]=3,此时len=1,最小末尾是3

  A[2]=1,因为1比3小,所以可以把B[1]中的3替换为1,此时B[1]=1,此时len=1,最小末尾是1

  A[3]=2,2大于1,就把2放进B[2]=2,此时B[]={1,2},len=2

  同理,A[4]=6,把6放进B[3]=6,B[]={1,2,6},len=3

  A[5]=4,4在2和6之间,比6小,可以把B[3]替换为4,B[]={1,2,4},len=3

  A[6]=5,B[4]=5,B[]={1,2,4,5},len=4 

  A[7]=10,B[5]=10,B[]={1,2,4,5,10},len=5

  A[8]=7,7在5和10之间,比10小,可以把B[5]替换为7,B[]={1,2,4,5,7},len=5

  最终我们得出LIS长度为5。但是,但是!!这里的1 2 4 5 7很明显并不是正确的最长上升子序列。是的,B序列并不表示最长上升子序列,它只表示相应最长子序列长度的排好序的最小序列。这有什么用呢?我们最后一步7替换10并没有增加最长子序列的长度,而这一步的意义,在于记录最小序列,代表了一种“最可能性”。假如后面还有两个数据8和9,那么B[6]将更新为8,B[7]将更新为9,len就变为7。读者可以自行体会它的作用。

  因为在B中插入的数据是有序的,不需要移动,只需要替换,所以可以用二分查找插入的位置,那么插入n个数的时间复杂度为〇(logn),这样我们会把这个求LIS长度的算法复杂度降为了〇(nlogn)。

      大致思路就是:我们可以用数组模拟栈。所以每遇到一个比栈顶元素大的数,就放进栈里。遇到比栈顶元素小的就二分查找前边的元素,找到一个“最应该被换掉的元素”(即数组中第一个大于等于key的元素),用新数去更新前边的元素(赋值)。

  1. #include<cstdio>
  2. #include<algorithm>
  3. #include <bits/stdc++.h>
  4. const int MAXN=200001;
  5. int a[MAXN];
  6. int d[MAXN];
  7. int find(int a[],int start,int end,int key)//二分查找,返回a数组中第一个>=key的位置
  8. {
  9. int left = start;
  10. int right = end;
  11. int mid;
  12. while(left <= right)
  13. {
  14. mid=(left + right)/2;
  15. if(d[mid] > key)
  16. right = mid - 1;
  17. else
  18. left = mid + 1;
  19. }
  20. return left;
  21. }
  22. int main()
  23. {
  24. int n;
  25. scanf("%d",&n);
  26. for(int i=0;i<n;i++)
  27. scanf("%d",&a[i]);
  28. d[0]=a[0];//代表下标都从0开始
  29. int len=0;
  30. for(int i=0;i<n;i++)//用数组模拟栈
  31. {
  32. if(a[i]>=d[len])//每遇到一个比栈顶元素大的数,就放进栈里,
  33. d[++len]=a[i];
  34. else//遇到比栈顶元素小的就二分查找前边的元素
  35. {
  36. //int j=std::lower_bound(d+1,d+len+1,a[i])-d;//找到下标
  37. int j = find(a,1,len,a[i]);//返回第一个大于等于key的元素
  38. d[j]=a[i];//并用它替换掉栈顶元素
  39. //if(len < j)
  40. //len = j;
  41. }
  42. }
  43. printf("%d\n",len);
  44. return 0;
  45. }

当然这里头把下标改为从1开始可能更直观些,此时代码如下代码

  1. #include<cstdio>
  2. #include<algorithm>
  3. #include <bits/stdc++.h>
  4. const int MAXN=200001;
  5. int a[MAXN];
  6. int d[MAXN];
  7. int find(int a[],int start,int end,int key)//二分查找,返回a数组中第一个>=key的位置
  8. {
  9. int left = start;
  10. int right = end;
  11. int mid;
  12. while(left <= right)
  13. {
  14. mid=(left + right)/2;
  15. if(d[mid] > key)
  16. right = mid - 1;
  17. else
  18. left = mid + 1;
  19. }
  20. return left;
  21. }
  22. int main()
  23. {
  24. int n;
  25. scanf("%d",&n);
  26. for(int i=1;i<=n;i++)
  27. scanf("%d",&a[i]);
  28. d[1]=a[1];//代表下标都从0开始
  29. int len=1;
  30. for(int i=2;i<=n;i++)//用数组模拟栈
  31. {
  32. if(a[i]>=d[len])//每遇到一个比栈顶元素大的数,就放进栈里,
  33. d[++len]=a[i];
  34. else//遇到比栈顶元素小的就二分查找前边的元素
  35. {
  36. //int j=std::lower_bound(d+1,d+len+1,a[i])-d;//找到下标
  37. int j = find(a,1,len,a[i]);//返回第一个大于等于key的元素
  38. d[j]=a[i];//并用它替换掉栈顶元素
  39. //if(len < j)
  40. //len = j;
  41. }
  42. }
  43. printf("%d\n",len);
  44. return 0;
  45. }

此外本题二分法的实现还可以使用stl的容器及函数

补充两种用stl的方法:

原理:

Set不但自动排序,里面还有upper_bound(num)函数,查找与num最接近的比num大的数,省去了内层循环,时间复杂度缩小到了O(n)。

  1. #include <bits/stdc++.h>
  2. using namespace std;
  3. int main()
  4. {
  5.     int num,n;
  6.     cin>>n;
  7.     set<int> s;
  8.     for(int i=0;i<n;i++)
  9.     {
  10.         cin>>num;
  11.         if(s.upper_bound(num)!=s.end())//如果找到了最接近num且比num大的数(==s.end代表没找到)
  12.             s.erase(s.upper_bound(num));//就把那个数删了(num照常插入)(更新过程)
  13.         s.insert(num);
  14.     }
  15.     cout<<s.size();
  16.     return 0;
  17. }

或者使用lower_bound( begin,end,num)函数

在从小到大的排序数组中,

lower_bound( begin,end,num):从数组的begin位置到end-1位置二分查找第一个大于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。

upper_bound( begin,end,num):从数组的begin位置到end-1位置二分查找第一个大于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。

关于它的使用方法可参考链接:https://blog.csdn.net/weixin_42110638/article/details/83412189

  1. #include<cstdio>
  2. #include<algorithm>
  3. const int MAXN=200001;
  4. int a[MAXN];
  5. int d[MAXN];
  6. int main()
  7. {
  8. int n;
  9. scanf("%d",&n);
  10. for(int i=1;i<=n;i++)
  11. scanf("%d",&a[i]);
  12. d[1]=a[1];
  13. int len=1;
  14. for(int i=2;i<=n;i++)
  15. {
  16. if(a[i]>d[len])
  17. d[++len]=a[i];
  18. else
  19. {
  20. int j=std::lower_bound(d+1,d+len+1,a[i])-d;
  21. d[j]=a[i];
  22. }
  23. }
  24. printf("%d\n",len);
  25. return 0;
  26. }

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/运维做开发/article/detail/1004947
推荐阅读
相关标签
  

闽ICP备14008679号