1、问题描述
给定一个无序的整数数组,找到其中最长上升子序列的长度。如:[5, 3, 4, 8, 6, 7] 返回 4。
2、算法分析
面对这个问题,首先要定义一个"状态"来代表它的子问题, 并且找到它的解。
注意,大部分情况下,某个状态只与它前面出现的状态有关,而独立于后面的状态。
假如考虑求 A[1], A[2], ..., A[i], i < N 的最长非降子序列的长度,缩小问题规模,让 i = 1, 2, 3... 来分析,然后定义 d(i)表示前 i 个数中以 A[i] 结尾的最长非降子序列的长度。
这个 d(i) 就是我们要找的状态。 如果我们把 d(1) 到 d(N) 都计算出来,那么最终我们要找的答案就是这里面最大的那个。 状态找到了,下一步找出状态转移方程。
以上面的例子来方便理解如何找到状态转移方程的,N 个数的序列是:
5 3 4 8 6 7
根据上面找到的状态,可以得到:
- i = 1 的 LIS 长度 d(1) = 1, d[] = {5}
- i = 2 的 LIS 长度 d(2) = 1, d[] = {3}
- i = 3 的 LIS 长度 d(3) = d(2) + 1 = 2, d[] = {3, 4}
- i = 4 的 LIS 长度 d(4) = max{ d(1), d(2), d(3) } + 1 = 3, d[] = {3, 4, 8}
状态转移方程已经很明显了,如果已经求出了 d(1) 到 d(i-1), 那么 d(i) 可以用下面的状态转移方程得到:
d(i) = max{ 1, d(j) + 1 }, 其中 j < i, A[j] <= A[i]
想要求 d(i),就把 i 前面的各个子序列中, 最后一个数不大于 A[i] 的序列长度加 1,然后取出最大的长度即为 d(i)。 当然了,有可能 i 前面的各个子序列中最后一个数都大于 A[i],那么 d(i) = 1, 即它自身成为一个长度为 1 的子序列。
分析完了,上图。

3、复杂度分析
时间复杂度:O(n2)
空间复杂度:O(n)
4、代码实现
int lengthOfLIS(int* nums, int numsSize) {
if (numsSize == 0) return 0;
int *d = (int *)malloc(sizeof(int) * numsSize);
int len = 1;
for(int i = 0; i < numsSize; ++i){
d[i] = 1;
for(int j = 0; j < i; ++j)
// 如果当前的数值 A[i] 大于 它之前的数值 A[j] && 最长的段
if(nums[j] < nums[i] && d[j] + 1 > d[i])
d[i] = d[j] + 1;
if(d[i] > len) len = d[i];
}
free(d);
return len;
}
int main()
{
int A[] = {
5, 3, 4, 8, 6, 7
};
printf("%d", lengthOfLIS(A, 6));
return 0;
}
5、进阶:O(nlogn)算法
假设序列 d[9] = { 2, 1, 5, 3, 6, 4, 8, 9, 7 }。
定义一个序列 B,令 i = 1 to 9 循环考察 d 数组。用一个变量 Len 来记录最大的递增长度。注意:B 的索引从 1 开始。
①、把 d[0] 有序地放到 B 里,令 B[1] = 2,即当只有一个数字 2 的时候,Len = 1 的 LIS 的末尾最大值是 2。
②、把 d[2] 有序地放到 B 里,令 B[1] = 1,即 Len = 1 的 LIS 的末尾最大值是 1,d[1] = 2 已经没用了,因为 2 > 1。
③、d[3] = 5,因为 d[3] > B[1],所以令 B[1+1] = B[2] = d[3] = 5,即 Len = 2 的 LIS 的最小末尾是 5,这时 B[] = { 1, 5 }。
④、d[4] = 3,B[1] < d[3] < B[2],放在 B[1] 的位置显然不合适,因为 1 < 3,不应该替换到小的值,而应该淘汰掉大的值,因为这样容易产生更长的序列,所以 Len = 2 的 LIS 最小末尾是 3,将 5 淘汰掉,这时 B[] = { 1, 3 }。
⑤、d[5] = 6,因为 d[5] > B[2],所以令 B[2+1] = B[3] = d[5] = 6,即 Len = 3 的 LIS 的最小末尾是 6,这时 B[] = { 1, 3, 6 }。
⑥、d[6] = 4,3 < d[6] < 6,于是把 6 替换掉,这时 Len = 3, B[] = { 1, 3, 4 }。
⑦、d[7] = 8,d[7] > B[3],将 8 追加到 B 数组末尾,这时 Len = 4, B[] = { 1, 3, 4, 8 }。
⑧、d[8] = 9,d[8] > B[4],将 9 追加到 B 数组末尾,这时 Len = 5, B[] = { 1, 3, 4, 8, 9 }。
⑨、d[9] = 7,B[3]=4 < d[9] < B[4]=8,所以最新的 B[4] = 7,这时 Len = 5, B[] = 1, 3, 4, 7, 9。
注意:{ 1, 3, 4, 7, 9 } 不是 LIS,它只是存储的对应长度 LIS 的最小末尾。

有了这个末尾,就可以一个一个地插入数据。虽然最后一个 d[9] = 7 更新进去对于这组数据没有什么意义,但是如果后面再出现两个数字 8 和 9(d[11] = { 2, 1, 5, 3, 6, 4, 8, 9, 7, 8, 9 }),那么继续执行下去,8 更新到 d[5],9 更新到 d[6],得出 LIS 的长度为 6,B[] = { 1, 3, 4, 7, 8, 9 }。
在 B 中插入数据是有序的,而且是进行替换而不需要挪动,所以可以利用二分查找,将每一个数字的插入时间优化到 O(logn),于是算法的时间复杂度就降低到了 O(nlogn)。
// 在非递减序列 [left, right](闭区间)上二分查找第一个大于等于 key 的位置,如果都小于 key,就返回 left+1
int upper_bound(int B[], int left, int right, int key)
{
int mid;
// 将 key 插入到数组末尾
if (B[right] < key)
return right + 1;
// num[left] ≤ key < nums[right] 之后 left 将大于 right,循环结束
while (left < right) {
mid = (left + right) / 2;
if (B[mid] < key) {
left = mid + 1;
}
else {
right = mid;
}
}
return left;
}
int lengthOfLIS(int* nums, int numsSize)
{
if (numsSize < 2) return numsSize;
int* B = (int *)malloc(sizeof(int) * (numsSize + 1));
B[0] = 0; // 无意义
B[1] = nums[0]; // 从 1 开始是为了让 len、pos 不需要 -1 或 +1
int len = 1;
for (int i = 1; i < numsSize; i++) {
// 找到插入位置
int pos = upper_bound(B, 1, len, nums[i]);
B[pos] = nums[i];
// 打印 B 数组,看看每次循环的变化,B[0] 无意义
printf("%d ", pos);
for (int k = 0; k <= pos; k++) {
printf("%d", B[k]);
}
printf("
");
if (len < pos) {
len = pos;
}
}
return len;
}
int main()
{
int A[] = { 2, 1, 5, 3, 6, 4, 8, 9, 7, 8, 9 };
printf("%d", lengthOfLIS(A, 11));
return 0;
}