11

プログラミングパールのセクション15.2から

C コードはここで見ることができます: http://www.cs.bell-labs.com/cm/cs/pearls/longdup.c

suffix-array を使用して Python で実装すると、次のようになります。

example = open("iliad10.txt").read()
def comlen(p, q):
    i = 0
    for x in zip(p, q):
        if x[0] == x[1]:
            i += 1
        else:
            break
    return i

suffix_list = []
example_len = len(example)
idx = list(range(example_len))
idx.sort(cmp = lambda a, b: cmp(example[a:], example[b:]))  #VERY VERY SLOW

max_len = -1
for i in range(example_len - 1):
    this_len = comlen(example[idx[i]:], example[idx[i+1]:])
    print this_len
    if this_len > max_len:
        max_len = this_len
        maxi = i

ステップが非常に遅いことがわかりましたidx.sort。Python は (上記の C コードのように) ポインターではなく値で部分文字列を渡す必要があるため、遅いと思います。

テストしたファイルはここからダウンロードできます

C コードは 0.3 秒で終了します。

time cat iliad10.txt |./longdup 
On this the rest of the Achaeans with one voice were for
respecting the priest and taking the ransom that he offered; but
not so Agamemnon, who spoke fiercely to him and sent him roughly
away. 

real    0m0.328s
user    0m0.291s
sys 0m0.006s

しかし、Python コードの場合、私のコンピューターでは決して終了しません (10 分間待って強制終了しました)。

コードを効率的にする方法を知っている人はいますか? (例: 10 秒未満)

4

4 に答える 4

15

私のソリューションはSuffix arrayに基づいています。Longest common prefixを2 倍にした Prefixによって構築されます。最悪の場合の複雑さは O(n (log n)^2) です。ファイル「iliad.mb.txt」は、ラップトップで 4 秒かかります。この関数は短く、簡単に変更できます。たとえば、重複しない最長の 10 個の部分文字列を検索する場合などです。この Python コードは 、重複する文字列が 10000 文字を超える場合、質問の元の C コードよりも高速です。longest_common_substring

from itertools import groupby
from operator import itemgetter

def longest_common_substring(text):
    """Get the longest common substrings and their positions.
    >>> longest_common_substring('banana')
    {'ana': [1, 3]}
    >>> text = "not so Agamemnon, who spoke fiercely to "
    >>> sorted(longest_common_substring(text).items())
    [(' s', [3, 21]), ('no', [0, 13]), ('o ', [5, 20, 38])]

    This function can be easy modified for any criteria, e.g. for searching ten
    longest non overlapping repeated substrings.
    """
    sa, rsa, lcp = suffix_array(text)
    maxlen = max(lcp)
    result = {}
    for i in range(1, len(text)):
        if lcp[i] == maxlen:
            j1, j2, h = sa[i - 1], sa[i], lcp[i]
            assert text[j1:j1 + h] == text[j2:j2 + h]
            substring = text[j1:j1 + h]
            if not substring in result:
                result[substring] = [j1]
            result[substring].append(j2)
    return dict((k, sorted(v)) for k, v in result.items())

def suffix_array(text, _step=16):
    """Analyze all common strings in the text.

    Short substrings of the length _step a are first pre-sorted. The are the 
    results repeatedly merged so that the garanteed number of compared
    characters bytes is doubled in every iteration until all substrings are
    sorted exactly.

    Arguments:
        text:  The text to be analyzed.
        _step: Is only for optimization and testing. It is the optimal length
               of substrings used for initial pre-sorting. The bigger value is
               faster if there is enough memory. Memory requirements are
               approximately (estimate for 32 bit Python 3.3):
                   len(text) * (29 + (_size + 20 if _size > 2 else 0)) + 1MB

    Return value:      (tuple)
      (sa, rsa, lcp)
        sa:  Suffix array                  for i in range(1, size):
               assert text[sa[i-1]:] < text[sa[i]:]
        rsa: Reverse suffix array          for i in range(size):
               assert rsa[sa[i]] == i
        lcp: Longest common prefix         for i in range(1, size):
               assert text[sa[i-1]:sa[i-1]+lcp[i]] == text[sa[i]:sa[i]+lcp[i]]
               if sa[i-1] + lcp[i] < len(text):
                   assert text[sa[i-1] + lcp[i]] < text[sa[i] + lcp[i]]
    >>> suffix_array(text='banana')
    ([5, 3, 1, 0, 4, 2], [3, 2, 5, 1, 4, 0], [0, 1, 3, 0, 0, 2])

    Explanation: 'a' < 'ana' < 'anana' < 'banana' < 'na' < 'nana'
    The Longest Common String is 'ana': lcp[2] == 3 == len('ana')
    It is between  tx[sa[1]:] == 'ana' < 'anana' == tx[sa[2]:]
    """
    tx = text
    size = len(tx)
    step = min(max(_step, 1), len(tx))
    sa = list(range(len(tx)))
    sa.sort(key=lambda i: tx[i:i + step])
    grpstart = size * [False] + [True]  # a boolean map for iteration speedup.
    # It helps to skip yet resolved values. The last value True is a sentinel.
    rsa = size * [None]
    stgrp, igrp = '', 0
    for i, pos in enumerate(sa):
        st = tx[pos:pos + step]
        if st != stgrp:
            grpstart[igrp] = (igrp < i - 1)
            stgrp = st
            igrp = i
        rsa[pos] = igrp
        sa[i] = pos
    grpstart[igrp] = (igrp < size - 1 or size == 0)
    while grpstart.index(True) < size:
        # assert step <= size
        nextgr = grpstart.index(True)
        while nextgr < size:
            igrp = nextgr
            nextgr = grpstart.index(True, igrp + 1)
            glist = []
            for ig in range(igrp, nextgr):
                pos = sa[ig]
                if rsa[pos] != igrp:
                    break
                newgr = rsa[pos + step] if pos + step < size else -1
                glist.append((newgr, pos))
            glist.sort()
            for ig, g in groupby(glist, key=itemgetter(0)):
                g = [x[1] for x in g]
                sa[igrp:igrp + len(g)] = g
                grpstart[igrp] = (len(g) > 1)
                for pos in g:
                    rsa[pos] = igrp
                igrp += len(g)
        step *= 2
    del grpstart
    # create LCP array
    lcp = size * [None]
    h = 0
    for i in range(size):
        if rsa[i] > 0:
            j = sa[rsa[i] - 1]
            while i != size - h and j != size - h and tx[i + h] == tx[j + h]:
                h += 1
            lcp[rsa[i]] = h
            if h > 0:
                h -= 1
    if size > 0:
        lcp[0] = 0
    return sa, rsa, lcp

Pythonには非常に高速なリストソートアルゴリズム(Timsort)があるため、より複雑なO(n log n)よりもこのソリューションを好みます。Python のソートは、その記事のメソッドで必要な線形時間操作よりもおそらく高速です。これは、ランダムな文字列と小さなアルファベット (DNA ゲノム分析に典型的) の非常に特別な仮定の下では O(n) である必要があります。Gog 2011で、私のアルゴリズムの最悪の場合の O(n log n) は、CPU メモリ キャッシュを使用できない多くの O(n) アルゴリズムよりも実際には高速になる可能性があることを読みました。

grow_chainsに基づく別の回答のコードは、テキストに 8 kB の長さの繰り返し文字列が含まれている場合、質問の元の例よりも 19 倍遅くなります。長く繰り返されるテキストは、古典文学では一般的ではありませんが、「独立した」学校の宿題コレクションなどでは頻繁に使用されます。プログラムがフリーズすることはありません。

Python 2.7、3.3 - 3.6 の同じコードで例とテストを作成しました。

于 2012-12-03T23:45:41.637 に答える
4

アルゴリズムの Python への変換:

from itertools import imap, izip, starmap, tee
from os.path   import commonprefix

def pairwise(iterable): # itertools recipe
    a, b = tee(iterable)
    next(b, None)
    return izip(a, b)

def longest_duplicate_small(data):
    suffixes = sorted(data[i:] for i in xrange(len(data))) # O(n*n) in memory
    return max(imap(commonprefix, pairwise(suffixes)), key=len)

buffer()コピーせずに部分文字列を取得できます:

def longest_duplicate_buffer(data):
    n = len(data)
    sa = sorted(xrange(n), key=lambda i: buffer(data, i)) # suffix array
    def lcp_item(i, j):  # find longest common prefix array item
        start = i
        while i < n and data[i] == data[i + j - start]:
            i += 1
        return i - start, start
    size, start = max(starmap(lcp_item, pairwise(sa)), key=lambda x: x[0])
    return data[start:start + size]

私のマシンでは、iliad.mb.txt.

原則として、 lcp 配列で拡張されたサフィックス配列を使用して、O(n) 時間と O(n) メモリで重複を見つけることができます。


注:バージョン*_memoryview()によって廃止されました*_buffer()

よりメモリ効率の高いバージョン (longest_duplicate_small() と比較して):

def cmp_memoryview(a, b):
    for x, y in izip(a, b):
        if x < y:
            return -1
        elif x > y:
            return 1
    return cmp(len(a), len(b))

def common_prefix_memoryview((a, b)):
    for i, (x, y) in enumerate(izip(a, b)):
        if x != y:
            return a[:i]
    return a if len(a) < len(b) else b

def longest_duplicate(data):
    mv = memoryview(data)
    suffixes = sorted((mv[i:] for i in xrange(len(mv))), cmp=cmp_memoryview)
    result = max(imap(common_prefix_memoryview, pairwise(suffixes)), key=len)
    return result.tobytes()

私のマシンでは、iliad.mb.txt. 結果は次のとおりです。

これについて、残りのアカイア人は声を一つにして敬意を表した
司祭と彼が提供した身代金を取った。しかし、アガメムノンはそうではありません。
彼は彼に激しく話しかけ、乱暴に彼を追い出しました。

Python 3 では比較によって例外が発生するか、Python 2 では間違った結果が生成されるmemoryviewため、オブジェクトを比較するカスタム関数を定義する必要がありました。memoryview

>>> s = b"abc"
>>> memoryview(s[0:]) > memoryview(s[1:])
True
>>> memoryview(s[0:]) < memoryview(s[1:])
True

関連する質問:

最長の繰り返し文字列と、特定の文字列で繰り返される回数を見つけます

大量の文字列で長く繰り返される部分文字列を見つける

于 2012-11-26T23:18:44.520 に答える
4

主な問題は、Python がコピーによるスライスを行うことのようです: https://stackoverflow.com/a/5722068/538551

コピーではなく参照を取得するには、代わりにメモリビューを使用する必要があります。私がこれを行ったとき、プログラムは関数のidx.sortにハングしました(これは非常に高速でした)。

少しの作業で、残りの作業を行うことができると確信しています。

編集:

上記の変更はcmp、 と同じようには機能しないため、ドロップイン置換としては機能しませんstrcmp。たとえば、次の C コードを試してください。

#include <stdio.h>
#include <string.h>

int main() {
    char* test1 = "ovided by The Internet Classics Archive";
    char* test2 = "rovided by The Internet Classics Archive.";
    printf("%d\n", strcmp(test1, test2));
}

結果をこの python と比較します。

test1 = "ovided by The Internet Classics Archive";
test2 = "rovided by The Internet Classics Archive."
print(cmp(test1, test2))

Cコード-3は私のマシンに印刷され、Pythonバージョンは印刷され-1ます。サンプルCコードはの戻り値を悪用しているようですstrcmp(結局、使用されqsortています)。strcmpが以外の値を返すタイミングに関するドキュメントは見つかりませんでしたが、元のコードに を[-1, 0, 1]追加するprintfpstrcmp、その範囲外の多くの値が表示されました (3、-31、5 は最初の 3 つの値でした)。

エラー コードではないことを確認するために-3、test1 と test2 を逆にすると、3.

編集:

上記は興味深いトリビアですが、コードのいずれかのチャンクに影響を与えるという点では、実際には正しくありません。ラップトップを閉じてwifiゾーンを離れたときにこれに気付きました... を押す前に、すべてを再確認する必要がありますSave

FWIW、cmp最も確実にmemoryviewオブジェクトで動作します(期待どおりに印刷-1されます):

print(cmp(memoryview(test1), memoryview(test2)))

コードが期待どおりに機能しない理由がわかりません。マシンでリストを印刷すると、期待どおりに表示されません。私はこれを調べて、ストローをつかむのではなく、より良い解決策を見つけようとします.

于 2012-11-26T07:30:28.267 に答える
0

このバージョンは、まったく異なるアルゴリズムを使用して、2007 年頃のデスクトップで約 17 秒かかります。

#!/usr/bin/env python

ex = open("iliad.mb.txt").read()

chains = dict()

# populate initial chains dictionary
for (a,b) in enumerate(zip(ex,ex[1:])) :
    s = ''.join(b)
    if s not in chains :
        chains[s] = list()

    chains[s].append(a)

def grow_chains(chains) :
    new_chains = dict()
    for (string,pos) in chains :
        offset = len(string)
        for p in pos :
            if p + offset >= len(ex) : break

            # add one more character
            s = string + ex[p + offset]

            if s not in new_chains :
                new_chains[s] = list()

            new_chains[s].append(p)
    return new_chains

# grow and filter, grow and filter
while len(chains) > 1 :
    print 'length of chains', len(chains)

    # remove chains that appear only once
    chains = [(i,chains[i]) for i in chains if len(chains[i]) > 1]

    print 'non-unique chains', len(chains)
    print [i[0] for i in chains[:3]]

    chains = grow_chains(chains)

基本的な考え方は、部分文字列とそれらが発生する位置のリストを作成することです。これにより、同じ文字列を何度も比較する必要がなくなります。結果のリストは次のようになります[('ind him, but', [466548, 739011]), (' bulwark bot', [428251, 428924]), (' his armour,', [121559, 124919, 193285, 393566, 413634, 718953, 760088])]。一意の文字列は削除されます。次に、すべてのリスト メンバーが 1 文字ずつ大きくなり、新しいリストが作成されます。一意の文字列が再び削除されます。などなど…。

于 2012-11-26T10:05:04.950 に答える