动态规划之——最长递增子序列

最长递增子序列(Longest Increasing Subsequence)是指在给定的一组数字中,按照从左向右顺序,由递增的数字组成的子序列(中间可以有间隔)中,取长度最大的子序列即为最长递增子序列。

如给定“1 2 4 3”,则递增的子序列有“1 2”、“1 4”、“1 3”、“2 4”、“2 3”、“1 2 4”、“1 2 3”,则最长的为“1 2 4”和“1 2 3”,长度为3。

 

如何求这个最长递增子序列呢?

由于是递增序列,数字往右是不断变大的,左侧的数必然小于右侧数,即最右侧的数字是最大的。

我们先分析个简单的例子“1 2 4”:

4左侧为“1 2”,都小于4,如果不考虑1 2之间的大小关系(即无论组合是1 4、2 4还是1 2 4),则4左侧至少有一个数字可以组成递增子序列,最长递增子序列至少为2(加上4自身长度1)。即只要左侧存在小于4的数字,就会导致递增子序列的长度增加。

我们再往回看数字2,位于整个序列的第二位,因此它只需要对比一次,2大于1,所以截止到“2”这个数字,最长递增子序列长度为2,且这个长度是固定的,即后续数字无论如何组合,也不会影响2和之前数字1构成的最长递增子序列(子问题的最优解)。

也就说明后续如果有数字大于2,可以参考这个最长递增子序列。

这时我们再返回到4,4大于2,因此可以直接取左侧小于4的数字对应的最长递增子序列,再加1后就是当前的最长递增子序列,如图-1,顶部的红色数字表示对应序列中数字的最长递增子序列的长度。

                                            图-1

但还有可能是,左侧数字小于4的可能有多个,距离4最近的不一定就是最长的,因此需要挨个对比这些数字的最大递增子序列,取一个最大值。如图-2中的演示数据,这里显然要取4左侧的“3”对应的最长子序列长度,而不是“1”的对应值。

                                                         图-2

因此我们总结出如下步骤:

1 从左侧开始处理数字,且每个数字对应的默认最长子序列都是1。

2 从第二个数字a开始,在左侧所有小于a的数字的最长子序列长度中,取最大的长度+1,即为当前最长递增子序列的长度。

3 后续数字参考步骤2依次处理。

我们先用递归的方式来实现,代码如下。对应演示数据为“5, 2, 8, 6, 3, 6, 9, 5”

 1 import java.util.Arrays;
 2 
 3 public class LongestIncreasingSubsequence {
 4     public static void main(String[] args) {
 5         int[] numbers = {5, 2, 8, 6, 3, 6, 9, 5};
 6         System.out.println(Arrays.toString(numbers));
 7         recursive(numbers);
 8     }
 9 
10     public static void recursive(int[] numbers) {
11         //从左到右依次计算截止到每个数字的最长递增子序列的长度
12         for (int i = 0; i < numbers.length; i++) {
13             System.out.printf("index=%d num=%d max=%d\n", i, numbers[i], cmp(numbers, i));
14         }
15     }
16 
17     private static int cmp(int[] numbers, int k) {
18         //数据中第一个位置的最长子序列是1
19         if (k == 0) {
20             return 1;
21         }
22         //小于当前数字(基准数字)的列表中最大的子序列长度,默认长度为1
23         int max = 1;
24         //从左侧第一个位置开始依次和基准数字对比
25         for (int i = 0; i < k; i++) {
26             //小于基准数字,说明可以组成递增子序列
27             if (numbers[i] < numbers[k]) {
28                 //取截至到k对应数字的最大子序列长度。反应到当前基准数字的列长度需要返回结果+1
29                 int length = cmp(numbers, i) + 1;
30                 //取所有小于基准数字中最大的那一个数
31                 if (length > max) {
32                     max = length;
33                 }
34             }
35         }
36         return max;
37     }
38 }

 输出

[5, 2, 8, 6, 3, 6, 9, 5]
index=0 num=5 max=1
index=1 num=2 max=1
index=2 num=8 max=2
index=3 num=6 max=2
index=4 num=3 max=2
index=5 num=6 max=3
index=6 num=9 max=4
index=7 num=5 max=3

可以看到第29行的 cmp(numbers, i) + 1,是通过递归获取依赖的最长递增子序列。而由于外层循环变量 i 每次都从0开始,就可能存在重复调用。因此我们可以参考动态规划方法做一些优化,代码如下。

 1     public static void dp(int[] numbers) {
 2         //保存每个数字的下标对应的最长递增子序列长度值
 3         int[] dp = new int[numbers.length];
 4         //数据初始化:每个数字对应的默认序列长度都是1
 5         for (int i = 0; i < dp.length; i++) {
 6             dp[i] = 1;
 7         }
 8         System.out.println(Arrays.toString(dp));
 9 
10         //由于第一个位置(index=0)不存在前驱数字,也就无需比较,直接从index=1开始即可。
11         for (int i = 1; i < numbers.length; i++) {
12             System.out.printf("index=%d num=%d\n", i, numbers[i]);
13             //当前数字和之前的依次比较
14             for (int j = 0; j < i; j++) {
15                 //小于当前数字说明是一个递增序列,序列长度可以加1
16                 if (numbers[i] > numbers[j]) {
17                     System.out.printf("\t%d>%d dp[%d]=%d dp[%d]=%d\n", numbers[i], numbers[j], i, dp[i], j, dp[j]);
18                     //但需要取最大的
19                     if (dp[j] + 1 > dp[i]) {
20                         dp[i] = dp[j] + 1;
21                     }
22                 } else {
23                     System.out.printf("\t%d<=%d skip\n", numbers[i], numbers[j]);
24                 }
25             }
26             System.out.printf("\tdp[%d]=%d %s\n", i, dp[i], Arrays.toString(dp));
27         }
28     }

增加对应调用dp(numbers),输出如下

[dp]
[1, 1, 1, 1, 1, 1, 1, 1]
index=1 num=2
	2<=5 skip
	dp[1]=1 [1, 1, 1, 1, 1, 1, 1, 1]
index=2 num=8
	8>5 dp[2]=1 dp[0]=1
	8>2 dp[2]=2 dp[1]=1
	dp[2]=2 [1, 1, 2, 1, 1, 1, 1, 1]
index=3 num=6
	6>5 dp[3]=1 dp[0]=1
	6>2 dp[3]=2 dp[1]=1
	6<=8 skip
	dp[3]=2 [1, 1, 2, 2, 1, 1, 1, 1]
index=4 num=3
	3<=5 skip
	3>2 dp[4]=1 dp[1]=1
	3<=8 skip
	3<=6 skip
	dp[4]=2 [1, 1, 2, 2, 2, 1, 1, 1]
index=5 num=6
	6>5 dp[5]=1 dp[0]=1
	6>2 dp[5]=2 dp[1]=1
	6<=8 skip
	6<=6 skip
	6>3 dp[5]=2 dp[4]=2
	dp[5]=3 [1, 1, 2, 2, 2, 3, 1, 1]
index=6 num=9
	9>5 dp[6]=1 dp[0]=1
	9>2 dp[6]=2 dp[1]=1
	9>8 dp[6]=2 dp[2]=2
	9>6 dp[6]=3 dp[3]=2
	9>3 dp[6]=3 dp[4]=2
	9>6 dp[6]=3 dp[5]=3
	dp[6]=4 [1, 1, 2, 2, 2, 3, 4, 1]
index=7 num=5
	5<=5 skip
	5>2 dp[7]=1 dp[1]=1
	5<=8 skip
	5<=6 skip
	5>3 dp[7]=2 dp[4]=2
	5<=6 skip
	5<=9 skip
	dp[7]=3 [1, 1, 2, 2, 2, 3, 4, 3]

最终各数字的依赖关系如图-3:

                                                                                                              图-3

从图-3中可以看到,这些数字可以理解为数据结构“图”中的“顶点”,对应为一张有向无环图,每个顶点的前驱顶点都是固定的(由序列本身的顺序来决定),依赖关系由它前驱顶点(小于顶点值的数)决定。

 

参考资料

最长递增子序列

Longest increasing subsequence

动态规划解决问题的5个简单步骤

posted @ 2022-08-31 14:20  binary220615  阅读(3516)  评论(0编辑  收藏  举报