1. 程式人生 > >演算法設計與分析(十一)

演算法設計與分析(十一)

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;
  }
};