算法: 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上测试文件与代码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容