动态规划之最长递增子序列

    题目一:给定一个长度为N的数组,找出一个最长的单调递增子序列(不一定连续,但是顺序不能乱)。并返回单调递增子序列的长度。
    例如:给定一个长度为8的数组A{1,3,5,2,4,6,7,8},则其最长的单调递增子序列为{1,2,4,6,7,8},我们返回其长度6。
    题目二:在题目一的基础上,我们要返回该子序列中的元素。例如,给定一个长度为8的数组A{1,3,5,2,4,6,7,8},我们返回的是单调递增子序列{1,2,4,6,7,8}。
    解析:我们使用动态规划的思想来解决此问题,假设给定的数组为nums,其长度为N。设置一个辅助数组len,其长度和给定数组的相同。在辅助数组中存储的是以该元素结尾的最长的单调递增子序列的长度,如下图所示:


    那么我们如何得到数组len中的值。把len数组中的每个值初始化为1,当遍历nums数组,每遍历一个数都把len数组相应位置的值更新。假如我们当前访问的是nums[i]。则需要更新len[i]中的值,我们需要再次访问nums数组前i-1,用nums[i]和前i-1个数相比,如果nums[i]大于当前访问的数nums[j],且len[j]+1的值大于len[i]。则len[i]的值更新为len[j]+1的值。那么最后得到的结果就是该元素结尾的最长的单调递增子序列的值。比如说我们更新len[2]时,我们需要访问nums[0]和nums[1],当访问nums[0]时,因为nums[0]小于nums[2],且len[2]小于len[0]+1,所以这一步我们把len[2]更新为2,当访问nums[1]时,因为nums[1]小于nums[2],且len[2]小于len[1]+1,所以这一步我们把len[2]更新为3,则最后len[2]的值为3,表示以nums[2]结尾的最长递增子序列的长度为3。当我们更新len[3]时,我们需要访问nums[0]、nums[1]和nums[2],当访问nums[0]时,因为nums[3]大于nums[0],且len[3]小于len[0]+1,所以这一步len[3]更新为2,当访问nums[1]时,因为nums[3]小于nums[0],所以len[3]在这一步不需要更新。同理,访问nums[2]时,len[3]也不需要更新。最后len[3]为2。如上图所示。因为我们更新len[i]的值需要访问前i-1个元素,所以此方法的时间复杂度为o(N*N)。其中N表示数组的长度。代码如下所示:
int maxLongSub(vector<int> &nums)
{
	//当数组为空时,返回0
	if (nums.empty())
		return 0;
	int size = nums.size();
	vector<int> len(size);//len数组
	len[0] = 1;
	//更新每个len[i]的值
	for (int i = 1; i < size; ++i)
	{
		len[i] = 1;
		//访问前i-1个元素
		for (int j = 0; j < i; ++j)
		{
			//判断当前len[i]是否需要更新。
			if (nums[i] > nums[j] && len[i] < len[j] + 1)
			{
				len[i] = len[j] + 1;
			}
		}
	}
	int index = 0;
	//找len[index]的最大值
	for (int i = 1; i < size; ++i)
	{
		if (len[i] > len[index])
		{
			index = i;
		}
	}
	return len[index];
}

上述代码使用leetcode第300题的测试用例,测试用例总共有22个。运行时间为112ms。我们可不可以降低时间复杂度呢?答案是可以的,上述代码的时间复杂度为o(N*N)。我们可以把时间复杂度降低为o(NlogN)。因为我们每次更新len[i]时,都需要顺序访问前i-1个元素,实际上有些访问是不必要的。我们设置一个有序的数组res,数组的初始化为空,每次访问nums中的元素时,我们都用该元素到有序的数组中进行查找,找到第一个比该元素的大的元素,然后把有序数组中的值更新为该元素的值,如果数组中没有元素比该元素大,也就是说该元素比有序数组的最后一个元素还要大,则我们在有序数组的末尾插入此元素,最后有序数组的长度就是最长递增子序列的长度。因为在有序数组进行查找操作的时间复杂度为o(logN),所以此思想的时间复杂度为o(NlogN)。如图所示:



    那么为什么最后res数组的长度就是最长递增子序列的长度呢?因为res末尾元素是当前已经访问过的元素的最大值,当访问下一个元素时,如果下一个元素的值比res末尾元素的值大,则我们可以找到一个比当前res.size()更长的递增的子序列。如果下一个元素的值比res末尾元素的值小,则一定可以找到第一个大于该元素的值,我们用此元素更新res中的元素。代码如下:
//返回给定元素要插入的位置
int getPos(vector<int> &nums, int val)
{
	int low = 0;
	int high = nums.size() - 1;
	while (low <= high)
	{
		int mid = (low + high) / 2;
		if (nums[mid] < val)
		{
			low = mid + 1;
		}
		else
		{
			high = mid - 1;
		}
	}
	return low;
}

int lengthOfLIS(vector<int>& nums)
{
	if (nums.empty())
		return 0;
	vector<int> res;
	res.push_back(nums[0]);//加入第一个元素
	for (int i = 1; i < nums.size(); ++i)
	{
		int pos = getPos(res, nums[i]);
		//如果比最后一个元素大
		if (pos == res.size())
		{
			res.push_back(nums[i]);
		}
		else
		{
			res[pos] = nums[i];
		}
	}
	return res.size();
}

我们使用leetcode中测试用例,运行时间为4ms,比第一种方法的112ms,改进了很多。

   在第一种方法的基础上做改进,我们可以回答题目二,再次设置一个辅助数组pre,数组中存储的是以当前位置结尾的最长递增子序列中的倒数第二个元素的下标。代码如下:
int maxLongSub(vector<int> &nums)
{
	if (nums.empty())
		return 0;
	int size = nums.size();
	vector<int> len(size);
	vector<int> pre(size, -1);
	len[0] = 1;
	pre[0] = -1;
	for (int i = 1; i < size; ++i)
	{
		len[i] = 1;
		for (int j = 0; j < i; ++j)
		{
			if (nums[i] > nums[j] && len[i] < len[j] + 1)
			{
				len[i] = len[j] + 1;
				pre[i] = j;
			}
		}
	}
	int index = 0;
	for (int i = 1; i < size; ++i)
	{
		if (len[i] > len[index])
		{
			index = i;
		}
	}

	vector<int> res;
	while (pre[index] != -1)
	{
		res.push_back(nums[index]);
		index = pre[index];
	}
	res.push_back(nums[index]);
	for (int i = res.size() - 1; i > 0; --i)
	{
		cout << res[i] << " ";
	}
	cout << res[0] << endl;
	return len[index];
}

方法二中最后res中的元素即为一个最长递增子序列。

时间: 2024-10-13 23:07:13

动态规划之最长递增子序列的相关文章

动态规划之最长递增子序列问题详解

最近重新开始看动态规划,动态规划算法的有效性依赖于问题本身所具有的两个重要性质:最优子结构性质和子问题重叠性质. 1.最优子结构:当问题的最优解包含了其子问题的最优解时,称该问题具有最优子结构性质. 2.重叠子问题:在用递归算法自顶向下解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次.动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只解一次,而后将其解保存在一个表格中,在以后尽可能多地利用这些子问题的解. (二).动态规划算法的基本步骤设计一个标准的动态规划算法,通常

动态规划求解最长递增子序列的长度

一,问题描述 给定一个序列,求解它的最长 递增 子序列 的长度.比如: arr[] = {3,1,4,1,5,9,2,6,5}   的最长递增子序列长度为4.即为:1,4,5,9 二,算法分析 有两种方式来求解,一种是转化为LCS问题.即,首先对数组排序,将排序后的结果存储在辅助数组中.排序时间复杂度O(NlogN),排序后的数组与原数组组成了LCS(N,N)问题.解决LCS问题的时间复杂度为O(N^2),故整个算法的时间复杂度为O(N^2),空间复杂度为O(N) 另一种方式是直接用DP求解,算

[算法]动态规划之最长递增子序列

最长递增子序列 #include<stdio.h> #include<stdlib.h> #include<time.h> #include<string.h> #define N 4 int solve(int *array, int n) { int *dp = (int *)malloc(n * sizeof(int)); int i; int j; int result; bzero((void *)dp, n * sizeof(int)); dp[

动态规划之最长递增子序列(LIS)

       在一个已知的序列{ a1,a2,--am}中,取出若干数组成新的序列{ ai1, ai2,-- aim},其中下标 i1,i2, --im保持递增,即新数列中的各个数之间依旧保持原数列中的先后顺序,那么称{ ai1, ai2,--aim}为原序列的一个子序列.若在子序列中,当下标 ix > iy时,aix > aiy,那么称其为原序列的一个递增子序列.最长递增子序列问题就是在一个给定的原序列中,求得其最长递增子序列的长度.       求最长递增子序列的递推公式为:      

最长递增子序列(LIS)求解

问题描述 最长递增子序列也称 "最长上升子序列",简称LIS ( longest increasing subsequence).设L=<a1,a2,-,an>是n个不同的实数的序列,L的递增子序列是这样一个子序列Lis=<ak1,ak2,-,akm>,其中k1<k2<-<km且ak1<ak2<-<akm.求最大的m值. 如:求一个一维数组arr[i]中的最长递增子序列的长度,如在序列{ 7, 1, 6, 5, 3, 4, 8

【动态规划】拦截导弹_dilworth定理_最长递增子序列

问题 K: [动态规划]拦截导弹 时间限制: 1 Sec  内存限制: 256 MB提交: 39  解决: 10[提交][状态][讨论版] 题目描述 张琪曼:“老师,修罗场是什么?” 墨老师:“修罗是佛家用语,修罗毕生以战斗为目标,修罗场指的是他们之间的死斗坑,人们通常用‘修罗场’来形容惨烈的战场.后来又引申出‘一个人在困境中做绝死奋斗’的意思.所以,这其实也在暗示我们,即使是身处绝境,也不要放弃奋斗.再说了,情况其实没有这么糟糕,因为我们最新的导弹拦截系统已经研制好了.” 魔法世界为了防御修罗

尝试解释LIS(最长递增子序列)的一种动态规划算法

最长上升子序列就是求给定序列的最长的递增序列,其中不要求序列的元素在原序列中保持连续. 为了方便理解,可以举个例子: inta[] = {0,2,1,5,3,6,4,8,9,7}(数组下标从1开始)的一个最长的子序列1,3,4,7,9. 利用动态规划的思想,可以方便的求取这个解. 为了方便解释,我们定义dp(n)为长度为1至下标为n的最长子序列的长度(数组下标假设从1开始),{a[1],a[2],..,a[n]}为dp(n)对应的序列. 为了和程序对应,我采取自底向上的方式进行解释. 1.显然对

动态规划(DP),最长递增子序列(LIS)

题目链接:http://poj.org/problem?id=2533 解题报告: 状态转移方程: dp[i]表示以a[i]为结尾的LIS长度 状态转移方程: dp[0]=1; dp[i]=max(dp[k])+1,(k<i),(a[k]<a[i]) #include <stdio.h> #define MAX 1005 int a[MAX];///存数据 int dp[MAX];///dp[i]表示以a[i]为结尾的最长递增子序列(LIS)的长度 int main() { int

动态规划--最长递增子序列

经典的最长子序列问题,最近编程训练遇到此题苦无思路,在网上找到比较规范的解答,细思两天后还是觉得有点问题,现在整理总结如下: 参照 https://www.cnblogs.com/hapjin/p/5597658.html 1. 问题描述: 给定一个序列,求解它的最长 递增 子序列 的长度.比如: arr[] = {3,1,4,1,5,9,2,6,5}   的最长递增子序列长度为4.即为:1,4,5,9 2.DP算法分析: 按照上述作者的解答 ①最优子问题 设lis[i] 表示索引为 [0...