题目链接:http://ybt.ssoier.cn:8088/problem_show.php?pid=1281
题目大意:
给你一个数列 \(a_1, a_2, \ldots, a_n\),你需要找出一些数列下标 \(i_1, i_2, \ldots, i_k\) 同时满足:
- \(1 \le i_1 \lt i_2 \lt \ldots \lt i_k \le n\);
- \(a_{i_1} \lt a_{i_2} \lt \ldots \lt a_{i_k}\)
即:找出数列的一个子序列满足子序列中每一个元素都小于它后面那个元素。
所有满足这个性质的子序列中的最长(包含元素最多)的子序列称为数列的 “最长上升子序列” 。
注:“最长上升子序列”的英文是“Longest increasing subsequence”,简称 “LIS”,所以我们一般把 “最长上升子序列” 问题简称为 LIS问题 。
本题求解的就是最长上升子序列的长度。
解题思路:
本题使用动态规划求解(大家注意:接下来我就以“LIS”来表示“最长上升子序列”了)。
定义状态 \(f_i\) 表示以 \(a_i\) 结尾的 LIS 长度。
注意:这里说的 “以 \(a_i\) 结尾” 指的是这个 LIS 必须包含 \(a_i\)(也就是说 \(a_i\) 是 LIS 中的最后一个元素)
则首先可以肯定的是 \(f_i\) 至少为 \(1\)(因为 \(a_i\) 单独一个数就可以组成一个长度为 \(1\) 的 LIS)。
其次我们可以想到的话,对于任意 \(j(1 \le j \lt i)\),若 \(a_j \lt a_i\),则 \(a_i\) 就可以跟在 \(a_j\) 后面组成一个上升子序列,而以 \(a_j\) 结尾的 LIS 长度为 \(f_j\),则 \(a_i\) 跟在 \(a_j\) 后面能够组成的 LIS 长度为 \(f_j + 1\)。
据此,我们可以推导出状态转移方程如下:
\(f_i = \max\limits_{j = 1 \text{ to }i-1 \text{ and } a_j \lt a_i} \{ f_j \} + 1\)
且 \(f_i\) 至少为 \(1\)。
求出所有的 \(f_i(1 \le i \le n)\) 之后,我们可以发现,这个数列的 LIS 必定是以某一个 \(a_i\) 结尾的,所以可以得出数列的 LIS 为 \(\max\limits_{i = 1 \text{ to } n}\{ f_i \}\) 。
实例程序如下:
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1010;
int n, a[maxn], f[maxn], ans;
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++)
scanf("%d", a+i);
for (int i = 1; i <= n; i ++)
{
f[i] = 1;
for (int j = 1; j < i; j ++)
if (a[j] < a[i])
f[i] = max(f[i], f[j] + 1);
}
for (int i = 1; i <= n; i ++)
ans = max(ans, f[i]);
printf("%d\n", ans);
return 0;
}
上面代码的时间复杂度为 \(O(n^2)\),而对于 LIS 问题有一个二分优化的 \(O(n \cdot log n)\) 算法,有兴趣的同学可以自行研究一下。