1. 程式人生 > >[LeetCode] Longest Increasing Subsequence 最長遞增子序列

[LeetCode] 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:

  • There may be more than one LIS combination, it is only necessary for you to return the length.
  • Your algorithm should run in O(n2) complexity.

Follow up: Could you improve it to O(n log n) time complexity?

這道題讓我們求最長遞增子串Longest Increasing Subsequence的長度,簡稱LIS的長度。我最早接觸到這道題是在LintCode上,可參見我之前的部落格Longest Increasing Subsequence 最長遞增子序列,那道題寫的解法略微複雜,下面我們來看其他的一些解法。首先來看一種動態規劃Dynamic Programming的解法,這種解法的時間複雜度為O(n2
),類似brute force的解法,我們維護一個一維dp陣列,其中dp[i]表示以nums[i]為結尾的最長遞增子串的長度,對於每一個nums[i],我們從第一個數再搜尋到i,如果發現某個數小於nums[i],我們更新dp[i],更新方法為dp[i] = max(dp[i], dp[j] + 1),即比較當前dp[i]的值和那個小於num[i]的數的dp值加1的大小,我們就這樣不斷的更新dp陣列,到最後dp陣列中最大的值就是我們要返回的LIS的長度,參見程式碼如下: 解法一:
class Solution {
public:
    int lengthOfLIS(vector<int
>& nums) { vector<int> dp(nums.size(), 1); int res = 0; for (int i = 0; i < nums.size(); ++i) { for (int j = 0; j < i; ++j) { if (nums[i] > nums[j]) { dp[i] = max(dp[i], dp[j] + 1); } } res = max(res, dp[i]); } return res; } };

下面我們來看一種優化時間複雜度到O(nlgn)的解法,這裡用到了二分查詢法,所以才能加快執行時間哇。思路是,我們先建立一個數組ends,把首元素放進去,然後比較之後的元素,如果遍歷到的新元素比ends陣列中的首元素小的話,替換首元素為此新元素,如果遍歷到的新元素比ends陣列中的末尾元素還大的話,將此新元素新增到ends陣列末尾(注意不覆蓋原末尾元素)。如果遍歷到的新元素比ends陣列首元素大,比尾元素小時,此時用二分查詢法找到第一個不小於此新元素的位置,覆蓋掉位置的原來的數字,以此類推直至遍歷完整個nums陣列,此時ends陣列的長度就是我們要求的LIS的長度,特別注意的是ends陣列的值可能不是一個真實的LIS,比如若輸入陣列nums為{4, 2, 4, 5, 3, 7},那麼算完後的ends陣列為{2, 3, 5, 7},可以發現它不是一個原陣列的LIS,只是長度相等而已,千萬要注意這點。參見程式碼如下:

解法二:

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        if (nums.empty()) return 0;
        vector<int> ends{nums[0]};
        for (auto a : nums) {
            if (a < ends[0]) ends[0] = a;
            else if (a > ends.back()) ends.push_back(a);
            else {
                int left = 0, right = ends.size();
                while (left < right) {
                    int mid = left + (right - left) / 2;
                    if (ends[mid] < a) left = mid + 1;
                    else right = mid;
                }
                ends[right] = a;
            }
        }
        return ends.size();
    }
};

我們來看一種思路更清晰的二分查詢法,跟上面那種方法很類似,思路是先建立一個空的dp陣列,然後開始遍歷原陣列,對於每一個遍歷到的數字,我們用二分查詢法在dp陣列找第一個不小於它的數字,如果這個數字不存在,那麼直接在dp陣列後面加上遍歷到的數字,如果存在,則將這個數字更新為當前遍歷到的數字,最後返回dp數字的長度即可,注意的是,跟上面的方法一樣,特別注意的是dp陣列的值可能不是一個真實的LIS。參見程式碼如下:

解法三:

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        vector<int> dp;
        for (int i = 0; i < nums.size(); ++i) {
            int left = 0, right = dp.size();
            while (left < right) {
                int mid = left + (right - left) / 2;
                if (dp[mid] < nums[i]) left = mid + 1;
                else right = mid;
            }
            if (right >= dp.size()) dp.push_back(nums[i]);
            else dp[right] = nums[i];
        }
        return dp.size();
    }
};

下面我們來看兩種比較tricky的解法,利用到了C++中STL的lower_bound函式,lower_bound返回陣列中第一個不小於指定值的元素,跟上面的演算法類似,我們還需要一個一維陣列v,然後對於遍歷到的nums中每一個元素,找其lower_bound,如果沒有lower_bound,說明新元素比一維陣列的尾元素還要大,直接新增到陣列v中,跟解法二的思路相同了。如果有lower_bound,說明新元素不是最大的,將其lower_bound替換為新元素,這個過程跟演算法二的二分查詢法的部分實現相同功能,最後也是返回陣列v的長度,注意陣列v也不一定是真實的LIS,參見程式碼如下:

解法四:

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        vector<int> v;
        for (auto a : nums) {
            auto it = lower_bound(v.begin(), v.end(), a);
            if (it == v.end()) v.push_back(a);
            else *it = a;
        }
       return v.size(); } };

既然能用lower_bound,那麼upper_bound就耐不住寂寞了,也要出來解個題。upper_bound是返回陣列中第一個大於指定值的元素,和lower_bound的區別時,它不能返回和指定值相等的元素,那麼當新進來的數和陣列中尾元素一樣大時,upper_bound無法返回這個元素,那麼按演算法三的處理方法是加到陣列中,這樣就不是嚴格的遞增子串了,所以我們要做個處理,在處理每個新進來的元素時,先判斷陣列v中有無此元素,有的話直接跳過,這樣就避免了相同數字的情況,參見程式碼如下:

解法五:

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        vector<int> v;
        for (auto a : nums) {
            if (find(v.begin(), v.end(), a) != v.end()) continue;
            auto it = upper_bound(v.begin(), v.end(), a);
            if (it == v.end()) v.push_back(a);
            else *it = a;
        }
        return v.size();
    }
};

還有一種稍微複雜點的方法,參見我的另一篇部落格Longest Increasing Subsequence 最長遞增子序列,那是LintCode上的題,但是有點不同的是,那道題讓求的LIS不是嚴格的遞增的,允許相同元素存在。

類似題目:

參考資料: