线性DP - 最长上升子序列

141 阅读1分钟
  • 问题背景

    • 给定一个长度为N的数列,求数值严格单调递增的子序列的长度最长是多少。(子序列不一定连续)
  • Dp分析

Snipaste_2023-03-23_17-17-53.png

  • 状态表示

    • 一维状态表示f(i)
    • f(i,j)表示的是哪一个集合:所有满足如下条件的集合
      • 所有以第i个数结尾的上升子序列
    • f(i,j)存的是什么属性:MaxMin数量;在这里f(i,j)存的应该是最大值,即上升子序列长度的最大值
  • 状态计算:f(i)可以怎么算出来?

    • 这里可以将集合分为i个子集
      • 前一个数来自第 j 个数,j0 取到 i - 1
        • f( i ) = f( j ) + 1
    • 因此 f(i, j) = max( f( j ) + 1 )
  • 代码(n^2^)

    • for (int i = 1; i <= n; i++) {
          //前一个数来自第0个数,即自己就是第一个数
          f[i] = 1;
          for (int j = 1; j < i; j ++) {
              if (a[j] < a[i]) {
                  f[i] = Math.max(f[i], f[j] + 1);
              }
          }
      }
      
  • 怎么记录最长子序列?

    • 开一个新数组记录以当前数结尾的最长的上升子序列的倒数第二个数的下标
  • 优化到(nlog(n))

    • 看习题

练习

01 最长上升子序列

  • 题目

Snipaste_2023-03-23_19-24-02.png

  • 题解1
import java.io.*;
import java.util.*;

public class Main {
    public static final int N = 1010;
    public static int[] a = new int[N];
    //存所有以第i个数结尾的上升子序列的长度的最大值
    public static int[] f = new int[N];
    public static int n;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out));
        n = Integer.parseInt(br.readLine());
        String[] str1 = br.readLine().split(" ");
        for (int i = 1; i <= n; i++) {
            a[i] = Integer.parseInt(str1[i - 1]);
        }

        for (int i = 1; i <= n; i++) {
            //前一个数来自第0个数,即自己就是第一个数
            f[i] = 1;
            for (int j = 1; j < i; j ++) {
                if (a[j] < a[i]) {
                    f[i] = Math.max(f[i], f[j] + 1);
                }
            }
        }
        int res = 0;
        for (int i = 1; i <= n; i++) {
            res = Math.max(res, f[i]);
        }
        pw.println(res);
        br.close();
        pw.close();
    }
}
  • 题解2
    • 记录路径
//记录路径
import java.io.*;
import java.util.*;

public class Main {
    public static final int N = 1010;
    public static int[] a = new int[N];
    //存所有以第i个数结尾的上升子序列的长度的最大值
    public static int[] f = new int[N];
    //记录以当前数结尾的最长的上升子序列的倒数第二个数的下标
    public static int[] g = new int[N];
    public static int n;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out));
        n = Integer.parseInt(br.readLine());
        String[] str1 = br.readLine().split(" ");
        for (int i = 1; i <= n; i++) {
            a[i] = Integer.parseInt(str1[i - 1]);
        }

        for (int i = 1; i <= n; i++) {
            //前一个数来自第0个数,即自己就是第一个数
            f[i] = 1;
            g[i] = 0;
            for (int j = 1; j < i; j ++) {
                if (a[j] < a[i]) {
                    if (f[i] < f[j] + 1) {
                        f[i] = f[j] + 1;
                        g[i] = j;
                    }
                }
            }
        }
        //k存的是最长上升子序列的最后一个数的下标
        int k = 1;
        for (int i = 1; i <= n; i++) {
            if (f[k] < f[i]) {
                k = i;
            }
        }
        pw.println(f[k]);
        //通过下标在g数组中获取路径
        while (k != 0) {
            pw.print(a[k] + " ");
            k = g[k];
        }
        br.close();
        pw.close();
    }
}

02 最长上升子序列 II

  • 题目

Snipaste_2023-03-23_19-25-55.png

  • 题解
import java.io.*;
import java.util.*;

public class Main {
    public static final int N = 100010;
    public static int[] a = new int[N];
    //存的是长度为i的上升子序列的最后一个数字的最小值
    public static int[] q = new int[N];
    public static int len, n;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out));
        n = Integer.parseInt(br.readLine());
        String[] str1 = br.readLine().split(" ");
        for (int i = 0; i < n; i++) {
            a[i] = Integer.parseInt(str1[i]);
        }
        //哨兵,防止出现边界问题
        q[0] = (int) -2e9;
        len = 0;
        //二分查找寻找 小于a[i]的最大值,并以这个值为结尾的最长上升子序列的长度l
        for (int i = 0; i < n; i++) {
            int l = 0, r = len;
            while (l < r) {
                int mid = l + r + 1 >> 1;
                if (q[mid] < a[i]) {
                    l = mid;
                } else {
                    r = mid - 1;
                }
            }
            //更新一下len
            len = Math.max(len, l + 1);
            //a[i]一定小于等于q[l + 1] 所以直接更新
            q[l + 1] = a[i];
        }
        pw.println(len);
        br.close();
        pw.close();
    }
}