为了让自己更好的记住学过的知识,我要写博客,将做题中用到的一些方法和一类问题进行总结。
今天做题,碰到了一题关于最长上升子序列的题目,先上题
牛牛现在有一个n个数组成的数列,牛牛现在想取一个连续的子序列,并且这个子序列还必须得满足:最多只改变一个数,就可以使得这个连续的子序列是一个严格上升的子序列,牛牛想知道这个连续子序列最长的长度是多少。
输入描述:
输入包括两行,第一行包括一个整数n(1 ≤ n ≤ 10^5),即数列的长度;
第二行n个整数a_i, 表示数列中的每个数(1 ≤ a_i ≤ 10^9),以空格分割。
输出描述:
输出一个整数,表示最长的长度。
输入例子:
6
7 2 3 1 5 6
输出例子:
5
对于这类笔试题,目前我也只学习了动态规划、双指针、递归、二分、哈希表、单调队列几个基础方法,具体问题具体分析的能力还得多多锻。用了双指针,可惜只有20%的样例通过,之前没做过LIS的题目,今天先总结一下最长子序列问题(LIS),主要也是以LeetCode上的题目展开,让读到这篇文章的人尽可能的将此问题理解的深点。
问题描述
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。说明:
- 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
- 你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
解决方法
DP动态规划—O(n^2)、二分+贪心法—O(nlogn)、树状数组优化的DP—O(nlogn)结题思路
最长上升子序列包含的意思
- 子序列元素在原数组中的下标是有序的,但不是连续的;
- 子序列元素值是上升的。
主要讲一下dp方法,用dp[i]表示以第i个元素结尾的长上升子序列得长度,例如
dp[0]表示[10]的结果是1;
dp[1]表示[10,9]的结果还是1;
dp[2]表示[10,9,5]的结果还是1;
dp[3]表示[10,9,2,5]的结果是2;
dp[4]表示 [10,9,2,5,3]的结果还是2;
dp[5]表示 [10,9,2,5,3,7]的结果是3;
dp[i]的结果与d[0]~dp[i-1]都有关,我们假设0<=j<=i-1;那么我们能够得到
dp状态转移方程:dp[i]=max(dp[i],dp[j]+1);
注意的一点是初始化dp[i]=1;dp的时间复杂度是O(n^2),因此不算最优。
二分+贪心法—O(nlogn)、树状数组优化的DP—O(nlogn)的思路大家可参考LeetCode 300 。
C++代码
#include
#include
#include
using namespace std;
int main()
{
int n;
cin>>n;
vector res(n);
for(int i=0;i>res[i];
int ans=0;
vector dp(n,1);
for(int i=0;ires[j]) //res[i]>res[j]说明相对于以j结尾的数组的最长子序列又可以增1啦
dp[i]=max(dp[i],dp[j]+1);
}
ans=max(ans,dp[i]);
}
cout<
问题描述
给定一个未排序的整数数组,找出最长连续序列的长度。
要求算法的时间复杂度为 O(n)。
示例:
输入: [100, 4, 200, 1, 3, 2]
输出: 4
解释: 最长连续序列是 [1, 2, 3, 4]。它的长度为 4。解决方法
利用哈希表unordered_set结题思路
最长上升子序列包含的意思
- 子序列元素在原数组中的下标是无序的;
- 子序列元素值是递增加1的。
这题的难点在于规定了时间复杂度为O(n),那么排序肯定是用不上了。
这里主要讲一下用unordered_set方法,LeetCode上用的都是unordered——map
,感兴趣的可以去查看一下。 解题思路主要是先将数据放到哈希表中。遍历哈希表,对每一个数据左右两边进行查找哈希表,用到了哈希表的count()函数,存在返回>0,反之返回0。知道左右查查找完毕,且每查找到一个数,则都要将其在哈希表中删除。这样才能保证所有的元素只遍历一次,复杂度是O(n)。
C++代码
#include
#include
#include
using namespace std;
int main()
{
int n;
cin>>n;
unordered_set myhash; //建立哈希表
int value;
for(int i=0;i>value;
myhash.insert(value);
}
int ans=1;
for(auto c:myhash)
{
int temp=1;
for(int j=c-1;myhash.count(j);j--) //向左遍历
{
myhash.erase(j);
temp++;
}
for(int j=c+1;myhash.count(j);j++) //向右遍历
{
myhash.erase(j);
temp++;
}
//myhash.erase(c); 不能删除自己,会段错误。
ans=max(ans,temp);
}
cout<
问题描述
我们把数组 A 中符合下列属性的任意连续子数组 B 称为 “山脉”:
B.length >= 3
存在 0 < i < B.length - 1 使得 B[0] < B[1] < … B[i-1] < B[i] > B[i+1] > … > B[B.length - 1]
(注意:B 可以是 A 的任意子数组,包括整个数组 A。)
给出一个整数数组 A,返回最长 “山脉” 的长度。
如果不含有 “山脉” 则返回 0。
示例 1:
输入:[2,1,4,7,3,2,5]
输出:5
解释:最长的 “山脉” 是 [1,4,7,3,2],长度为 5。示例 2:
输入:[2,2,2]
输出:0
解释:不含 “山脉”。提示:
0 <= A.length <= 10000
0 <= A[i] <= 10000解决方法
利用哈希表unordered_set结题思路
这题的难度不大,不过是最接近牛牛那题的,算是简单版,难的题目其实也都是从简单的过来的,那么难题的解题方法也是从小的方法合起来解决的,所以多多积累知识点还很重要的。
言归正传,这题的主要解法是用双指针,和上一题有点类似,不过这个不用删除元素。因为约束条件要求山脉的长度>3,那么我们的遍历就从:1<=i<=n-2;
C++代码
#include
#include
#include
#include
using namespace std;
int main()
{
int n;
cin>>n;
vector res(n);
for(int i=0;i>res[i];
if ( n< 3) return 0;
int ans = 1;
for (int i = 1; i < res.size() - 1; i++) {
if (res[i] > res[i - 1] && res[i] > res[i + 1]) {
int l = i - 1;
int r = i + 1;
while (l > 0 && res[l - 1] < res[l]) {
l--;
}
while (r < res.size() - 1 && res[r] > res[r + 1]) {
r++;
}
ans = max(ans, r - l + 1);
}
}
cout<
这三个题目是LIS问题中比较基础的题目,但是笔试一般不会这么简单,像牛牛这题,就加了一个限制条件—最多只改变一个数,那么这题的解法就变困难些,那么这题该如何解呢?
空间换时间,额外建立两个数组,一个left,一个right
left存放得是以第i个元素为结尾点自左向右的最长连续上升子序列(下标连续,值递增)
right存放得是以第i个元素为起始点自左向右的最长连续上升子序列(下标连续,值递增)
以[7 2 3 1 5 6]为例
left[0]=1,right[5]=1
left[1]=1,right[4]=2
left[2]=2,right[3]=3
left[3]=1,right[2]=1
left[4]=2,right[1]=2
left[5]=3,right[0]=1
用的是dp动态规划
状态转移矩阵
left[i]=nums[i]>nums[i-1]?left[i-1]+1:1;
right[i]=nums[i]
然后得到dp[i]=max(left[i]+right[i]-1);
C++代码
#include
#include
#include
#include
using namespace std;
int main()
{
int n;
cin>>n;
vector res(n);
for(int i=0;i>res[i];
vector left(n);
vector right(n);
left[0]=1;
for(int i=1;ires[i-1]?left[i-1]+1:1;
right[n-1]=1;
for(int i=n-2;i>=0;i--)
right[i]=res[i]0 && i=2)
ans=max(ans,right[i+1]+left[i-1]+1);
}
cout<
以上就是自己对LIS问题的总结,都是一些解决基础问题的方法总结,最近经常用到的方法是动态规划、双指针、哈希表,下次总结一点双指针的问题。