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