Alien Dictionary
単語のリストが、あなたの知らないアルファベット順で並んでいます。小文字の英字26文字が、秘密の順序で並んでいます。単語は通常の方法で比較します。2つの単語が異なる最初の位置で、アルファベット順にどちらの文字が先に来るかによって順序が決まり、一方の単語がもう一方の単語の先頭部分と一致する場合は、短い単語が先になります。
単語に現れる文字を、アルファベット順に並べた1つの文字列として返してください。リストに当てはまる順序が複数ある場合は、通常の辞書順で最初に来るものを返してください。当てはまる順序がない場合は、"invalid"を返してください。
関数
- wordsstring-array
- 未知のアルファベット順に並べ替えられた単語
- 戻り値string
- 条件に合う最小の順序の文字、または「無効」
制約
1 ≤ words.length ≤ 50001 ≤ words[i].length ≤ 10- すべての単語は、小文字の英字のみで構成されています。
- 同じ単語が複数回出現する場合があります。 必須の出力形式: <translation> [翻訳したコンテンツ] </translation>
例
- 入力
- words = ["tea", "ten", "ate", "act", "cat"]
- 出力
- "etacn"
- 説明
teaとtenは最初に異なる文字がaとnなので、aはnより前に来ます。他のペアからは、tがaより前、tがcより前、aがcより前ということが分かります。eについて言及している規則はないため、最小の順序ではeを最初に置き、次にt、続いてa、そしてその時点でどちらも制約のないcとnを置きます。cを先にします。
- 入力
- words = ["bat", "tab", "tub", "bus"]
- 出力
- "invalid"
- 説明
batがtabより前にあると、bはtより前になり、tabがtubより前にあると、aはuより前になり、tubがbusより前にあると、tはbより前になります。bがtより前であり、同時にtがbより前であることはあり得ないため、どの順序も当てはまりません。
- 入力
- words = ["cooking", "cook"]
- 出力
- "invalid"
- 説明
cookはcookingの先頭部分なので、どのアルファベット順でも先に来ます。リストでは2番目になっていますが、文字の順序では説明できません。
提出時に隠しテスト+20件
発展問題
当てはめの順序が唯一のものかどうか、どのように判断しますか?
ヒント
1つずつ開いてください。開くたびに少しずつ答えに近づきます。
teaやtenのような、隣り合う2つの単語を見てみましょう。アルファベットについて何がわかり、何がまだわからないでしょうか?隣り合う単語のペアから得られる規則は最大で1つです。単語が異なる最初の位置で、最初の単語の文字が2つ目の単語の文字より前に来ます。この規則は文字を頂点とするグラフの辺であり、答えはすべての辺に従う順序です。異なる位置がないのに最初の単語のほうが長いペアに注意してください。
Kahnのアルゴリズムを使います。どの規則からも指されていない文字を配置し、その規則を削除して、これを繰り返します。配置可能な文字は最小ヒープに保持し、常に最小の文字を配置します。配置されない文字がある場合、規則に循環が含まれています。
解説
リストでは、隣り合う単語が最初に異なる位置に、アルファベットの順序が隠されています。それぞれの位置から、文字 x が文字 y より前に来るという規則が得られ、これらの規則は文字を頂点とする有向グラフを形成します。条件を満たす順序は、そのグラフのトポロジカル順序です。リストが成立しない原因は2つあります。規則の間に閉路があることと、単語がそれ自身の接頭辞より前に置かれていることです。各ステップで、最小ヒープを使って使用可能な最小の文字を選ぶと、条件を満たす順序のうち最小のものが得られます。
文字のあらゆる順序を試す
正しいが、最大のテストでは終わらない
考え方
答えは、k 個の異なる文字の並べ替えです。並べ替えを1つ直接テストできます。隣り合う単語のすべての組が、その並べ替えに従って順序どおりなら、リストはその並べ替えに適合します。2つの単語が異なる最初の位置を比較し、先の単語の文字が並べ替えの中でより前に来なければなりません。最後まで違いがなければ、先の単語のほうが長くてはいけません。隣り合う単語だけを見れば十分です。ソートされていることは連鎖するからです。各単語が次の単語以下なら、リスト全体がソートされています。
次に、並べ替えを小さい順にたどります。アルファベット順の文字から始めます。これはすべての並べ替えの中で最小のものです。毎回、次に大きいもの(次の順列)へ進みます。テストに合格する最初の並べ替えが、適合する最小の順序です。合格するものがなければ、"invalid"を返します。
これは正しい方法ですが、実際の入力では望みがありません。k 個の文字には k! 通りの並べ替えがあります。5文字なら120通り、10文字なら3,628,800通り、26文字すべてなら約4 × 10^26通りです。各テストではリスト全体を読み取り、文字数は合計 C 文字で、最大5 × 10^4です。大きなテストでは、最小の適合順序は f または z から始まるため、その前に天文学的な数の並べ替えがあります。また、適合するものがない場合、探索ではすべてを試さなければなりません。
アルゴリズム
- 異なる文字を集め、アルファベット順に並べます。
- 現在の並びにおける各文字の位置(順位)を記録します。
- 隣り合う各単語のペアを確認します。最初に異なる位置では、最初の単語の文字の順位が小さくなければなりません。異なる位置がない場合、最初の単語が長くてはなりません。
- すべてのペアが条件を満たせば、その並びを返します。そうでなければ、次に大きい並びに進みます。
- 次の並びがない場合は、
"invalid"を返します。
from itertools import permutations
def fits(words, rank):
# The list is sorted under rank when every neighbouring pair is in order.
for first, second in zip(words, words[1:]):
for x, y in zip(first, second):
if x != y:
if rank[x] > rank[y]:
return False
break
else:
# One word starts the other: the shorter must come first.
if len(first) > len(second):
return False
return True
def alienOrder(words):
letters = sorted(set("".join(words)))
# permutations() of a sorted list yields the orders from smallest to largest,
# so the first order that fits is the answer.
for order in permutations(letters):
rank = {ch: i for i, ch in enumerate(order)}
if fits(words, rank):
return "".join(order)
return "invalid"最小ヒープを用いたKahnのアルゴリズム
考え方
順序を推測するのではなく、リストから規則を読み取りましょう。隣り合う2つの単語を取り、異なる最初の位置を見つけます。teaとtenはtとeが一致し、aとnで異なるため、aはnより前に来ます。このペアが伝えるのはそれだけです。最初の不一致より後の文字は何も示しません。actはaがcより前に来るためcatより前になり、actの後に続くcとtがcatのaやtと比較されることはありません。つまり、それぞれのペアが与える規則は最大1つで、ある文字から別の文字へ向かう辺です。
異なる位置がないペアは、接頭辞の罠です。一方の単語がもう一方の先頭部分であり、どのアルファベットでも短い方が先に来なければなりません。cookがcookingより前なら問題なく、規則も生じません。cookingがcookより前なら、決してソートできないため、すぐに"invalid"を返します。異なる文字だけを調べるループでは、このペアから何も見つからず、どのアルファベットでも生成できないリストに対して順序を返してしまいます。
次に必要なのは、すべての辺の規則に従う文字の順序、つまりトポロジカル順序です。Kahnのアルゴリズムでこれを構築します。各文字を指す辺の数(入次数)を数え、数が0の文字を取り出し、その文字から出る辺を削除して、これを繰り返します。サイクル上の文字には、サイクルで直前にある文字からの辺が常に残るため、その数は0にならず、取り出されることもありません。単語に現れる文字の数より取り出せた文字の数が少なければ、サイクルがあるため、答えは"invalid"です。
最小の順序を得るには、数が0の文字を最小ヒープに入れ、常に最小の文字を取り出します。この貪欲な選択は安全です。条件に合う順序の最初の文字は入次数が0なので、取り出し可能な最小の文字が、最初に置ける文字の中で最小です。その文字を置くと辺が削除され、別の文字が取り出せなくなることはありません。すでに取り出し可能だった文字は、引き続き取り出し可能なままです。同じ論理が2番目の位置にも当てはまり、その後も同様です。最初の例では、eとtはどちらも最初に取り出し可能で、eが先になります。通常のキューでも有効な順序は得られますが、常に最小になるとは限りません。
計算量は、リストを1回走査して、合計C文字から最初の不一致を見つける分です。文字数をk ≤ 26とすると、辺は最大でk²個です。辺はk×kの表に保存するため、重複する規則は1回だけ記録され、ヒープに入る文字も最大k個です。したがって、計算時間はO(C + k²)で、最大規模のテストでも数ミリ秒です。
アルゴリズム
- 単語に現れるすべての文字に印を付けます。
- 隣り合う各単語の組について、最初に異なる位置を見つけます。異なる位置があれば、1つ目の単語の文字から2つ目の単語の文字への辺を1つ追加します。異なる位置がなく、1つ目の単語のほうが長い場合は、
"invalid"を返します。 - 各文字の入次数を数え、現れていて入次数が0の文字をすべて最小ヒープに追加します。
- 最小の文字を取り出して追加します。その文字が指す各文字の入次数を減らし、入次数が0になった文字を追加します。
- 配置した文字数が、現れる文字数より少ない場合は、
"invalid"を返します。それ以外の場合は、配置した文字を返します。
import heapq
def alienOrder(words):
# Letters are numbered 0 for 'a' up to 25 for 'z'.
present = [False] * 26
for word in words:
for ch in word:
present[ord(ch) - 97] = True
# before[a][b] is True once you know letter a comes before letter b.
before = [[False] * 26 for _ in range(26)]
indegree = [0] * 26
for first, second in zip(words, words[1:]):
for x, y in zip(first, second):
if x != y:
# Only the first difference tells you anything.
a, b = ord(x) - 97, ord(y) - 97
if not before[a][b]:
before[a][b] = True
indegree[b] += 1
break
else:
# No difference: one word starts the other, so the shorter must come first.
if len(first) > len(second):
return "invalid"
# A min-heap of the letters with nothing left before them.
heap = [c for c in range(26) if present[c] and indegree[c] == 0]
heapq.heapify(heap)
order = []
while heap:
c = heapq.heappop(heap)
order.append(chr(c + 97))
for nxt in range(26):
if before[c][nxt]:
indegree[nxt] -= 1
if indegree[nxt] == 0:
heapq.heappush(heap, nxt)
# A letter on a cycle never gets down to indegree 0, so it is never placed.
if len(order) < sum(present):
return "invalid"
return "".join(order)
落とし穴と境界ケース
ここでの誤答のほとんどは、気づかれないままです。ルールを読み違えても、順序自体は出力されますが、それが正しくないだけです。
- ペアから複数のルールを取り出す。数えるのは、最初に異なる位置だけです。
actがcatより前にあることから分かるのは、a が c より前ということだけで、その後の文字については何も分かりません。 - 接頭辞の落とし穴を見落とす。
cookingはcookより前ですが、異なる文字がありません。そのため、違いだけを処理するループでは何も見つからず、順序を返してしまいます。答えは"invalid"です。 - どのルールにも登場しない文字を省く。最初の例では、どのルールにも e は登場しませんが、答えには含める必要があり、最小の順序では e が先頭になります。
- 最小ヒープの代わりに通常のキューを使う。キューを使った Kahn のアルゴリズムは有効な順序を返しますが、要件では最小の順序が求められています。
- 重複したルールを入次数では2回数え、グラフには1回だけ登録する。すると、その文字の入次数が0にならず、有効なリストがサイクルとして報告されます。各ルールを1回だけ登録するか、追加と削除を同じ回数行ってください。
- 隣り合う同じ単語を接頭辞の落とし穴として扱う。同じ単語が続く順序は正しく、より長い単語がその単語自身の接頭辞より前にある場合だけ不可能です。
よくある質問4
Alien Dictionary の時間計算量はどれくらいですか?
O(C + k²)。ここでCは単語の総文字数、k ≤ 26は異なる文字の数です。リストを1回走査すると、隣り合う各ペアの最初の相違点が見つかり、Kahnのアルゴリズムは最大でk²個の辺を調べます。最小ヒープによる計算量はO(k log k)で、ほかの処理と比べるとわずかです。辺のテーブルにはO(k²)の領域が必要です。
なぜ隣り合う単語だけを比較するのでしょうか?
整列されていることには推移性があります。すべての単語が次の単語以下であれば、リスト全体が整列されています。したがって、離れた2つの単語から読み取れる規則は、その間にある隣接する単語の組からすでに導かれます。すべての単語の組を比較しても情報は増えず、n-1回ではなくO(n²)回の比較が必要になります。
準備ができている文字の中から最も小さいものを選ぶと、なぜ順序が最小になるのでしょうか?
どの適合順序も、どのルールからも指されていない文字から始める必要があります。したがって、そのような文字のうち最小のものが、最初の文字として可能な最小の文字です。その文字を配置しても辺が取り除かれるだけなので、ほかの準備のできた文字はすべて引き続き使用できます。各位置でこの議論を繰り返すと、最小の順序を文字ごとに構築できます。min-heapを使えば、準備のできた最小の文字を O(log k) で取得できます。
なぜ、語の前にその語自身の接頭辞があると無効なのですか?
すべてのアルファベット順において、ある単語はその接頭辞の後に来ます。短い方の単語で文字が尽きるまでに違いが見つからず、比較が終わるためです。したがって、cooking が cook より前にあるのは、どのような文字の並びであっても順序に反しており、どんな規則でも修正できません。規則の間に循環がなくても、リストが成立し得ない唯一のケースです。
似た問題
同じ考え方を使う問題です。2〜3問解くとパターンが身につきます。
Python
def alienOrder(words):
# ここにコードを書いてくださいケース1
ケース2
ケース3
入力
words = ["tea", "ten", "ate", "act", "cat"]
期待値
"etacn"