最长XX子序列

@

1 最长上升子序列

最长上升子序列(LIS, the Longest Increasing Subsequence),指对于一个数列,一个最长的子序列满足 ai<aj(i<j) (即严格递增)。该子序列被称为最长上升子序列。

例: 5 7 1 9 2 4 3 7 6 中最长子序列的长度是 4,最长上升子序列为 1 2 4 71 2 4 61 2 3 71 2 3 6

1.1 求最长上升子序列的长度-分治解

首先构造分治式:

考虑以 i 结尾的最长上升子序列,发现其长度只与上一个元素 j 有关,只要满足 aj<ai,便可以构造出一个以 ai 结尾、 aj 为倒数第二个元素的上升子序列。显然,其长度为以 aj 结尾的最长上升子序列长度加一。

  • 原问题:求以 ai 结尾的最长上升子序列的长度。

  • 子问题:求以 aj 结尾的最长上升子序列的长度。(其中 aj<ai,j<i)。

  • 基本情况:无(因为最长上升子序列的第一个元素必然没有比他更小的元素,若有则不构成最长上升子序列)

  • 合并:max{ 子问题的解 }+1

分析时间复杂度:由于重复计算,最坏情况下可能达到 O(2n)。进行记忆化搜索,时间复杂度降为 O(n2)

当然,有了记忆化,也可以逆推出其中一条最长上升子序列。时间复杂度 O(n)

代码:

#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解

根据分治解设计动态规划。

  • 状态:dpi 表示以 ai 结尾的最长上升子序列长度。

  • 转移:dpi=max{dpi,dpj+1|aj<ai,j<i}

  • 初值:dpi=1(1in)

  • 目标:max{dpi}(1in)

时间复杂度 O(n2)

代码(只放关键部分):

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 更优一些,因为更小的元素意味着后面可以接更多的值。如 a5=4 ,它只能接在 3 后面,而不能接在 7 后面。

我们可以见一个 b 数组,来存储这些信息。我们令 bi 表示长度为 i 的上升子序列的结尾的最小值。由于要让值尽量的小,我们设 b 的初值为 INF (无穷大)。我们进行如下模拟:

a1=2 ,发现 b 皆为 INF ,于是 a1 变为上升子序列的第一个元素,即 b1=1。这时 b 为:2 INF INF INF INF...

a2=7 ,发现它比 b1 即第一个元素大,可以作为第二个元素,即 b2=7。这时 b 为:2 7 INF INF INF INF...

a3=3 ,根据刚才的结论,由于 3 比 7 小,无法做为第三个元素,而 3 相较 7 更优,替换 7 ,即 b2=3。这时 b 为:2 3 INF INF INF INF...

a4=9 ,由于它比 b2 即最后一个元素大,所以把它作为一个新的元素,即 b3=9。这时 b 为:2 3 9 INF INF INF INF...

a5=4 ,由于不能作为下一个元素,所以我们寻找它可以替换的元素。我们最小只能把 b3=9 替换,即 b3=4。这时 b 为:2 3 4 INF INF INF INF...

我们发现:b 无论如何变化,始终保持单调递增

这样,我们便可以得到如下操作:

  1. 初始化 b 为 INF,cnt 设为 0 (表示目前最长上升子序列的最长长度,也是 b 中最后一个不为 INF 的位置)

  2. 得到一个 ai ,若它比 bcnt 大,则把它作为新的元素,cntcnt+1,bcntai 。否则二分查找第一个大于等于该值的元素 bj ,替换 bjai

  3. 重复执行操作 2

这样,最终 cnt 便是最长上升子序列的长度。不过要注意了,b 数组不一定是最长上升子序列

二分查找时间复杂度 O(log2(n)) ,遍历 a 时间复杂度 O(n) ,总体时间复杂度 O(nlog2(n))

这里还顺便还原了一下最长上升子序列。

其中 ti 表示 ai 作为最长上升子序列结尾的前驱(上一个元素在 t 中的坐标)。由于放入 ba 数组的坐标可能会乱,所以用 ri 来表示 bia 中的坐标。

在长度增加时,更新皆为元素的前驱;而在替换元素使,也要继承原来元素的前驱。

代码:

#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;
}

Last

就讲到这里啦觉得还不错的话留个赞赞在走吧
LINKS: ?

posted @   TimelessWelkin  阅读(20)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示