算法--最長遞增子序列相關問題(更新)


引: 

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM