演算法設計與分析(十一)
300. Longest Increasing Subsequence
Given an unsorted array of integers, find the length of longest increasing subsequence.
Example
Input: [10,9,2,5,3,7,101,18]
Output: 4
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4.
Note
Note:
- There may be more than one LIS combination, it is only necessary for you to return the length.
- Your algorithm should run in
O(n^2)
complexity.
Follow up:
Could you improve it to
O(n log n)
time complexity?
思路
最近幾周演算法課都沒有講新的演算法內容,一直在講動態規劃的習題,大概了也說明DP有多麼的重要了。DP這個演算法真的讓人又愛又恨,它的程式碼量並不大,關鍵程式碼其實就狀態轉移方程那幾行,但要找出正確的狀態轉移方程是一個十分傷腦筋的過程,有時候難起來真的要人命。經過這幾周的練習,深深感覺到我與大佬之間的差距,知道自己有多菜,但前進的腳步不能停下來,還是要儘自己所能去挑戰一些有難度的動態規劃題目的,所以這周選了這一道題。
題目相當簡潔明瞭,就是在一串亂序數組裡找最長的上升序列,數字之間不需要連續,其實這也是廢話,如果數字要求連續的話也就不關動態規劃什麼事了,簡簡單單把陣列遍歷一遍就能找到最長上升子串了。Note裡說讓我們先用時間複雜度為O(n^2)
的方法做,這個算挺簡單的,不一會就做出來。接著讓我們嘗試用時間複雜度O(n log n)
的方法,這還是有一點難度的,不過最後還是花費了不少時間做出來了。
先來說下O(n^2)
的方法吧,當前狀態的最大上升子串肯定是由前面狀態的最大上升子串加1得到的。因為不是重點就不詳細講了,狀態轉移方程和具體的程式碼解答如下:
j∈[0..i-1] dp[i] = max(dp[i], dp[j] + 1) //if nums[i] > nums[j]
class Solution {
public:
int lengthOfLIS(vector<int>& nums)
{
int ans = 0;
int index = 0;
int *dp = new int[nums.size()];
for (int i = 0; i < nums.size(); i++)
{
dp[i] = 1;
for (int j = 0; j < i; j++)
if (nums[i] > nums[j])
dp[i] = max(dp[i], dp[j] + 1);
ans = max(ans, dp[i]);
}
delete []dp;
return ans;
}
int max(int num1, int num2)
{
if (num1 > num2) return num1;
else return num2;
}
};
接下來我們來講一下O(n log n)
的解法。提到動態規劃裡的log n
的話,首先想到的肯定是二分了。問題是怎麼利用二分,排序?肯定不對,僅僅是二分排序的時間複雜度就有O(n log n)
了,算上DP的時間肯定超了。那麼是查詢?因為題目是找上升子串,在有序數列裡用二分查詢,好像可行?所以我就按著這個方向去考慮了。
我們可以用一個數組把目前找到的最長上升子串存起來,當訪問到一個新節點時,看情況選擇把它放進陣列或者是不放,判斷的標準就用二分查詢的結果,最後這個陣列的長度就是答案。因為對每個節點我們用二分查詢的結果來判斷情況,不用把前面的所有狀態節點遍歷一遍,所以時間複雜度是O(n log n)
而不是O(n^2)
。判斷的標準如下:
- 如果能在數組裡查詢到數字num,那麼保持陣列狀態不變
- 如果不能找到:
- 如果num < LIS[0],LIS[0] = num
- 如果num < LIS[len - 1],LIS[len - 1] = num,len為陣列長度
- 其餘情況,找到有序數組裡第一個比num大的數字,num替代它在數組裡的位置
顯而易見的,按照上面的做法得到的LIS陣列,裡面數字的相對順序跟原數列的相對順序是不同的,不符合題意。然而這並不礙事,即使數字的順序不對,它所處的位置卻是對的,假如num在LIS陣列的第3位,證明前面有2個數字比它小,也許經過LIS陣列的更新把num後面的數字放在了它前面,但事實上確實有一個符合題意的長度為3的子串曾經在LIS數組裡出現過。
O(n log n)
方法的思路大概就這樣了。在寫程式碼時還有一點要注意的,因為我們二分查詢的目的是找到LIS數組裡第一個比num大的數字,並不是平時那樣在數組裡知道num的位置,因此要改變一下二分查詢的函式,注意不要陷進死迴圈。
class Solution {
public:
int lengthOfLIS(vector<int>& nums)
{
int ans = 0;
int index = 0;
int *LIS = new int[nums.size()];
for (int i = 0; i < nums.size(); i++)
{
index = binarySearch(nums[i], LIS, ans);
LIS[index] = nums[i];
if (index + 1 > ans)
ans = index + 1;
}
delete []LIS;
return ans;
}
int binarySearch(const int num, int* LIS, const int& len)
{
if (len == 0 || num < LIS[0]) return 0;
if (num > LIS[len - 1]) return len;
int head = 0;
int tail = len - 1;
int mid;
while (true)
{
mid = (head + tail) / 2;
if (LIS[mid] == num || (LIS[mid] > num && LIS[mid - 1] < num)) break;
else if (LIS[mid] > num) tail = mid -1;
else head = mid + 1;
}
return mid;
}
};