最长递增子序列(Longest Increasing Subsequence)-C语言实现

最长递增子序列(Longest Increasing Subsequence)

  1. 前言

最长递增子序列属于经典的动态规划问题,属于约束条件下求最大子集的问题。这里的约束条件意思是,子序列需要严格按照递增条件产生,在这个前提之下,我们要求到最长的子序列。这类问题的衍生问题有很多,其本质都是穷举最大化子集。

  1. 问题描述

问题其实非常简单,给你一个整数数组 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。

如果考虑最长递增子序列,那么就要穷举数组中的所有子序列,然后找到最大值。

  1. 求解过程

为了深入学习动态规划的基本思维,我们坚持采用《算法导论》中的 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路径上结尾的元素进行递归,只需要对未遍历的元素进行递归即可。

以下列数组进行说明:

Index01234567
arr1092537114101
visitedtruetruetruetruetruetruefalsetrue
LIS111223-14

以index=7结尾的元素的LIS[7]=4, 但在遍历过程中arr[6]>arr[7],导致在递归流程中,index=6演变成遍历的死角,所以需要继续以index=6结尾的元素在进行递归遍历,完成后本数组的每个元素的LIS的数值都完整求出。

Index01234567
arr1092537114101
visitedtruetruetruetruetruetruetruetrue
LIS11122344

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]=10<=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]空间。

参考资料

  1. 《Introduction to algorithm 4ed, 4th edition》

  2. 300. 最长递增子序列 - 力扣(Leetcode), LeetCode 习题

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值