最长XX子序列
@
1 最长上升子序列
最长上升子序列(LIS, the Longest Increasing Subsequence),指对于一个数列,一个最长的子序列满足
例: 5 7 1 9 2 4 3 7 6
中最长子序列的长度是 4,最长上升子序列为 1 2 4 7
、1 2 4 6
、1 2 3 7
和1 2 3 6
。
1.1 求最长上升子序列的长度-分治解
首先构造分治式:
考虑以
原问题:求以
结尾的最长上升子序列的长度。 子问题:求以
结尾的最长上升子序列的长度。(其中 )。 基本情况:无(因为最长上升子序列的第一个元素必然没有比他更小的元素,若有则不构成最长上升子序列)
合并:
子问题的解
分析时间复杂度:由于重复计算,最坏情况下可能达到
当然,有了记忆化,也可以逆推出其中一条最长上升子序列。时间复杂度
代码:
#include <bits/stdc++.h>
using namespace std;
int a[1005],f[1005];
int solve(int i) { // 以 i 为最后一个元素的最长上升子序列长度
if(f[i]!=0) return f[i];// 这个答案已经被搜索过
f[i]=1;// 初值为一(只有该元素)
for(int j=1;j<i;j++)// 子问题
if(a[j]<a[i]) f[i]=max(f[i],f[j]+1);// 递归分治
return f[i];
}
void LIS(int i) { // 以 i 结尾的最长上升子序列
for(int j=1;j<i;j++)
if(a[j]<a[i]&&f[j]+1==f[i]) {
LIS(j);// 输出一条最长上升子序列即可
break;
}
cout<<a[i]<<" ";
return ;
}
int main() {
int n,ans=0;
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++)
if(solve(i)>f[ans]) ans=i;
cout<<f[ans]<<endl;// 最长上升子序列长度
LIS(ans);
return 0;
}
1.2 最长上升子序列-DP解
根据分治解设计动态规划。
状态:
表示以 结尾的最长上升子序列长度。 转移:
初值:
目标:
时间复杂度
代码(只放关键部分):
for(int i=1;i<=n;i++) {
dp[i]=1;
for(int j=1;j<i;j++)
if(a[j]<a[i]) dp[i]=max(dp[i],dp[j]+1);
ans=max(ans,dp[i]);
}
1.3 最长上升子序列-贪心解
优化前,先举个栗子看看:
2 7 3 9 4 6 10 1 4
其中最长上升子序列为 2 3 4 6 10
。
根据 DP 模拟一下:
a[] 2 7 3 9 4 6 10 1 4
dp[] 1 2 2 3 3 4 5 1 3
观察: ^ ^
我们发现:当以 3 结尾的LIS长度变成 2 时,相比较 7 ,显然是 3 更优一些,因为更小的元素意味着后面可以接更多的值。如
我们可以见一个
2 INF INF INF INF...
2 7 INF INF INF INF...
2 3 INF INF INF INF...
2 3 9 INF INF INF INF...
2 3 4 INF INF INF INF...
我们发现:
这样,我们便可以得到如下操作:
初始化
为 INF, 设为 0 (表示目前最长上升子序列的最长长度,也是 中最后一个不为 INF 的位置) 得到一个
,若它比 大,则把它作为新的元素, 。否则二分查找第一个大于等于该值的元素 ,替换 重复执行操作 2
这样,最终
二分查找时间复杂度
这里还顺便还原了一下最长上升子序列。
其中
在长度增加时,更新皆为元素的前驱;而在替换元素使,也要继承原来元素的前驱。
代码:
#include <bits/stdc++.h>
using namespace std;
int a[1005],b[1005];
int t[1005],r[1005];// 用于还原最长上升子序列
// t[i] 表示 a[i] 作为结尾的最长上升子序列的上一个元素
// r[i] 表示 b[i] 在 a 中的坐标
const int INF=1e9;
void LIS(int i) {
if(i==0) return ;
LIS(t[i]);
cout<<a[i]<<" ";
return ;
}
int main() {
int n,cnt=0,ansi=0;
cin>>n;
for(int i=1;i<=n;i++) b[i]=INF;
for(int i=1;i<=n;i++) {
cin>>a[i];
int p=lower_bound(b+1,b+1+n,a[i])-b;// 这里使用了 C++ 内置的二分查找
// lower_bound 返回的是第一个大于等于查找元素的值的地址
if(b[p]==INF) {
b[++cnt]=a[i];// 作为结尾元素,长度加一
t[i]=r[cnt-1];// t[i] 指向的是上一个元素
ansi=i;
}
else t[i]=t[r[p]],b[p]=a[i];// a[i] 比 b[p] 更优,替换
r[p]=i;// b 中第 p 个元素为 i
}
cout<<cnt<<endl;// 最长上升子序列长度
LIS(ansi);// 还原 LIS
return 0;
}
2 其他最长XX子序列
其实只要稍微修改一下二分查找就行了(有些可能要自己打)。
这里列出了四种最长子序列的贪心(手写 lower_bound )版本的代码:
2.1 最长严格上升子序列 实现
#include <bits/stdc++.h>
using namespace std;
int a[1005],b[1005];
int t[1005],r[1005];// 用于还原最长上升子序列
// t[i] 表示 a[i] 作为结尾的最长上升子序列的上一个元素
// r[i] 表示 b[i] 在 a 中的坐标
const int INF=1e9;
void LIS(int i) {
if(i==0) return ;
LIS(t[i]);
cout<<a[i]<<" ";
return ;
}
int LowerBound(int l,int r,int val) {// [l,r]
while(l<=r) {
int mid=l+r>>1;
if(val>b[mid]) l=mid+1;
else r=mid-1;
}
return l;
}
int main() {
int n,cnt=0,ansi=0;
cin>>n;
for(int i=1;i<=n;i++) b[i]=INF;
for(int i=1;i<=n;i++) {
cin>>a[i];
int p=LowerBound(1,n,a[i]);
if(b[p]==INF) {
b[++cnt]=a[i];// 作为结尾元素,长度加一
t[i]=r[cnt-1];// t[i] 指向的是上一个元素
ansi=i;
}
else t[i]=t[r[p]],b[p]=a[i];// a[i] 比 b[p] 更优,替换
r[p]=i;// b 中第 p 个元素为 i
}
cout<<cnt<<endl;// 最长上升子序列长度
LIS(ansi);// 还原 LIS
return 0;
}
2.2 最长不严格上升子序列 实现
#include <bits/stdc++.h>
using namespace std;
int a[1005],b[1005];
int t[1005],r[1005];// 用于还原最长上升子序列
// t[i] 表示 a[i] 作为结尾的最长上升子序列的上一个元素
// r[i] 表示 b[i] 在 a 中的坐标
const int INF=1e9;
void LIS(int i) {
if(i==0) return ;
LIS(t[i]);
cout<<a[i]<<" ";
return ;
}
int LowerBound(int l,int r,int val) {// [l,r]
while(l<=r) {
int mid=l+r>>1;
if(val>=b[mid]) l=mid+1;
else r=mid-1;
}
return l;
}
int main() {
int n,cnt=0,ansi=0;
cin>>n;
for(int i=1;i<=n;i++) b[i]=INF;
for(int i=1;i<=n;i++) {
cin>>a[i];
int p=LowerBound(1,n,a[i]);
if(b[p]==INF) {
b[++cnt]=a[i];// 作为结尾元素,长度加一
t[i]=r[cnt-1];// t[i] 指向的是上一个元素
ansi=i;
}
else t[i]=t[r[p]],b[p]=a[i];// a[i] 比 b[p] 更优,替换
r[p]=i;// b 中第 p 个元素为 i
}
cout<<cnt<<endl;// 最长上升子序列长度
LIS(ansi);// 还原 LIS
return 0;
}
2.3 最长严格下降子序列 实现
#include <bits/stdc++.h>
using namespace std;
int a[1005],b[1005];
int t[1005],r[1005];// 用于还原最长上升子序列
// t[i] 表示 a[i] 作为结尾的最长上升子序列的上一个元素
// r[i] 表示 b[i] 在 a 中的坐标
const int INF=1e9;
void LIS(int i) {
if(i==0) return ;
LIS(t[i]);
cout<<a[i]<<" ";
return ;
}
int LowerBound(int l,int r,int val) {// [l,r]
while(l<=r) {
int mid=l+r>>1;
if(val<b[mid]) l=mid+1;
else r=mid-1;
}
return l;
}
int main() {
int n,cnt=0,ansi=0;
cin>>n;
for(int i=1;i<=n;i++) {
cin>>a[i];
int p=LowerBound(1,n,a[i]);
if(b[p]==0) {
b[++cnt]=a[i];// 作为结尾元素,长度加一
t[i]=r[cnt-1];// t[i] 指向的是上一个元素
ansi=i;
}
else t[i]=t[r[p]],b[p]=a[i];// a[i] 比 b[p] 更优,替换
r[p]=i;// b 中第 p 个元素为 i
}
cout<<cnt<<endl;// 最长上升子序列长度
LIS(ansi);// 还原 LIS
return 0;
}
2.4 最长不严格下降子序列 实现
#include <bits/stdc++.h>
using namespace std;
int a[1005],b[1005];
int t[1005],r[1005];// 用于还原最长上升子序列
// t[i] 表示 a[i] 作为结尾的最长上升子序列的上一个元素
// r[i] 表示 b[i] 在 a 中的坐标
const int INF=1e9;
void LIS(int i) {
if(i==0) return ;
LIS(t[i]);
cout<<a[i]<<" ";
return ;
}
int LowerBound(int l,int r,int val) {// [l,r]
while(l<=r) {
int mid=l+r>>1;
if(val<=b[mid]) l=mid+1;
else r=mid-1;
}
return l;
}
int main() {
int n,cnt=0,ansi=0;
cin>>n;
for(int i=1;i<=n;i++) {
cin>>a[i];
int p=LowerBound(1,n,a[i]);
if(b[p]==0) {
b[++cnt]=a[i];// 作为结尾元素,长度加一
t[i]=r[cnt-1];// t[i] 指向的是上一个元素
ansi=i;
}
else t[i]=t[r[p]],b[p]=a[i];// a[i] 比 b[p] 更优,替换
r[p]=i;// b 中第 p 个元素为 i
}
cout<<cnt<<endl;// 最长上升子序列长度
LIS(ansi);// 还原 LIS
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!