最长递增子序列(LIS)
---
最长递增子序列又叫做最长上升子序列;子序列,正如LCS一样,元素不一定要求连续。本节讨论实现三种常见方法,主要是练手。
题:求一个一维数组arr[i]中的最长递增子序列的长度,如在序列1,-1,2,-3,4,-5,6,-7中,最长递增子序列长度为4,可以是1,2,4,6,也可以是-1,2,4,6。
方法一:DP
像LCS一样,从后向前分析,很容易想到,第i个元素之前的最长递增子序列的长度要么是1(单独成一个序列),要么就是第i-1个元素之前的最长递增子序列加1,可以有状态方程:
LIS[i] = max{1,LIS[k]+1},其中,对于任意的k<=i-1,arr[i] > arr[k],这样arr[i]才能在arr[k]的基础上构成一个新的递增子序列。
代码如下:在计算好LIS长度之后,output函数递归输出其中的一个最长递增子序列。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
#include <iostream>
using namespace std;
/* 最长递增子序列 LIS
* 设数组长度不超过 30
* DP
*/
int dp[31]; /* dp[i]记录到[0,i]数组的LIS */
int lis; /* LIS 长度 */
int LIS(int * arr, int size)
{
for(int i = 0; i < size; ++i)
{
dp[i] = 1;
for(int j = 0; j < i; ++j)
{
if(arr[i] > arr[j] && dp[i] < dp[j] + 1)
{
dp[i] = dp[j] + 1;
if(dp[i] > lis)
{
lis = dp[i];
}
}
}
}
return lis;
}
/* 输出LIS */
void outputLIS(int * arr, int index)
{
bool isLIS = 0;
if(index < 0 || lis == 0)
{
return;
}
if(dp[index] == lis)
{
--lis;
isLIS = 1;
}
outputLIS(arr,--index);
if(isLIS)
{
printf("%d ",arr[index+1]);
}
}
void main()
{
int arr[] = {1,-1,2,-3,4,-5,6,-7};
/* 输出LIS长度; sizeof 计算数组长度 */
printf("%d\n",LIS(arr,sizeof(arr)/sizeof(int)));
/* 输出LIS */
outputLIS(arr,sizeof(arr)/sizeof(int) - 1);
printf("\n");
}
|
这个方法也最容易想到也是最传统的解决方案,对于该方法和LIS,有以下两点说明:
- 由LIS可以衍生出来最长非递减子序列,最长递减子序列,道理是一样的。
- 对于输出序列,也是可以再申请一数组pre[i]记录子序列中array[i]的前驱,道理跟本节的实现也是一样的
方法二:排序+LCS
这个方法是在Felix’blog(见参考资料)中看到的,因为简单,他在博文中只是提了一句,不过为了练手,虽然懒,还是硬着头皮写一遍吧,正好再写一遍快排,用quicksort + LCS,这个思路还是很巧妙的,因为LIS是单调递增的性质,所以任意一个LIS一定跟排序后的序列有LCS,并且就是LIS本身。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
#include <iostream>
using namespace std;
/* 最长递增子序列 LIS
* 设数组长度不超过 30
* quicksort + LCS
*/
void swap(int * arr, int i, int j)
{
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
void qsort(int * arr, int left, int right)
{
if(left >= right) return ;
int index = left;
for(int i = left+1; i <= right; ++i)
{
if(arr[i] < arr[left])
{
swap(arr,++index,i);
}
}
swap(arr,index,left);
qsort(arr,left,index-1);
qsort(arr,index+1,right);
}
int dp[31][31];
int LCS(int * arr, int * arrcopy, int len)
{
for(int i = 1; i <= len; ++i)
{
for(int j = 1; j <= len; ++j)
{
if(arr[i-1] == arrcopy[j-1])
{
dp[i][j] = dp[i-1][j-1] + 1;
}else if(dp[i-1][j] > dp[i][j-1])
{
dp[i][j] = dp[i-1][j];
}else
{
dp[i][j] = dp[i][j-1];
}
}
}
return dp[len][len];
}
void main()
{
int arr[] = {1,-1,2,-3,4,-5,6,-7};
int arrcopy [sizeof(arr)/sizeof(int)];
memcpy(arrcopy,arr,sizeof(arr));
qsort(arrcopy,0,sizeof(arr)/sizeof(int)-1);
/* 计算LCS,即LIS长度 */
int len = sizeof(arr)/sizeof(int);
printf("%d\n",LCS(arr,arrcopy,len));
}
|
方法三:DP+二分查找
《编程之美》对于这个方法有提到,不过它的讲解我看得比较难受,好长时间才明白,涉及到的数组也比较多,除了源数据数组,有LIS[i]和MaxV[LIS[i]],后来看了大牛Felix的讲解,我才忽然发现编程之美中的这个数组MaxV[LIS[i]]在记录信息上其实是饶了弯的,因为我们在寻找某一长度子序列所对应的最大元素最小值时,完全没必要通过LIS[i]去定位,即没必要与数据arr[i]挂钩,直接将MaxV[i]的下标作为LIS的长度,来记录最小值就可以了(表达能力太次,囧。。。),一句话,就是不需要LIS[i]这个数组了,只用MaxV[i]即可达到效果,而且原理容易理解,代码表达也比较直观、简单。
下面说说原理:
目的:我们期望在前i个元素中的所有长度为len的递增子序列中找到这样一个序列,它的最大元素比arr[i+1]小,而且长度要尽量的长,如此,我们只需记录len长度的递增子序列中最大元素的最小值就能使得将来的递增子序列尽量地长。
方法:维护一个数组MaxV[i],记录长度为i的递增子序列中最大元素的最小值,并对于数组中的每个元素考察其是哪个子序列的最大元素,二分更新MaxV数组,最终i的值便是最长递增子序列的长度。这个方法真是太巧妙了,妙不可言。
代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
#include <iostream>
using namespace std;
/* 最长递增子序列 LIS
* 设数组长度不超过 30
* DP + BinarySearch
*/
int MaxV[30]; /* 存储长度i+1(len)的子序列最大元素的最小值 */
int len; /* 存储子序列的最大长度 即MaxV当前的下标*/
/* 返回MaxV[i]中刚刚大于x的那个元素的下标 */
int BinSearch(int * MaxV, int size, int x)
{
int left = 0, right = size-1;
while(left <= right)
{
int mid = (left + right) / 2;
if(MaxV[mid] <= x)
{
left = mid + 1;
}else
{
right = mid - 1;
}
}
return left;
}
int LIS(int * arr, int size)
{
MaxV[0] = arr[0]; /* 初始化 */
len = 1;
for(int i = 1; i < size; ++i) /* 寻找arr[i]属于哪个长度LIS的最大元素 */
{
if(arr[i] > MaxV[len-1]) /* 大于最大的自然无需查找,否则二分查其位置 */
{
MaxV[len++] = arr[i];
}else
{
int pos = BinSearch(MaxV,len,arr[i]);
MaxV[pos] = arr[i];
}
}
return len;
}
void main()
{
int arr[] = {1,-1,2,-3,4,-5,6,-7};
/* 计算LIS长度 */
printf("%d\n",LIS(arr,sizeof(arr)/sizeof(int)));
}
|
这个方法的实现巧妙而直观,让人有种“啊,原来还可以这样”的感慨,感谢Felix。
本文相关代码可以到这里下载。
(全文完)
参考资料:
《编程之美》 2.16
Felix’s Blog:最长递增子序列 O(NlogN)算法
勘误:
第三种方法细节有错误,见下面评论,感谢@冰上游鱼