前题引入戳一戳
有关最长上升子序列问题,从前只会用动态规划解决,直到看了P1020 导弹拦截这个题才知道还有更省时更巧妙的方法,也拓一拓思路,搜索了一些资料了解了LIS和LCS,下面做个简单总结。
子序列问题
一、LIS
1.概念
•LIS即最长上升子序列。对于固定的数组,LIS序列不一定是唯一的,但LIS的长度一定是唯一的。
例如:序列{2,4,3,7,9}
LIS序列可以是{2,4,7,9}或{2,3,7,9},长度都为4。
•求LIS的三种方法:O(n2)的DP,O(nlogn)的二分+贪心法,O(nlogn)的树状数组优化的DP。
2.方法
动态规划 O(n2)
这里先明确一下子序列的概念,简单地说子序列不一定连续但在原序列里的前后顺序不可变。再说这个题的思路,每个状态可以理解为以每个位置为终点的最长上升子序列。这里需要通过双重循环实现,外循环记录终点位置,内循环记录终点前的点,利用一个一维数组记录终点位置子序列的最大长度和一个变量记录终点前点递增位置的子序列长度。
简单说就是如果以当前点为终点,找之前小于当前位置的点的最大子序列,在此基础上加1作为当前点的最大子序列。这只是一种情况,还需结合每一个位置为终点的情况再求出一个最终解。
详细回顾
最长上升子序列模板:
#include<iostream>
#include<cstdio>
using namespace std;
int a[1005],b[1005];//b[i]表示以第i个数为终点的最长上升子序列的长度
int main()
{
int x,n=1,sum=0;
int n;
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<=n;i++)
{
b[i]=1;
for(int j=1;j<i;j++)
{
if(a[j]<a[i])
b[i]=max(b[i],b[j]+1);
}
sum=max(sum,b[i]);
}
printf("%d",sum);
}
二分+贪心 O(nlogn)
假设我们构建一个数组a[]存最长上升子序列,len表示a的最大下标。当我们从头扫描已知序列时,遇到一个数比a[len]大时,直接把它加到a[len]后面即可,即a[len++],但如果比a[len]小或者相等时,本应跳过,这里考虑两种情况,设当前扫描到已知序列的数为y:
•如果a[len-1]<y<a[len],根据贪心思想,肯定是序列a的末尾数越小越好,这样后面就可以接更多数。所以用y替换当前的a[len],即a[len]=y。
•如果y<…<a[len-1]<a[len],此时并不影响序列以后的连接。
以上两种情况怎么实现呢?对于情况1,我们可以理解为找数组a中第一个大于等于y的下标k,情况1是k=len,然后替换a[k]=y,这时你就会发现该做法代到情况2里也成立 ,因为序列a的末尾根本没受影响。
但为啥要找第一个大于等于x的数,找第一个大于x的数不行吗?
举个例子:
原始序列为{9,7,3,3,5,4,2}
当扫描到9时,{9}
当扫描到7时,{7}
当扫描到3时,{3}
当扫描到3时,此时最长上升子序列里找不到第一个大于3的数,所以应该找第一个大于等于3的数,确保替换成功。
下面有两种方法找第一个大于等于y的数:
法1:自己写二分查找函数:二分回顾
模板:
#include<iostream>
#include<functional>
#include<algorithm>
#include<cstring>
using namespace std;
int a[100005],b[100005];
int n=0,len=0;
int Find(int m)
{
int l=0,r=len;//数组存数的最大下标
while(l<r)
{
int mid=l+r>>1;
if(m<=b[mid]) r=mid;
else l=mid+1;
}
return l;
}
int main( )
{
cin>>n;
for(int i=0; i<n; i++)
cin>>a[i];
b[0]=a[0];
for(int i=1; i<n; i++)
{
if(b[len]<a[i])
{
b[++len]=a[i];
}
else
{
b[Find(a[i])]=a[i];
}
}
cout<<len+1<<endl;
}
法2:
lower_bound会找出序列中第一个大于等于x的数
upper_bound会找出序列中第一个大于x的数
lower_bound与upper_bound都是在序列已经相对递增有序的基础上实现的
我们可以让k=lower_bound(b,b+len,a[i])-b,k即为所要替换的下标。
模板:
#include<iostream>
#include<functional>
#include<algorithm>
#include<cstring>
using namespace std;
int a[100005],b[100005];
int main( )
{
int n,len=0;
cin>>n;
for(int i=0;i<n;i++)
cin>>a[i];
b[0]=a[0];
for(int i=1; i<n; i++)
{
if(b[len]<a[i])
{
b[++len]=a[i];
}
else
{
int k=lower_bound(b,b+len,a[i])-b;
b[k]=a[i];
}
}
cout<<len+1<<endl;
}
树状数组优化的DP O(nlogn)
树状数组回顾
数状数组优化的DP的参考来源
把a[]从小到大排序,同时把a[i]在排序之前的序号记录下来,然后从小到大枚举a[i],每次用编号小于等于a[ i ]编号的元素的LIS长度+1来更新答案,同时把编号大于等于a[ i ]编号元素的LIS长度+1。因为a数组已经是有序的,所以可以直接更新。
注意:树状数组求LIS不去重的话就变成了最长不下降子序列了。
树状数组 1. 求前缀最大值, 2.单点修改(往大里改)
#include<iostream>
#include<math.h>
#include<algorithm>
#include<cstdio>
#define lowbit(x) ((x)&-(x))
using namespace std;
const int INF=0x7f7f7f7f;
struct Node
{
int val,num;
} a[100005];
int n,b[100005];
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)//把b[x]替换为b[x]和y中较大的数
{
while(x<=n)
{
b[x]=max(b[x],y);
x+=lowbit(x);
}
}
int query(int x)//返回b[1]~b[x]中的最大值
{
int res=-INF;
while(x)
{
res=max(res,b[x]);
x-=lowbit(x);
}
return res;
}
int main( )
{
int ans=0;
scanf("%d",&n);
for(int i=1; i<=n; i++)
{
scanf("%d",&a[i].val);
a[i].num=i;//记住编号,有点类似于离散化的处理,但没有去重
}
sort(a+1,a+n+1,cmp);//以权值为第一关键字从小到大排序
for(int i=1; i<=n; i++) //按权值从小到大枚举
{
int maxx=query(a[i].num);//查询编号小于等于num的LIS最大长度
modify(a[i].num,++maxx);//把长度+1,再去更新前面的LIS长度
ans=max(ans,maxx);//更新答案
}
printf("%d\n",ans);
}
3.推广
最长下降子序列
O(n2)动态规划:
#include<iostream>
#include<cstdio>
using namespace std;
int a[1005],b[1005];//b[i]表示以第i个数为终点的最长下降子序列的长度
int main()
{
int x,n=1,sum=0;
int n;
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<=n;i++)
{
b[i]=1;
for(int j=1;j<i;j++)
{
if(a[j]>a[i])
b[i]=max(b[i],b[j]+1);
}
sum=max(sum,b[i]);
}
printf("%d",sum);
}
O(nlogn)二分+贪心:
#include<iostream>
#include<functional>
#include<algorithm>
#include<cstring>
using namespace std;
int a[100005],b[100005];
int main( )
{
int n,len=0;
cin>>n;
for(int i=0;i<n;i++)
cin>>a[i];
b[0]=a[0];
for(int i=1; i<n; i++)
{
if(b[len]>a[i])
{
b[++len]=a[i];
}
else
{
int k=lower_bound(b,b+len,a[i],greater<int>())-b;
b[k]=a[i];
}
}
cout<<len+1<<endl;
}
或
#include<iostream>
#include<functional>
#include<algorithm>
#include<cstring>
using namespace std;
int a[100005],b[100005];
int n=0,len=0;
int Find(int m)
{
int l=0,r=len;//数组存数的最大下标
while(l<r)
{
int mid=l+r>>1;
if(m>=b[mid]) r=mid;
else l=mid+1;
}
return l;
}
int main( )
{
cin>>n;
for(int i=0; i<n; i++)
cin>>a[i];
b[0]=a[0];
for(int i=1; i<n; i++)
{
if(b[len]>a[i])
{
b[++len]=a[i];
}
else
{
b[Find(a[i])]=a[i];
}
}
cout<<len+1<<endl;
}
最长不下降子序列
O(n2)动态规划:
#include<iostream>
#include<cstdio>
using namespace std;
int a[1005],b[1005];//b[i]表示以第i个数为终点的最长不下降子序列的长度
int main()
{
int x,n=1,sum=0;
int n;
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<=n;i++)
{
b[i]=1;
for(int j=1;j<i;j++)
{
if(a[j]<=a[i])
b[i]=max(b[i],b[j]+1);
}
sum=max(sum,b[i]);
}
printf("%d",sum);
}
O(nlogn)二分+贪心:
#include<iostream>
#include<functional>
#include<algorithm>
#include<cstring>
using namespace std;
int a[100005],b[100005];
int main( )
{
int n=0,sum=0,x;
while(cin>>x)
{
a[n++]=x;
}
b[0]=a[0];
for(int i=1; i<n; i++)
{
if(b[sum]<=a[i])
{
b[++sum]=a[i];
}
else
{
int t=upper_bound(b,b+sum,a[i],greater<int>())-b;
b[t]=a[i];
}
}
cout<<sum+1<<endl;
}
或
#include<iostream>
#include<functional>
#include<algorithm>
#include<cstring>
using namespace std;
int a[100005],b[100005];
int n=0,len=0;
int Find(int m)
{
int l=0,r=len;//数组存数的最大下标
while(l<r)
{
int mid=l+r>>1;
if(m<b[mid]) r=mid;
else l=mid+1;
}
return l;
}
int main( )
{
cin>>n;
for(int i=0; i<n; i++)
cin>>a[i];
b[0]=a[0];
for(int i=1; i<n; i++)
{
if(b[len]<=a[i])
{
b[++len]=a[i];
}
else
{
b[Find(a[i])]=a[i];
}
}
cout<<len+1<<endl;
}
最长不上升子序列
O(n2)动态规划:
#include<iostream>
#include<cstdio>
using namespace std;
int a[1005],b[1005];//b[i]表示以第i个数为终点的最长不上升子序列的长度
int main()
{
int x,n=1,sum=0;
int n;
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<=n;i++)
{
b[i]=1;
for(int j=1;j<i;j++)
{
if(a[j]>=a[i])
b[i]=max(b[i],b[j]+1);
}
sum=max(sum,b[i]);
}
printf("%d",sum);
}
O(nlogn)二分+贪心:
#include<iostream>
#include<functional>
#include<algorithm>
#include<cstring>
using namespace std;
int a[100005],b[100005];
int main( )
{
int n=0,sum=0,x;
while(cin>>x)
{
a[n++]=x;
}
b[0]=a[0];
for(int i=1; i<n; i++)
{
if(b[sum]>=a[i])
{
b[++sum]=a[i];
}
else
{
int t=upper_bound(b,b+sum,a[i],greater<int>())-b;
b[t]=a[i];
}
}
cout<<sum+1<<endl;
}
或
#include<iostream>
#include<functional>
#include<algorithm>
#include<cstring>
using namespace std;
int a[100005],b[100005];
int n=0,len=0;
int Find(int m)
{
int l=0,r=len;//数组存数的最大下标
while(l<r)
{
int mid=l+r>>1;
if(m>b[mid]) r=mid;
else l=mid+1;
}
return l;
}
int main( )
{
cin>>n;
for(int i=0; i<n; i++)
cin>>a[i];
b[0]=a[0];
for(int i=1; i<n; i++)
{
if(b[len]>=a[i])
{
b[++len]=a[i];
}
else
{
b[Find(a[i])]=a[i];
}
}
cout<<len+1<<endl;
}
小总结
最长上升子序列和最长下降子序列都用lower_bound()函数
最长不上升子序列和最长不下降子序列都用upper_bound()函数
二、LCS
1.概念
·公共子序列:如果Z既是X的子序列,又是Y的子序列,则称Z为X和Y的公共子序列
·LCS就是最长公共子序列。
2.方法
动态规划
要抽象出一个二维数组,这个二维数组的第0行和第0列我们不用,让它默认为0,从第一行和列开始,行代表A串的每个字符位置,列代表B串的每个字符位置。这里需要用到双重循环实现,大循环循环A,小循环寻找B的每个位置与A的公共子序列,如果B的某位置与A当前位置字符相同,就在前一个公共子序列的基础上加一,如果不同,就找前一步最大的当做当前位置的值
状态转移方程:
当i=0或j=0时:DP[i][j]=0
当A[i] == B[j] 时 :DP[i][j] = DP[i-1][j-1] + 1
当A[i] != B[j] 时 : DP[i][j] = MAX(DP[i-1][j] , DP[i][j-1])
#include <iostream>
#include <cstdio>
#include <cstring>
#define MAX 1000
int dp[MAX+1][MAX+1];
char s[MAX],t[MAX];
int max(int a,int b)
{return a>b?a:b;}
int main()
{
int N,i,j,n,m;
scanf("%d",&N);
while(N--)
{
scanf("%s%s",s,t);
int x=strlen(s),y=strlen(t);
for(i=0;i<x;i++)
{
for(j=0;j<y;j++)
if(s[i]==t[j])
dp[i+1][j+1]=dp[i][j]+1;
else
dp[i+1][j+1]=max(dp[i+1][j],dp[i][j+1]);
}
printf("%d\n",dp[i][j]);
}
return 0;
}