赞
踩
最长递增子序列属于经典的动态规划问题,属于约束条件下求最大子集的问题。这里的约束条件意思是,子序列需要严格按照递增条件产生,在这个前提之下,我们要求到最长的子序列。这类问题的衍生问题有很多,其本质都是穷举最大化子集。
问题其实非常简单,给你一个整数数组 arr ,找到其中最长严格递增子序列的长度。我们任意提取数组中的元素,元素下标有:
i
k
<
i
j
<
i
l
<
i
m
i_k<i_j<i_l<i_m
ik<ij<il<im
如果对应的值严格递增(不包括等于),
a
r
r
[
i
k
]
<
a
r
r
[
i
j
]
<
a
r
r
[
i
l
]
<
a
r
r
[
i
m
]
arr[i_k]<arr[i_j]<arr[i_l]<arr[i_m]
arr[ik]<arr[ij]<arr[il]<arr[im]
那么此子序列就是递增的子序列,其长度为4。
如果考虑最长递增子序列,那么就要穷举数组中的所有子序列,然后找到最大值。
为了深入学习动态规划的基本思维,我们坚持采用《算法导论》中的 CRCC方法对其进行分析,以便后续不断提升动态规划算法思维。求解动态规划框架分为四步。
a.) 表征优化问题的结构(Characterized the structure of the optimal solution)
首先给出例子,帮助理解整体的过程,
如果求出以index=6结尾的数组的最长递增子序列,那么我们可以遍历index=6之前所有元素,如果此元素小于6号元素的值,那么仅需把此元素处的LIS的值递增1即可,最后比较所有的可能。如果用图的术语解释,就是需要找到元素单调递增的最长路径(权值为1)。
如果需要计算以index=6为尾部的LIS值,可以用公式求解:
L
I
S
[
6
]
=
m
a
x
{
L
I
S
[
2
]
,
L
I
S
[
3
]
+
1
,
L
I
S
[
4
]
+
1
}
=
m
a
x
{
1
,
3
,
3
}
;
LIS[6]=max\{LIS[2],LIS[3]+1,LIS[4]+1\}=max\{1,3,3\};
LIS[6]=max{LIS[2],LIS[3]+1,LIS[4]+1}=max{1,3,3};
所以观察到,以index=6结尾的LIS有两个选择:
这两种路径的长度都为3,均满足LIS的要求。
如果采用递归,那么返回的结果是LIS[i], 其中最长的LIS元素中一定包含index[i],这就要求需要多次调用递归,确保每个以i结尾的数组的LIS[i]都可以计算获得。
b.) 递归定义最优化问题的值(Recursively define the value of the optimal solution)
通过上面分析,我们可以清楚理解子问题的结构,而且每个子问题具有相互的独立性。接下来就需要用抽象的归纳法定义最优化的值,这些值得定义一定是递归结构,否则就无法产生子问题和求解子问题。如果用函数表示f(i)表示以arr[i]结尾(包含arr[i])的数组的最长递增子序列,那么就有:
f
(
i
)
=
m
a
x
{
f
(
j
)
+
1
}
a
s
l
o
n
g
a
s
j
<
i
a
n
d
a
r
r
[
j
]
<
a
r
r
[
i
]
f(i)=max\{f(j)+1\}\ as\ long\ as\ j<i\ and\ arr[j]<arr[i]
f(i)=max{f(j)+1} as long as j<i and arr[j]<arr[i]
表达式的意思是,扫描所有i之前的元素,如果某个位置j的取值小于f(i),那么f(i)=f(j)+1; 值得一提的是f(j)也是以元素j结尾(包含j处的元素)的f(j),这样子f(i)和f(j)就具有相同的解结构,只是问题规模大小不同,f(j)的规模比f(i)问题规模要小。
如果要对此问题进行递归求解,其中重要的举措之一便是,对以不同index结尾的数组进行多次递归,细心的读者可能会有疑问,是否需要对所有不同的index结尾的数组进行全部递归呢? 答案显而易见,不需要。我们不需要对已经在LIS路径上结尾的元素进行递归,只需要对未遍历的元素进行递归即可。
以下列数组进行说明:
Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
arr | 10 | 9 | 2 | 5 | 3 | 7 | 114 | 101 |
visited | true | true | true | true | true | true | false | true |
LIS | 1 | 1 | 1 | 2 | 2 | 3 | -1 | 4 |
以index=7结尾的元素的LIS[7]=4, 但在遍历过程中arr[6]>arr[7],导致在递归流程中,index=6演变成遍历的死角,所以需要继续以index=6结尾的元素在进行递归遍历,完成后本数组的每个元素的LIS的数值都完整求出。
Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
arr | 10 | 9 | 2 | 5 | 3 | 7 | 114 | 101 |
visited | true | true | true | true | true | true | true | true |
LIS | 1 | 1 | 1 | 2 | 2 | 3 | 4 | 4 |
c) 计算最优化问题的值(Compute the value of the optimal solution)
接下来我们就需要计算最优化问题的值,本问题可以选择递归或者迭代两种方式之一,其中递归计算稍显复杂,没有迭代看起来简洁与直观。
让我们先从递归计算开始,深入理解LIS的基本算法。
在递归过程中,首先需要定义递归的截止条件,递归的截止条件实际上有两类:
f(i)当中的i==0的时候,LIS=1;
f(i)当前置比前置的任何值都要小,那么LIS=1,表格中的index=2的便是这类情况,遇到这类情况,也就是递归中判断条件没有任何一个成立,循环完成后,直接返回1这个具体的值
当递归完成后,需要对没有遍历的元素,设定其为最后一个元素(包括此元素),然后再进行递归遍历,直至所有元素都完成遍历为止,也即所有的visited[i]的值都等于1为止。所以需要对单个递归进行循环。
I. 头文件 (LIS_recursive.h)
/** * @file LIS_recursive.h * @author your name (you@domain.com) * @brief * @version 0.1 * @date 2023-02-28 * * @copyright Copyright (c) 2023 * */ #ifndef LIS_RECURSIVE_H #define LIS_RECURSIVE_H #include <stdio.h> #include <stdlib.h> #include <string.h> /** * @brief Use recursive method to get longest increasing subsequence * * @param arr Array list * @param n Number of array list * @param s Store the number of each elmenent * @return int Return longest increasing subsequence */ int lis_recursive(int *arr, int n, int *s, int *visited); /** * @brief Use recursive method to get longest increasing subsequence * * @param arr Array list * @param n Number of array list * @param s Store the number of each elmenent * @return int Return longest increasing subsequence */ int lis_recursive_aux(int *arr, int n, int *s, int *visited); int find_longest_increasing_subsequence(int *arr, int n, int *s,int *visited); /** * @brief Find maximum number between m and n * * @param m * @param n * @return int */ int max(int m,int n); #endif
II. 函数的实现
/** * @file LIS_recursive.c * @author your name (you@domain.com) * @brief * @version 0.1 * @date 2023-02-28 * * @copyright Copyright (c) 2023 * */ #ifndef LIS_RECURSIVE_C #define LIS_RECURSIVE_C #include "LIS_recursive.h" int lis_recursive(int *arr, int n, int *s, int *visited) { int i; for(i=0;i<=n;i++) { *(s+i)=-1; *(visited+i)=0; } return find_longest_increasing_subsequence(arr,n,s,visited); } int lis_recursive_aux(int *arr, int n, int *s, int *visited) { int i; if(s[n]!=-1) { return s[n]; } if((n)==0) { s[n]=1; visited[n]=1; } else { s[n] = 1; visited[n] = 1; for (i = 0; i < n; i++) //int arr[] = {10, 9, 2, 5, 3, 7, 101, 18}; { if (arr[i] < arr[n]) { s[n] = max(s[n], lis_recursive_aux(arr, i, s,visited) + 1); } } } return s[n]; } int find_longest_increasing_subsequence(int *arr, int n, int *s, int *visited) { int i; int max_value=1; for(i=n;i>=0;i--) { if(!visited[i]) { s[i] = lis_recursive_aux(arr, i, s, visited); } max_value=max(max_value,s[i]); } return max_value; } int max(int m, int n) { return (m>n?m:n); } #endif
III. 测试函数
/** * @file LIS_recursive_main.h * @author your name (you@domain.com) * @brief * @version 0.1 * @date 2023-02-28 * * @copyright Copyright (c) 2023 * */ #ifndef LIS_RECURSIVE_MAIN_C #define LIS_RECURSIVE_MAIN_C #include "LIS_recursive.c" #define N 8 int main(void) { int arr[] = {10, 9, 2, 5, 3, 7, 114, 101}; int n; int s[N]; int visited[N]; int lis; n = N-1; lis=lis_recursive(arr,n,s,visited); printf("The longest increasing susbequence number is %d\n",lis); getchar(); return EXIT_SUCCESS; } #endif
接下来继续讨论迭代形式的实现,有了递归作为基础,迭代本就是水到渠成的事情,递归过程中实际变化参数只有数组的下标, 很自然地就以dp[N]作为迭代的保存数组。
关键的是状态转移方程的确认,要确认dp[i]的状态,必须对前面i-1元素和当前的第i元素进行比较,那么就有状态转移方程:
d
p
[
i
]
=
m
a
x
{
d
p
[
i
]
,
d
p
[
j
]
+
1
}
,
j
<
i
a
n
d
a
r
r
[
i
]
>
a
r
r
[
j
]
dp[i]=max\{dp[i],dp[j]+1\},\ j<i\ and \ arr[i]>arr[j]
dp[i]=max{dp[i],dp[j]+1}, j<i and arr[i]>arr[j]
状态转移方程的基准也很明确,对于单个元素,其LIS的值都等于1,也即,其自身是最长的LIS。基于上述理解,就很容易书写出来。
d
p
[
i
]
=
1
,
0
<
=
i
<
n
;
dp[i]=1, 0<=i<n;
dp[i]=1,0<=i<n;
其核心代码实现:
/** * @file LIS_recursive.c * @author your name (you@domain.com) * @brief * @version 0.1 * @date 2023-02-28 * * @copyright Copyright (c) 2023 * */ #ifndef LIS_BOTTOMUP_C #define LIS_BOTTOMUP_C #include "LIS_bottomup.h" int lis_bottomup(int *arr, int n, int *dp) { int i; int j; int max_value; //when there is only one element in the array, //the longest increasing subsequence is 1 for(i=0;i<=n;i++) { *(dp+i)=1; } //If the array is not empty, the max_value will be initialized as 1 max_value=1; for(i=0;i<=n;i++) { for(j=0;j<i;j++) { if(arr[j]<arr[i]) { dp[i]=max(dp[i],dp[j]+1); } } max_value=max(max_value,dp[i]); } return max_value; } int max(int m, int n) { return (m>n?m:n); } #endif
在此不再赘述dp的运算的详细过程。
4. 小结
对于LIS问题,可以清晰的采用迭代实现,但是如果采用递归实现就需要多余的函数和判断,所以很多时候,如果能清晰的判断迭代的边界条件并能给出初始化的base,迭代也是一种最佳选择。如果涉及到很多子问题的剪枝,那么递归就比迭代效率更高,比如背包问题采用迭代,可以节省很多dp[N+1][Total_Weight+1]空间。
参考资料
《Introduction to algorithm 4ed, 4th edition》
300. 最长递增子序列 - 力扣(Leetcode), LeetCode 习题
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。