最长递增子序列(Longest Increasing Subsequence)
- 前言
最长递增子序列属于经典的动态规划问题,属于约束条件下求最大子集的问题。这里的约束条件意思是,子序列需要严格按照递增条件产生,在这个前提之下,我们要求到最长的子序列。这类问题的衍生问题有很多,其本质都是穷举最大化子集。
- 问题描述
问题其实非常简单,给你一个整数数组 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》文章来源:https://www.toymoban.com/news/detail-785907.html
-
300. 最长递增子序列 - 力扣(Leetcode), LeetCode 习题文章来源地址https://www.toymoban.com/news/detail-785907.html
到了这里,关于最长递增子序列(Longest Increasing Subsequence)-C语言实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!