引:
最长递增子序列问题, 是一个很基本, 很常见的问题, 它的英文专用名词是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. 代码省略.
谢谢打赏!
您的支持就是我的最大动力!
支付宝
微信
Just remember: Half is worse than none at all.
本文是由SilentKnight诚意发布, 欢迎转载! 但转载请注明出处: 【SilentKnight】
如果觉得还有帮助的话,可以点一下右下角的【推荐】,希望能够持续的为大家带来更好的技术文章!
想跟我一起进步么?那就【关注】我吧