最长上升子序列,一种DP问题,给一个乱序数组,问最长(严格)上升/下降子序列的长度/个数(可重复/不可重复)

300. 最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18] 输出:4 解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3] 输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7] 输出:1

提示:

  • 1 <= nums.length <= 2500
  • -104 <= nums[i] <= 104

进阶:

  • 你可以设计时间复杂度为 O(n2) 的解决方案吗?
  • 你能将算法的时间复杂度降低到 O(n log(n)) 吗?

思路:
方法1:暴力DP,时间复杂度O(n2)
方法2:二分优化,时间复杂度O(nlogn)

  1. // 方法1
  2. class Solution {
  3. public int lengthOfLIS(int[] nums) {
  4. int n = nums.length;
  5. int[] f = new int[n];
  6. int max = 0;
  7. for (int i = 0; i < n; i++) {
  8. f[i] = 1;
  9. for (int j = 0; j < i; j++) {
  10. if (nums[j] < nums[i])
  11. f[i] = Math.max(f[i], f[j] + 1);
  12. }
  13. max = Math.max(max, f[i]);
  14. }
  15. return max;
  16. }
  17. }
  1. //方法2
  2. class Solution {
  3. public int lengthOfLIS(int[] nums) {
  4. List<Integer> q = new ArrayList<>();
  5. for (int x : nums) {
  6. if (q.isEmpty() || x > q.get(q.size() - 1))
  7. q.add(x);
  8. else {
  9. int l = 0, r = q.size() - 1;
  10. while (l < r) {
  11. int mid = l + r >> 1;
  12. if (q.get(mid) < x)
  13. l = mid + 1;
  14. else
  15. r = mid;
  16. }
  17. q.set(r, x);
  18. }
  19. }
  20. return q.size();
  21. }
  22. }

Acwing. 1012. 友好城市

经典LIS应用!!!

673. 最长递增子序列的个数

给定一个未排序的整数数组,找到最长递增子序列的个数。
示例 1:
输入: [1,3,5,4,7] 输出: 2 解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。
示例 2:
输入: [2,2,2,2,2] 输出: 5 解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。
注意: 给定的数组长度不超过 2000 并且结果一定是32位有符号整数。

思路:
问最长上升子序列的个数,可重复
数据范围不超过2000,直接暴力DP即可

  1. class Solution {
  2. public int findNumberOfLIS(int[] nums) {
  3. int n = nums.length;
  4. int[] f = new int[n], g = new int[n];
  5. int max = 1, res = 0;
  6. for (int i = 0; i < n; i++) {
  7. f[i] = 1;
  8. g[i] = 1;
  9. for (int j = 0; j < i; j++) {
  10. if (nums[i] > nums[j]) {
  11. if (f[i] < f[j] + 1) {
  12. f[i] = f[j] + 1;
  13. g[i] = g[j];
  14. } else if (f[i] == f[j] + 1) {
  15. g[i] += g[j];
  16. }
  17. }
  18. }
  19. if (f[i] > max) {
  20. max = f[i];
  21. }
  22. }
  23. for (int i = 0; i < n; i++) {
  24. if (f[i] == max)
  25. res += g[i];
  26. }
  27. return res;
  28. }
  29. }

Acwing 314. 低买

给定一段时间内股票的每日售价(正 16 位整数)。
你可以选择在任何一天购买股票。
每次你选择购买时,当前的股票价格必须严格低于你之前购买股票时的价格。
编写一个程序,确定你应该在哪些天购进股票,可以使得你能够购买股票的次数最大化。
例如,下面是一个股票价格时间表:
Day 1 2 3 4 5 6 7 8 9 10 11 12
Price 68 69 54 64 68 64 70 67 78 62 98 87
如果每次购买都必须遵循当前股票价格严格低于之前购买股票时的价格,那么投资者最多可以购买四次该股票。
买进方案之一为:
Day 2 5 6 10
Price 69 68 64 62
输入格式
第 1 行包含整数 N,表示给出的股票价格的天数。
第 2 至最后一行,共包含 N 个整数,每行 10 个,最后一行可能不够 10 个,表示 N 天的股票价格。
同一行数之间用空格隔开。
输出格式
输出占一行,包含两个整数,分别表示最大买进股票次数以及可以达到最大买进次数的方案数。
如果两种方案的买入日序列不同,但是价格序列相同,则认为这是相同的方案(只计算一次)。
数据范围
1≤N≤5000
输入样例1:
12 68 69 54 64 68 64 70 67 78 62 98 87
输出样例1:
4 2
输入样例2:
5 4 3 2 1 1
输出样例2:
4 1

思路:
完673的困难版,要求去重,只需在代码中间多加一个循环去重即可(PS:真不好想)

  1. import java.util.*;
  2. public class Main {
  3. public static void main(String[] args) {
  4. Scanner sc = new Scanner(System.in);
  5. int n = sc.nextInt();
  6. int[] a = new int[n];
  7. for (int i = 0; i < n; i++)
  8. a[i] = sc.nextInt();
  9. int[] f = new int[n], g = new int[n];
  10. int max = 0, cnt = 0;
  11. for (int i = 0; i < n; i++) {
  12. f[i] = g[i] = 1;
  13. for (int j = 0; j < i; j++) {
  14. if (a[j] > a[i]) {
  15. if (f[i] < f[j] + 1) {
  16. f[i] = f[j] + 1;
  17. g[i] = g[j];
  18. } else if (f[i] == f[j] + 1) {
  19. g[i] += g[j];
  20. }
  21. }
  22. }
  23. //去重 例[5 4 3 1 3 1]
  24. //对于两个3来说,他们的下降子序列是完全一致的,需要将前一个3的子序列个数清空
  25. //这样在遍历到最后一个1时才不会重复加相同的子序列
  26. for (int j = 0; j < i; j++) {
  27. if (f[i] == f[j] && a[i] == a[j])
  28. g[j] = 0;
  29. }
  30. max = Math.max(max, f[i]);
  31. }
  32. for (int i = 0; i < n; i++) {
  33. if (f[i] == max)
  34. cnt += g[i];
  35. }
  36. System.out.println(max + " " + cnt);
  37. }
  38. }

873. 最长的斐波那契子序列的长度

如果序列 X1, X_2, …, X_n 满足下列条件,就说它是 斐波那契式 _的:

  • n >= 3
  • 对于所有 i + 2 <= n,都有 Xi + X{i+1} = X_{i+2}

给定一个严格递增的正整数数组形成序列 arr ,找到 arr 中最长的斐波那契式的子序列的长度。如果一个不存在,返回 0 。
(回想一下,子序列是从原序列 arr 中派生出来的,它从 arr 中删掉任意数量的元素(也可以不删),而不改变其余元素的顺序。例如, [3, 5, 8] 是 [3, 4, 5, 6, 7, 8] 的一个子序列)

示例 1:
输入: arr = [1,2,3,4,5,6,7,8] 输出: 5 解释: 最长的斐波那契式子序列为 [1,2,3,5,8] 。
示例 2:
输入: arr = [1,3,7,11,12,14,18] 输出: 3 解释: 最长的斐波那契式子序列有 [1,11,12]、[3,11,14] 以及 [7,11,18] 。

提示:

  • 3 <= arr.length <= 1000
  • 1 <= arr[i] < arr[i + 1] <= 10^9

思路:
方法1:暴力枚举以所有两个数的为起始的最长斐波那契数列的长度,选最大值

  1. class Solution {
  2. public int lenLongestFibSubseq(int[] arr) {
  3. int n = arr.length;
  4. int[][] f = new int[n][n];
  5. Set<Integer> set = new HashSet<>();
  6. for (int x : arr)
  7. set.add(x);
  8. int ans = 0;
  9. for (int i = 0; i < n; i++) {
  10. for (int j = i + 1; j < n; j++) {
  11. int a = arr[i], b = arr[j];
  12. int len = 2;
  13. while (set.contains(a + b)) {
  14. int t = a + b;
  15. a = b;
  16. b = t;
  17. ans = Math.max(++len, ans);
  18. }
  19. }
  20. }
  21. return ans;
  22. }
  23. }

方法2:DP,类似于LIS,但不完全一样。状态表示是二维,而转移是一维。
f[i][j]表示以a[i]a[j]为最后两项的斐波那契数列的最大长度
状态转移, 其转移可以在O(1)时间内求出,存储一个值到下标的映射即可。

  1. if (map.containsKey(a[i] - a[j] && map.get(a[i] - a[j]) < j)
  2. f[i][j] = f[j][i] + 1;
  1. class Solution {
  2. public int lenLongestFibSubseq(int[] a) {
  3. Map<Integer, Integer> map = new HashMap<>();
  4. int n = a.length;
  5. int[][] f = new int[n][n];
  6. for (int i = 0; i < n; i++)
  7. map.put(a[i], i);
  8. int ans = 0;
  9. for (int i = 1; i < n; i++) {
  10. for (int j = 0; j < i; j++) {
  11. int x = a[i] - a[j];
  12. int idx = map.getOrDefault(x, -1);
  13. if (idx != -1 && idx < j) {
  14. f[i][j] = Math.max(f[i][j], f[j][idx] + 1);
  15. ans = Math.max(ans, f[i][j]);
  16. }
  17. }
  18. }
  19. return ans > 0 ? ans + 2 : ans;
  20. }
  21. }

长为恰为3的最长上升子序列

题目描述:
求满足下列条件的序列个数:

  • 长度为n
  • 序列的每个元素值都在[1,m]
  • 最长严格上升子序列的长度恰好为3

数据范围
3≤n≤1000
3≤m≤10

思路:
状态表示:f[i][a][b][c]表示长度为i的数组,且最长上升子序列长为1的末尾数字是a,最长上升子序列长为2的末尾数字是b,最长上升子序列长为3的末尾数字是c的方案数
初始化:f[0][m + 1][m + 1][m + 1] = 1 表示长为0的数组,不存在最长上升子序列,用m + 1表示不存在这一状态
状态转移:枚举第ii个元素的数值,记为x。若1≤x≤a,那么x可以替换a,则fi,x,b,c=fi,x,b,c+fi−1,a,b,c;若a+1≤x≤b,那么x可以替换b,则fi,a,x,c=fi,a,x,c+fi−1,a,b,c;若b+1≤x≤c,那么x可以替换c,则fi,a,b,x=fi,a,b,x+fi−1,a,b,c
注意:若a=m+1,表示长度为1的最长上升子序列不存在。b,c=m+1同理。

  1. import java.util.*;
  2. public class Main {
  3. public static void main(String[] args) {
  4. Scanner sc = new Scanner(System.in);
  5. final int mod = 998244353;
  6. int n = sc.nextInt(), m = sc.nextInt();
  7. int[][][][] f = new int[n + 1][m + 2][m + 2][m + 2];
  8. // 初始化
  9. f[0][m + 1][m + 1][m + 1] = 1;
  10. for (int i = 1; i <= n; i++) {
  11. for (int a = 1; a <= m + 1; a++) {
  12. for (int b = 1; b <= m + 1; b++) {
  13. for (int c = 1; c <= m + 1; c++) {
  14. for (int x = 1; x <= a && x <= m; x++)
  15. f[i][x][b][c] = (f[i][x][b][c] + f[i - 1][a][b][c]) % mod;
  16. for (int x = a + 1; x <= b && x <= m; x++)
  17. f[i][a][x][c] = (f[i][a][x][c] + f[i - 1][a][b][c]) % mod;
  18. for (int x = b + 1; x <= c && x <= m; x++)
  19. f[i][a][b][x] = (f[i][a][b][x] + f[i - 1][a][b][c]) % mod;
  20. }
  21. }
  22. }
  23. }
  24. int res = 0;
  25. for (int i = 1; i <= m; i++)
  26. for (int j = 1; j <= m; j++)
  27. for (int k = 1; k <= m; k++)
  28. res = (res + f[n][i][j][k]) % mod;
  29. System.out.println(res);
  30. }
  31. }
Acwing 3499. 序列最大收益 一道类似于LIS的DP题