題意
如果一個下標從 0 0 0 開始的陣列 a r r arr a rr 滿足以下條件,則稱為 山形陣列(Mountain Array) :
a r r . l e n g t h ≥ 3 arr.length \geq 3 a rr . l e n g t h ≥ 3
存在某個索引 i i i 滿足 0 < i < a r r . l e n g t h − 1 0 < i < arr.length - 1 0 < i < a rr . l e n g t h − 1 並且:
a r r [ 0 ] < a r r [ 1 ] < . . . < a r r [ i − 1 ] < a r r [ i ] arr[0] < arr[1] < ... < arr[i - 1] < arr[i] a rr [ 0 ] < a rr [ 1 ] < ... < a rr [ i − 1 ] < a rr [ i ]
a r r [ i ] > a r r [ i + 1 ] > . . . > a r r [ a r r . l e n g t h − 1 ] arr[i] > arr[i + 1] > ... > arr[arr.length - 1] a rr [ i ] > a rr [ i + 1 ] > ... > a rr [ a rr . l e n g t h − 1 ]
給定一個整數陣列 n u m s nums n u m s ,返回使 n u m s nums n u m s 成為 山形陣列(Mountain Array) 所需刪除的 最少 元素數量。
約束條件
3 ≤ n u m s . l e n g t h ≤ 1000 3 \leq nums.length \leq 1000 3 ≤ n u m s . l e n g t h ≤ 1000
1 ≤ n u m s [ i ] ≤ 1 0 9 1 \leq nums[i] \leq 10^9 1 ≤ n u m s [ i ] ≤ 1 0 9
保證可以從 n u m s nums n u m s 中形成一個山形陣列
思路:前後綴分解
首先轉換問題,題目要求的是使 n u m s nums n u m s 成為山形陣列所需刪除的 最少 元素數量,等價於求 n u m s nums n u m s 中最長的山形子序列的長度 m a x _ l e n max\_len ma x _ l e n ,然後用 n u m s nums n u m s 的長度減去 m a x _ l e n max\_len ma x _ l e n 即為答案。
而若 n u m s [ i ] nums[i] n u m s [ i ] 是山形子序列的最大值,則 n u m s [ i ] nums[i] n u m s [ i ] 必須同時滿足:
n u m s [ i ] nums[i] n u m s [ i ] 左邊的元素嚴格遞增,換句話說,n u m s [ i ] nums[i] n u m s [ i ] 是一個嚴格 遞增 子序列的 結尾
n u m s [ i ] nums[i] n u m s [ i ] 右邊的元素嚴格遞減,換句話說,n u m s [ i ] nums[i] n u m s [ i ] 是一個嚴格 遞減 子序列的 開頭
故可以將問題轉化為:
求以 n u m s [ i ] nums[i] n u m s [ i ] 結尾的 最長遞增子序列(LIS) 的長度 p r e [ i ] pre[i] p re [ i ]
求以 n u m s [ i ] nums[i] n u m s [ i ] 開頭的 最長遞減子序列(LDS) 的長度 s u f [ i ] suf[i] s u f [ i ]
對於以 i i i 為山頂的山形子序列,其長度為 p r e [ i ] + s u f [ i ] − 1 pre[i] + suf[i] - 1 p re [ i ] + s u f [ i ] − 1 ,扣除重複計算的山頂 n u m s [ i ] nums[i] n u m s [ i ] 本身。
最後,在所有可能的山頂中,找出最長的山形子序列長度 m a x _ l e n max\_len ma x _ l e n ,此時可以得到最少需要刪除的元素數量 n − m a x _ l e n n - max\_len n − ma x _ l e n 。
此外,求以 n u m s [ i ] nums[i] n u m s [ i ] 開頭的 LDS 等價於對反轉後的 n u m s nums n u m s 後求 LIS,但需要把結果再反轉回來。
方法一:動態規劃(Dynamic Programming)
首先定義問題,由於求以 n u m s [ i ] nums[i] n u m s [ i ] 開頭的 LDS 等價於對反轉後的 n u m s nums n u m s 後求 LIS,因此這裡只做如何求 LIS 的說明。
令 p r e [ i ] pre[i] p re [ i ] 表示以 n u m s [ i ] nums[i] n u m s [ i ] 結尾的最長遞增子序列的長度,則對於 i i i 我們可以考慮 n u m s [ i ] nums[i] n u m s [ i ] 前面的所有位置 j j j (0 ≤ j < i 0 \leq j < i 0 ≤ j < i ),如果 n u m s [ j ] < n u m s [ i ] nums[j] < nums[i] n u m s [ j ] < n u m s [ i ] ,則可以將 n u m s [ i ] nums[i] n u m s [ i ] 接在 n u m s [ j ] nums[j] n u m s [ j ] 後面,從而更新 p r e [ i ] pre[i] p re [ i ] 的值。
故可以得到以下遞迴式:
p r e [ i ] = max 0 ≤ j < i and n u m s [ j ] < n u m s [ i ] ( p r e [ j ] + 1 ) pre[i] = \displaystyle\max_{0 \leq j < i \text{ and } nums[j] < nums[i]}(pre[j] + 1)
p re [ i ] = 0 ≤ j < i and n u m s [ j ] < n u m s [ i ] max ( p re [ j ] + 1 )
計算 LIS 的具體步驟如下:
從左到右遍歷 n u m s nums n u m s ,對於每個元素 n u m s [ i ] nums[i] n u m s [ i ] ,求以 n u m s [ i ] nums[i] n u m s [ i ] 結尾的最長遞增子序列的長度 p r e [ i ] pre[i] p re [ i ]
初始時,所有位置的 p r e [ i ] pre[i] p re [ i ] 都設為 1,因為每個元素本身就是一個長度為1的遞增子序列
對於每個位置 i i i ,檢查之前所有位置 j j j (0 ≤ j < i 0 \leq j < i 0 ≤ j < i ),如果 n u m s [ i ] > n u m s [ j ] nums[i] > nums[j] n u m s [ i ] > n u m s [ j ] ,則可以將 n u m s [ i ] nums[i] n u m s [ i ] 接在 n u m s [ j ] nums[j] n u m s [ j ] 後面,從而更新 p r e [ i ] pre[i] p re [ i ] 的值。
對於 s u f [ i ] suf[i] s u f [ i ] 的求法,可以將 n u m s nums n u m s 反轉後求 LIS,然後再反轉回來。因此,對於山頂 i i i ,可以得到其山形子序列的長度為 p r e [ i ] + s u f [ i ] − 1 pre[i] + suf[i] - 1 p re [ i ] + s u f [ i ] − 1 ,之所以要扣除 1 1 1 是因為山頂 n u m s [ i ] nums[i] n u m s [ i ] 被計算了兩次。最後,在所有可能的山頂中找出最長的山形子序列長度 m a x _ l e n max\_len ma x _ l e n ,此時可以得到最少需要刪除的元素數量 n − m a x _ l e n n - max\_len n − ma x _ l e n ,即為答案。
複雜度分析
時間複雜度:O ( n 2 ) \mathcal{O}(n^2) O ( n 2 ) ,其中 n n n 是 n u m s nums n u m s 的長度。
計算 LIS 和 LDS 時所需的狀態數量皆為 O ( n ) \mathcal{O}(n) O ( n ) ,每個狀態需要枚舉最多 O ( n ) \mathcal{O}(n) O ( n ) 個位置,因此總時間複雜度為 O ( n 2 ) \mathcal{O}(n^2) O ( n 2 ) 。
空間複雜度:O ( n ) \mathcal{O}(n) O ( n ) ,用於存儲 p r e pre p re 和 s u f suf s u f 陣列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution : def minimumMountainRemovals (self, nums: List [int ] ) -> int : n = len (nums) pre = [1 ] * n for i in range (1 , n): for j in range (i): if nums[i] > nums[j]: pre[i] = max (pre[i], pre[j] + 1 ) suf = [1 ] * n for i in range (n-2 , -1 , -1 ): for j in range (n-1 , i, -1 ): if nums[i] > nums[j]: suf[i] = max (suf[i], suf[j] + 1 ) max_len = 0 for i in range (1 , n-1 ): if pre[i] > 1 and suf[i] > 1 : max_len = max (max_len, pre[i] + suf[i] - 1 ) return n - max_len
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Solution {public : int minimumMountainRemovals (vector<int >& nums) { int n = nums.size (); vector<int > pre (n, 1 ) , suf (n, 1 ) ; for (int i = 1 ; i < n; ++i) for (int j = 0 ; j < i; ++j) if (nums[i] > nums[j]) pre[i] = max (pre[i], pre[j] + 1 ); for (int i = n - 2 ; i >= 0 ; --i) for (int j = n - 1 ; j > i; --j) if (nums[i] > nums[j]) suf[i] = max (suf[i], suf[j] + 1 ); int max_len = 0 ; for (int i = 1 ; i < n - 1 ; ++i) if (pre[i] > 1 && suf[i] > 1 ) max_len = max (max_len, pre[i] + suf[i] - 1 ); return n - max_len; } };
方法二:貪心(Greedy) + 二分搜尋(Binary Search)
透過 貪心(Greedy) 策略結合 二分搜尋(Binary Search) ,我們可以將求 LIS 和 LDS 的時間複雜度優化至 O ( n log n ) \mathcal{O}(n \log n) O ( n log n ) 。
假設有一個長度為 l n ln l n 的子序列,基於貪心策略,若末尾的值越小,後續的元素更有可能接在這個子序列後面,使得整體的 LIS 更長。因此我們可以維護一個的 單調遞增 陣列 t a i l tail t ai l ,其中 t a i l [ i ] tail[i] t ai l [ i ] 表示長度為 i + 1 i+1 i + 1 的 LIS 的最後一個元素的 最小值 。
在遇到一個新的數字 x x x 時,我們要考慮他可以接在哪個位置後面,使得整體的 LIS 更長。若原本存在一個長度為 j + 1 j + 1 j + 1 的 LIS,其最後一個元素的最小值為 t a i l [ j ] tail[j] t ai l [ j ] ,則若 x ≤ t a i l [ j ] x \leq tail[j] x ≤ t ai l [ j ] ,則 x x x 可以替代 t a i l [ j ] tail[j] t ai l [ j ] ,並保持 t a i l tail t ai l 陣列的單調遞增性。
因此可以利用二分搜尋找到第一個大於等於 x x x 的元素位置 j j j ,並更新 t a i l [ j ] tail[j] t ai l [ j ] 為 x x x 。但若 j j j 等於 t a i l tail t ai l 陣列的大小,則代表 x x x 比 t a i l tail t ai l 中的所有元素都大,此時將 x x x 添加到 t a i l tail t ai l 中。而此時,對於以 n u m s [ i ] nums[i] n u m s [ i ] 結尾的 LIS 長度最多為 j + 1 j + 1 j + 1 。
對於 s u f [ i ] suf[i] s u f [ i ] 的求法,同樣可以將 n u m s nums n u m s 反轉後求 LIS,然後再反轉回來即可。
同樣地,對於山頂 i i i ,可以得到其山形子序列的長度為 p r e [ i ] + s u f [ i ] − 1 pre[i] + suf[i] - 1 p re [ i ] + s u f [ i ] − 1 ,之所以要扣除 1 1 1 是因為山頂 n u m s [ i ] nums[i] n u m s [ i ] 被計算了兩次。最後,在所有可能的山頂中找出最長的山形子序列長度 m a x _ l e n max\_len ma x _ l e n ,此時可以得到最少需要刪除的元素數量 n − m a x _ l e n n - max\_len n − ma x _ l e n ,即為答案。
複雜度分析
時間複雜度:O ( n log n ) \mathcal{O}(n \log n) O ( n log n ) ,其中 n n n 是 n u m s nums n u m s 的長度。
計算 LIS 和 LDS 時皆需要考慮 O ( n ) O(n) O ( n ) 個位置,每個位置都需要做一次二分搜尋來找到合適的插入位置,因此總時間複雜度為 O ( n log n ) \mathcal{O}(n \log n) O ( n log n ) 。
空間複雜度:O ( n ) \mathcal{O}(n) O ( n ) ,用於存儲 p r e pre p re 和 s u f suf s u f 陣列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Solution : def minimumMountainRemovals (self, nums: List [int ] ) -> int : n = len (nums) pre = [-1 ] * n tail = [] for i, x in enumerate (nums): j = bisect_left(tail, x) if j == len (tail): tail.append(x) else : tail[j] = x pre[i] = j + 1 suf = [-1 ] * n head = [] for i in range (n - 1 , -1 , -1 ): x = nums[i] j = bisect_left(head, x) if j == len (head): head.append(x) else : head[j] = x suf[i] = j + 1 max_len = 0 for i in range (n): if pre[i] > 1 and suf[i] > 1 : max_len = max (max_len, pre[i] + suf[i] - 1 ) return n - max_len
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Solution {public : int minimumMountainRemovals (vector<int >& nums) { int n = nums.size (); vector<int > pre (n, 1 ) , suf (n, 1 ) ; function<vector<int >(vector<int >&)> LIS = [&](vector<int >& nums) { int n = nums.size (); vector<int > tail; vector<int > f (n) ; for (int i = 0 ; i < n; ++i) { int j = lower_bound (tail.begin (), tail.end (), nums[i]) - tail.begin (); if (j == tail.size ()) tail.push_back (nums[i]); else tail[j] = nums[i]; f[i] = j + 1 ; } return f; }; pre = LIS (nums); reverse (nums.begin (), nums.end ()); suf = LIS (nums); reverse (suf.begin (), suf.end ()); int max_len = 0 ; for (int i = 1 ; i < n - 1 ; ++i) if (pre[i] > 1 && suf[i] > 1 ) max_len = max (max_len, pre[i] + suf[i] - 1 ); return n - max_len; } };
參考資料
最长递增子序列【基础算法精讲 20】
经典问题最长递增子序列的三种解法。力扣 No.300 最长递增子序列,真·动画教编程,适合语言初学者或编程新人。
寫在最後
Cover photo is remixed from @hans , thanks for their work!
筆力有限,方法二的說明可能不夠清晰,還是建議去閱讀參考資料。
本題和交大113軟體的手寫第 3 題基本上相同,同樣需要做前後綴分解求 LIS 和 LDS,只差別在最後求答案的計算方式不同而已。關於交大113軟體手寫的參考解答,可以參考我之前在 HackMD 上的 這篇文章 。