Alien Dictionary
単語のリストが、あなたには分からないアルファベット順でソートされています。つまり、英語の小文字26字が秘密の順序に並んでいます。単語は通常どおりに比較します。2つの単語で最初に異なる位置の文字で順序が決まり、一方の単語がもう一方の先頭部分と一致する場合は、短い単語が先になります。
単語に現れる文字を、アルファベット順に1つの文字列として返してください。複数の順序が当てはまる場合は、通常の辞書順で最初にくるものを返してください。当てはまる順序がない場合は、"invalid"を返してください。
関数
- wordsstring-array
- 未知のアルファベット順に並べ替えられた単語
- 戻り値string
- 条件に合う最小の順序の文字、または「無効」
制約
1 ≤ words.length ≤ 50001 ≤ words[i].length ≤ 10- すべての単語は小文字の英字のみを含みます。
- 同じ単語が複数回登場することがあります。
例
- 入力
- words = ["tea", "ten", "ate", "act", "cat"]
- 出力
- "etacn"
- 説明
teaはtenより前にあるので、a は n より前に置かれます。他の組み合わせからは、t が a より前、t が c より前、a が c より前であることが分かります。e について言及するルールはないため、最小の順序は e から始まります。つまり、e, t, a, c, n です。
- 入力
- words = ["bat", "tab", "tub", "bus"]
- 出力
- "invalid"
- 説明
batをtabより前に置くとbがtより前になり、tubをbusより前に置くとtがbより前になります。両方を同時に満たすことはできないため、当てはまる順序はありません。
- 入力
- words = ["cooking", "cook"]
- 出力
- "invalid"
- 説明
cookはcookingの始まりなので、どのアルファベット順でも先に来ます。リストでは2番目になっていますが、文字の並び順では説明できません。
発展問題
フィッティングの順序が唯一のものかどうか、どうすれば判断できますか?
ヒント
1つずつ開いてください。開くたびに少しずつ答えに近づきます。
teaとtenのような、隣り合う2つの単語を見てみましょう。これらからアルファベットについて何がわかり、何がまだわからないでしょうか?隣り合う単語のペアから得られる規則は最大1つです。単語が異なる最初の位置で、1つ目の単語の文字が2つ目の単語の文字より前に来るという規則です。これらの規則は文字を頂点とするグラフの辺であり、答えはすべての辺に従う順序です。異なる位置がないペアで、1つ目の単語のほうが長い場合に注意してください。
Kahnのアルゴリズムを使います。どの規則からも指し示されていない文字を配置し、その文字に関する規則を削除して、これを繰り返します。配置可能な文字は最小ヒープに保持し、常に最も小さい文字を配置します。配置されない文字があれば、規則に循環が含まれています。
解説
隣り合う単語の各ペアから、異なる最初の位置における規則が最大1つ得られます。つまり、文字 x が文字 y より前に来るという規則です。適切な順序とは、これらの規則のトポロジカル順序です。循環がある場合、または単語がそれ自身の接頭辞より前に置かれている場合、リストは不可能です。最小ヒープを使って、順序付け可能になった文字のうち最小のものを常に選べば、最小の順序が得られます。
文字のあらゆる順序を試してください
正しいが、最大のテストでは終わらない
考え方
k 個の異なる文字の並びを 1 つ選び、隣り合う各ペアを調べて確認します。最初に異なる位置では、最初の単語の文字が先に来なければなりません。違いがない場合は、最初の単語が長くなっていてはいけません。アルファベット順の並びから next permutation で順にたどり、条件を満たす最初の並びが、条件に合う最小の順序です。
k 個の文字には k! 通りの並びがあります。10 個では 3,628,800 通り、26 個では約 4 × 10^26 通りで、各テストでは最大 5 × 10^4 文字を読み取ります。条件に合うものがない場合、探索ではすべての並びを試します。
アルゴリズム
- 異なる文字を集め、アルファベット順に並べます。
- 現在の並び順における各文字の順位を記録します。
- 隣り合うすべての組を確認します。最初に異なる位置では、最初の単語の文字の順位がより小さくなければなりません。違いがない場合、最初の単語のほうが長くてはいけません。
- すべての組が条件を満たせば、その並び順を返します。そうでなければ、次に大きい並び順に進みます。
- 次の並び順がなければ、
"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"最小ヒープを使ったカーンのアルゴリズム
考え方
隣接する2つの単語では、最初に異なる文字だけが重要です。act と cat では、a が c より前にあることが示され、それ以降の文字が比較されることはありません。したがって、各ペアから得られる辺は最大1本です。
Kahnのアルゴリズムは、辺から順序を導きます。各文字の入次数を数え、入次数が0の文字を並べ、その文字から出る辺を削除して、これを繰り返します。サイクル上の文字には、サイクル上で直前の文字からの辺が常に残るため、並べられることはありません。登場する文字よりも並べられた文字が少なければ、答えは"invalid"です。
処理可能な文字を最小ヒープに入れ、最小のものを並べます。どのような有効な順序も、処理可能な文字から始まるため、これは最小の先頭文字です。また、それを並べることで辺が削除されるだけなので、ほかの文字を妨げることはありません。最初の例では、最初に e と t が処理可能で、e が先になります。通常のキューでも有効な順序は得られますが、常に最小の順序になるとは限りません。
アルゴリズム
- 単語に現れるすべての文字に印を付けます。
- 隣り合う各単語のペアについて、最初に異なる位置を見つけ、その2つの文字の間に辺を一度だけ追加します。違いがなく、最初の単語のほうが長い場合は、
"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)
落とし穴と境界ケース
- 1組の単語から複数の規則を取り出してしまうこと。
actとcatの2番目の位置を読むと、a の前に c が来ることになり、最初の例は誤った循環になります。 - 接頭辞の落とし穴を見落とすこと。
cookingはcookより前にありますが、異なる文字がないため、違いだけを処理するループは順序を返します。 - どの規則にも現れない文字を除外してしまうこと。最初の例では、どの規則にも e は出てきませんが、答えには含める必要があります。
- 重複する規則を入次数では2回数え、保存時には1回だけにすること。その文字の入次数は決して0にならず、有効なリストが循環として報告されます。
- 隣り合う2つの単語が同じである場合を接頭辞の落とし穴として扱ってしまうこと。同じ単語の繰り返しは順序どおりです。
よくある質問3
Alien Dictionary の時間計算量は何ですか?
O(C + k²)。C は文字数の合計、k ≤ 26 は異なる文字の数です。リストを1回走査すると、最初に異なる箇所をすべて見つけられます。また、Kahnのアルゴリズムは最大で k² 本の辺を調べます。辺のテーブルには O(k²) の領域が必要です。
なぜ隣り合う単語だけを比較するのでしょうか?
ソートされていることには推移性があります。各単語が次の単語以下であれば、リスト全体もソートされています。したがって、離れた2つの単語から読み取れる規則は、その間にある隣り合うペアからすでに導かれます。
なぜ単語がそれ自体の接頭辞より前にあるのは無効なのでしょうか?
すべてのアルファベット順において、ある単語はその単語自身の接頭辞より後に来ます。短いほうの単語の文字が尽きるまでに違いが見つからず、比較がそこで終わるからです。これは、規則の間に循環がなくてもリストが実現不可能になる唯一のケースです。
似た問題
同じ考え方を使う問題です。2〜3問解くとパターンが身につきます。
def alienOrder(words):
# ここにコードを書いてくださいテスト #1
テスト #2
テスト #3
引数
words = ["tea", "ten", "ate", "act", "cat"]
期待される出力
"etacn"