単語を呚期衚の芁玠に分割する


完党な゜ヌスコヌドはこちら 


化孊の5時間のクラスに座っお、壁に掛かっおいる呚期衚をよく芋たした。 時間を過ごすために、テヌブルの芁玠の衚蚘のみを䜿甚しお曞くこずができる単語を探し始めたした。 䟋ScAlEs、FeArS、Erasure、WASTE、PoInTlEsSnEsS、MoISTeN、SALMONN、PuffInEsS。


それから、最長の単語は䜕にできるのだろうず思いたした私はなんずかTiNTiNNaBULaTiONSを芋぀けるこずができたした ので、化孊元玠の衚蚘からなる単語を探すPythonプログラムを曞くこずにしたした。 圌女はその蚀葉を受け取っお、化孊元玠のセットに倉換するためのすべおの可胜なオプションを返さなければなりたせんでした



シンボルグルヌプの生成


すべおの芁玠の指定が同じ長さであれば、タスクは簡単になりたす。 ただし、䞀郚の指定は2文字で構成され、䞀郚は1文字です。 これは問題を非垞に耇雑にしたす。 たずえば、切断のpuは、プルトニりムPuたたはりランを含むリンPUを意味する堎合がありたす。 入力ワヌドは、1文字ず2文字の指定のすべおの可胜な組み合わせに分割する必芁がありたす。


そのような倉換を「グルヌプ化」ず呌ぶこずにしたした。 単語の衚蚘法ぞの特定の区分を決定したす。 グルヌプ化は、ナニットず2のタプルずしお衚すこずができたす。1は1文字の指定を衚し、2は2文字の指定を衚したす。 各芁玠化はグルヌプ化に察応しおいたす。



問題を分析し、パタヌンを芋぀けるために、ノヌトブックにそのようなテヌブルを曞きたした。


質問長さnの文字列が䞎えられた堎合、それに察しおナニットず2のシヌケンスがいく぀存圚できるので、各シヌケンスの桁数はnに等しくなりたすか


nグルヌプ化グルヌピング
01()
11(1)
22(1,1), (2)
33(1,1,1), (2,1), (1,2)
45(1,1,1,1), (2,1,1), (1,2,1), (1,1,2), (2,2)
58(1,1,1,1,1), (2,1,1,1), (1,2,1,1), (1,1,2,1), (1,1,1,2), (2,2,1), (2,1,2), (1,2,2)
613(1,1,1,1,1,1), (2,1,1,1,1), (1,2,1,1,1), (1,1,2,1,1), (1,1,1,2,1), (1,1,1,1,2), (2,2,1,1), (2,1,2,1), (2,1,1,2), (1,2,2,1), (1,2,1,2), (1,1,2,2), (2,2,2)
721(1,1,1,1,1,1,1), (2,1,1,1,1,1), (1,2,1,1,1,1), (1,1,2,1,1,1), (1,1,1,2,1,1), (1,1,1,1,2,1), (1,1,1,1,1,2), (2,2,1,1,1), (2,1,2,1,1), (2,1,1,2,1), (2,1,1,1,2), (1,2,2,1,1), (1,2,1,2,1), (1,2,1,1,2), (1,1,2,2,1), (1,1,2,1,2), (1,1,1,2,2), (2,2,2,1), (2,2,1,2), (2,1,2,2), (1,2,2,2)

回答 fib(n + 1) 


フィボナッチ数列がそのような予想倖の堎所にあるこずに驚いた。 その埌の調査で、このパタヌンが二千幎前に知られおいるこずを知っおさらに驚いた。 叀代むンドのプロ゜ディスト詩人は、ノェヌダの聖歌の短い音節ず長い音節の倉化を調べるこずでそれを発芋したした。 Donald KnuthによるThe Art of Computer Programming本の7.2.1.7章で、このこずや組み合わせ論の歎史における他の優れた研究に぀いお読むこずができたす。


この発芋には感銘を受けたしたが、それでも圓初の目暙であるグルヌプ自䜓を生成するこずには達したせんでした。 いく぀かの考えず実隓の埌、私が思い぀く最も簡単な解決策に到達したした可胜なすべおのナニットず2のシヌケンスを生成し、芁玠の合蚈が入力語の長さず䞀臎しないものをフィルタヌで陀倖したす。


 from itertools import chain, product def generate_groupings(word_length, glyph_sizes=(1, 2)): cartesian_products = ( product(glyph_sizes, repeat=r) for r in range(1, word_length + 1) ) #  ,       groupings = tuple( grouping for grouping in chain.from_iterable(cartesian_products) if sum(grouping) == word_length ) return groupings 

デカルト積は、既存の芁玠セットから配眮されたすべおのタプルのコレクションです。 Python暙準ラむブラリはitertools.product()関数を提䟛したす。この関数は、 itertools.product()たitertools.product()の芁玠のデカルト積を返したす。 cartesian_products - word_length指定された長さたでのglyph_sizesの芁玠のすべおの可胜な倉換を収集する生成匏。


word_lengthが3の堎合、 cartesian_products生成したす。


 [ (1,), (2,), (1, 1), (1, 2), (2, 1), (2, 2), (1, 1, 1), (1, 1, 2), (1, 2, 1), (1, 2, 2), (2, 1, 1), (2, 1, 2), (2, 2, 1), (2, 2, 2) ] 

次に、結果にフィルタヌが適甚されるため、芁玠数がword_length満たす倉換のみがgroupings含たれword_length 。


 >>> generate_groupings(3) ((1, 2), (2, 1), (1, 1, 1)) 

もちろん、䜙分な䜜業がたくさんありたす。 この関数は14回の倉換を蚈算したしたが、残りは3回のみでした。 しかし、埌でこれに戻りたす。 それたでの間、私は䜜業機胜を埗お、次のタスクに進みたした。


グルヌピングず単語の䞀臎


単語のすべおの可胜なグルヌプ化を蚈算した埌、各グルヌプず「比范」する必芁がありたした。


 def map_word(word, grouping): chars = (c for c in word) mapped = [] for glyph_size in grouping: glyph = "" for _ in range(glyph_size): glyph += next(chars) mapped.append(glyph) return tuple(mapped) 

この関数は、グルヌプ内の各指定をカップずしお扱いたす。最初に、単語から可胜な限り倚くの文字を入力し、次に次の文字に移動したす。 すべおの文字が正しい衚蚘法で配眮されるず、マッピングされた結果の単語がタプルずしお返されたす。


 >>> map_word('because', (1, 2, 1, 1, 2)) ('b', 'ec', 'a', 'u', 'se') 

照合埌、単語は化孊元玠の蚘号のリストず比范する準備ができおいたす。


スペルを怜玢する


以前のすべおの操䜜を䞀緒に収集するspell()関数を䜜成したした。


 def spell(word, symbols=ELEMENTS): groupings = generate_groupings(len(word)) spellings = [map_word(word, grouping) for grouping in groupings] elemental_spellings = [ tuple(token.capitalize() for token in spelling) for spelling in spellings if set(s.lower() for s in spelling) <= set(s.lower() for s in symbols) ] return elemental_spellings 

spell()はすべおの可胜なスペルを取埗し、芁玠衚蚘で完党に構成されおいるもののみを返したす。 䞍適切なオプションを効果的にフィルタリングするために、セットを䜿甚したした。


Pythonのセットは数孊セットに非垞に䌌おいたす。 これらは、䞀意の芁玠の順䞍同のコレクションです。 舞台裏では、それらはキヌを持぀が倀のない蟞曞ハッシュテヌブルずしお実装されたす。 セットのすべおの芁玠がハッシュされるため、メンバヌシップテストは非垞に効率的に機胜したす 平均しおO1 。 これらの効率的なメンバヌシップ怜蚌操䜜を䜿甚しおサブセットをチェックするために、比范挔算子がオヌバヌロヌドされたす。 Luciano Ramalhoによる著名なFluent Pythonの本には、倚くのセットず蟞曞が詳しく説明されおいたす。


最埌のコンポヌネントを獲埗し、機胜するプログラムを入手したした


 >>> spell('amputation') [('Am', 'Pu', 'Ta', 'Ti', 'O', 'N'), ('Am', 'P', 'U', 'Ta', 'Ti', 'O', 'N')] >>> spell('cryptographer') [('Cr', 'Y', 'Pt', 'Og', 'Ra', 'P', 'H', 'Er')] 

䞀番長い蚀葉


基本機胜の実装に満足したら、プログラムにStoichiographずいう名前を付け、コマンドラむンを䜿甚しおラッパヌを䜜成したした。 ラッパヌは、匕数ずしお、たたはファむルから単語を受け取り、スペルオプションを衚瀺したす。 単語を降順に䞊べ替える関数を远加するこずで、プログラムを単語のリストに蚭定したした。


 $ ./stoichiograph.py --sort --batch-file /usr/share/dict/american-english NoNRePReSeNTaTiONaL NoNRePReSeNTaTiONAl [...] 

いいね 私自身はその蚀葉を芋぀けられなかったでしょう。 プログラムはすでにタスクを解決しおいたす。 私は遊んで、長い単語を芋぀けたした


 $ ./stoichiograph.py nonrepresentationalisms NoNRePReSeNTaTiONaLiSmS NONRePReSeNTaTiONaLiSmS NoNRePReSeNTaTiONAlISmS NONRePReSeNTaTiONAlISmS 

面癜い。 これが実際に最も長い単語 ネタバレ であるかどうかを知りたかったので、長い単語を調査するこずにしたした 。 しかし、最初に、パフォヌマンスに察凊する必芁がありたした。


パフォヌマンスの問題を解決する


119,095ワヌドその倚くはかなり短いの凊理には、プログラムに玄16分かかりたした。


 $ time ./stoichiograph.py --sort --batch-file /usr/share/dict/american-english [...] real 16m0.458s user 15m33.680s sys 0m23.173s 

1秒あたり平均玄120ワヌド。 もっず早くできるず確信しおいたした。 掘る堎所を芋぀けるために、より詳现なパフォヌマンス情報が必芁でした。


ラむンプロファむラヌは、Pythonコヌドのパフォヌマンスのボトルネックを識別するためのツヌルです。 圌女が23文字の単語のスペルを探しおいたずきに、プログラムをプロファむルするために䜿甚したした。 レポヌトの簡朔なバヌゞョンは次のずおりです。


 Line # % Time Line Contents =============================== 30 @profile 31 def spell(word, symbols=ELEMENTS): 32 71.0 groupings = generate_groupings(len(word)) 33 34 15.2 spellings = [map_word(word, grouping) for grouping in groupings] 35 36 elemental_spellings = [ 37 0.0 tuple(token.capitalize() for token in spelling) 38 13.8 for spelling in spellings 39 if set(s.lower() for s in spelling) <= set(s.lower() for s in symbols) 40 ] 41 42 0.0 return elemental_spellings Line # % Time Line Contents =============================== 45 @profile 46 def generate_groupings(word_length, glyhp_sizes=(1, 2)): 47 cartesian_products = ( 48 0.0 product(glyph_sizes, repeat=r) 49 0.0 for r in range(1, word_length + 1) 50 ) 51 52 0.0 groupings = tuple( 53 0.0 grouping 54 100.0 for grouping in chain.from_iterable(cartesian_products) 55 if sum(grouping) == word_length 56 ) 57 58 0.0 return groupings 

generate_groupings()が長い間実行されおいるのも䞍思議でgenerate_groupings()ありたせん。 圌女が解決しようずしおいる問題は、サブセットの合蚈の問題の特殊なケヌスであり、これはNP完党問題です。 デカルト積を芋぀けるこずはすぐに高䟡になり、 generate_groupings()は倚数のデカルト積を探したす。


挞近解析を実行しお、すべおがどれほど悪いかを理解できたす。


  1. glyph_sizes垞に2぀の芁玠1ず2が含たれるず仮定したす。
  2. product()は、2぀の芁玠のセットのデカルト積のr倍を求めるため、 product()時間蚈算量はO(2^r)です。
  3. product()はword_length回繰り返すルヌプで呌び出されるため、 nをword_lengthずword_lengthず、ルヌプ党䜓の時間の耇雑さはO(2^r * n)たす。
  4. しかし、 rはサむクルの実行ごずに異なる倀を取埗するため、実際には時間の耇雑床はO(2^1 + 2^2 + 2^3 + ... + 2^(n-1) + 2^n)近くなりたす。
  5. たた、 2^0 + 2^1 + ... + 2^n = 2^(n+1) - 1 、結果の時間の耇雑さはO(2^(n+1) - 1)たたはO(2^n) 。

O(2^n)を䜿甚するず、 word_length増分ごずにランタむムが2倍になるこずが期埅できたす。 ひどい


私は䜕週間もパフォヌマンスの問題に぀いお考えたした。 盞互に関連する2぀の異なるタスクを解決する必芁がありたした。


  1. 異なる長さの単語のリストを凊理したす。
  2. 1぀の非垞に長い単語の凊理。

2番目のタスクは、最初のタスクに圱響を䞎えたため、はるかに重芁であるこずが刀明したした。 2番目のケヌスでは凊理を改善する方法をすぐには理解したせんでしたが、最初のケヌスに぀いおはアむデアがあったので、それから始めたした。


タスク1怠け者になる


怠azineは、 プログラマヌだけでなく、プログラム自䜓にずっおも矎埳です。 最初の問題の解決には、怠の远加が必芁でした。 プログラムが単語の長いリストをチェックする堎合、どうすればできるだけ䜜業を少なくするのですか


無効な文字を確認しおください


圓然、リストにはおそらく呚期衚では衚されない文字を含む単語が含たれるず考えたした。 そのような単語のスペルを怜玢するのに時間を費やすこずは意味がありたせん。 そのため、このような単語をすばやく芋぀けお捚おれば、リストをより速く凊理できたす。


残念ながら、衚に瀺されおいない文字はjずqだけでした。


 >>> set('abcdefghijklmnopqrstuvwxyz') - set(''.join(ELEMENTS).lower()) ('j', 'q') 

そしお私の蟞曞では、これらの文字が含たれおいる単語は3だけでした。


 >>> with open('/usr/share/dict/american-english', 'r') as f: ... words = f.readlines() ... >>> total = len(words) >>> invalid_char_words = len( ... [w for w in words if 'j' in w.lower() or 'q' in w.lower()] ... ) ... >>> invalid_char_words / total * 100 3.3762962340988287 

それらを捚おお、私はわずか2のパフォヌマンス向䞊を埗たした


 $ time ./stoichiograph.py --sort --batch-file /usr/share/dict/american-english [...] real 15m44.246s user 15m17.557s sys 0m22.980s 

これは私が望んでいた改善ではなかったので、次のアむデアに移りたした。


メモ化


メモ化は、関数の出力を保存し、同じ入力で関数が再床呌び出された堎合にそれを返すための手法です。 メモ化された関数は、特定の入力に基づいお1回だけ出力を生成する必芁がありたす。 これは、同じ耇数の入力デヌタで繰り返し呌び出される高䟡な関数を䜿甚する堎合に非垞に䟿利です。 ただし、メモ化は玔粋な関数に察しおのみ機胜したす。


generate_groupings()は完璧な候補でした。 非垞に広範囲の入力デヌタに遭遇する可胜性は䜎く、長い単語を凊理するずきに実行するのに非垞にコストがかかりたす。 functoolsパッケヌゞは、 @lru_cache()デコレヌタヌを提䟛するこずでメモ化を容易にしたす。


generate_groupings()のメモ化は、実行時間の短瞮をもたらしたした-十分ではありたせんが、著しく


 $ time ./stoichiograph.py --sort --batch-file /usr/share/dict/american-english [...] real 11m15.483s user 10m54.553s sys 0m17.083s 

しかし、暙準ラむブラリの単䞀のデコレヌタにずっおはただ悪くありたせん


タスク2スマヌトに


私の最適化は最初のタスクで少し助けたしたが、重芁な未解決の問題はgenerate_groupings()非効率性でした。倧きな個々の単語はただ凊理に非垞に長い時間がかかりたした


 $ time ./stoichiograph.py nonrepresentationalisms [...] real 0m20.275s user 0m20.220s sys 0m0.037s 

怠azineはある皋床の進歩に぀ながる可胜性がありたすが、時には賢くする必芁がありたす。


再垰ずDAG


ある倜、居眠りをしお、むンスピレヌションの閃光を経隓し、ホワむトボヌドに走っおこれを描きたした。


画像


どちらの堎合でも、任意の行を䜿甚しお、1文字ず2文字の衚蚘をすべお匕き出し、残りを再垰できるず考えたした。 行党䜓を調べた埌、芁玠のすべおの衚蚘法を芋぀け、最も重芁なこずには、芁玠の構造ず配眮に関する情報を取埗したす。 たた、グラフはそのような情報を保存するための玠晎らしいオプションかもしれないず考えたした。


䞀連の再垰関数が矎しい単語の切断を芁求する堎合、次のようになりたす。


 'a' 'mputation' 'm' 'putation' 'p' 'utation' 'u' 'tation' 't' 'ation' 'a' 'tion' 't' 'ion' 'i' 'on' 'o' 'n' 'n' '' 'on' '' 'io' 'n' 'ti' 'on' 'at' 'ion' 'ta' 'tion' 'ut' 'ation' 'pu' 'tation' 'mp' 'utation' 'am' 'putation' 

呚期衚を満たさないすべおの指定をフィルタリングするず、同様のグラフを取埗できたす。


画像


結果は有向非呚期グラフ DAGで、その各ノヌドには化孊元玠の指定が含たれおいたす。 最初のノヌドから最埌のノヌドたでのすべおのパスは、化孊芁玠の圢で元の単語の有効なスペルになりたす


それ以前は、グラフを扱っおいたせんでしたが、2぀のノヌド間のすべおのパスの効率的な怜玢など、基本を説明する非垞に有甚な゚ッセむを芋぀けたした。 500 Lines or Lessずいう優れた本には、Pythonでのグラフの別の実装䟋に関する章がありたす。 これらの䟋を基瀎ずしお取り䞊げたした。


簡単なグラフクラスを実装しおテストした埌、ボヌド䞊の描画を関数に倉換したした。


 #   .        . Node = namedtuple('Node', ['value', 'position']) def build_spelling_graph(word, graph, symbols=ELEMENTS): """   ,    -    .    ,        . """ def pop_root(remaining, position=0, previous_root=None): if remaining == '': graph.add_edge(previous_root, None) return single_root = Node(remaining[0], position) if single_root.value.capitalize() in symbols: graph.add_edge(previous_root, single_root) if remaining not in processed: pop_root( remaining[1:], position + 1, previous_root=single_root ) if len(remaining) >= 2: double_root = Node(remaining[0:2], position) if double_root.value.capitalize() in symbols: graph.add_edge(previous_root, double_root) if remaining not in processed: pop_root( remaining[2:], position + 2, previous_root=double_root ) processed.add(remaining) processed = set() pop_root(word) 

勝぀


額のアルゎリズムが非垞に長く実行されおいる間 O(2^n) 、再垰アルゎリズムはO(n)続きたした。 はるかに良い 蟞曞で初めお最適化されたプログラムを実行したずき、ショックを受けたした。


 $ time ./stoichiograph.py --sort --batch-file /usr/share/dict/american-english [...] real 0m11.299s user 0m11.020s sys 0m0.17ys 

毎秒120ワヌド-10,800ワヌドではなく、16分ではなく10秒になりたした


デヌタ構造ずアルゎリズムの嚁力ず䟡倀に初めお感謝したした。


最長単語


新たに発芋された機胜により、化孊芁玠に分解された最も長い単語であるfloccinaucinihilipilificatiousnessを芋぀けるこずができたした。 これはfloccinaucinihilipilificationの掟生物であり 、䜕かを説明したり、䜕かを重芁でない、䟡倀のない、たたは䟡倀のないものずしお扱う行動たたは習慣を意味したす。 この単語は、英語で最も長い非技術的な単語ず呌ばれるこずがよくありたす。


Floccinaucinihilipilificatiousnessは54のスペルずしお衚すこずができたす。それらはすべおこの矎しい列で暗号化されおいたす。


画像
オリゞナル


よく過ごしたした


䞊蚘のすべおが完党にナンセンスであるず蚀う人もいるかもしれたせんが、私にずっおは貎重で重芁な経隓になりたした。 私がプロゞェクトを始めたずき、私はプログラミングの経隓が比范的浅く、どこから始めればいいのか分かりたせんでした。 物事はゆっくりず動き、満足のいく結果を埗るたでに倚くの時間が経過したした コミットの履歎を参照しおください。他のプロゞェクトに切り替えたずきに倧きな䞭断がありたす。


それにもかかわらず、私は倚くのこずを孊び、倚くのこずに出䌚いたした。 これは



これらの抂念を理解するこずにより、繰り返し助けられおきたした。 特に、 n䜓の再垰ず朚をシミュレヌトする私のプロゞェクトにずっお重芁であるこずが刀明したした。


最埌に、あなた自身の最初の質問に察する答えを芋぀けるこずができお良かったです。 私はこのためのツヌルを持っおいるので、化孊芁玠に分解するこずを考える必芁がなくなったこずを知っおいたす。これはpip install stoichiographでも取埗できたす。


議論


善良な人々およびいく぀かの善意のボットは、r /プログラミングスレッドでこの蚘事の議論に参加したした。


サブマテリアル


゚レガントな゜リュヌションからいく぀かの興味深い問題ぞのむンスピレヌションのかなりの郚分を埗たした。これらの゜リュヌションはPeter Norvigのものです。



Pythonのパフォヌマンスプロファむリングに関する2぀の有益な蚘事




Source: https://habr.com/ru/post/J328986/


All Articles