定義
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是什么意思。
假設有一幫人要形成一個集團,他們盡可能使集團人數最大,并且他們要在集團內按實力排位置。現在,他們一個個加入,進行友好的政治斗爭。
斗爭的規則很簡單:
-
Arr[i] = j
代表著第i
個人具有j
大小的實力,這種實力是他斗爭的籌碼。 - 最后形成的集團里,先來者不能管理后來者,他只能管理比他更早到的人。
- 每個人到來的人都知道,想要成立人數為
i
的集團,那么要找誰當這個集團的老大。這個用M
來記錄。M[i] = j
,形成人數為i
的集團,那么需要讓Arr[j]
來擔任集團長。 - 每個人經歷斗爭后都知道,將來選擇自己的下手,并使自己掌管的人達到最多,最好選擇誰。這個通過
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,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
}, [])
}