引:
最長遞增子序列問題, 是一個很基本, 很常見的問題, 它的英文專用名詞是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. 代碼省略.