点击题目链接
题目描述
给出一个由 n(n≤5000) 个不超过 1e6 的正整数组成的序列。请输出这个序列的最长上升子序列的长度。
最长上升子序列是指,从原序列中按顺序取出一些数字排在一起,这些数字是逐渐增大的。
输入格式
第一行,一个整数 n,表示序列长度。
第二行有 n 个整数,表示这个序列。
输出格式
一个整数表示答案。
输入输出样例
输入 #1
6 1 2 4 1 3 4
输出 #1
4
说明/提示
分别取出 1、2、3、4 即可。
解题思路
这是一个经典的动态规划问题。我们可以使用动态规划的方法来解决这个问题。
动态规划思路
-
定义状态:设dp[i]表示以第i个元素结尾的最长上升子序列的长度。
-
状态转移方程:对于每个元素i,我们遍历前面的所有元素j(0<=j<i)。如果a[j]<a[i],则说明可以将a[i]接到以a[j]结尾的子序列后面,形成一个更长的上升子序列。此时,dp[i] = max(dp[i], dp[j]+1)。
-
初始化:每个元素本身可以作为一个长度为1的子序列,因此dp数组初始化为1。
-
结果:遍历dp数组,取最大值即为所的求最长上升子序列的长度。
算法复杂度分析
-
时间复杂度:O(n^2)。对于每个元素i,我们需要遍历前面的所有元素j,因此时间复杂度为O(n^2)。
-
空间复杂度:O(n)。需要一个dp数组来存储每个位置的最长上升子序列长度。
代码
#include<bits/stdc++.h>
using namespace std;
int n;
int a[10000]={0};
int dp[10000]={0};//dp[i]是以a[i]结尾的最长上升子序列大小
int main(){
cin>>n;
for(int i=0;i<n;++i) cin>>a[i];
//dp
for(int i=0;i<n;++i)
{
dp[i] = 1;//初始化为1
for(int j=0;j<i;++j)
{
if(a[j]<a[i]) //j符合
{
//比较,有可能前面已经组成过了子序列
dp[i] = max(dp[i],dp[j] + 1);
}
}
}
int res=-1;
for(int i=0;i<n;++i)
{
if(res<dp[i])
{
res = dp[i];
}
}
cout<<res;
return 0;
}
优化方法
上述动态规划方法的时间复杂度是O(n^2),对于较大的n来说可能不够高效。我们可以使用一种更高效的方法,结合贪心算法和二分查找,将时间复杂度降低到O(n log n)。
具体来说,我们维护一个数组tail
,其中tail[i]
表示长度为i的上升子序列的最小末尾元素。对于每个元素a[i]
,我们用二分查找在tail
数组中找到第一个比a[i]
大的元素的位置,并用a[i]
更新该位置的值。这样,tail
数组的长度就表示当前的最长上升子序列的长度。
优化后的代码
#include <bits/stdc++.h>
using namespace std;
int tail[10000], n, a[10000];
int main() {
cin>>n;
for (int i = 1; i <= n; i++) {
cin>>a[i];
}
int len = 0;
for (int i = 1; i <= n; i++) {
int l = 0, r = len;
while (l <= r) {
int mid = (l + r) / 2;
if (tail[mid] < a[i]) {
l= mid + 1;
} else {
r = mid - 1;
}
}
tail[l] = a[i];
if (l > len) {
len++;
}
}
cout << len << endl;
return 0;
}
总结
动态规划方法虽然直观,但对于较大的输入规模可能效率较低。优化方法通过贪心和二分查找的结合,显著提高了算法的效率,适合处理大规模数据。在实际应用中,应根据问题的具体要求和数据规模选择合适的算法。