线性时间中最长的公共子串


45

挑战在于编写代码以解决以下问题。

给定两个字符串A和B,您的代码应输出具有以下属性的A子字符串的开始和结束索引。

  • A的子字符串也应该与B的某些子字符串匹配。
  • 不再有满足第一个属性的A子字符串。

例如:

A = xxxappleyyyyyyy

B = zapplezzz

apple带有索引4 8(索引从1开始)的子字符串将是有效的输出。

功能性

您可以选择输入是本地目录中文件中的标准输入还是您自己的选择。文件格式将只是两个字符串,用换行符分隔。答案应该是完整的程序,而不仅仅是功能。

我最终想在从http://hgdownload.cse.ucsc.edu/goldenPath/hg38/chromosomes/的字符串中提取的两个子字符串上测试您的代码。

得分

这是带有扭曲的代码高尔夫球。您的代码必须及时运行O(n),其中n表示输入的总长度。

语言和图书馆

您可以使用具有免费编译器/解释器/等的任何语言。对于Linux。您应该只使用不是为解决此任务而设计的标准开源库。如有争议,我将其视为您的语言所标配的任何库,或者可以从默认存储库安装在默认ubuntu计算机中的任何库。

有用的信息

在线性时间内至少有两种方法可以解决此问题。一种是首先计算后缀树,第二种是首先计算后缀数组和LCP数组。


4
O(n) time您确定有可能吗?
Savenkov Alexey 2015年

17
@Lembik对不起,但是这些是非常复杂的算法,而且要遍历100多行代码并不是很有趣。
FUZxxl 2015年

4
您在“有用的信息”下提供的第二个链接上的文章说:“构造[后缀树]需要O(N ^ 2)时间”
KSFT 2015年

3
@Lembik您应该提出一个问题[最快的代码],在这种情况下,采用大哦表示法的最坏情况下的最佳程序将获胜。然后,您至少会得到一些答案,即使有人可以在O(n)中解决问题,他们也会赢。
mbomb007'7

9
这必须是每个有效答案中删除的答案最多的问题……
FlipTack

Answers:


39

Python 2,646字节

G=range;w=raw_input;z=L,m,h=[0]*3
s=w();t=len(s);s+='!%s#'%w();u=len(s);I=z*u
def f(s,n):
 def r(o):
    b=[[]for _ in s];c=[]
    for x in B[:N]:b[s[x+o]]+=x,
    map(c.extend,b);B[:N]=c
 M=N=n--~n/3;t=n%3%2;B=G(n+t);del B[::3];r(2);u=m=p=r(1)>r(0);N-=n/3
 for x in B*1:v=s[x:x+3];m+=u<v;u=v;B[x/3+x%3/2*N]=m
 A=1/M*z or f(B+z,M)+z;B=[x*3for x in A if x<N];J=I[r(0):n];C=G(n)
 for k in C:b=A[t]/N;a=i,j=A[t]%N*3-~b,B[p];q=p<N<(s[i:i-~b],J[i/3+b+N-b*N])>(s[j+t/M:j-~b],J[j/3+b*N]);C[k]=x=a[q];I[x]=k;p+=q;t+=1-q
 return C
S=f(map(ord,s)+z*40,u)
for i in G(u):
 h-=h>0;j=S[I[i]-1]
 while s[i+h]==s[j+h]:h+=1
 if(i<t)==(t<j)<=h>m:m=h;L=min(i,j)
print-~L,L+m

这使用了Kärkkäinen和Sanders的“简单线性工作后缀数组构造”中描述的偏斜算法。该论文中包含的C ++实现已经有点“古怪”了,但是仍然有足够的空间使其更短。例如,我们可以递归直到到达长度为1的数组,而不是像本文中那样短路而不违反O(n)要求。

对于LCP部分,我遵循了Kusai等人的“后缀数组中的线性时间最长公共前缀计算及其应用”。

1 0如果最长的公共子字符串为空,则程序输出。

这是一些开发代码,其中包括该程序的早期版本,该版本更紧密地遵循C ++实现,一些比较慢的方法以及一个简单的测试用例生成器:

from random import *

def brute(a,b):
    L=R=m=0

    for i in range(len(a)):
        for j in range(i+m+1,len(a)+1):
            if a[i:j] in b:
                m=j-i
                L,R=i,j

    return L+1,R

def suffix_array_slow(s):
    S=[]
    for i in range(len(s)):
        S+=[(s[i:],i)]
    S.sort()
    return [x[1] for x in S]

def slow1(a,b):
    # slow suffix array, slow lcp

    s=a+'!'+b
    S=suffix_array_slow(s)

    L=R=m=0

    for i in range(1,len(S)):
        x=S[i-1]
        y=S[i]
        p=s[x:]+'#'
        q=s[y:]+'$'
        h=0
        while p[h]==q[h]:
            h+=1
        if h>m and len(a)==sorted([x,y,len(a)])[1]:
            m=h
            L=min(x,y)
            R=L+h

    return L+1,R

def verify(a,b,L,R):
    if L<1 or R>len(a) or a[L-1:R] not in b:
        return 0
    LL,RR=brute(a,b)
    return R-L==RR-LL

def rand_string():
    if randint(0,1):
        n=randint(0,8)
    else:
        n=randint(0,24)
    a='zyxwvutsrq'[:randint(1,10)]
    s=''
    for _ in range(n):
        s+=choice(a)
    return s

def stress_test(f):
    numtrials=2000
    for trial in range(numtrials):
        a=rand_string()
        b=rand_string()
        L,R=f(a,b)
        if not verify(a,b,L,R):
            LL,RR=brute(a,b)
            print 'failed on',(a,b)
            print 'expected:',LL,RR
            print 'actual:',L,R
            return
    print 'ok'

def slow2(a,b):
    # slow suffix array, linear lcp

    s=a+'!'+b+'#'
    S=suffix_array_slow(s)

    I=S*1
    for i in range(len(S)):
        I[S[i]]=i

    L=R=m=h=0

    for i in range(len(S)):
        if I[i]:
            j=S[I[i]-1]
            while s[i+h]==s[j+h]:
                h+=1
            if h>m and len(a)==sorted([i,j,len(a)])[1]:
                m=h
                L=min(i,j)
                R=L+h
            h-=h>0

    return L+1,R

def suffix_array(s,K):
    # skew algorithm

    n=len(s)
    s+=[0]*3
    n0=(n+2)/3
    n1=(n+1)/3
    n2=n/3
    n02=n0+n2
    adj=n0-n1

    def radix_pass(a,o,n=n02):
        c=[0]*(K+3)
        for x in a[:n]:
            c[s[x+o]+1]+=1
        for i in range(K+3):
            c[i]+=c[i-1]
        for x in a[:n]:
            j=s[x+o]
            a[c[j]]=x
            c[j]+=1

    A=[x for x in range(n+adj) if x%3]+[0]*3

    radix_pass(A,2)
    radix_pass(A,1)
    radix_pass(A,0)

    B=[0]*n02
    t=m=0

    for x in A[:n02]:
        u=s[x:x+3]
        m+=t<u
        t=u
        B[x/3+x%3/2*n0]=m

    A[:n02]=1/n02*[0]or suffix_array(B,m)
    I=A*1
    for i in range(n02):
        I[A[i]]=i+1

    B=[3*x for x in A if x<n0]
    radix_pass(B,0,n0)

    R=[]

    p=0
    t=adj
    while t<n02:
        x=A[t]
        b=x>=n0
        i=(x-b*n0)*3-~b
        j=B[p]
        if p==n0 or ((s[i:i+2],I[A[t]-n0+1])<(s[j:j+2],I[j/3+n0]) if b else (s[i],I[A[t]+n0])<(s[j],I[j/3])):R+=i,;t+=1
        else:R+=j,;p+=1

    return R+B[p:n0]

def solve(a,b):
    # linear

    s=a+'!'+b+'#'
    S=suffix_array(map(ord,s),128)

    I=S*1
    for i in range(len(S)):
        I[S[i]]=i

    L=R=m=h=0

    for i in range(len(S)):
        if I[i]:
            j=S[I[i]-1]
            while s[i+h]==s[j+h]:
                h+=1
            if h>m and len(a)==sorted([i,j,len(a)])[1]:
                m=h
                L=min(i,j)
                R=L+h
            h-=h>0

    return L+1,R

stress_test(solve)

1
如果我写错了,请纠正我,但这实际上不是739个字节吗?我复制到mothereff.in/byte-counter并从第6-9行删除了2个空格,但是我不确定这是否正确。
帕特里克·罗伯茨

2
@PatrickRoberts这些是标签。
米奇·施瓦兹

2
好答案!您可能想看看GSACA,它是2016年以来的一种新颖的线性时间SACA。参考实现是246行充满评论(170条没有评论),似乎很容易打高尔夫。您会在github上找到它。
Christoph

1
@MitchSchwartz我目前正尝试使用noPMO,所以我现在无法强烈感受到情绪(可能是由于大脑化学物质失衡所致)。在快速阅读代码时,我的语法高尔夫球发觉到了这一点,而且我不记得有任何特定的情绪。您是否想到过同一件事或为什么要提出这个问题?:)现在我很好奇。
Yytsi '17

1
@TuukkaX这是我没想到的有趣响应。好吧,我不确定我是否应该以某种特殊的方式措辞,但是您的原始评论实际上并不正确这一事实在我决定提出问题时起到了一定作用。:)
米奇·施瓦茨
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.