最长上升子序列
(1)字符子串指的是字符串中连续的n个字符
,如abcdefg中,ab,cde,fg等都属于它的字串。
(2)字符子序列指的是字符串中不一定连续但先后顺序一致的n个字符
,即可以去掉字符串中的部分字符,但不可改变其前后顺序。如abcdefg中,acdg,bdf属于它的子序列,而bac,dbfg则不是,因为它们与字符串的字符顺序不一致。
将问题分为较小的子问题:我们要求n个数的最长上升子序列,可以求前n-1个数的最长上升子序列,再跟第n个数进行判断。
让我们举个例子:求 2 7 1 5 6 4 3 8 9 的最长上升子序列。我们定义d(i) (i∈[1,n])来表示前i个数以A[i]结尾的最长上升子序列长度。
前1个数 d(1)=1 子序列为2;
前2个数 7前面有2小于7 d(2)=d(1)+1=2 子序列为2 7
前3个数 在1前面没有比1更小的,1自身组成长度为1的子序列 d(3)=1 子序列为1
前4个数 5前面有2小于5 d(4)=d(1)+1=2 子序列为2 5
前5个数 6前面有2 5小于6 d(5)=d(4)+1=3 子序列为2 5 6
前6个数 4前面有2小于4 d(6)=d(1)+1=2 子序列为2 4
前7个数 3前面有2小于3 d(3)=d(1)+1=2 子序列为2 3
前8个数 8前面有2 5 6小于8 d(8)=d(5)+1=4 子序列为2 5 6 8
前9个数 9前面有2 5 6 8小于9 d(9)=d(8)+1=5 子序列为2 5 6 8 9
d(i)=max{d(1),d(2),……,d(i)} 我们可以看出这9个数的LIS为d(9)=5
状态转移方程:
F [ i ] = max { F [ j ] + 1 ,F [ i ] } (1 <= j < i,A[ j ] < A[ i ])
代码模板:
for (int i = 0; i < n; i++) for (int j = 0; j < i; j++) if(list[i]>list[j]) dp[i] = max(dp[i], dp[j] + 1);
代码:
#include<iostream> #include<algorithm> using namespace std; int list[100]; int dp[100]; int main() { int n; scanf("%d", &n); for (int i = 0; i < n; i++) { scanf("%d", &list[i]); dp[i] = 1; } for (int i = 0; i < n; i++) for (int j = 0; j < i; j++) if(list[i]>list[j]) dp[i] = max(dp[i], dp[j] + 1); int maxx = -1; for (int i = 0; i < n; i++) maxx = max(maxx, dp[i]); printf("%d", maxx); }
输出LIS路径的代码:
思路:使用path标记前一个节点在list数组的下标
#include<iostream> #include<algorithm> #include<stack> using namespace std; int list[100]; int dp[100]; int path[100]; int main() { int n; int maxnum=-1, maxi; scanf("%d", &n); for (int i = 0; i < n; i++) { scanf("%d", &list[i]); dp[i] = 1; path[i] = -1; } for (int i = 0; i < n; i++) { for (int j = 0; j < i; j++) { if (list[i] > list[j]&&dp[i]<dp[j]+1) { dp[i] = dp[j] + 1; path[i] = j; } } if (dp[i] > maxnum) { maxnum = dp[i]; maxi = i; } } stack<int>out; printf("%d\n", maxnum); while (maxi != -1) { out.push(list[maxi]); maxi = path[maxi]; } while (!out.empty()) { printf("%d ", out.top()); out.pop(); } }
新建一个 low 数组,low [ i ]表示长度为i的LIS结尾元素的最小值
。对于一个上升子序列,显然其结尾元素越小,越有利于在后面接其他的元素,也就越可能变得更长。因此,我们只需要维护 low 数组,对于每一个a[ i ],如果a[ i ] > low [当前最长的LIS长度]
,就把 a [ i ]接到当前最长的LIS后面,即low [++当前最长的LIS长度] = a [ i ]。 那么,怎么维护 low 数组呢? 对于每一个a [ i ],如果a [ i ]能接到 LIS 后面,就接上去;否则,就用 a [ i ] 取更新 low 数组。具体方法是,在low数组中找到第一个大于等于a [ i ]的元素low [ j ],用a [ i ]去更新 low [ j ]。如果从头到尾扫一遍 low 数组的话,时间复杂度仍是O(n^2)。我们注意到 low 数组内部一定是单调不降的,所有我们可以二分 low 数组,找出第一个大于等于a[ i ]的元素。二分一次 low 数组的时间复杂度的O(lgn),所以总的时间复杂度是O(nlogn)。
但是,但是!!!序列并不一定是正确的最长上升子序列!只是序列的个数是对的!
代码
#include <cmath> #include <cstdio> #include <cstdlib> #include <cstring> #include <iostream> #include <algorithm> #include<set> using namespace std; const int maxn = 300003, INF = 0x7f7f7f7f; int list[maxn]; int n, ans; int main() { scanf("%d", &n); for (int i = 0; i <n; i++) { scanf("%d", &list[i]); } set<int>out; for (int i = 0; i < n; i++) { ans = list[i]; auto it = out.lower_bound(ans); if (it != out.end()) out.erase(it);//不是最大的,所以不在最后,所以替换这个位置的数字,使它变小 out.insert(ans); } printf("%d", out.size()); }
关于lower_bound( )和upper_bound( )的常见用法
(https://blog.csdn.net/qq_40160605/article/details/80150252)
lower_bound
( begin,end,num):从数组的begin位置到end-1位置二分查找第一个大于或等于
num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
upper_bound
( begin,end,num):从数组的begin位置到end-1位置二分查找第一个大于
num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
在从大到小的排序数组中,重载lower_bound()和upper_bound()
lower_bound
( begin,end,num,greater<type>
() ):从数组的begin位置到end-1位置二分查找第一个小于或等于
num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
upper_bound
( begin,end,num,greater<type>
() ):从数组的begin位置到end-1位置二分查找第一个小于
num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
数组用法:
int list[100],a; lower_bound(list,list+10,a);
STL用法:
set<int>list; int a; auto it=list.lower_bound(a);
我们再来回顾O(n^2)DP的状态转移方程:F [ i ] = max { F [ j ] + 1 ,F [ i ] } (1 <= j < i,A[ j ] < A[ i ])
我们在递推F数组的时候,每次都要把F数组扫一遍求F[ j ]的最大值,时间开销比较大。我们可以借助数据结构来优化这个过程。
用树状数组来维护F数组(据说分块也是可以的,但是分块是O(n*sqrt(n))的时间复杂度,不如树状数组跑得快)
首先把A数组从小到大排序,同时把A[ i ]在排序之前的序号记录下来。然后从小到大枚举A[ i ],每次用编号小于等于A[ i ]编号的元素的LIS长度+1来更新答案,同时把编号大于等于A[ i ]编号元素的LIS长度+1。因为A数组已经是有序的,所以可以直接更新。有点绕,具体看代码。
还有一点需要注意:树状数组求LIS不去重的话就变成了最长不下降子序列了。
#include <iostream> #include <cstdio> #include <algorithm> #include <cstdlib> #include <cstring> #include <cmath> using namespace std; const int maxn =103,INF=0x7f7f7f7f; struct Node{ int val,num; }z[maxn]; int T[maxn]; int n; bool cmp(Node a,Node b) { return a.val==b.val?a.num<b.num:a.val<b.val; } void modify(int x,int y)//把val[x]替换为val[x]和y中较大的数 { for(;x<=n;x+=x&(-x)) T[x]=max(T[x],y); } int query(int x)//返回val[1]~val[x]中的最大值 { int res=-INF; for(;x;x-=x&(-x)) res=max(res,T[x]); return res; } int main() { int ans=0; scanf("%d",&n); for(int i=1;i<=n;i++) { scanf("%d",&z[i].val); z[i].num=i;//记住val[i]的编号,有点类似于离散化的处理,但没有去重 } sort(z+1,z+n+1,cmp);//以权值为第一关键字从小到大排序 for(int i=1;i<=n;i++)//按权值从小到大枚举 { int maxx=query(z[i].num);//查询编号小于等于num[i]的LIS最大长度 modify(z[i].num,++maxx);//把长度+1,再去更新前面的LIS长度 ans=max(ans,maxx);//更新答案 } printf("%d\n",ans); return 0; }
例题
https://www.luogu.com.cn/problem/P1439
题目分析
题目中要求的是最长公共子序列,但是最长公共子序列需要使用动态规划计算,而题目的数据量为100000,二维DP数组太大,所以需要寻找别的办法
题目中两行数字是一个数从1到n的排列,也就是说这两行数字的种类是一样的,就是顺序不一样,所以我们可以将数字进行离散化,让第一行的数字变为升序,然后求第二行数字的
#include<iostream> #include<cstdio> #include<algorithm> #include<cmath> using namespace std; int list[100001]; int list2[100001]; int dp[100001]; int main() { int n; cin >> n; for (int i = 1; i <=n; i++) { int temp; scanf("%d", &temp); list[temp] = i; dp[i] = 1; } for (int i = 1; i <= n; i++) { int temp; scanf("%d", &temp); list2[i] = list[temp]; } for (int i = 1; i <=n; i++) for (int j = 1; j <i; j++) if (list2[i]>list2[j]) dp[i] = max(dp[i], dp[j] + 1); int maxx = -1; for (int i = 1; i <=n; i++) maxx = max(maxx, dp[i]); printf("%d", maxx); }
改进版:
#include<iostream> #include<cstdio> #include<algorithm> #include<cmath> #include<set> using namespace std; int list[100001]; int list2[100001]; int main() { int n,ans; cin >> n; for (int i = 1; i <= n; i++) { int temp; scanf("%d", &temp); list[temp] = i; } for (int i = 1; i <= n; i++) { int temp; scanf("%d", &temp); list2[i] = list[temp]; } set<int>out; for (int i = 1; i <=n; i++) { ans = list2[i]; auto it = out.lower_bound(ans); if (it != out.end()) out.erase(it);//不是最大的,所以不在最后,所以替换这个位置的数字,使它变小 out.insert(ans); } printf("%d", out.size()); }