赞
踩
定义:最长上升子序列(Longest Increasing Subsequence,LIS),在计算机科学上是指一个序列中最长的单调递增的子序列。
这里借鉴一个大佬的博客:https://www.cnblogs.com/GodA/p/5180560.html
我们都知道,动态规划的一个特点就是当前解可以由上一个阶段的解推出, 由此,把我们要求的问题简化成一个更小的子问题。子问题具有相同的求解方式,只不过是规模小了而已。最长上升子序列就符合这一特性。我们要求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)里面最大的那个就是最长上升子序列。
给个图大概就是:
具体实现代码如下:
- #include<cstdio>
- const int MAX=1001;
- int a[MAX];
- int lis(int x)
- {
- int num[MAX];
- for(int i=0;i<x;i++)
- {
- num[i]=1;//用来子序列统计长度
- for(int j=0;j<i;j++)
- {
- if(a[j]<a[i]&&num[j]+1>num[i])//满足向后找且前面存的序列的长度加一比现在的大(也就是说可以往后找)(其实就是再找num[j]+1的最大值)
- num[i]=num[j]+1;//那就把num里最长的加一后赋值过来
- }
- }
- int maxx=0;
- for(int i=0;i<x;i++)
- if(maxx<num[i])
- maxx=num[i];
- return maxx;
- }
- int main()
- {
- int n;
- scanf("%d",&n);
- for(int i=0;i<n;i++)
- scanf("%d",&a[i]);
- printf("%d\n",lis(n));
- return 0;
- }
这个算法的时间复杂度为〇(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的元素),用新数去更新前边的元素(赋值)。
- #include<cstdio>
- #include<algorithm>
- #include <bits/stdc++.h>
- const int MAXN=200001;
-
- int a[MAXN];
- int d[MAXN];
- int find(int a[],int start,int end,int key)//二分查找,返回a数组中第一个>=key的位置
- {
- int left = start;
- int right = end;
- int mid;
- while(left <= right)
- {
- mid=(left + right)/2;
- if(d[mid] > key)
- right = mid - 1;
- else
- left = mid + 1;
- }
- return left;
- }
-
- int main()
- {
- int n;
- scanf("%d",&n);
- for(int i=0;i<n;i++)
- scanf("%d",&a[i]);
- d[0]=a[0];//代表下标都从0开始
- int len=0;
- for(int i=0;i<n;i++)//用数组模拟栈
- {
- if(a[i]>=d[len])//每遇到一个比栈顶元素大的数,就放进栈里,
- d[++len]=a[i];
- else//遇到比栈顶元素小的就二分查找前边的元素
- {
- //int j=std::lower_bound(d+1,d+len+1,a[i])-d;//找到下标
- int j = find(a,1,len,a[i]);//返回第一个大于等于key的元素
- d[j]=a[i];//并用它替换掉栈顶元素
- //if(len < j)
- //len = j;
- }
- }
- printf("%d\n",len);
- return 0;
- }
当然这里头把下标改为从1开始可能更直观些,此时代码如下代码
-
- #include<cstdio>
- #include<algorithm>
- #include <bits/stdc++.h>
- const int MAXN=200001;
-
- int a[MAXN];
- int d[MAXN];
- int find(int a[],int start,int end,int key)//二分查找,返回a数组中第一个>=key的位置
- {
- int left = start;
- int right = end;
- int mid;
- while(left <= right)
- {
- mid=(left + right)/2;
- if(d[mid] > key)
- right = mid - 1;
- else
- left = mid + 1;
- }
- return left;
- }
-
- int main()
- {
- int n;
- scanf("%d",&n);
- for(int i=1;i<=n;i++)
- scanf("%d",&a[i]);
- d[1]=a[1];//代表下标都从0开始
- int len=1;
- for(int i=2;i<=n;i++)//用数组模拟栈
- {
- if(a[i]>=d[len])//每遇到一个比栈顶元素大的数,就放进栈里,
- d[++len]=a[i];
- else//遇到比栈顶元素小的就二分查找前边的元素
- {
- //int j=std::lower_bound(d+1,d+len+1,a[i])-d;//找到下标
- int j = find(a,1,len,a[i]);//返回第一个大于等于key的元素
- d[j]=a[i];//并用它替换掉栈顶元素
- //if(len < j)
- //len = j;
- }
- }
- printf("%d\n",len);
- return 0;
- }
此外本题二分法的实现还可以使用stl的容器及函数
补充两种用stl的方法:
原理:
Set不但自动排序,里面还有upper_bound(num)函数,查找与num最接近的比num大的数,省去了内层循环,时间复杂度缩小到了O(n)。
- #include <bits/stdc++.h>
- using namespace std;
- int main()
- {
- int num,n;
- cin>>n;
- set<int> s;
- for(int i=0;i<n;i++)
- {
- cin>>num;
- if(s.upper_bound(num)!=s.end())//如果找到了最接近num且比num大的数(==s.end代表没找到)
- s.erase(s.upper_bound(num));//就把那个数删了(num照常插入)(更新过程)
- s.insert(num);
- }
- cout<<s.size();
- return 0;
- }
或者使用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
- #include<cstdio>
- #include<algorithm>
- const int MAXN=200001;
-
- int a[MAXN];
- int d[MAXN];
-
- int main()
- {
- int n;
- scanf("%d",&n);
- for(int i=1;i<=n;i++)
- scanf("%d",&a[i]);
- d[1]=a[1];
- int len=1;
- for(int i=2;i<=n;i++)
- {
- if(a[i]>d[len])
- d[++len]=a[i];
- else
- {
- int j=std::lower_bound(d+1,d+len+1,a[i])-d;
- d[j]=a[i];
- }
- }
- printf("%d\n",len);
- return 0;
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。