zoukankan      html  css  js  c++  java
  • 算法--最长递增子序列相关问题(更新)

    引: 

    最长递增子序列问题, 是一个很基本, 很常见的问题, 它的英文专用名词是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. 代码省略.

  • 相关阅读:
    160809322-王翔君第十一次c语言作业
    c语言第九次作业
    王翔君第9次c语言作业
    王翔君160809322第六次作业
    160809322-王翔君第四次作业
    第三次作业160809322
    160809322-王翔君
    160809322-王翔君
    输出不可重复的质因数
    求最大数与最小数乘积
  • 原文地址:https://www.cnblogs.com/littlepanpc/p/7787932.html
Copyright © 2011-2022 走看看