原文地址:http://www.geeksforgeeks.org/dynamic-programming-set-3-longest-increasing-subsequence/
We have discussed Overlapping Subproblems and Optimal Substructure properties in Set 1 and Set 2respectively.
我们已经在前讨论了重叠的子问题与最优的子结构属性。
Let us discuss Longest Increasing Subsequence (LIS) problem as an example problem that can be solved using Dynamic Programming.
我们来讨论一下用动态规划来解决最长递增子序列(LIS)作为例子。
The longest Increasing Subsequence (LIS) problem is to find the length of the longest subsequence of a given sequence such that all elements of the subsequence are sorted in increasing order. For example, length of LIS for { 10, 22, 9, 33, 21, 50, 41, 60, 80 } is 6 and LIS is {10, 22, 33, 50, 60, 80}.
最长递增子序列问题是从给定的字符串中找到长度最长的子序列(注意:与自字符串的区别),这样子序列中所有的元素都是以递增的顺序排过序的。例如:{ 10, 22, 9, 33, 21, 50, 41, 60, 80 } 的LIS长度为6,其中LIS为 {10, 22, 33, 50, 60, 80}。
Optimal Substructure:
Let arr[0..n-1] be the input array and L(i) be the length of the LIS till index i such that arr[i] is part of LIS and arr[i] is the last element in LIS, then L(i) can be recursively written as.
L(i) = { 1 + Max ( L(j) ) } where j < i and arr[j] < arr[i] and if there is no such j then L(i) = 1To get LIS of a given array, we need to return max(L(i)) where 0 < i < n So the LIS problem has optimal substructure property as the main problem can be solved using solutions to subproblems,
最优的子结构:
arr[0..n-1] 作为输入数组,L(i)是到下标i的LIS的长度,这样arr[i]是LIS的部分,arr[i]是LIS中最后的元素,所以L(i)可以递归地写为:
L(i) = { 1 + Max ( L(j) ) } where j < i and arr[j] < arr[i] and if there is no such j then L(i) = 1
为了得到给定数组的LIS,我们需要返回max(L(i)) where 0 < i < n,所以LIS具有最优子结构的属性,主问题可以通过解决子问题来得到解决。
Overlapping Subproblems:Following is simple recursive implementation of the LIS problem. The implementation simply follows the recursive structure mentioned above. The value of lis ending with every element is returned using max_ending_here. The overall lis is returned using pointer to a variable max.
重叠的子问题:
下面是LIS问题的简单递归实现。这个实现简单第遵循了上面提到的递归。以每个元素结尾的LIS值用max_ending_here返回。总体的LIS用一个纸箱变量max的指针返回的。
C++代码:
/* A Naive C/C++ recursive implementation of LIS problem */运行结果:
#include<stdio.h>
#include<stdlib.h>
/* To make use of recursive calls, this function must return
two things:
1) Length of LIS ending with element arr[n-1]. We use
max_ending_here for this purpose
2) Overall maximum as the LIS may end with an element
before arr[n-1] max_ref is used this purpose.
The value of LIS of full array of size n is stored in
*max_ref which is our final result */
int _lis( int arr[], int n, int *max_ref)
{
/* Base case */
if (n == 1)
return 1;
// 'max_ending_here' is length of LIS ending with arr[n-1]
int res, max_ending_here = 1;
/* Recursively get all LIS ending with arr[0], arr[1] ...
arr[n-2]. If arr[i-1] is smaller than arr[n-1], and
max ending with arr[n-1] needs to be updated, then
update it */
for (int i = 1; i < n; i++)
{
res = _lis(arr, i, max_ref);
if (arr[i-1] < arr[n-1] && res + 1 > max_ending_here)
max_ending_here = res + 1;
}
// Compare max_ending_here with the overall max. And
// update the overall max if needed
if (*max_ref < max_ending_here)
*max_ref = max_ending_here;
// Return length of LIS ending with arr[n-1]
return max_ending_here;
}
// The wrapper function for _lis()
int lis(int arr[], int n)
{
// The max variable holds the result
int max = 1;
// The function _lis() stores its result in max
_lis( arr, n, &max );
// returns max
return max;
}
/* Driver program to test above function */
int main()
{
int arr[] = { 10, 22, 9, 33, 21, 50, 41, 60 };
int n = sizeof(arr)/sizeof(arr[0]);
printf("Length of lis is %d\n",
lis( arr, n ));
return 0;
}
Length of lis is 5Considering the above implementation, following is recursion tree for an array of size 4. lis(n) gives us the length of LIS for arr[].
想一下以上实现,下面是一个大小为4的递归树。lis(n)给出了数组arr[]的LIS的长度。
lis(4)
/ | \
lis(3) lis(2) lis(1)
/ \ /
lis(2) lis(1) lis(1)
/
lis(1)
We can see that there are many subproblems which are solved again and again. So this problem has Overlapping Substructure property and recomputation of same subproblems can be avoided by either using Memoization or Tabulation. Following is a tabluated implementation for the LIS problem.
我们可以看到许多子问题一遍又一遍地解决。所以这个问题有重叠子结构的属性,可以通过用记忆法或者制表法来避免重复计算相同的子问题。下面是LIS问题的制表法实现。
/* Dynamic Programming C/C++ implementation of LIS problem */输出:
#include<stdio.h>
#include<stdlib.h>
/* lis() returns the length of the longest increasing
subsequence in arr[] of size n */
int lis( int arr[], int n )
{
int *lis, i, j, max = 0;
lis = (int*) malloc ( sizeof( int ) * n );
/* Initialize LIS values for all indexes */
for (i = 0; i < n; i++ )
lis[i] = 1;
/* Compute optimized LIS values in bottom up manner */
for (i = 1; i < n; i++ )
for (j = 0; j < i; j++ )
if ( arr[i] > arr[j] && lis[i] < lis[j] + 1)
lis[i] = lis[j] + 1;
/* Pick maximum of all LIS values */
for (i = 0; i < n; i++ )
if (max < lis[i])
max = lis[i];
/* Free memory to avoid memory leak */
free(lis);
return max;
}
/* Driver program to test above function */
int main()
{
int arr[] = { 10, 22, 9, 33, 21, 50, 41, 60 };
int n = sizeof(arr)/sizeof(arr[0]);
printf("Length of lis is %d\n", lis( arr, n ) );
return 0;
}
Length of lis is 5
Note that the time complexity of the above Dynamic Programming (DP) solution is O(n^2) and there is a O(nLogn) solution for the LIS problem. We have not discussed the O(n Log n) solution here as the purpose of this post is to explain Dynamic Programming with a simple example. See below post for O(n Log n) solution.
Longest Increasing Subsequence Size (N log N)
注意到上面的动态规划(DP)解决方案的时间复杂度是 O(n^2) ,并且对于这个问题还有O(nLogn) 时间复杂度的解决方案。我们在这里还没有讨论O(nLogn)的方法作为这一章解释动态规划的例子。下面一章将看到O(nLogn) 的方法。