9

これは Scrabble スタイルの問題に分類できると思いますが、友人がイギリスのテレビのクイズ番組カウントダウンについて言及したことがきっかけでした。ショーのさまざまなラウンドでは、競技者はスクランブルされた一連の文字を提示され、可能な限り長い単語を考え出す必要があります. 友達が言っていたのは「RAEPKWAEN」でした。

かなり短い順序で、この問題を処理するために Python で何かを作成し、PyEnchant を使用して辞書検索を処理しましたが、実際にはそれほどうまくスケーリングできないことに気付きました。

これが私が現在持っているものです:

#!/usr/bin/python

from itertools import permutations
import enchant
from sys import argv

def find_longest(origin):
    s = enchant.Dict("en_US")
    for i in range(len(origin),0,-1):
        print "Checking against words of length %d" % i
        pool = permutations(origin,i)
        for comb in pool:
            word = ''.join(comb)
            if s.check(word):
                return word
    return ""

if (__name__)== '__main__':
    result = find_longest(argv[1])
    print result

ショーで使用されているような 9 文字の例では問題ありません。9 階乗 = 362,880 および 8 階乗 = 40,320 です。その規模では、考えられるすべての順列と語長をチェックする必要があるとしても、それほど多くはありません。

ただし、87,178,291,200 通りの組み合わせである 14 文字に達すると、14 文字の単語がすぐに見つかるかどうかは運次第です。

上記の単語の例では、私のマシンが "reawaken" を見つけるのに約 12 1/2 秒かかります。14 文字のスクランブルされた単語を使用すると、考えられる 14 文字の順列をすべてチェックするためだけに、23 日間の規模で話すことができます。

これを処理するより効率的な方法はありますか?

4

10 に答える 10

6

文字数を含む彼の答えからのJeroenCoupéのアイデアの実装:

from collections import defaultdict, Counter


def find_longest(origin, known_words):
    return iter_longest(origin, known_words).next()

def iter_longest(origin, known_words, min_length=1):
    origin_map = Counter(origin)
    for i in xrange(len(origin) + 1, min_length - 1, -1):
        for word in known_words[i]:
            if check_same_letters(origin_map, word):
               yield word

def check_same_letters(origin_map, word):
    new_map = Counter(word)
    return all(new_map[let] <= origin_map[let] for let in word)

def load_words_from(file_path):
    known_words =  defaultdict(list)
    with open(file_path) as f:
        for line in f:
            word = line.strip()
            known_words[len(word)].append(word)
    return known_words

if __name__ == '__main__':
    known_words = load_words_from('words_list.txt')
    origin = 'raepkwaen'
    big_origin = 'raepkwaenaqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnm'
    print find_longest(big_origin, known_words)
    print list(iter_longest(origin, known_words, 5))

出力(私の小さな58000語の辞書の場合):

counterrevolutionaries
['reawaken', 'awaken', 'enwrap', 'weaken', 'weaker', 'apnea', 'arena', 'awake',
 'aware', 'newer', 'paean', 'parka', 'pekan', 'prank', 'prawn', 'preen', 'renew',
 'waken', 'wreak']

ノート:

  • これは、最適化なしの単純な実装です。

  • words_list.txt--Linux上にある可能性があります/usr/share/dict/words

アップデート

単語を1回だけ検索する必要があり、単語が長さで並べ替えられた辞書がある場合、たとえば次のスクリプトを使用します。

with open('words_list.txt') as f:
    words = f.readlines()
with open('words_by_len.txt', 'w') as f:
    for word in sorted(words, key=lambda w: len(w), reverse=True):
        f.write(word)

完全なdictをメモリにロードせずに最長の単語を見つけることができます。

from collections import Counter
import sys


def check_same_letters(origin_map, word):
    new_map = Counter(word)
    return all(new_map[let] <= origin_map[let] for let in word)

def iter_longest_from_file(origin, file_path, min_length=1):
    origin_map = Counter(origin)
    origin_len = len(origin)
    with open(file_path) as f:
        for line in f:
            word = line.strip()
            if len(word) > origin_len:
                continue
            if len(word) < min_length:
                return
            if check_same_letters(origin_map, word):
                yield word

def find_longest_from_file(origin, file_path):
    return iter_longest_from_file(origin, file_path).next()

if __name__ == '__main__':
    origin = sys.argv[1] if len(sys.argv) > 1 else 'abcdefghijklmnopqrstuvwxyz'
    print find_longest_from_file(origin, 'words_by_len.txt')
于 2012-01-19T15:50:14.697 に答える
4

順列を避けたい。文字が両方の文字列 (元の文字列と辞書の文字列) に出現する回数を数えることができます。文字の頻度が同じでないすべての単語を辞書から除外します。

したがって、辞書から 1 つの単語をチェックするには、文字数を最大で MAX (26, n) 回カウントする必要があります。

于 2012-01-19T10:10:12.600 に答える
1

@marketの回答と同様の別のアプローチは、辞書内の各単語の「ビットマスク」を事前に計算することです。ワードに少なくとも1つのAが含まれている場合はビット0が設定され、少なくとも1つのBが含まれている場合はビット1が設定され、Zの場合はビット25までというように続きます。

文字の組み合わせで構成されている可能性のある辞書内のすべての単語を検索する場合は、文字のコレクションのビットマスクを作成することから始めます。wordBitmask & ~lettersBitMask次に、がゼロかどうかを確認することで、他の文字を使用するすべての単語を除外できます。これがゼロの場合、単語はコレクションで使用可能な文字のみを使用するため、有効である可能性があります。これがゼロ以外の場合、コレクションで使用できない文字を使用するため、許可されません。

このアプローチの利点は、ビット単位の演算が高速であることです。辞書にある単語の大部分は、指定されたコレクションにない17文字以上の文字の少なくとも1つを使用し、それらすべてを迅速に割引することができます。ただし、フィルターを通過する少数の単語については、まだ行う必要のあるチェックがもう1つあります。それでも、単語がコレクションに表示されるよりも頻繁に文字を使用していないことを確認する必要があります。たとえば、「weakener」という単語は3つの「e」があるため禁止する必要がありますが、RAEPKWAENの文字のコレクションには2つしかありません。単語の各文字がコレクションに表示されるため、ビット単位のアプローチだけではこの単語は除外されません。

于 2012-01-20T11:05:36.317 に答える
1
  1. 辞書をソート済み (単語) の単語ペアとして事前解析します。(例: giilnstu、言語学者)
  2. 辞書ファイルをソートします。

次に、特定の文字セットを検索する場合:

  1. 持っている文字を辞書でバイナリ検索し、最初に文字を並べ替えます。

これは、単語の長さごとに個別に行う必要があります。

range(len(letters), 0, -1)編集: ターゲットの単語の長さ ( )の並べ替えられた文字のすべての一意の組み合わせを検索していると言う必要があります。

于 2012-01-19T13:21:58.600 に答える
1

これは、私が以前取り組んだアナグラムの問題に似ています。素数を使用して各文字を表すことで解決しました。各単語の文字の積は数字を生成します。入力文字の特定のセットが作品を作るのに十分かどうかを判断するには、入力文字の積を確認したい数の積で割るだけです。残りがなければ、入力文字で十分です。以下に実装しました。出力は次のとおりです。

$ python longest.py rasdaddea aosddna raepkwaen
rasdaddea -->  sadder
aosddna -->  soda
raepkwaen -->  reawaken

アナグラムのケースの詳細と完全な説明については、http: //mostlyhighperformance.blogspot.com/2012/01/generating-anagrams-effective-and-easy.htmlを参照してください。

このアルゴリズムでは、辞書を作成するのに少し時間がかかります。その後、個々のチェックは、辞書内のすべての単語を 1 分割するのと同じくらい簡単です。文字がない場合に辞書の一部を閉じることに依存するより高速な方法があるかもしれませんが、入力文字が多数ある場合はパフォーマンスが低下する可能性があり、実際には辞書のどの部分も閉じることができません。

import sys


def nextprime(x):
    while True:
        x += 1
        for pot_fac in range(2,x):
            if x % pot_fac == 0:
                break
        else:
            return x

def prime_generator():
    '''Returns a generator that produces the next largest prime as
    compared to the one returned from this function the last time
    it was called. The first time it is called it will return 2.'''
    lastprime = 1
    while True:
        lastprime = nextprime(lastprime)
        yield lastprime


# Assign prime numbers to each lower case letter
gen = prime_generator()
primes = dict( [ (chr(x),gen.next()) for x in range(ord('a'),ord('z')+1) ] )


product = lambda x: reduce( lambda m,n: m*n, x, 1 )
make_key = lambda x: product( [ primes[y] for y in x ] )


try:
    words = open('words').readlines()
    words = [ ''.join( [ c for c in x.lower() \
                if ord('a') <= ord(c) <= ord('z') ] ) \
            for x in words ]
    for x in words:
        try:
            make_key(x)
        except:
            print x
            raise

except IOError:
    words = [ 'reawaken','awaken','enwrap','weaken','weaker', ]

words = dict( ( (make_key(x),x,) for x in words ) )


inputs = sys.argv[1:] if sys.argv[1:] else [ 'raepkwaen', ]
for input in inputs:
    input_key = make_key(input)
    results = [ words[x] for x in words if input_key % x == 0 ]

    result = reversed(sorted(results, key=len)).next()
    print input,'--> ',result
于 2012-01-19T16:44:17.090 に答える
1

あなたが質問をした直後に昨夜これを始めましたが、今までそれを磨くことができませんでした. これが私の解決策でした。これは基本的に修正されたトライであり、今日まで知りませんでした!

class Node(object):
    __slots__ = ('words', 'letter', 'child', 'sib')

    def __init__(self, letter, sib=None):
        self.words = []
        self.letter = letter
        self.child = None
        self.sib = sib

    def get_child(self, letter, create=False):
        child = self.child
        if not child or child.letter > letter:
            if create:
                self.child = Node(letter, child)
                return self.child
            return None
        return child.get_sibling(letter, create)

    def get_sibling(self, letter, create=False):
        node = self
        while node:
            if node.letter == letter:
                return node
            sib = node.sib
            if not sib or sib.letter > letter:
                if create:
                    node.sib = Node(letter, sib)
                    node = node.sib
                    return node
                return None
            node = sib
        return None

    def __repr__(self):
        return '<Node({}){}{}: {}>'.format(chr(self.letter), 'C' if self.child else '', 'S' if self.sib else '', self.words)

def add_word(root, word):
    word = word.lower().strip()
    letters = [ord(c) for c in sorted(word)]
    node = root
    for letter in letters:
        node = node.get_child(letter, True)
    node.words.append(word)

def find_max_word(root, word):
    word = word.lower().strip()
    letters = [ord(c) for c in sorted(word)]
    words = []
    def grab_words(root, letters):
        last = None
        for idx, letter in enumerate(letters):
            if letter == last: # prevents duplication
                continue
            node = root.get_child(letter)
            if node:
                words.extend(node.words)
                grab_words(node, letters[idx+1:])
            last = letter
    grab_words(root, letters)
    return words

root = Node(0)
with open('/path/to/dict/file', 'rt') as f:
    for word in f:
        add_word(root, word)

テスト:

>>> def nonrepeating_words():
...     return find_max_word(root, 'abcdefghijklmnopqrstuvwxyz')
... 
>>> sorted(nonrepeating_words(), key=len)[-10:]
['ambidextrously', 'troublemakings', 'dermatoglyphic', 'hydromagnetics', 'hydropneumatic', 'pyruvaldoxines', 'hyperabductions', 'uncopyrightable', 'dermatoglyphics', 'endolymphaticus']
>>> len(nonrepeating_words())
67590

私は、最も長い言葉である著作権のないものよりも、ダーマトグリフを好むと思います。パフォーマンスに関しては、約 500k 語の辞書 (ここから) を利用して、

>>> import timeit
>>> timeit.timeit(nonrepeating_words, number=100)
62.8912091255188
>>> 

つまり、繰り返し文字を含まない 6 万 7000 語をすべて見つけるのに、平均して 10 分の 6 秒 (私の i5-2500 では) かかります。

この実装とトライの大きな違い (これにより、一般的に DAWG からさらに遠ざかります) は次のとおりです。単語は、ソートされた文字に関連してトライに格納されます。したがって、'dog' という単語は 'god' と同じパス (dgo) に格納されます。2 番目のビットはfind_max_wordアルゴリズムです。このアルゴリズムは、継続的に先頭を切り落として検索を再実行することにより、考えられるすべての文字の組み合わせが確実にアクセスされるようにします。

ああ、そして笑いのために:

>>> sorted(tree.find_max_word('RAEPKWAEN'), key=len)[-5:]
['wakener', 'rewaken', 'reawake', 'reawaken', 'awakener']
于 2012-01-20T10:15:36.990 に答える
0
  1. 辞書からトライ(プレフィックスツリー)を作成します。あなたはそれをキャッシュしたいかもしれません。
  2. このトライを歩き、手紙の袋に合わない枝全体を取り除きます。

この時点で、トライは、文字のバッグから作成できる辞書内のすべての単語の表現です。

  1. 長い方をとってください:-)

編集:頂点が少ないDAGW(有向非巡回単語グラフ)を使用することもできます。まだ読んでいませんが、このウィキペディアの記事には、世界最速のスクラブルプログラムに関するリンクがあります。

于 2012-01-19T13:53:05.803 に答える
0

DAWG (Directed Acyclic Word Graph) の Mark Wutka は親切にもここでいくつかのパスカル コードを提供してくれました。

于 2012-01-19T22:00:33.053 に答える
0

10 文字を超える単語を探す場合は、10 文字を超える単語 (10 文字の単語はそれほど多くないと思います) を反復処理して、セットに必要な文字があるかどうかを確認してみてください。

問題は、最初に len(word) >= 10 個の単語をすべて見つけなければならないことです。

だから、私は何をしますか:辞書を読むとき、単語を2つのカテゴリに分けます:ショートとロング. 考えられるすべての順列を反復処理することで、ショートを処理できます。それを繰り返してロングを処理し、それらが可能であることを確認するよりも。

もちろん、両方のパスに対して可能な多くの最適化があります。

于 2012-01-19T14:06:22.203 に答える