【题目链接】
ybt 1281:最长上升子序列
OpenJudge NOI 2.6 1759:最长上升子序列
【题目考点】
1. 动态规划:线性动规
- 最长上升子序列
动规方法:复杂度: O ( n 2 ) O(n^2) O(n2)
贪心方法:复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
【解题思路】
解法1:动态规划
方法1: 状态定义为以i为结尾的最长上升子序列的长度
1. 确定状态
分析状态:
阶段:子序列存在的区间
决策:每一次是否选择一个元素
策略集合:上升的子序列
条件:序列长度最长
统计量:长度
状态定义:
dp[i]
:以第i元素为结尾的最长上升子序列的长度。
2. 确定状态转移方程
- 分割集合:以第i元素为结尾的上升子序列构成的集合。
- 子集1:对所有满足j < i的j, 如果第i元素大于第j元素,则以第j元素为结尾的上升子序列加上第i元素,形成新的上升子序列。
- 子集2:否则,只有一个第i元素构成上升子序列。
- 分析状态变量:
- 子集1:
dp[i]
为:所有满足j<i且第j元素小于第i元素的j取dp[j]+1
- 子集2:
dp[i]
为1
dp[i]
为所有可能的状态值中的最大值。
- 子集1:
题目要求最长上升子序列,那么就是求以每个位置为结尾的最长上升子序列中最长的长度,即求dp
数组中的最大值。
方法2:状态定义为以i为起始的最长上升子序列的长度
1. 确定状态
状态定义:
dp[i]
:以第i元素为起始的最长上升子序列的长度。
2. 确定状态转移方程
- 分割集合:以第i元素为起始的上升子序列构成的集合。
- 子集1:对所有满足j > i的j, 如果第i元素小于第j元素,则以第j元素为起始的上升子序列前面加上第i元素,形成新的上升子序列。长度为
dp[i]=dp[j]+1
- 子集2:否则,只有一个第i元素构成上升子序列。长度为:
dp[i]=1
- 子集1:对所有满足j > i的j, 如果第i元素小于第j元素,则以第j元素为起始的上升子序列前面加上第i元素,形成新的上升子序列。长度为
dp[i]
为所有可能的状态值中的最大值。
题目要求最长上升子序列,那么就是求以每个位置为结尾的最长上升子序列中最长的长度,即求dp
数组中的最大值。
以上两种方法的复杂度为
O
(
n
2
)
O(n^2)
O(n2)
解法2:贪心
记a[i]
为第i个数字。记d[i]
表示长度为i的上升子序列最后一个数字可能的最小值。len为d数组的长度。
初始情况,len为0。d数组下标为1~len,d[len]
为d数组最后一个元素。
先做一次d[++len] = a[1]
。
i从2到n遍历数组a
- 如果
a[i]
大于d[len]
,那么d[++len] = a[i]
。 - 如果
a[i]
小于等于d[len]
,那么通过二分查找在d[1]
~d[len]
中找到大于等于a[i]
的最小值的下标为l,让d[l] = a[i]
。 - 最后len的值即为最长上升子序列的长度。
解析:
该算法的整体思路是:
- 让
d[len]
尽量小,这样后面遇到的a[i]
就有更大可能接在以d[len]
为末尾的长为len的上升子序列的后面,延长上升子序列。 - 而要使
d[len]
尽可能小,则需要使d[len-1]
尽可能小。如果可能的话,把d[len-1]
更新得更小,后面才可能把d[len]
更新得更小。 - 继续推广,就是要尽量把d中每个元素都更新得更小。所以
d[len]
存储的是长为len的上升子序列的末尾元素的最小值。
分步解析:
- 如果
a[i]
大于d[len]
,那么可以在长为len的以d[len]
为结尾的上升子序列后面,添加一个a[i]
,形成长为len+1的上升子序列,末尾元素为a[i]
。所以有d[++len] = a[i]
。 - 如果
a[i]
小于等于d[len]
,考虑长为几的上升子序列能够以a[i]
为结尾。由于d[i]
是长为i的上升子序列末尾的最小值,所以要让d数组的元素尽量小。
根据第1种向d添加元素的方式,d数组一定是升序的。- l较小时,
a[i] > d[l-1]
且a[i] > d[l]
,此时不需要a[i]
来替换哪个元素。 - 当
a[i] > d[l-1]
且a[i] <= d[l]
时,此时存在一个以d[l-1]
为结尾的长为l-1的上升子序列,可以把a[i]
添加在这个上升子序列的后面,构成一个以a[i]
为结尾的长为l的上升子序列,即d[l] = a[i]
,做这步操作可以使d[l]
变小。 - 由于
d[l-1] < a[i] <= d[l] < d[l+1]
,所以更新后d数组仍然是升序的。
- l较小时,
注意:d数组并非一个上升子序列,它只是每个上升子序列的末尾元素。
设下标o < p < q < i
假设d[l-1]
值为a[o]
,
d[l]
是在d[l-1]
表示的上升子序列末尾添加了a[p]
得到的,即d[l]=a[p]
d[l+1]
是在d[l]
表示的上升子序列末尾添加了a[q]
得到的,即d[l+1]=a[q]
此时有数字a[i]
,根据上述更新规则,发现有d[l-1] < a[i] <= d[l]
,所以更新d[l] = a[i]
。
此时d[l]
表示的上升子序列的最后两个元素为a[o] a[i]
而d[l+1]
表示的上升子序列的最后两个元素不变,与a[i]
无关,仍然是a[o] a[p] a[q]
由于 d数组是升序的,那么就可以通过二分查找在d[1]
~d[len]
中找到大于等于a[i]
的最小值。
该方法的总体复杂度为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
【题解代码】
解法1.1:状态定义为以i为结尾的最长上升子序列的长度
#include<bits/stdc++.h>
using namespace std;
#define N 1005
int a[N], dp[N];//a[i]:第i个数 dp[i]:以i为结尾的最长上升子序列的长度
int main()
{
int n, mxlen = 0;
cin >> n;
for(int i = 1; i <= n; ++i)
cin >> a[i];
for(int i = 1; i <= n; ++i)
{
dp[i] = 1;//第j元素自己构成上升子序列
for(int j = 1; j < i; ++j)
if(a[i] > a[j])
dp[i] = max(dp[i], dp[j]+1);
mxlen = max(mxlen, dp[i]);
}
cout << mxlen;
return 0;
}
解法1.2:状态定义为以i为起始的最长上升子序列的长度
#include<bits/stdc++.h>
using namespace std;
#define N 1005
int a[N], dp[N];//dp[i]:以i为起始的最长上升子序列的长度
int main()
{
int n, mxlen = 0;
cin >> n;
for(int i = 1; i <= n; ++i)
cin >> a[i];
for(int i = n; i >= 1; --i)
{
dp[i] = 1;
for(int j = i+1; j <= n; ++j)
if(a[i] < a[j])
dp[i] = max(dp[i], dp[j] + 1);
mxlen = max(mxlen, dp[i]);
}
cout << mxlen;
return 0;
}
解法2:贪心
- 写法1:手写二分查找
#include<bits/stdc++.h>
using namespace std;
#define N 1005
int n, a[N], d[N], len;//d[i]:长为i的上升子序列最后一个数字可能的最小值
int main()
{
cin >> n;
for(int i = 1; i <= n; ++i)
cin >> a[i];
d[++len] = a[1];
for(int i = 2; i <= n; ++i)
{
if(a[i] > d[len])
d[++len] = a[i];
else
{//找d中大于等于a[i]的最小值的下标
int l = 1, r = len;
while(l < r)
{
int m = (l+r)/2;
if(d[m] >= a[i])
r = m;
else
l = m+1;
}
d[l] = a[i];
}
}
cout << len;
return 0;
}
- 写法2:使用stl lower_bound
#include<bits/stdc++.h>
using namespace std;
#define N 1005
int n, a[N], d[N], len;//d[i]:长为i的上升子序列最后一个数字可能的最小值
int main()
{
cin >> n;
for(int i = 1; i <= n; ++i)
cin >> a[i];
d[++len] = a[1];
for(int i = 2; i <= n; ++i)
{
if(a[i] > d[len])
d[++len] = a[i];
else
{//找d中大于等于a[i]的最小值的下标
int l = lower_bound(d+1, d+1+len, a[i])-d;
d[l] = a[i];
}
}
cout << len;
return 0;
}