最长上升子序列问题(LIS问题)与最长不上升子序列问题的四种方法(c++ 模板代码)

这篇具有很好参考价值的文章主要介绍了最长上升子序列问题(LIS问题)与最长不上升子序列问题的四种方法(c++ 模板代码)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

最大上升子序列问题也叫做LIS问题,与最大公共子序列LCS问题是一类经典问题,在本章我们将总结一下求解LIS最大上升子序列几种方法,同时也会给出对应的最大不上升子序列的求解方法。 关于LCS问题,我在后面会再出一篇博客来讲解,

废话不多说,我们直接进入正题,如果你还一点都不了解LIS问题,那么请不要看这篇博客,本篇博客只是对于LIS的求解的总结与归纳,但凡是涉及结论公式求证的我一概不会论证,其实是我不会 ,在这里我将会直接使用


最大上升子序列: [4,2,3,6,9] 是一个序列,那么显而易见他的LIS应该是 [2,3,6,9],长度为4吗,注意LIS问题是可以不连续的,我这个例子是连续的。

动态规划

我们定义dp[i] 作为以i结尾的子序列的最大上升子序列的长度

因此,我们可以知道,如果我们要求dp[i],必须首先对i之前位置的元素 j 进行逐一遍历,然后和i位置的元素比较,只要j位置的元素小于i位置的元素,那么就是一个满足条件的元素,注意整个过程中需要满足
j < i , a [ j ] < a [ i ] j<i ,a[j]<a[i] j<ia[j]<a[i]
那么我们便可以通过来求得dp[i]
d p [ i ] = m a x ( d p [ i ] , d p [ j ] + 1 ) dp[i]=max(dp[i],dp[j]+1) dp[i]=max(dp[i],dp[j]+1)
因为如果找到了一个符合条件的 a[j]<a[i] ,那么j这个位置的元素就可以是dp[i]的一个最大上升子序列的元素,因此dp[j]+1 将此长度加1,由于dp是一个状态转移的过程,我们还要确保之前的值与当前求得最长的长度要取得一个最大值

代码如下:

/*
最长上升子序列
*/
for (int i = 1; i <= n; i++)
{	
	dpAdd[i] = 1;	//以每一个i位置开始的元素的默认最长上升子序列长度是1(该元素本身)
	for (int j = 1; j < i; j++)	//遍历所有比i位置小的元素
	{
		if (a[j] < a[i])	//查找前面的位置的元素是否小于i位置
		{
			//存在一个比i位置小的元素在j位置,则记录j位置dp要加1,表示在j位置的后面可以找到一个i位置,使得i位置的元素比j位置大,i和j满足一个上升的子序列,所以取dp[j]+1 和 它本身的dp[i]的较大者
			dpAdd[i] = max(dpAdd[i], dpAdd[j] + 1);
		}
	}
	//dp[i]更新为[0,i]区间内的最长上升子序列的最大长度
	res = max(res, dpAdd[i]);
}

求解最大不上升子序列的问题也是如此:
最大不上升子序列指的是当前元素的后面的所有元素都小于等于当前元素,即
i < j , a [ i ] > = a [ j ] i<j, a[i]>=a[j] i<ja[i]>=a[j]
它的dp状态转移方程是一样的,因此我们便可以通过将遍历顺序倒过来,来求解最大不上升子序列问题。

/*
	最长不上升子序列
	*/
	for (int i = n; i >= 1; i--)
	{
		dpNAdd[i] = 1;
		for (int j = i + 1; j <= n; j++)	//在i的后面的元素开始
		{
			if (b[j] <= b[i])	//i后面的要小于等于i,所以才是不上升的
			{
				dpNAdd[i] = max(dpNAdd[i], dpNAdd[j] + 1);
			}
		}
		ans = max(ans, dpNAdd[i]);
	}

可见,在用DP方法来求解LIS问题中,我们用到了两重循环,因此在最差的情况下我们的时间复杂度为 O ( N 2 ) O(N^2) O(N2)

另外我们可以发现,我们在DP的过程中,注意到在转移的过程中,我们需要反复地从 i - i中找满足条件的 j的位置,并且寻找最大的 a[j],那么我们可以把这个过程抽象为一个从区间中寻找最大值的过程,那么我们便可以使用树状数组或者线段树来对这个过程进行维护。


树状数组

我们使用树状数组来查询 1-i 中的区间的最大值,这里我简单画了一个树状数组的实现的过程:
最长不上升子序列,动态规划,c++,动态规划,算法
需要注意的点:

  1. 在查询最大上升子序列的时候,由于前面的元素j不能等于 i位置,所以我们干脆就查询 a[i]-1,这样就做到了在 i之前查询
  2. 在查询最大不上升子序列的时候,看作是逆序的上升子序列,我们就把遍历的顺序从后往前,这样就做到了查询了i之后的元素,同时是小于等于的,直接a[i]即可。
  3. 每次查询的时候都需要最后加上1,因为还要包含其自身

为什么可以使用树状数组?

我认为如上图所示,树状数组每次查询的时候都是查询所有比它小的位置的元素,而这些位置对于i来说都是 j<i 的,所以说只要在查询的时候获得一个最大值,即获得了以i结尾的最大不上升子序列的长度。

代码如下:

int tree[N];		
inline int lowbit(int i)
{
	return i & (-i);
}
void update(int i,int val)
{
	while (i <= maxn)
	{
		tree[i] = max(tree[i], val);
		i += lowbit(i);
	}
}
int query(int i)
{
	int sum = 0;
	while (i > 0)
	{
		sum = max(sum, tree[i]);
		i -= lowbit(i);
	}
	return sum;
}

int main()
{
	/*
	最长上升子序列
	*/
	maxn = 9;	//数组的元素最大值
	for (int i = 1; i <= n; i++)
	{
		int num = query(a[i] - 1) + 1;
		/*
		相当于for (int j=1;j<i;j++) if (a[j]<a[i])
			查询以a[i]结尾的最长上升子序列的最大长度,注意:查询时不包含a[i]元素,所以长度要再加1
		*/
		update(a[i], num);//维护树状数组,即维护了各个位置元素的最大上升子序列的长度
		res = max(res, num);
	}
	cout << res << endl << endl;

	memset(tree, 0, sizeof(tree));
	/*
	最长不上升子序列
	*/
	for (int i = n; i >= 1; i--)
	{
		//要查询的是不上升,所以后面的小于等于前面的,所以说查询时包括b[i],无须向上面一样减1
		int num = query(b[i]) + 1;	

		update(b[i], num);
		ans = max(ans, num);
	}
	cout << ans;
	return 0;
}

代码中首先定义了一些常量和变量,其中N表示数组的最大长度,nums是存储输入数字的数组,num表示当前输入的数字,p表示数字中的最大值,n表示输入数字的个数,tree是树状数组的数组。其中,树状数组的长度为p,因为输入数字的最大值为p。
接着定义了一个lowbit函数,用来计算i的二进制表示中最低位的1的位置,这个函数在树状数组中会被用到。
update函数是用来更新树状数组的,它的参数i表示要更新的位置,val表示要更新的值。在这个函数中,先使用while循环将tree[i]和val取最大值,然后将i加上lowbit(i),继续更新下一个位置。
query函数是用来查询树状数组的,它的参数i表示要查询的位置。在这个函数中,先将ans赋值为负无穷,然后使用while循环将tree[i]和ans取最大值,再将i减去lowbit(i),继续查询上一个位置。
最后,在主函数中,先使用while(cin>>num)循环读入数字,并将其存入nums数组中,同时更新p的值。接着,使用for循环遍历nums数组,对于每一个数字num,先调用query函数查询num-1的位置,然后加上1,就是以num结尾的最长上升子序列的长度。接着,调用update函数更新tree数组。最后,输出ans1即可。


图例:

一直到最后元素5进入后:[1,n] 表示的对于全部6个元素来说,大小从 1到n 范围内,不是下标

  • [1,1]内的LIS长度为1
  • [1,2]内的LIS长度为2
  • [1,3]内的LIS长度为2
  • [1,4]内的LIS长度为3
  • [1,5]内的LIS长度为4
  • [1,6]内的LIS长度为4
  • [1,8]内的LIS长度为4
    最长不上升子序列,动态规划,c++,动态规划,算法

对于最大不上升子序列也是同理:

  • 可以看作是逆序的最大上升子序列,其中可以取等号。

整个代码的时间复杂度为O(nlogn),因为树状数组的update和query操作的时间复杂度均为O(logn)。


树状数组的查询是 O ( N l o g N ) O(NlogN) O(NlogN)


线段树

线段树和树状数组的解法相同,只不过线段树代码量多,但是线段树易懂,树状数组比较抽象

代码如下:

/*
线段树
*/
int add[N];

inline void push_up(int i)
{
	//上移,维护区间最大值
	tree[i] = max(tree[i << 1], tree[i << 1 | 1]);
}
void build(int i, int pl, int pr)
{
	if (pl == pr)
	{
		tree[i] = 0;
		return;
	}
	int mid = (pl + pr) >> 1;
	build(i << 1, pl, mid);
	build(i << 1 | 1, mid + 1, pr);
	push_up(i);
}
void update(int i, int pl, int pr, int x, int val)
{
	if (pl == pr)
	{
		tree[i] = max(tree[i], val);
		return;
	}
	int mid = (pl + pr) >> 1;
	if (x <= mid) update(i << 1, pl, mid, x,val);	//递归左子树
	if (x > mid) update(i << 1 | 1, mid + 1, pr, x, val);//递归右子树
	push_up(i);
}
int query(int i, int pl, int pr, int L, int R)
{
	if (L > R) return 0;
	if (L <= pl && pr <= R)
	{
		return tree[i];
	}
	int ans = 0;
	int mid = (pl + pr) >> 1;
	if (L <= mid) ans =max(ans,query(i << 1, pl, mid, L, R));
	if (R > mid) ans =max(ans, query(i << 1 | 1, mid + 1, pr, L, R));
	return ans;
}

int main3()
{
	build(1, 1, n);
	/*
	最大上升子序列
	*/
	for (int i = 1; i <= n; i++)
	{
		//查询以i结尾的最大上升子序列长度的最大值,是小于号,所以说要减1
		int num = query(1, 1, n, 1, a[i] - 1) + 1;	

		update(1, 1, n, a[i], num);
		res = max(res, num);
	}
	cout << res << endl << endl;
	return 0;
}

线段树的时间复杂度也是 O ( N l o g N ) O(NlogN) O(NlogN)


二分查找

关于二分查找的详细的解释:
二分查找的详细解释

  • 最长上升子序列:

    • 如果尾元素小于下一个元素,则满足
    • 否则在升序序列中查找第一个大于等于x的元素,然后替换
  • 最长不上升子序列

    • 如果最后一个元素大于等于下一个元素,则满足
    • 否则在降序序列中查找第一个大于x的元素,然后替换
/*
二分查找
*/

int d1[N], d2[N];
int main()
{
	int len1 = 1, len2 = 1;
	d1[1] = d2[1] = a[1];
	for (int i = 2; i <= n; i++)
	{
		//最长上升子序列,最后一个元素一定小于下一个元素
		if (d1[len1] < a[i]) d1[++len1] = a[i];
		else *lower_bound(d1 + 1, d1 + 1 + len1, a[i]) = a[i];
		//最长不上升子序列,最后一个元素一定大于等于下一个元素
		if (d2[len2] >= a[i]) d2[++len2] = a[i];
		else *upper_bound(d2 + 1, d2 + 1 + len2, a[i], greater<int>()) = a[i];
	}
	cout << len1 << " " << len2 << endl;

	return 0;
}

有关lower_bound与upper_bound的比较器以下总结:

  • 升序:

    • 第一个大于等于
    • 第一个大于
  • 降序:

    • 第一个小于等于
    • 第一个小于

只需要记住是相反的即可

//升序:
*lower_bound(nums + 1, nums + 1 + 12, 4) = 9999; //第一个大于等于4的位置 
*upper_bound(nums + 1, nums + 1 + 12, 4) = 9999; //第一个大于4的位置 

//降序:
*lower_bound(nums1 + 1, nums1 + 1 + 12, 6,greater<int>()) = 9999;//第一个小于等于6的位置
*upper_bound(nums2 + 1, nums2 + 1 + 12, 6, greater<int>()) = 9999;//第一个小于6的位置

时间复杂度 O ( N l o g N ) O(NlogN) ONlogN文章来源地址https://www.toymoban.com/news/detail-771025.html

到了这里,关于最长上升子序列问题(LIS问题)与最长不上升子序列问题的四种方法(c++ 模板代码)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 最长公共上升子序列(LCIS)

    目录 一、前言 二、最长公共上升子序列 1、问题描述 2、基本思路 (1)状态表示 (2)状态计算 三、题例 1、上链接 2、基本思路 3、代码 (1)python未优化版 (2)python优化版 对于学计算机的同学来说,学习算法是一件非常重要的事情,废话不多讲,我们来讲讲“最长公共上

    2023年04月09日
    浏览(34)
  • [ACM 学习] 最长上升子序列

    LIS(最长上升子序列)的三种经典求法 - 一只不咕鸟 - 博客园 (cnblogs.com) 理解一下第三种方法(贪心+二分查找) 因为构建的是上升子序列,所以是可以用二分查找找到最大的小于当前 A[i] 的在子序列中的 F[j],并更新 F[j+1] 注:刚开始看的时候还在疑惑只换一个的话,后面的

    2024年01月16日
    浏览(37)
  • 动态规划——最长上升子序列模型

    状态表示 f [ i ] : { 集合:所有以第 i 个数结尾的上升子序列  属性:子序列长度的 M a x 状态表示f[i]: begin{cases} 集合:所有以第 i 个数结尾的上升子序列\\\\ 属性:子序列长度的rm Max end{cases} 状态表示 f [ i ] : { 集合:所有以第 i 个数结尾的上升子序列   属性:子序列长

    2024年04月17日
    浏览(45)
  • C++---最长上升子序列模型---最大上升子序列和(每日一道算法2023.3.3)

    注意事项: 本题为\\\"线性dp—最长上升子序列的长度\\\"的扩展题,所以dp思路这里就不再赘述。 题目: 比如,对于序列(1,7,3,5,9,4,8),有它的一些上升子序列,如(1,7),(3,4,8)等。 这些子序列中和最大为18,为子序列(1,3,5,9)的和。 你的任务,就是对于给定的序列,求出最大上升子序

    2024年02月03日
    浏览(37)
  • 动态规划__最长上升子序列

    目录 一.最长上升子序列 最长上升子序列模板 O(n ^ 2) 最长上升子序列二分优化 O(nlongn) 1017. 怪盗基德的滑翔翼 1014. 登山 1012. 友好城市 1016. 最大上升子序列和 1010. 拦截导弹 187. 导弹防御系统 二.最长公共上升子序列 最长公共子序列 最长公共上升子序列 给定一个长度为 N 的数

    2024年02月04日
    浏览(41)
  • 动态规划—— 最长上升子序列模型 解题记录

    一些想法:         现在是2024-3-15 06:01:22 哈哈卷死我可爱的舍友们~ 这两天又想起来开学的时候立下的刷完kuangbin专题的flag(快进到干不完) 总是先把Acwing的提高课看完吧 每天这样干一点总能干完的hhhhh,这会在喝npy买的奶茶,超多椰果真的好喝爱了爱了。 解题报告:

    2024年04月13日
    浏览(43)
  • C++动态规划之最长上升子序列

    一个序列A={a1,a2,...an}中任意删除若干项,剩余的序列叫做A的一个子序列。例如序列A={1,3,5,4,2},删除其中的第3项和第5项,得到序列B={1,3,4},删除其中的第3项和第4项,得到序列C={1,3,2},此时序列B和C是序列A的子序列。 如果序列中的元素是从小到大排列的,则该序列为上升

    2023年04月14日
    浏览(44)
  • 【C++】洛谷B3637 最长上升子序列

    这是一个简单的动规板子题。 给出一个由 n ( n ≤ 5000 ) n(nle 5000) n ( n ≤ 5000 ) 个不超过 1 0 6 10^6 1 0 6 的正整数组成的序列。请输出这个序列的 最长上升子序列 的长度。 最长上升子序列是指,从原序列中 按顺序 取出一些数字排在一起,这些数字是 逐渐增大 的。 第一行,一

    2024年02月19日
    浏览(32)
  • AcWing 1.2.1 最长上升子序列模型 + 动态规划 + 图解(详细)

    (1)acwing 4557. 最长上升子序列  4557. 最长上升子序列 - AcWing题库 给定一个长度为 N 的整数序列  a1,a2,…,aN 。请你计算该序列的 最长上升子序列的长度 。上升子序列是指数值 严格单调递增的子序列 输入格式 第一行包含整数 N 第二行包含 N个整数 a1,a2,…,aN 输出格式 一行

    2024年02月06日
    浏览(39)
  • 动态规划系列 | 最长上升子序列模型(下)| 拦截导弹一网打尽!

    题目描述 某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。 但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。 某天,雷达捕捉到敌国的导弹来袭。 由于该系统还在试用阶段,所以只有一套

    2024年02月03日
    浏览(49)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包