最长递增子序列c语言版(动态规划或贪心+二分查找)

300. 最长递增子序列

题目:

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

方法一(动态规划)

问题分析:

可以看出数组 [0,3,1,6,2,2,7]的最长递增子序列为[0,1,2,7]或者[0,3,6,7],其长度为4,故最长严格递增子序列的长度为4。

让我们想想,我们是如何看出来最长严格递增子序列的,是不是从第一位数开始,看看后面的数如果比前一位数大,那就长度加1,然后一直找到最后没有数比该位数大就停止,所有情况的max即为答案;

例如:

(1)0>>3>>6>>7 || 0>>1>>6>>7等等

当然这是我们用眼睛看出来的,让我们想想该如何用代码实现呢,这里就用到了递归的想法

假如你要求n位数组中最长严格递增子序列的长度  是不是 可以先求前n-1位数组中最长严格递增子序列的长度max ,求n-1则需要求n-2的长度max,一直到前1位数组。最后我们就能得出答案;当然递归时间肯定太大了,我们转换为动态规划,用数组存放每一层递归的结果。

  • 递归是一种自上而下的方法,通过调用自身来解决问题。递归不会缓存子问题的结果,因此可能产生重复计算。
  • 而动态规划是一种自下而上的方法,先解决子问题并缓存结果,再根据缓存的结果逐步解决更大的问题,避免了重复计算的问题。

具体实现:

1.我们定义一个dp数组,用来存储前i位数组中的最长严格递增子序列的长度;

2.如果nums[i]>nums[j],则说明nums[i]严格递增于nums[j],可以补到nums[j]后面增加为更长的子序列,dp[i]=dp[j]+1;又因为我们要求最长子序列长度,所以选取每次符合条件(nums[i]>nums[j])的最大值:dp[i]=max(dp[i],dp[j]+1)

状态转移方程:

for(j=0;j<i;j++)//遍历i之前的所有元素
{
    if(nums[j]<nums[i])//i可以作为j的后一位加上去
    {
        dp[i]=max(dp[i],dp[j]+1);//取最大的dp[i];
    }
}

 3.最后求出dp数组里面存放的最大值就是最长子序列长度最大值,返回最大值即可。

 代码如下:

int lengthOfLIS(int* nums, int numsSize){
    int dp[numsSize];
    int i,j;
    memset(dp,0,sizeof(dp));
    if(numsSize==0)
    return 0;
    int max=0;
    for(i=0;i<numsSize;i++)
    {
        dp[i]=1;//因为只要有数,那长度最少为1
        for(j=0;j<i;j++)
        {
            if(nums[j]<nums[i])
            {
                dp[i]=fmax(dp[i],dp[j]+1);
            }
        }
        max=fmax(max,dp[i]);
    }
    return max;
}

 复杂度:

时间复杂度o(n*n):其中 n为数组 nums 的长度。动态规划的状态数为 n,计算状态 dp[i],需要 O(n)的时间遍历 dp[0…i−1] 的所有状态

空间复杂度o(n):开辟了一个一维数组来存放长度

方法二(贪心+二分)

解题思路:

方法一时间复杂度还是有点高,我们看看能不能给他再降一降
因为是求最长递增子序列,我们知道,数字递增越慢,其长度就可能更长。基于上面的贪心思路,我们维护一个数组 d[i] ,表示长度为 i 的最长上升子序列的末尾元素的最小值,用 len 记录目前最长上升子序列的长度,起始时 len 为 1,d[1]=nums[0]。例如

原数组nums = [10,9,2,5,3,7,101,18]

dp[0]=10;        [10]

dp[0]=9;        [9]

dp[0]=2;        [2]

dp[1]=5;        [2,5]

dp[1]=3;        [2,3]

dp[2]=7;        [2,3,7]

dp[3]=101;        [2,3,7,101]

dp[3]=18;        [2,3,7,18]

最后dp数组里面应该是[2,3,7,18],最大递增子序列长度为4

具体需要列一个状态方程:

如果nums[i]>dp[k],我们就把 nums[i] 存放进dp中,len++

 如果nums[i]<dp[k],,用二分查找法前k位数,把num[i]替换到合适位置,代码如下

        if(nums[i]>dp[k])
        {
            k++;
            dp[k]=nums[i];
            len++;
        }
        else if(nums[i]<dp[k])
        {
             dp[twofen(dp,k+1,nums[i])]=nums[i];//twofen(dp,k+1,nums[i])为自定义二分函数
        }

问题分析:

看到这里有些同学就会想,为什么这样不按顺序却会得出正确答案  有没有可能会出现错误呢?

其实该过程不会影响dp[i]数组下一位数的选取,只改变前i-1位数中某一位的大小,并不会引起子序列长度的变化

证明: 当 j<i,若 dp[j]>=dp[i],代表较短子序列的尾部元素的值 大于 较长子序列的尾部元素的值。这是没有可能的,因为从长度为 i 的子序列尾部倒序删除 i-j 个元素,剩下的为长度为 j 的子序列,设此序列尾部元素值为 p,则一定有 p<dp[i](即长度为 j 的子序列尾部元素值一定更小)

最终代码:

int twofen(int *dp,int n,int target)
{
    int left=0,right=n-1;
    if(n==1)
    return 0;
    else{
        int mid;
        while(left<=right)
        {
            mid=(left+right)/2;
            if(dp[mid]<target)
            {
                left=mid+1;
            }
            else if(dp[mid]>target)
            {
                right=mid-1;
            }
            else
            {
                return mid;
            }
        }
    }
    return left;//最后左边位置就会成为target正确存放位置
}
int lengthOfLIS(int* nums, int numsSize){
    int dp[numsSize];
    int i;
    memset(dp,0,sizeof(dp));
    int len=1,k=0;
    dp[0]=nums[0];
    for(i=1;i<numsSize;i++)
    {
        if(nums[i]>dp[k])
        {
            k++;
            dp[k]=nums[i];
            len++;
        }
        else if(nums[i]<dp[k])
        {
             dp[twofen(dp,k+1,nums[i])]=nums[i];
        }
    }
    return len;
}

 复杂度:

时间复杂度O(nlog⁡n):数组 nums的长度为 n,我们依次用数组中的元素去更新 dp 数组,而更新 dp 数组时需要进行O(logn) 的二分搜索,所以总时间复杂度为 O(nlogn)

空间复杂度O(n):需要额外使用长度为 n的dp数组

博主是个新手,如果有错误的地方,请联系我改正,谢谢!!!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值