题目一:给定一个长度为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-12-19 05:08:09