最长上升子序列**----线性DP,贪心,单调栈

目录

题目:

DP分析:

代码:

3.6更新 贪心 第一个思考方式

先上代码:

解析:

贪心 第二个思考方式 (与上面的思路差不多,但是换了个角度)

思路:

代码:


 所有的思路很重要!!!

题目:

给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。

输入格式

第一行包含整数 N。

第二行包含 N 个整数,表示完整序列。

输出格式

输出一个整数,表示最大长度。

数据范围

1≤N≤1000,
−10^9≤数列中的数≤10^9

输入样例:

7
3 1 2 1 8 5 6

输出样例:

4

DP分析:

代码:

import java.io.*;
import java.util.*;

class Main{
    static int N = 1010;
    static int[] w = new int[N];
    static int[] f = new int[N];
    public static void main(String[] args) throws IOException{
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(in.readLine()); 
        String[] s = in.readLine().split(" ");
        for(int i=1;i<=n;i++){
                w[i] = Integer.parseInt(s[i-1]);
        }
        
        // DP
        for(int i=1;i<=n;i++){
            f[i] = 1; // 只有自身
            for(int j=1;j<i;j++){
                if(w[j]<w[i]) // 保证序列单调上升
                    f[i] = Math.max(f[i],f[j]+1);
            }
        }
        
        // 不一定f[n]就是最长的,因为f[n]必定包含w[n]这个数,最长上升子序列可能不包含w[n]
        int res = 0;
        for(int i=1;i<=n;i++) res = Math.max(res,f[i]);
        System.out.println(res);
    }
}

 PS:

本来想尝试单调栈的。发现比如  3 1 2 1 8 5 6 。栈会删去 2 ,存进 1。而最长上升子序列为 1 2 5 6。


3.6更新 贪心 第一个思考方式

        上面说到不可以使用单调栈。并且使用上面的代码,时间复杂度是O(n^2),如果N增大,就会超时。

单调栈:单调栈(思路+示例)-CSDN博客

但是参考了一篇大佬的代码 AcWing 896. 最长上升子序列 II - AcWing  我悟了。

先上代码:

// 类似于单调栈的做法,但是更多的有贪心的思想在。
// 替换掉第一个大于等于该值的在栈中的元素,目的是保证可以最大限度的增加上升子序列的长度。
/* ex:3 1 2 1 8 5 6
    使用st[]存储值
    i = 1, st[] = {3}
    i = 2, st[] = {1}
    i = 3, st[] = {1,2}
    i = 4, st[] = {1,2} 此时这个1是被w[4]替换掉w[2]的1
    i = 5, st[] = {1,2,8}
    i = 6, st[] = {1,2,5}
    i = 7, st[] = {1,2,5,6}
*/

import java.io.*;
import java.util.*;

class Main{
    static int N = 100010;
    static int[] w = new int[N];
    static int[] st = new int[N]; // 单调栈
    public static void main(String[] args) throws IOException{
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(in.readLine()); 
        String[] s = in.readLine().split(" ");
        for(int i=1;i<=n;i++){
                w[i] = Integer.parseInt(s[i-1]);
        }
        
        int res = 0;
        int tt = 0; // 栈顶
        for(int i=1;i<=n;i++){
            if(tt==0||w[i]>st[tt]){ // 如果栈中没有值或者当前值大于栈顶元素,加入进去
                st[++tt] = w[i];   
                res = Math.max(res,tt);
            }
            else{ // 否则,替换掉第一个大于等于该值的在栈中的元素
                int idx = tt;
                while(idx>0&&w[i]<=st[idx])
                    idx--;
                st[idx+1] = w[i];
            }
        }
        
        System.out.println(res);
    }
}

解析:

        使用了单调栈+贪心的思想。

        如果当前该值 w[i] 大于栈顶元素,就加入栈 st[ \ ]进去。

        否则,找到栈中第一个大于等于 w[i] 的值,用 w[i] 替换掉该值。

                                                                                (用二分,虽然这个代码没用)

ex:

         a=[3,1,4,1,5,9,2]
         i = 1, st[\ ] = {3}\\ i = 2, st[\ ] = {1}\\ i = 3, st[\ ] = {1,4}\\ i = 4, st[\ ] = {1,4} \\ i = 5, st[\ ] = {1,4,5}\\ i = 6, st[\ ] = {1,4,5,9}\\ i = 7, st[\ ] = {1,2,5,9}\\

        疑惑:

  1. 一般的单调栈会将第 4 步的时候,删去 1,4 ,再将 a[4] 装入进去,但是会缩短上升子序列的长度。并且在 7 步的时候,数字 9 被保留了下来,没有被去除换成 2
  2. 最后的序列为 (1,2,5,9),而准确的最长上升子序列应该是 ({1,4,5,9}),这是为什么。

        第一个问题:为什么有这种替换操作?主要是贪心,我们并不想在求解的过程中导致最长上升子序列越算越短。因此,如果我们目前算出的结果还没以前的长,会暂时保留以前的结果,当然也不丢弃目前的结果,因为之后继续计算的话,目前的结果可能更优。

        为了实现上述目的,我们可以用新序列从左到右逐渐覆盖掉旧序列。当新序列长度 <原序列长度时,原序列没有被完全覆盖,因此保证长度不减小;当新序列长度 ≥原序列长度时,原序列已经被完全覆盖,现在就是以新序列为基础进行计算了。

        因此就产生了这种特殊的替换方式。

        第二个问题最后的最长上升子序列不是准确的?因为由于贪心的思想,存在这种替换方式,导致最长上升子序列中某些值被替换掉,但是上升子序列的长度没有发生改变,只有里面的值发生了变化,因此,在求解该题中最长上升子序列的长度时可以被使用,但是要输出最长上升子序列的值的时候,就不行了。

        核心就是因为栈中储存的不只有一个序列,是旧序列和新序列合并的产物,因此不一定是最终最长上升子序列。


贪心 第二个思考方式 (与上面的思路差不多,但是换了个角度)

思路:

        换一种思路思考贪心的问题,如果我们要将 w[i] 作为上升子序列的末尾元素。那么我们可以设计一个数组 q[\ ],存储当上升子序列长度分别为 [1,2,3...\ n] 的时候最小的末尾值,其中,末尾值都是跟随数组下标的上升而严格上升的

        如果想要将 w[i] 装入到序列中,我们只需要在长度为 q[\ ] 中找到大于等于 w[i] 的第一个末尾值 q[j] ,此时 q[j-1] < w[i],\ q[j]>=w[i] 。此时我们将 w[i] 装入到 q[j-1] 的末尾,此时,序列的长度就增加了1,因此就要更新 q[j] 的末尾值为 w[i] (这个操作就跟上面那种方法替换大于等于 w[i] 的第一个值思路一样),从而保证了序列中的值尽可能小来增加最长子序列的可能性。

        因为数组 q[\ ] 中的值是严格单调递增的,因此可以使用二分来找到该 j 点。

代码:

import java.io.*;
import java.util.*;

class Main{
    static int N = 100010;
    static int[] w = new int[N];
    static int[] q = new int[N]; // 存储每个上升子序列长度的最小末尾值
    public static void main(String[] args) throws IOException{
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(in.readLine()); 
        String[] s = in.readLine().split(" ");
        for(int i=1;i<=n;i++){
                w[i] = Integer.parseInt(s[i-1]);
        }
        
        int len = 0; // q数组的长度
        for(int i=1;i<=n;i++){
            int l = 0, r = len; // l只能从0开始,因为存值的时候要+1
            while(l<r){ // 找到q[l]<w[i]<=q[l+1] 的l点
                int mid = l+r+1>>1;
                if(q[mid]<w[i]) l = mid;
                else r = mid-1;
            }
            q[r+1] = w[i]; // 更新l+1的末尾值
            len = Math.max(len,r+1); // 取当前数组q和更新的数组q长度的最大值
        }
        
        System.out.println(len);
    }
}

  • 39
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值