Fork me on GitHub

引: 

最长递增子序列问题, 是一个很基本, 很常见的问题, 它的英文专用名词是LIS: longest increasing subsequence. 但是它的解法却并不那么显而易见, 也并不好理解. 它需要比较深入的思考和良好的算法素养才能得出较好的答案. 本文中将利用动态规划算法思想, 给出相关问题的时间复杂度为O(nlogn)的解法.

问题1:

给定无序数组, 求它的最长递增子序列. 例如给定数组[0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15], 它的最长递增子序列为[0, 2, 6, 9, 11, 15].

分析:

在正式开始之前, 我们先忘记递归或者什么动态规划. 先举小例子, 然后再拓展到更大的实例. 即使起初看起来很复杂, 一旦我们理解了其中的逻辑, 代码写起来就会非常简单.

例如数组A=[2, 5, 3]. 通过观察, 我们可以看出它的LIS是[2, 5]/[2, 3]. 当然, 我们只考虑严格的递增序列. 

然后, 我们再添加两个元素[7, 11], 然后数组A=[2, 5 ,3, 7, 11]. 通过观察, 我们可以看出它的LIS变成[2, 5, 7, 11]/[2, 3, 7, 11].

如果我们再加入一个元素8进入数组A, 则A=[2, 5 ,3, 7, 11, 8]. 然而8比以上两个活跃子串(稍后会讨论这个概念)的最后元素(11)都要小. 那么我们该如何用8来拓展以上两个活跃的LIS? 当然, 首先是, 8能够成为LIS的其中一部分吗? 如果8能够成为LIS的其中一部分, 那么该怎么做呢? 如果我们想要添加8, 那么它应该出现在7之后(通过取代11).

因为我们并不知道8之后是否还有元素要添加, 所以我们并不确定加入8是否会拓展LIS. 假如元素8之后有个9, 例如A= [2, 5 ,3, 7, 11, 8, 7, 9], 这样可以用8取代11, 之后的最佳后选元素9可以拓展[2, 5, 7, 8]/[2, 3, 7, 8].

假设已有的最长子串的末尾元素为E. 当前循环到的元素为A[i], 如果存在元素A[j](j > i)满足条件E < A[i] < A[j]或者E > A[i] < A[j] , 那么我们就可以添加元素A[i]到当前最长子串的末尾.

在我们的原始输入[2, 5, 3]中, 当我们添加3到[2, 5]的时候, 就面临着如上的解决方案. 我之所以创建了两个序列[2, 5]和[2, 3], 是为了解释起来比较简单. 事实上, 我们要把3取代5, 从来只保留[2, 3].

我知道这有些困惑, 但是请继续听我说.

在已有序列当中添加或者取代元素, 什么时候才是安全的呢?

例如A=[2, 5, 3], 当它的下一个元素是1的时候, 该如何拓展当前序列[2, 5]/[2, 3]呢? 显然1不能拓展两者中的任何一个. 因为1有可能是一个新的LIS序列的最小的元素. 例如A=[2, 5, 3, 1, 2, 3, 4, 5, 6]的时候, 1就是LIS([1, 2, 3, 4, 5, 6])的最小元素.

通过观察可以发现, 新的最小元素有可能生成一个新的序列.

通过以上的观察, 在循环中, 我们需要维护一个递增序列的列表.

通常情况下, 我们有一个变长列表的集合. 这些变长列表按照长度递增的顺序排列. 然后我们将数组元素A[i]添加到这些列表中. 然后逆序搜索集合中这些列表的末尾元素. 从而找到第一个末尾元素小于A[i]的列表.

我们的策略是:

  • 1, 如果A[i]小于当前所有列表的末尾元素, 那么就新建一个新的长度为1的列表. 同时取代已有的长度为1的列表(如果有的话).
  • 2, 如果A[i]大于当前所有列表的末尾元素, 就把它添加在已有的长度最长的列表的末尾.
  • 3, 如果A[i]既不是当前所有列表的末尾元素的最大值, 也不是最小值, 那么就按照长度递减的顺序扫描这些列表的末尾元素, 直到找到第一个末尾元素小于A[i]的列表, 然后用A[i]拓展该列表, 同时用拓展过的列表取代已有的同等长度的列表.

当然在构建活跃列表的过程中, 这条原则一定要记住: "更小列表的末尾元素总是小于更大列表的末尾元素".

下面, 我们就按照以上原则, 输入题目中给的例子, 整个过程如下:

A[0] = 0. Case 1. 没有活跃列表时, 创建一个.
0.
-----------------------------------------------------------------------------
A[1] = 8. Case 2. 复制并拓展.
0.
0, 8.
-----------------------------------------------------------------------------
A[2] = 4. Case 3. 复制, 拓展, 然后抛弃旧的相同长度的列表.
0.
0, 4.
0, 8. Discarded
-----------------------------------------------------------------------------
A[3] = 12. Case 2. 复制并拓展.
0.
0, 4.
0, 4, 12.
-----------------------------------------------------------------------------
A[4] = 2. Case 3. 复制, 拓展, 然后抛弃旧的相同长度的列表.
0.
0, 2.
0, 4. Discarded.
0, 4, 12.
-----------------------------------------------------------------------------
A[5] = 10. Case 3. 复制, 拓展, 然后抛弃旧的相同长度的列表.
0.
0, 2.
0, 2, 10.
0, 4, 12. Discarded.
-----------------------------------------------------------------------------
A[6] = 6. Case 3. 复制, 拓展, 然后抛弃旧的相同长度的列表.
0.
0, 2.
0, 2, 6.
0, 2, 10. Discarded.
-----------------------------------------------------------------------------
A[7] = 14. Case 2. 复制并拓展.
0.
0, 2.
0, 2, 6.
0, 2, 6, 14.
-----------------------------------------------------------------------------
A[8] = 1. Case 3. 复制, 拓展, 然后抛弃旧的相同长度的列表.
0.
0, 1.
0, 2. Discarded.
0, 2, 6.
0, 2, 6, 14.
-----------------------------------------------------------------------------
A[9] = 9. Case 3. 复制, 拓展, 然后抛弃旧的相同长度的列表.
0.
0, 1.
0, 2, 6.
0, 2, 6, 9.
0, 2, 6, 14. Discarded.
-----------------------------------------------------------------------------
A[10] = 5. Case 3. 复制, 拓展, 然后抛弃旧的相同长度的列表.
0.
0, 1.
0, 1, 5.
0, 2, 6. Discarded.
0, 2, 6, 9.
-----------------------------------------------------------------------------
A[11] = 13. Case 2. 复制并拓展.
0.
0, 1.
0, 1, 5.
0, 2, 6, 9.
0, 2, 6, 9, 13.
-----------------------------------------------------------------------------
A[12] = 3. Case 3. 复制, 拓展, 然后抛弃旧的相同长度的列表.
0.
0, 1.
0, 1, 3.
0, 1, 5. Discarded.
0, 2, 6, 9.
0, 2, 6, 9, 13.
-----------------------------------------------------------------------------
A[13] = 11. Case 3. 复制, 拓展, 然后抛弃旧的相同长度的列表.
0.
0, 1.
0, 1, 3.
0, 2, 6, 9.
0, 2, 6, 9, 11.
0, 2, 6, 9, 13. Discarded.
-----------------------------------------------------------------------------
A[14] = 7. Case 3. 复制, 拓展, 然后抛弃旧的相同长度的列表.
0.
0, 1.
0, 1, 3.
0, 1, 3, 7.
0, 2, 6, 9. Discarded.
0, 2, 6, 9, 11.
----------------------------------------------------------------------------
A[15] = 15. Case 2. 复制并拓展.
0.
0, 1.
0, 1, 3.
0, 1, 3, 7.
0, 2, 6, 9, 11.
0, 2, 6, 9, 11, 15. <--结果: LIS List
----------------------------------------------------------------------------

 

然后给出我的实现. 其中使用的数据结构为TreeMap<Integer, ArrayList<Integer>>, key是列表的长度, value为已存在的活跃列表. 同时TreeMap是有序的Map, 它按照key的自然序列进行排序, 在这里当然是按照长度的大小从小到大升序排列, 同时又可以很好的进行逆序遍历, 在这里是很满足条件的数据结构.

具体的Java代码实现如下:

 1     public List<Integer> lis(int[] nums) {
 2         if (nums == null || nums.length == 0) {
 3             return null;
 4         }
 5         TreeMap<Integer, List<Integer>> sequences = new TreeMap<>();
 6         for (int num : nums) {
 7             if (sequences.isEmpty()) {
 8                 List<Integer> list = new ArrayList<>();
 9                 list.add(num);
10                 sequences.put(1, list);
11             } else {
12                 int lastKey = sequences.lastKey();
13                 List<Integer> lastValue = sequences.get(lastKey);
14                 if (num > lastValue.get(lastValue.size() - 1)) {
15                     List<Integer> newLastValue = new ArrayList(lastValue);
16                     newLastValue.add(num);
17                     sequences.put(lastKey + 1, newLastValue);
18                 } else {
19                     int key = -1;
20                     NavigableMap<Integer, List<Integer>> descMap = sequences.descendingMap();
21                     for (Map.Entry<Integer, List<Integer>> entry : descMap.entrySet()) {
22                         List<Integer> value = entry.getValue();
23                         if (value.get(value.size() - 1) < num) {
24                             key = entry.getKey();
25                             break;
26                         }
27                     }
28                     if (key == -1) {
29                         List<Integer> newList = new ArrayList<>();
30                         newList.add(num);
31                         sequences.put(1, newList);
32                     } else {
33                         List<Integer> value = new ArrayList(sequences.get(key));
34                         value.add(num);
35                         sequences.put(key + 1, value);
36                     }
37                 }
38             }
39         }
40         return sequences.lastEntry().getValue();
41     }

 

问题2: 

给定无序数组, 求它的最长递增子序列的长度. 例如输入[2, 5, 3], 它的最长递增子序列的长度为2.

分析:

最长递增子序列长度的求解过程如问题1的解决过程. 方法lis(int[])的返回值就是LIS本身, lis(int[]).size()就是LIS的长度. 但是如果要利用问题1的解法的话, 则比较浪费空间, 又因为在循环的过程中不断地生成新的LIS列表并取代老的同等长度的LIS列表, 所以具体的空间复杂度比较难以计算.

如果我们不关心LIS的每个元素, 只关注LIS的最后一个元素呢? 所以, 是不是可以建立个列表只存储活跃列表的尾元素? 如果A[i]大于尾元素列表的最后一个元素, 即满足问题1分析过程中的case 3, 然后只需要将A[i]添加到该尾元素列表的末尾. 如果A[i]小于尾元素列表的所有元素, 则将A[i]取代list[0]. 如果A[i]处于最大尾元素和最小尾元素之间, 则逆序遍历二分查找该尾元素列表(因为尾元素列表在生成过程中是按照升序排序的), 找到第一个list[j]使得list[j]<A[i], 则令A[i]取代list[j]. 因为该方法利用了循环和二分查找, 所以该方法的时间复杂度为O(nlogn). 因为生成了一个尾元素列表, 所以该方法的空间复杂度为O(n).

所有具体的Java实现代码如下:

 1 public int sizeOfLIS(int[] nums) {
 2     if (nums == null || nums.length == 0){
 3         return 0;
 4     }
 5     List<Integer> list = new ArrayList<>();//尾元素列表
 6     for (int num : nums){
 7         if (list.isEmpty() || list.get(list.size() - ) < num){//如果list为空或者num大于list的最后一个元素, 也是最长活跃LIS的最大值
 8             list.add(num);
 9         } else {//查找第一个小于num的尾元素的位置j, 并list[j] = num
10             int i = 0;
11             int j = list.size() - 1;
12             while(i < j){
13                 int mid = (i + j)/2;
14                 if (num > list.get(mid)) {
15                     i = mid + 1;
16                 } else {
17                     j = mid;
18                 }
19             }
20             list.set(j, num);
21         }
22     } 
23     return list.size();
24 }

 

问题3:

给定无序数组, 求它的递增子序列的个数. 例如输入[2, 5, 3], 它的递增子序列为5, 子序列分别为[2], [5], [3], [2, 5], [2, 3].

分析:

已经更新, 详情请查看 无序数组及其子序列的相关研究 .

问题4:

给定无序数组, 删除最少的元素, 使剩余元素先严格递增后严格递减. 假如数组本身是严格单调的, 也符合条件. 例如给定数组[9, 5, 6, 7, 5, 6, 5, 3, 1], 删除9和5(第二个), 得到[5, 6, 7, 6, 5, 3, 1].

分析:

"删除最少的元素, 使余下元素...", 其实就是"求最长的序列, 使该序列先严格递增后严格递减". 所以该问题的解决方案就是: 遍历给定无序数组, 遍历元素A[i]时, 对序列的前半部分, 即A[0, ..., i], 求最长递增子序列; 对序列的后半部分, 即A[i+1, ..., n-1], 求最长递减子序列, 然后两个序列先后顺序连接在一起, 成序列list_i. 在遍历结束时会产生一个先严格递增后严格递减的子序列的集合List<List<Integer>>, 然后求出最长的子序列, 就是我们的目标子序列.

好的, 废话少说, 本题目的Java代码实现为:

 1     public static List<Integer> findLongestIncreasingDecreasingSubsequence(int[] nums) {
 2         if (nums != null && nums.length != 0) {
 3             TreeMap<Integer, List<Integer>> map = new TreeMap<>();//size mapping to list
 4             for (int i = 0; i < nums.length; i++) {
 5                 List<Integer> lis = lis(nums, 0, i);//最长递增子序列
 6                 List<Integer> lds = lds(nums, i + 1, nums.length - 1);//最长递减子序列
 7                 List<Integer> increasingDecreasingList = new ArrayList<>();
 8                 if (lis != null) {
 9                     increasingDecreasingList.addAll(lis);
10                 }
11                 if (lds != null) {
12                     increasingDecreasingList.addAll(lds);
13                 }
14                 map.put(increasingDecreasingList.size(), increasingDecreasingList);//相同长度的先增后减列表, 会保留后出现的
15             }
16             return map.lastEntry().getValue();
17         }
18         return null;
19     }

其中的方法lis(int[], int, int)是求前半段的最长递增子序列, 与问题1相同的解决思路, lds(int[], int, int)是求后半段的最长递减子序列, 与lis思路一致, 只不过是所求的序列是递减的. 两者的具体现实如下: 

 1     public static List<Integer> lds(int[] nums, int start, int end) {
 2         if (nums != null && nums.length != 0 && start > -1 && end < nums.length && start <= end) {
 3             TreeMap<Integer, List<Integer>> map = new TreeMap<>();
 4             for (int i = start; i < end + 1; i++) {
 5                 int num = nums[i];
 6                 if (map.isEmpty()) {
 7                     List<Integer> firstList = new ArrayList<>();
 8                     firstList.add(num);
 9                     map.put(1, firstList);
10                 } else {
11                     int lisKey = map.lastKey();
12                     List<Integer> curLis = map.get(lisKey);
13                     if (num < curLis.get(curLis.size() - 1)) {
14                         List<Integer> newLis = new ArrayList<>(curLis);
15                         newLis.add(num);
16                         map.put(lisKey + 1, newLis);
17                     } else {
18                         int key = -1;
19                         for (Integer nKey : map.descendingKeySet()) {
20                             List<Integer> list = map.get(nKey);
21                             if (num < list.get(list.size() - 1)) {
22                                 key = nKey;
23                                 break;
24                             }
25                         }
26                         if (key == -1) {
27                             List<Integer> firstList = new ArrayList<>();
28                             firstList.add(num);
29                             map.put(1, firstList);
30                         } else {
31                             List<Integer> list = map.get(key);
32                             List<Integer> newList = new ArrayList<>(list);
33                             newList.add(num);
34                             map.put(key + 1, newList);
35                         }
36                     }
37                 }
38             }
39             return map.lastEntry().getValue();
40         }
41         return null;
42     }
43 
44     public static List<Integer> lis(int[] nums, int start, int end) {
45         if (nums != null && nums.length != 0 && start > -1 && end < nums.length && start <= end) {
46             TreeMap<Integer, List<Integer>> map = new TreeMap<>();
47             for (int i = start; i < end + 1; i++) {
48                 int num = nums[i];
49                 if (map.isEmpty()) {
50                     List<Integer> list = new ArrayList<>();
51                     list.add(num);
52                     map.put(1, list);
53                 } else {
54                     List<Integer> tmp = map.get(map.lastKey());
55                     if (num > tmp.get(tmp.size() - 1)) {
56                         List<Integer> list = new ArrayList<>(tmp);
57                         list.add(num);
58                         map.put(map.lastKey() + 1, list);
59                     } else {
60                         int lisSize = -1;
61                         NavigableMap<Integer, List<Integer>> descendingMap = map.descendingMap();
62                         for (Map.Entry<Integer, List<Integer>> entry : descendingMap.entrySet()) {
63                             List<Integer> lis = entry.getValue();
64                             if (num > lis.get(lis.size() - 1)) {
65                                 lisSize = entry.getKey();
66                                 break;
67                             }
68                         }
69                         if (lisSize == -1) {
70                             List<Integer> list = new ArrayList<>();
71                             list.add(num);
72                             map.put(1, list);
73                         } else {
74                             List<Integer> lis = map.get(lisSize);
75                             List<Integer> newLis = new ArrayList<>(lis);
76                             newLis.add(num);
77                             map.put(lisSize + 1, newLis);
78                         }
79                     }
80                 }
81             }
82             return map.lastEntry().getValue();
83         }
84         return null;
85     }

 

问题6:

给定无序数组, 删除最少的元素, 使其严格递增.

分析: 

同于LIS问题. 如问题1. 代码省略.

 

posted on 2017-11-05 17:24  SilentKnight  阅读(4684)  评论(5编辑  收藏  举报