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

  • 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(n 2),類似 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 不是嚴格的遞增的,允許相同元素存在。

 

Github 同步地址:

https://github.com/grandyang/leetcode/issues/300

 

類似題目:

Increasing Triplet Subsequence

Russian Doll Envelopes

Maximum Length of Pair Chain

Number of Longest Increasing Subsequence

Minimum ASCII Delete Sum for Two Strings

 

參考資料:

https://leetcode.com/problems/longest-increasing-subsequence/

https://leetcode.com/problems/longest-increasing-subsequence/discuss/74825/Short-Java-solution-using-DP-O(n-log-n)

https://leetcode.com/problems/longest-increasing-subsequence/discuss/74848/9-lines-C%2B%2B-code-with-O(NlogN)-complexity

https://leetcode.com/problems/longest-increasing-subsequence/discuss/74824/JavaPython-Binary-search-O(nlogn)-time-with-explanation

https://leetcode.com/problems/longest-increasing-subsequence/discuss/74989/C%2B%2B-Typical-DP-N2-solution-and-NLogN-solution-from-GeekForGeek

 

LeetCode All in One 題目講解匯總(持續更新中...)


免責聲明!

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



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