LIS是典型的DP问题,之前有做到过一些类型的LIS题目,现在把LIS的相关题型做一个总结,也是对思路的一个梳理。
一、最基础的LIS问题:
给出一个序列,只要求输出最长递增子序列的长度,对于这种题目我们可以用两种方法定义dp数组:
1.O(N^2)
dp[i]:以ai为末尾的最长上升子序列长度
//方法一:
//O(N^2)
//定义:dp[i]:以ai为末尾的最长上升子序列长度
//dp[i]=max(1,dp[j]+1(j<i&&a[j]<a[i]))
void solve()
{
int result=0;
for( int i = 0; i < n; i++ )
{
dp[i]=1;
for( int j = 0; j < i; j++ )
{
if( a[i] > a[j] )
dp[i] = max(dp[i],dp[j]+1);
}
result = max(result,dp[i]);
}
printf("%d\n",result);
return ;
}
2.O(NlogN)
dp[i]:长度为i的最长上升子序列中末尾元素的最小值(初始值为INF)
//方法二:
//O(NlogN)
//定义:dp[i]:长度为i的最长上升子序列中末尾元素的最小值(初始值为INF)
//dp[i]=min(dp[i],a[j](i==0 || dp[i-1]<a[j]));
void solve()
{
int result=0,pos;
fill(dp,dp+n,INF);
dp[0]=0;
for( int i = 0; i < n; i++ )
{
pos=lower_bound(dp,dp+n,a[i]) - dp;
dp[pos] = a[i];
//*lower_bound(dp,dp+n,a[i])=a[i];
}
result = lower_bound(dp,dp+n,INF)-dp;
printf("%d\n",result);
return ;
}二、最长递增子序列(打印)
在输出最长递增子序列长度的基础上,输出任意一组可满足解。这就要求我们不仅要求出最长递增子序列,而且在求的过程中要保存路径,并最终输出。同样,用两种方法求解:
1.O(N^2)
若按照基础LIS的方法二我们无法对路径进行保存,但是方法一中dp数组在保存了长度的同时还对元素进行了标记,利用这一点我们可以实现路径的保存,最终递归输出。
//方法一:
//O(N^2)
//定义:dp[i]:以ai为末尾的最长上升子序列长度
//dp[i]=max(1,dp[j]+1(j<i&&a[j]<a[i]))
int result=0;
void solve()
{
for( int i = 0; i < n; i++ )
{
dp[i]=1;
for( int j = 0; j < i; j++ )
{
if( a[i] > a[j] )
dp[i] = max(dp[i],dp[j]+1);
}
result = max(result,dp[i]);
}
printf("%d\n",result);
return ;
}
//传入参数n表示数组a中对应下标
void print(int n)
{
bool flag = false;
if( n < 0 || result == 0 )
return ;
if( dp[n] == result )
{
flag = true;
result--;
}
print(n-1);
if( flag )
printf("%d\n",a[n]);
}
2.O(NlogN)
从LIS的定义出发,我们要得到最长的递增子序列我们就需要让这个序列中前面的数尽量的小,所以我们在从前往后遍历的时候不断地用较小的数替换已有序列中的数,使序列有更大的机会变长。又因为已有序列是单调递增的,所以替换的过程我们可以利用lower_bound()以O(logN)实现。为了实现最后的序列输出,我们使用两个数组pos&fa来保存位置信息
定义:
pos存储长度为i的序列末尾最小值的角标;
fa存储长度为i的末尾最小值的前度;
主实现函数如下:
void solve()
{
memset(dp,0x3f,sizeof(dp));
pos[0]=-1;
int lpos;
for( int i = 0; i < n; i++ )
{
lpos=lower_bound(dp,dp+n,a[i])-dp;
dp[lpos]=a[i];
pos[lpos]=i;
if( lpos != 0 )
fa[i]=pos[lpos-1];
else
fa[i]=-1;
}
result = lower_bound(dp,dp+n,inf)-dp;
printf("%d\n",result);
print(pos[result-1]);
return ;
}
但是怎样确保这样就能得到正确的LIS并准确输出呢??参考同学的思想把这种插入操作看作是一棵二叉树的生成过程,答案就很明显了。
假定a序列为 1 3 5 2 4 6 5 3 LIS为1 2 4 5或者1 3 4 5
① 插入1, pos[1] = 1, fa[1] = -1; dp:1
② 插入3, pos[2] = 2, fa[2]= pos[1]=1; dp:1 3
③ 插入5, pos[3] = 3, fa[3]= pos[2]=2; dp:1 3 5
④ 插入2, pos[2] = 4, fa[4] = pos[1]=1; dp:1 2 5
⑤ 插入4, pos[3] = 5, fa[5] = pos[2]=4; dp: 1 2 4
⑥ 插入 6,pos[4] = 6, fa[6] = pos[3]=5; dp:1 2 4 6
⑦ 插入5, pos[4] = 7, fa[7] = pos[3]=5; dp:1 2 4 5
⑧ 插入3, pos[3] = 8, fa[8] = pos[2]=3; dp:1 2 3 5
dp[4] = 5对应的fa[4] = 7, 子序列最后一个元素为a[7],(a从1开始)
最终结果就是a[7], a[5], a[4], a[1],逆序即可。
为什么这样就能得到结果。其实认真看你会发现,它是一棵树。
1(1)
/ \
3(2) 2(2)——3(3)
/ \
5(3) 4(3)(更新这个节点的时候,3已经不是dp[2]了,但是树没被破坏)
/ \
5(4) 6(4)
pos表示的是层数,fa数组才是用来保存树的。
在一棵二叉树的生成过程中,我们每插入一个数都可以看做添加一个叶子的过程(叶子之后还可以继续发展成为枝干),通过我们每一次添加树叶的过程可以将所有的可能的子串都记录了下来,并最终得到最长子串。叶子添加的位置由叶子和原有二叉树最长枝干的相对大小决定,我们可以看到树叶的添加并不会影响最长枝干,只有深度与最长枝干相同时,最长子串(dp数组)才会被更新,在此之前我们随时可以依据已有保存信息找到最长子串并输出。
完整代码实现:
//方法二:
//O(NlogN)
//定义:dp[i]:长度为i的最长上升子序列中末尾元素的最小值(初始值为INF)
//dp[i]=min(dp[i],a[j](i==0 || dp[i-1]<a[j]));
int dp[mx];//储存长度为i的LIS的末尾值
int pos[mx];//储存长度为i的LIS末尾值的角标
int fa[mx];//储存长度为i的LIS末尾值前驱的角标
int result;
void solve()
{
memset(dp,0x3f,sizeof(dp));
pos[0]=-1;
int lpos;
for( int i = 0; i < n; i++ )
{
lpos=lower_bound(dp,dp+n,a[i])-dp;
dp[lpos]=a[i];
pos[lpos]=i;
if( lpos != 0 )
fa[i]=pos[lpos-1];
else
fa[i]=-1;
}
result = lower_bound(dp,dp+n,inf)-dp;
printf("%d\n",result);
print(pos[result-1]);
return ;
}
void print(int pos)
{
if( pos != -1 )
{
print(fa[pos]);
}
printf("%d\n",a[pos]);
}
三、最长不降子序列:
还是按照基本最长子串的方法二,只是把lower_bound()的比较函数重构一下。
代码实现:
//最长不降子序列
//O(NlogN)
//定义:dp[i]:长度为i的最长不降子序列中末尾元素的最小值(初始值为INF)
//dp[i]=min(dp[i],a[j](i==0 || dp[i-1]<=a[j]));
bool comp(int &a,int &b)
{
return a <= b;
}
void solve()
{
int result=0,pos;
fill(dp,dp+n,INF);
dp[0]=0;
for( int i = 0; i < n; i++ )
{
pos=lower_bound(dp,dp+n,a[i]) - dp;
dp[pos] = a[i];
//*lower_bound(dp,dp+n,a[i])=a[i];
}
result = lower_bound(dp,dp+n,INF,comp)-dp;
printf("%d\n",result);
return ;
}四、最长递减子序列:
与类型三相似。
代码实现:
//最长递减子序列
//O(NlogN)
//定义:dp[i]:长度为i的最长递减子序列中末尾元素的最小值(初始值为INF)
//dp[i]=min(dp[i],a[j](i==0 || dp[i-1] > a[j]));
bool comp(int &a,int &b)
{
return a > b;
}
void solve()
{
int result=0,pos;
fill(dp,dp+n,INF);
dp[0]=0;
for( int i = 0; i < n; i++ )
{
pos=lower_bound(dp,dp+n,a[i]) - dp;
dp[pos] = a[i];
//*lower_bound(dp,dp+n,a[i])=a[i];
}
result = lower_bound(dp,dp+n,INF,comp)-dp;
printf("%d\n",result);
return ;
}
本文详细解析了最长递增子序列(LIS)问题的不同解决方法,包括O(N^2)和O(NlogN)的算法实现,并介绍了如何打印LIS及扩展到最长不降和最长递减子序列。

638

被折叠的 条评论
为什么被折叠?



