CodingTour
ARTS #9

Algorithm

本周选择的算法题是: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(\(n^{2}\)) complexity.

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

Solution

我实现的方案:

解法一:dp

Runtime:1132 ms,快过 24.85%。

Memory:14.1 MB,低于 5.61%。

class Solution:

    def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums: return 0
        
        length = len(nums)
        dp = [1] * length
        for i in range(1, length):
            for j in range(i):
                if nums[i] > nums[j]:
                    dp[i] = max(dp[j] + 1, dp[i])

        return max(dp)

dp 的解法比较简单粗暴,时间耗时也长。

时间复杂度是 O(\(n^{2}​\)),该题最后有一个追加:用 O(n log n) 解决它。

Runtime:48 ms,快过 89.69%。

Memory:13.9 MB,低于 5.61%。

class Solution:

    def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums: return 0

        length = len(nums)
        dp = [None] * length
        dp_length = 0

        def binary_search(num: int, length: int) -> int:
            low, high = 0, length
            while low < high:
                middle = low + (high - low) // 2

                if dp[middle] == num:
                    return middle
                elif dp[middle] < num:
                    low = middle + 1
                else:
                    high = middle

            return low

        for num in nums:
            position = binary_search(num, dp_length)
            dp[position] = num
            dp_length += dp[dp_length] == num

        return dp_length

这个算法在 GeeksforGeeks 详细解释过,推导过程如下。

构建 active lists

active lists 记录了所有潜在的最长上升子序列,构建过程根据以下三个原则(i 表示当前正在遍历的索引):

  1. 如果 A[i] 小于所有 active lists 的最后一个元素,则开启一个新的长度为1的 active list
  2. 如果 A[i] 大于所有 active lists 的最后一个元素,则克隆最长的那个 active list,并追加 A[i]
  3. 如果 A[i] 处于两者之间,则找到小于 A[i] 的最长 active list,克隆并追加 A[i],然后将与这个新 active list 长度相同的其他 list 丢弃

以序列 [0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15] 为例,整个过程如下:

A[0] = 0. Case 1. There are no active lists, create one.
0.
-----------------------------------------------------------------------------
A[1] = 8. Case 2. Clone and extend.
0.
0, 8.
-----------------------------------------------------------------------------
A[2] = 4. Case 3. Clone, extend and discard.
0.
0, 4.
0, 8. Discarded
-----------------------------------------------------------------------------
A[3] = 12. Case 2. Clone and extend.
0.
0, 4.
0, 4, 12.
-----------------------------------------------------------------------------
A[4] = 2. Case 3. Clone, extend and discard.
0.
0, 2.
0, 4. Discarded.
0, 4, 12.
-----------------------------------------------------------------------------
A[5] = 10. Case 3. Clone, extend and discard.
0.
0, 2.
0, 2, 10.
0, 4, 12. Discarded.
-----------------------------------------------------------------------------
A[6] = 6. Case 3. Clone, extend and discard.
0.
0, 2.
0, 2, 6.
0, 2, 10. Discarded.
-----------------------------------------------------------------------------
A[7] = 14. Case 2. Clone and extend.
0.
0, 2.
0, 2, 6.
0, 2, 6, 14.
-----------------------------------------------------------------------------
A[8] = 1. Case 3. Clone, extend and discard.
0.
0, 1.
0, 2. Discarded.
0, 2, 6.
0, 2, 6, 14.
-----------------------------------------------------------------------------
A[9] = 9. Case 3. Clone, extend and discard.
0.
0, 1.
0, 2, 6.
0, 2, 6, 9.
0, 2, 6, 14. Discarded.
-----------------------------------------------------------------------------
A[10] = 5. Case 3. Clone, extend and discard.
0.
0, 1.
0, 1, 5.
0, 2, 6. Discarded.
0, 2, 6, 9.
-----------------------------------------------------------------------------
A[11] = 13. Case 2. Clone and extend.
0.
0, 1.
0, 1, 5.
0, 2, 6, 9.
0, 2, 6, 9, 13.
-----------------------------------------------------------------------------
A[12] = 3. Case 3. Clone, extend and discard.
0.
0, 1.
0, 1, 3.
0, 1, 5. Discarded.
0, 2, 6, 9.
0, 2, 6, 9, 13.
-----------------------------------------------------------------------------
A[13] = 11. Case 3. Clone, extend and discard.
0.
0, 1.
0, 1, 3.
0, 2, 6, 9.
0, 2, 6, 9, 11.
0, 2, 6, 9, 13. Discarded.
-----------------------------------------------------------------------------
A[14] = 7. Case 3. Clone, extend and discard.
0.
0, 1.
0, 1, 3.
0, 1, 3, 7.
0, 2, 6, 9. Discarded.
0, 2, 6, 9, 11.
----------------------------------------------------------------------------
A[15] = 15. Case 2. Clone and extend.
0.
0, 1.
0, 1, 3.
0, 1, 3, 7.
0, 2, 6, 9, 11.
0, 2, 6, 9, 11, 15. <-- LIS List
----------------------------------------------------------------------------

[0, 2, 6, 9, 11, 15] 就是最长上升子序列。

再次观察

我们有必要存储完整的 active lists 么?毕竟题目的要求是返回最长上升子序列的长度就够了。

我们用一个一维的辅助数组记录每个 list 最后一个元素即可。最终的问题其实是:

何时拓展或替换最后一个元素是安全的?

问题到这里就结束了。

我实现的 leftmost 不够简洁优雅,这里有一个更好的版本:

def CeilIndex(A, l, r, key): 
    while (r - l > 1): 
        m = l + (r - l)//2
        if (A[m] >= key): 
            r = m 
        else: 
            l = m 
    return r

Review

耐心排序,这是最长上升子序列背后的算法,来自于纸牌游戏 patience,其规则是:

  1. 初始时,没有任何牌组,所以第一张牌会开始一个新的牌组
  2. 后续的牌如果小于等于现有牌组的最后一个元素,就插入到第一个小于等于的牌组里
  3. 后续的牌如果大于现有牌组的最后一个元素,就创建一个新的牌组
  4. 发牌游戏结束;用多路归并算法得到最终结果

这里贴一个 python 的实现

from functools import total_ordering
from bisect import bisect_left
from heapq import merge
 
@total_ordering
class Pile(list):
    def __lt__(self, other): return self[-1] < other[-1]
    def __eq__(self, other): return self[-1] == other[-1]
 
def patience_sort(n):
    piles = []
    # sort into piles
    for x in n:
        new_pile = Pile([x])
        i = bisect_left(piles, new_pile)
        if i != len(piles):
            piles[i].append(x)
        else:
            piles.append(new_pile)
 
    # use a heap-based merge to merge piles efficiently
    n[:] = merge(*[reversed(pile) for pile in piles])
 
if __name__ == "__main__":
    a = [4, 65, 2,-31, 0, 99, 83, 782, 1]
    patience_sort(a)
    print(a)

Tip

本周学习到的一些内容:

  • Xcode 11 Beta (iOS 13)中不再返回 PHImageFileURLKey,作为替代方案,有几个选择:

    • 利用 PHContentEditingInput 获取:

      asset.requestContentEditingInput(with: nil, completionHandler: { (input, info) in
          if let input = input {
              print(input.fullSizeImageURL) // file:///xxx    
          }
      })
      
    • 利用 KVC 从 PHAsset 获取原始文件名:

      asset.value(forKey: "filename") // IMG_xxx.HEIC
      
    • 利用 PHAssetResource 获取原始文件名和文件路径:

      if let resource = PHAssetResource.assetResources(for: asset).first {
      	print(resource.originalFilename) // IMG_xxx.HEIC
      	print(resource.value(forKey: "_privateFileURL")) // file:///xxx    
      }
      
    • 还有一种 bad way,使用正则从 debugDescription 中提取

Share

本周分享:

The most important skill a programmer can learn 做了一个翻译,记录在这里:

传送门