算法: Longest increasing subsequence (LIS)

定義

Longest increasing subsequence(lis)算法是為了找到一個數列里最長的遞增子串。

lis(array) -> longest increasing subseq of array

lis([1, 3, 4, 2, 6, 5, 7]) -> [1, 3, 4, 6, 7]
lis([5, 2, 1, 3, 4, 7, 8, 6, 9]) -> [2,3,4,7,8,9]

暴力解法

總體思路

function lis(arr) {
  findAllSubseq(arr)   // 找到所有的子串
  .filter((subseq) => checkMonotonic(subseq))    // 過濾所有不單調遞增的子串
  .reduce((ret, cur) => { //找到最長的單調遞增子串
    cur.length > ret.length ? ret = cur: ret
    return ret
  })
}

找到所有的子串
對于[1,2,3],其擁有的非空子串包括[1],[2],[3],[1,2],[1,3],[2,3],[1,2,3]共7個,對于一個長度為n的數列,它的非空子串個數為2^n-1

這是最簡單的排列組合,給定一個子串,對于數列中每一個數,它只有兩種可能,要么在子串中,要么不在子串中。

使用二進制數表示[1,2,3]的所有非空子串

binary presentation subseq
001 [1]
010 [2]
100 [3]
011 [1,2]
101 [1,3]
110 [2,3]
111 [1,2,3]

每一個子串都對應了一個數的二進制表示
利用上面的發現我們可以編寫找出所有子串的函數(空子串沒有影響)

function findAllSubseq(arr) {
  var len = arr.length
  var ret = []
  var i, j
  for (i = 1; i < Math.pow(2, len); i++) {
    var _temp = [];
    for (j = 0; j < len; j++) {
        if (i & (1 << j)) {
            _temp.push(arr[j]);
        }
    }
    ret.push(_temp)
  }
  return ret
}

驗證子串的單調遞增性
很容易實現,這里用map模擬了zip

function checkMonotonic(arr){
    return arr.map((val, idx, arr) => [val, arr[idx-1]])
            .every((val) => val[1] ? val[1] < val[0] : true)
}

LIS暴力破解

function lisDumb(arr) {
    return findAllSubseq(arr)
            .filter((val) => checkMonotonic(val))
            .reduce((ret, cur) => {
              cur.length > ret.length ? ret = cur: ret 
              return ret},[])
}

暴力破解的增長級別是O(2^n),效率低得可怕。

WIKI推薦解法

解決LIS問題,一般采用wiki推薦的方法,這個算法利用了二分查找算法和鏈表數據結構,最終的增長級別與快排相同,都是O(nlogn)

非專業解釋

wiki推薦的算法很像patience sorting,但是實現得更復雜。要理解wiki推薦的這個算法,關鍵是理解M和P是什么意思。

假設有一幫人要形成一個集團,他們盡可能使集團人數最大,并且他們要在集團內按實力排位置。現在,他們一個個加入,進行友好的政治斗爭。

斗爭的規則很簡單:

  1. Arr[i] = j 代表著第i個人具有j大小的實力,這種實力是他斗爭的籌碼。
  2. 最后形成的集團里,先來者不能管理后來者,他只能管理比他更早到的人。
  3. 每個人到來的人都知道,想要成立人數為i的集團,那么要找誰當這個集團的老大。這個用M來記錄。M[i] = j,形成人數為i的集團,那么需要讓Arr[j]來擔任集團長。
  4. 每個人經歷斗爭后都知道,將來選擇自己的下手,并使自己掌管的人達到最多,最好選擇誰。這個通過P來記錄,P[i] = j,第i個人將來擔任位置,那么他將選擇第j個來的人當他的下手。第j個人在第0~i-1號人里,能召集最多的人。
import search from './binarySearch.js'

export default (arr)=>{
    var M = []
    var P = arr.slice(0)
// 一個個到來,進行斗爭
    arr.forEach((val,idx,arr) => {
// 現在這里最大的集團人數為length,leader代表著最大集團的集團長
        let leader = M[M.length-1]
// 看看這里最大集團的集團長
        if (leader !== undefined) {
// 有,看看新來的人和這位集團長誰厲害
            if (val > arr[leader]) {
// 新來的人厲害,他直接選擇那位集團長成為他未來的最佳下手
// 他成為了更大集團的集團長
                M.push(idx)
                P[idx] = leader
                return 
            }
        } else {
// 沒有,這里沒有集團,新來的人自己建立一個集團
            M.push(idx)
            P[idx] = undefined
            return 
        }
// 不用放棄,還有機會
// 他用二分法,查看自己在所有集團(人數從1到length)的集團長中的實力排行
// 0 | arr[M[0]] | 1 | arr[M[1]] | 2 | arr[M[3]] | 3 | arr[M[3]] | ...
// 如果他最菜,那么返回0
// 如果他僅次于最大人數集團的集團長,那么返回length
// 如果他比M[i]厲害,卻比M[i+1]弱,那么返回i
// 這個算法的精妙之處在于arr[M[i]]序列是遞增的,所有可以使用二分查找
        let replaceIdx = search(M.reduce((pre,cur)=>{
            pre.push(arr[cur]) 
            return pre
        }, []), val)
// 找到了他的位置,那么他就要取代這個位置上的人了
// 因為他與M[replaceIdx] 的人一樣,可以管理同樣多的人,但是他比M[replaceIdx]的人弱
// 讓他來當這個人數集團的集團長,有利于后來的擴張
// 他讓replaceIdx-1人數的集團長做他未來的最佳下手
        P[idx] = P[M[replaceIdx]]
// 取代M[replaceIdx]上的人
        M[replaceIdx] = idx

        return 
    })
// 所有斗爭完畢,最大的集團有lisLength人數
    var lisLength = M.length
// 應該找Arr[leader]做集團長
    var leader = M[lisLength - 1]
// 每個人都找尋自己的最佳下手,直到最后一個沒有最佳下手的人
    for (; lisLength > -1; lisLength--) {
        M[lisLength] = leader
// 找尋最佳下手
        leader = P[leader]
    }
// 去除undefined
    return M.slice(1)
}

Patience Sorting

使用patience sorting來尋找lis是最容易的方法,無論是從理解,還是實現上看。

patience sorting的講解在Princeton的講解里被描述地很清楚。

詭異的思路

假設數列是一組牌,每次都抽取一張,按規則放置,最后形成幾組牌,每組牌都從大到小排列,組數對應這個數列lis的長度,每組第一張牌形成lis。
規則是:

  1. 每組牌都是從大到小排列,小的牌不能放在大的牌上。
  2. 放置牌時,從左至右查看,將牌放置能最左的能放置的組里,如果所有組都不能放置,那么這張牌在最右形成新的組。

假設數列是[1,3,5,0,4,2,7,6,8,9]

每次放置牌時,組的結果如下:
init: [ ]
0: [ [1] ] 序號0代表放置完第0張牌
1: [ [1], [3] ]
2: [ [1], [3], [5] ]
3: [ [1, 0], [3], [5]]
4: [ [1, 0], [3], [5, 4]]
5: [ [1, 0], [3, 2], [5, 4]]
6: [ [1, 0], [3, 2], [5, 4], [7]]
7: [ [1, 0], [3, 2], [5, 4], [7, 6]]
8: [ [1, 0], [3, 2], [5, 4], [7, 6], [8] ]
9: [ [1, 0], [3, 2], [5, 4], [7, 6], [8], [9]]

一共6組,代表該數列lis長度為6。
取每個組的第一個數形成[1,3,5,7,8,9],這是該數列的最長遞增子串。

實現patience sorting也非常簡單

patience sorting實現

// patience sorting
export default (arr) => {
    return arr.reduce((ret, cur, idx, arr) => {
        let lo = 0
        let hi = ret.length

        while(lo <= hi) {
            let mid = ((lo + hi) / 2) | 0
            let pile = ret[mid] || []
            if (cur >= pile[pile.length - 1]) {
                lo = mid + 1
            } else {
                hi = mid - 1
            }
        }

        let insertIdx = lo 

        if (insertIdx === ret.length) {
            ret.push([cur])
        } else {
            ret[insertIdx].push(cur)
        }

        return ret
    }, []) 
}

LIS實現

import pSort from './patienceSorting.js'

export default (arr) => {
    var piles = pSort(arr)
    return piles.reduce((ret, cur) => {
        ret.push(cur[0])
        return ret
    }, [])
}

最后

git上測試文件與代碼

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容