• 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

  • 相关阅读:
    Failed to load resource: the server responded with a status of 413 (Request Entity Too Large)
    钱能解决的都是小事——北漂18年(78)
    mysql 排序
    innodb和myisam表排序
    perl 内部字节字符转换
    encode_json 转换给定的perl数据结构为一个UTF-8编码的 2进制字符串 decode_json把UTF-8字节转换成字符
    perl /g
    perl 循环截取字符串
    eclipse报错:Compilation unit name must end with .java, or one of the registered Java-like exten
    用 Flask 来写个轻博客 (30) — 使用 Flask-Admin 增强文章管理功能
  • 原文地址:https://www.cnblogs.com/golangguo/p/9440193.html
Copyright © 2020-2023  润新知