Alien Dictionary
단어 목록이 당신이 모르는 알파벳 순서로 정렬되어 있습니다. 소문자 영어 알파벳 26개가 어떤 비밀 순서로 배열되어 있습니다. 단어는 일반적인 방식으로 비교합니다. 두 단어가 처음으로 다른 위치에서 알파벳상 어느 글자가 먼저 오는지에 따라 순서가 결정되며, 한 단어가 다른 단어의 시작 부분과 같으면 더 짧은 단어가 먼저 옵니다.
단어들에 나타나는 글자들을 알파벳 순서대로 하나의 문자열로 반환하세요. 목록에 맞는 순서가 여러 개라면 일반적인 사전 순서에서 가장 먼저 오는 순서를 반환하세요. 어떤 순서도 맞지 않으면 "invalid"를 반환하세요.
함수
- wordsstring-array
- 알 수 없는 알파벳 순으로 정렬된 단어
- 반환값string
- 조건에 맞는 가장 작은 순서의 문자들 또는 "invalid"
제약 조건
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의 시작이므로 어떤 알파벳에서도 먼저 옵니다. 그런데 목록에서는 두 번째에 있어, 어떤 글자 순서로도 이를 설명할 수 없습니다.
제출 시 숨은 테스트 +20개
후속 질문
맞춤 순서가 유일한지 어떻게 알 수 있을까요?
힌트
하나씩 열어 보세요. 열 때마다 조금 더 알려 줍니다.
tea와ten처럼 서로 이웃한 두 단어를 살펴보세요. 이 단어들은 알파벳에 대해 무엇을 알려 주며, 무엇을 미결 상태로 남겨 두나요?서로 이웃한 단어 쌍은 최대 하나의 규칙을 제공합니다. 두 단어가 다른 첫 번째 위치에서 첫 번째 단어의 문자가 두 번째 단어의 문자보다 앞에 온다는 규칙입니다. 이 규칙들은 문자들로 이루어진 그래프의 간선이며, 답은 모든 간선을 따르는 순서입니다. 서로 다른 위치가 하나도 없는데 첫 번째 단어가 더 긴 쌍이 있는지 확인하세요.
Kahn 알고리즘을 사용하세요. 자신을 가리키는 규칙이 없는 문자를 하나 배치하고, 해당 문자의 규칙을 제거한 다음 반복하세요. 배치할 준비가 된 문자는 최소 힙에 보관하고 항상 가장 작은 문자를 배치하세요. 배치되지 않은 문자가 남아 있다면 규칙에 사이클이 포함되어 있습니다.
풀이
이 목록은 이웃한 단어들이 처음으로 달라지는 위치에 알파벳 순서를 숨기고 있습니다. 이러한 각 위치는 문자 x가 문자 y보다 앞선다는 하나의 규칙을 만들며, 이 규칙들은 문자들을 정점으로 하는 방향 그래프를 이룹니다. 조건에 맞는 순서는 이 그래프의 위상 정렬입니다. 목록을 불가능하게 만드는 경우는 두 가지입니다. 규칙 사이에 사이클이 있는 경우와, 어떤 단어가 자기 자신의 접두사보다 앞에 놓인 경우입니다. 최소 힙을 사용해 매 단계에서 사용 가능한 가장 작은 문자를 배치하면 가장 작은 조건 충족 순서를 얻을 수 있습니다.
문자의 모든 순서를 시도해 보세요
정답이지만 가장 큰 테스트에서는 끝나지 않음
핵심 아이디어
정답은 서로 다른 k개의 문자를 배열한 것입니다. 배열 하나를 직접 검사할 수 있습니다. 이 배열에서 이웃한 모든 단어 쌍이 순서에 맞으면 목록은 그 배열에 들어맞습니다. 두 단어가 처음으로 서로 다른 위치에서 비교합니다. 첫 번째 단어의 문자는 배열에서 더 앞에 와야 합니다. 끝까지 서로 다르지 않다면 첫 번째 단어가 더 길어서는 안 됩니다. 각 단어가 다음 단어 이하이면 전체 목록이 정렬된 것이므로, 이웃한 단어만 확인하면 충분합니다.
이제 배열을 가장 작은 것부터 가장 큰 것까지 살펴봅니다. 알파벳순으로 정렬된 문자들, 즉 모든 배열 중 가장 작은 배열에서 시작하고, 매번 그다음으로 큰 배열(다음 순열)로 이동합니다. 검사를 통과하는 첫 번째 배열이 조건에 맞는 가장 작은 순서입니다. 통과하는 배열이 없으면 "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"최소 힙을 사용한 칸 알고리즘
핵심 아이디어
순서를 추측하지 말고 목록에서 규칙을 읽어 내세요. 이웃한 두 단어를 골라 처음으로 서로 다른 위치를 찾으세요. tea와 ten은 t와 e까지 같고 a와 n에서 다르므로, a가 n보다 앞에 옵니다. 이것이 그 쌍이 알려 주는 전부입니다. 처음으로 다른 위치 뒤의 글자들은 아무것도 알려 주지 않습니다. act는 a가 c보다 앞에 오기 때문에 cat보다 앞에 오며, act의 뒤따르는 c와 t는 cat의 a와 t와 비교하지 않습니다. 따라서 각 쌍은 많아야 하나의 규칙, 즉 한 글자에서 다른 글자로 향하는 간선을 줍니다.
서로 다른 위치가 없는 쌍은 접두사 함정입니다. 한 단어가 다른 단어의 시작 부분과 같으며, 어떤 알파벳에서든 짧은 단어가 먼저 와야 합니다. cook이 cooking보다 앞에 오는 것은 괜찮고 규칙도 만들지 않습니다. cooking이 cook보다 앞에 오면 정렬할 수 없으므로 즉시 "invalid"를 반환하세요. 서로 다른 글자만 찾는 루프는 이 쌍에서 아무것도 찾지 못한 채 계속 진행하여, 어떤 알파벳으로도 만들 수 없는 목록의 순서를 반환하게 됩니다.
이제 모든 간선을 따르는 글자 순서, 즉 위상 정렬이 필요합니다. 칸 알고리즘은 이를 만들어 냅니다. 각 글자를 가리키는 간선의 수(진입 차수)를 세고, 그 수가 0인 글자를 배치한 다음, 그 글자에서 나가는 간선을 제거하고 반복합니다. 사이클에 속한 글자는 사이클에서 바로 앞에 있는 글자로부터 오는 간선을 항상 유지하므로, 그 수는 절대 0이 되지 않아 배치되지 않습니다. 단어에 나타나는 글자보다 적은 수의 글자만 배치했다면 사이클이 있으므로 답은 "invalid"입니다.
가장 작은 순서를 얻으려면 수가 0인 글자들을 최소 힙에 넣고 항상 가장 작은 글자를 배치하세요. 이 탐욕적 선택은 안전합니다. 조건을 만족하는 순서의 첫 글자는 진입 차수가 0이어야 하므로, 준비된 글자 중 가장 작은 글자가 가능한 첫 글자 중 가장 작습니다. 그 글자를 배치하면 간선이 제거될 뿐 다른 글자가 막히지는 않습니다. 준비된 상태였던 모든 글자는 계속 준비된 상태를 유지합니다. 그러면 같은 논리를 두 번째 위치에도 적용할 수 있고, 이후에도 마찬가지입니다. 첫 번째 예에서는 e와 t가 모두 처음에 준비되어 있으므로 e가 먼저 옵니다. 일반 큐를 사용해도 유효한 순서를 얻을 수 있지만, 항상 가장 작은 순서가 되지는 않습니다.
비용은 목록을 한 번 순회하며 전체 C개의 문자를 살펴보고 첫 번째 차이를 찾는 것입니다. k ≤ 26개의 글자가 있을 때 간선은 최대 k²개이며, k×k 테이블에 저장하여 같은 규칙은 한 번만 저장하고, 힙에는 최대 k개의 글자만 들어갑니다. 따라서 시간 복잡도는 O(C + k²)이며, 가장 큰 테스트에서도 몇 밀리초면 됩니다.
알고리즘
- 단어에 나타나는 모든 문자를 표시합니다.
- 서로 이웃한 각 단어 쌍에서 처음으로 다른 위치를 찾습니다. 다른 위치가 있으면 첫 번째 단어의 문자에서 두 번째 단어의 문자로 이어지는 간선을 한 번 추가합니다. 다른 위치가 없고 첫 번째 단어가 더 길면
"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도 정답에 포함되어야 하며 가장 작은 순서에서는 e가 맨 앞에 옵니다.
- 최소 힙 대신 일반 큐를 사용하는 경우. 큐를 사용하는 Kahn 알고리즘은 유효한 순서를 반환하지만, 이 문제의 조건은 가장 작은 순서를 요구합니다.
- 진입 차수에는 중복 규칙을 두 번 세고 그래프에는 한 번만 저장하는 경우. 그러면 해당 글자의 진입 차수가 0이 되지 않아 유효한 목록이 사이클로 판정됩니다. 각 규칙을 한 번만 저장하거나, 같은 횟수만큼 추가하고 제거하세요.
- 서로 같은 두 단어가 이웃한 경우를 접두사 함정으로 처리하는 경우. 어떤 단어 뒤에 같은 단어가 오는 것은 순서에 어긋나지 않습니다. 더 긴 단어가 자기 자신의 접두사보다 앞에 오는 경우에만 불가능합니다.
자주 묻는 질문4
Alien Dictionary의 시간 복잡도는 무엇인가요?
O(C + k²)이며, 여기서 C는 단어의 총 문자 수이고 k ≤ 26은 서로 다른 문자의 수입니다. 목록을 한 번 순회하면 모든 인접 쌍의 첫 번째 차이를 찾을 수 있으며, Kahn 알고리즘은 최대 k²개의 간선을 방문합니다. 최소 힙은 O(k log k)를 더하지만, 나머지에 비하면 작습니다. 간선 테이블은 O(k²)의 공간을 차지합니다.
왜 인접한 단어만 비교하나요?
정렬 상태는 추이적입니다. 각 단어가 다음 단어보다 크지 않다면 전체 목록은 정렬되어 있습니다. 따라서 서로 멀리 떨어진 두 단어에서 읽어낼 수 있는 어떤 규칙이든 그 사이의 인접한 쌍들로부터 이미 따라옵니다. 모든 단어 쌍을 비교해도 새로운 정보는 얻을 수 없으며, n-1번 대신 O(n²)번의 비교가 필요합니다.
준비된 글자 중 가장 작은 것을 선택하면 왜 가장 작은 순서가 될까요?
모든 가능한 순서는 어떤 규칙에서도 가리키지 않는 글자로 시작해야 합니다. 따라서 그런 글자 중 가장 작은 글자가 가능한 가장 작은 첫 글자이며, 이 글자를 배치하면 간선만 제거되므로 다른 준비된 글자는 모두 계속 사용할 수 있습니다. 각 위치에서 이 논리를 반복하면 글자를 하나씩 선택해 가장 작은 순서를 만들 수 있습니다. 최소 힙을 사용하면 O(log k)에 준비된 글자 중 가장 작은 글자를 꺼낼 수 있습니다.
단어가 자신의 접두사보다 앞에 오는 것은 왜 유효하지 않을까요?
모든 알파벳에서 단어는 자신의 접두사 뒤에 옵니다. 비교할 때 차이를 찾기 전에 더 짧은 단어의 글자가 먼저 바닥나기 때문입니다. 따라서 cooking이 cook보다 앞에 오는 것은 글자가 무엇이든 순서가 맞지 않으며, 어떤 규칙으로도 이를 바로잡을 수 없습니다. 규칙들 사이에 순환이 없어도 목록이 불가능해지는 유일한 경우입니다.
비슷한 문제
같은 아이디어를 쓰는 문제입니다. 두세 개를 풀면 패턴이 몸에 익습니다.
Python
def alienOrder(words):
# 여기에 코드를 작성하세요케이스 1
케이스 2
케이스 3
입력
words = ["tea", "ten", "ate", "act", "cat"]
기대값
"etacn"