LCS(最长公共字序列)实现

LCS(Loggest common subsequence)

最长公共子序列的解法,可以说是老生常谈了。最近一段时间工作上在用到了该算法。打算记录下,同时也准备重启下博客之路,作下自我驱动。

 

何为LCS,举例来讲,假设 字符串 A={a,b,d,e,c}, B={b,f,g,e,c},那么AB的LCS即是 bec,因为是序列(sequence),所以它在两个字符串中必须是从前往后保持序列一致。

LCS并不是唯一的,一般应用场景下获取一个实例即可。

LCS经典的解法一般是时间复杂度为O(n^2)的动态规划(DP)算法,可参考下网易公开课-算法导论,老爷子的经典视频,这里不做讨论。

 

James W. Hunt和Thomas G. Szymansky 的论文"A Fast Algorithm for Computing Longest Common Subsequence"提出了一种下限为O(nlogn)的算法。

定理:设序列A长度为n,{A1A2A...AiA...An},序列B长度为m,{B1B2B...BjB...Bm},获取A中所有元素在B中的序号,形成一个新序列{C1C2C...Ck},,获取新序列的最长严格递增子序列(LIS),然后根据该序列的序号从B中还原,即对应为A、B的最长公共子序列。

举例来说,A={a,b,d,e,c},B={b,f,g,e,c,b},则

a对应在B的序号为空

b对应序号为{0,5}

d对应序号为空

e对应为{3}

c对应为{4}

根据以上结果生成的新序列为{0,5,3,4},其最长严格递增子序列为{0,3,4},我们根据这个序号,从B序列中找出对应字符, 对应的公共子序列为{b, e, c}。

 

具体的证明过程可以Google原论文查看,根据以上的过程我们其实可以总结它具体的流程。

1)定义一个结构,记录B的字符的序号和值,然后排序(我们例子是可以肉眼看出,实际过程需要排序,以便二分查找),得到一个C

2)遍历A序列,从C中二分查找得到一个新序列D

3)对D做LIS计算,得到最长严格递增子序列。

4)根据此序列,从B中还原出LCS

 

那么我们按照这个步骤来实现代码。我们用Golang实现。

第一步 

type comparedValPos struct {
    val rune //原始值
    pos int //原始位置
}

type comparedValPosSlice []comparedValPos

func (b comparedValPosSlice) Len() int { return len(b) }
func (b comparedValPosSlice) Less(i, j int) bool {
    if b[i].val < b[j].val {
        return true
    } else if b[i].val > b[j].val {
        return false
    } else {
        return !(b[i].pos < b[j].pos)
    }
}
func (b comparedValPosSlice) Swap(i, j int) { b[i], b[j] = b[j], b[i] }

//遍历原始字符串并排序 func lcsSort(target
string) comparedValPosSlice { var slice comparedValPosSlice runeSlice := []rune(target) for i := 0; i < len(runeSlice); i++ { var l comparedValPos l.pos = i l.val = runeSlice[i] slice = append(slice, l) } sort.Sort(slice) return slice

 

第二步,二分查找并得到新序列,我们需要新序列中的值的原始的位置,代码如下

//需要记录原始位置
type lcsTargetPosInfo struct {
    slicePos int
    rawPos   int
}
//二分查找 由于字符可能有重复 返回具体匹配的长度
func findMatchList(ch rune, slice comparedValPosSlice, left int, right int, start *int) (matchLen int) {
    var middle, matchedLen1, matchedLen2 int
    var start1, start2 int
    if left > right {
        matchLen = 0
        return
    } else if left == right {
        if ch == slice[left].val {
            *start = left
            matchLen = 1
            return
        }
        matchLen = 0
        return
    }

    middle = (left + right) >> 1
    if slice[middle].val < ch {
        matchedLen1 = findMatchList(ch, slice, middle+1, right, &start1)
    } else if slice[middle].val > ch {
        matchedLen1 = findMatchList(ch, slice, left, middle-1, &start1)
    } else {
        matchedLen1 = findMatchList(ch, slice, left, middle-1, &start1)
        matchedLen2 = findMatchList(ch, slice, middle+1, right, &start2) + 1
        if matchedLen1 == 0 {
            start1 = middle
        }
        matchedLen1 += matchedLen2
    }
    *start = start1
    matchLen = matchedLen1
    return
}
//在前面生成的comparedValPosSlice的对rawStr进行二分查找
//并生成新序列
func matchListLcs(rawStr string, slice comparedValPosSlice) (matchedSlice []lcsTargetPosInfo) {
    var start int
    runeSlice := []rune(rawStr)
        //遍历字符串 并进行二分查找
    for i := 0; i < len(runeSlice); i++ {
        matchLen := findMatchList(runeSlice[i], slice, 0, len(slice)-1, &start)
               //获取到该字符匹配的个数 位置信息全部记录下来
        for k := 0; k < matchLen; k++ {
            var l lcsTargetPosInfo
            l.slicePos = slice[start+k].pos
            l.rawPos = i
            matchedSlice = append(matchedSlice, l)
        }
    }
    return
}

 

第三步 对新序列做lis匹配 最长严格递增子序列 可以参考 最长递增子序列nlogn算法

func findPos(l []int, currLen int, value int) int {
	left := 0
	right := currLen - 1
	middle := 0
	for left <= right {
		middle = (left + right) >> 1
		if l[middle] < value {
			left = middle + 1
		} else {
			right = middle - 1
		}
	}
	return left
}

func lis(slice []lcsTargetPosInfo, incSeq []int) int {
	if len(slice) == 0 {
		return 0
	}
	L := make([]int, len(slice))
	M := make([]int, len(slice))
	prev := make([]int, len(slice))
	L[0] = slice[0].slicePos
	M[0] = 0
	prev[0] = -1
	currLen := 1
	for i := 1; i < len(slice); i++ {
		pos := findPos(L, currLen, slice[i].slicePos)
		L[pos] = slice[i].slicePos
		M[pos] = i
		if pos > 0 {
			prev[i] = M[pos-1]
		} else {
			prev[i] = -1
		}
		if pos+1 > currLen {
			currLen++
		}
	}

	pos := M[currLen-1]
	for i := currLen - 1; i >= 0 && pos != -1; i-- {
		incSeq[i] = slice[pos].rawPos
		pos = prev[pos]
	}
	return currLen

}

  最后我们根据生成的lis来得到LCS

func Lcs(rawStr string, targetStr string) (int ,string) {
     slice :=  lcsSort(targetStr)
     return calcLcsUsingLis(rawStr,slice)
}

func calcLcsUsingLis(rawStr string, slice comparedValPosSlice) (int, string) {
    var maxIncreaseSequencerLen int
    if len(rawStr) > slice.Len() {
        maxIncreaseSequencerLen = slice.Len()

    } else {
        maxIncreaseSequencerLen = len(rawStr)
    }
    lcsPosSlice := matchListLcs(rawStr, slice)

        //LIS 保存的是字符位置
    increaseSequnceSlice := make([]int, maxIncreaseSequencerLen)
    ret := lis(lcsPosSlice, increaseSequnceSlice)
    var result []rune
    runeSlice := []rune(rawStr)

        //利用具体字符位置 得到最后的LCS
    for i := 0; i < ret; i++ {
        result = append(result, runeSlice[increaseSequnceSlice[i]])
    }

    return ret, string(result)
}

 全部代码在 github

posted @ 2018-09-25 22:35  又土口玉  阅读(678)  评论(0编辑  收藏  举报