我们要区分一下子序列和子串。
简单的来讲就是子序列是可以不连续但必须保证与给定的原数组相同的顺序,
子串就是必须连续并且保证与给定的原数组顺序相同。
我们讨论的主要是子序列。
- 最长单调子序列
- 以最长上升子序列为例,讨论最长单调子序列。
① d[i] = max{ d[j] | j < i && a[j] < a[i] } + 1;
很眼熟的一个状态转移方程,但是只会这个 远远不够 。 因为他的复杂度 为 O(n2);
如果我给你的数组大小为 100000;是不是妥妥的 超时呢。
我们就来看一种新的思路:
② 根据数据的范围来看 如果我们有一个 O (nlogn)的算法就可以啦。
我们在 一个数组里 计算的 以这个数组下标为长度的 最小末尾是什么 。
有了这数组我们就可以根据数组的最大下标求得最长的上升子序列。
来 , 我举个栗子;
B[4]=8; 代表的就是最长的上升子序列的长度为4,这个序列的 最后一个数在所有可选值里面最小的是 8 。
有了这个东西,我们就可以用二分啦。 这样子我们的复杂度就可以到 O(nlogn)了。
怎么更新呢 ?
假设给定数组为 arr数组,maxx为数组下标。
我们用B数组来表示我们要维护的数组,最开始的时候我们让马下maxx=1,B【maxx】= arr【1】;
每次就是更新就是在B数组里面去找 arr【i】; 因为里面的特性一定是有序的,就在查找的时候做二分。看二分的返回值 , 如果这个值 如果这个返回值tmp,B[tmp] >=arr[i],说明arr[i]不是一个可以使序列变长的数字,但是他可能是一个使得序列最小末尾变小的一个数字,所以我们就要更新相应的最小末尾。
B【tmp】=arr[i];
反之,则说明,arr【i】> B【maxx】所以我们的序列可以变长了。
B【++maxx】= arr【i】;
例题:Stock Exchange
AC code:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 1e5+10;
int arr[maxn];
int b[maxn]; //存的是最长长度 为数组下标的 LIS 的最小末尾
int maxx;
int binary(int k){
int l=1,r=maxx,mid;
while(l<r){
mid=(l+r)>>1;
if(b[mid]>=k)
r=mid;
else{
l=mid+1;
}
}
return l;
}
int main()
{
int n;
while(~scanf("%d",&n)){
for(int i=0;i<n;i++)
scanf("%d",&arr[i]);
maxx=1;
b[maxx]=arr[0];
for(int i=1;i<n;i++){
int tmp=binary(arr[i]);
if(b[tmp]>=arr[i]){
b[tmp]=arr[i];
}
else{
b[++maxx]=arr[i];
}
}
printf("%d\n",maxx);
}
}
③ 一个序列的最长上升子序列不一定是唯一的,所以我们要学会如何求解一个序列的最长子序列的个数。
所以我们需要在最长上升子序列的基础上去再增加一个 动态规划 ,就是以 arr【i】结尾的 子序列里有几个长度为 dp[i]的 记为 num[i];
看一个最长下降子序列的题目:POJ-1952
这题目就是求解最长下降子序列的个数。
if (dp[i] == dp[j]+1) num[i]+= num[j];
if(dp[j]+1>dp[i]) sum[i]=sum[j];
意思就是如果dp 转移之后,如果最长的长度相同证明有多种转移方式,就把num加起来,如果是一个新的转移就直接等于 num【j】.这个时候如果数据是321321就会有很多种重复情况,这时就要去重,有两个地方要去重,第一个是在计算dp的值的时候,对于同一个个数转移过来的要去重,如32xxxx和3xxx2x这两种,这时如果在3xxx2x 中间的xxx都没有比2大的数那么肯定是取前面的2,后面的2就可以去掉了。第二个是在计算sum的时候,一旦遇到相同的数,就要跳出循环,否则会重复计算,详见代码理解。
此题目参考博客:大佬博客
ac code
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <vector>
using namespace std;
const int maxn = 5e3+10;
int arr[maxn];
int dp[maxn]; //以arr[i]结尾的最长递减子序列的长度。
int num[maxn]; //以arr[i]结尾的最长的个数。
int vis[maxn];
int main()
{
int n;
while(~scanf("%d",&n)){
for(int i=0;i<n;i++){
scanf("%d",&arr[i]);
vis[i]=1;
num[i]=1;
dp[i]=1;
}
int maxx=0,sum=0;
for(int i=0;i<n;i++){
for(int j=i-1;j>=0;j--){
if(arr[i]<arr[j]&&vis[j]==1){
if(dp[i]==dp[j]+1){
num[i]+=num[j];
}
else{
if(dp[i]<dp[j]+1){
dp[i]=dp[j]+1;
num[i]=num[j];
}
}
}
else{
if(arr[i]==arr[j]){
if(dp[i]==1) vis[i]=0;
break;
}
}
}
if(maxx<dp[i])maxx=dp[i];
}
for(int i=0;i<n;i++){
if (maxx==dp[i])
sum+=num[i];
}
printf("%d %d\n",maxx,sum);
}
return 0;
}
④ 将一个序列分为x个不下降子序列,求最小的x。
可以转化成求原序列的最长递增子序列。
证明:
因为x堆中每一堆的元素都是单调不增的,所以对于原序列的最长递增子序列的每个元素在x 堆中每堆至多一个元素 所以最长递增子序列长度L的最大值 为 x ,
所以有 x > = L;
我们要求的 x 的最小值就是 L .
例题 : Wooden Sticks POJ - 1065
AC Code
/*这道题的要求其实是将所有stick分为x个不下降子序列( Ai <= Ai+1 ),然后问题归结于求x的最小值。*/
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxn = 5e3+10;
struct node {
int l,w;
friend bool operator < (node a,node b){
return a.l<b.l;
}
}arr[maxn];
int dp[maxn];
int main()
{
int t,n;
scanf("%d",&t);
while(t--){
scanf("%d",&n);
for(int i=0;i<n;i++){
scanf("%d%d",&arr[i].l,&arr[i].w);
}
sort(arr,arr+n);
int maxx=0;
for(int i=0;i<n;i++){
dp[i]=1;
for(int j=0;j<i;j++){
if(arr[j].w>arr[i].w){
dp[i]=max(dp[j]+1,dp[i]);
}
}
maxx=max(maxx,dp[i]);
}
cout<<maxx<<endl;
}
}
请各位批评指正,如有错误请留言。