[PS] BOJ 8905 / 리트
문제 링크: https://www.acmicpc.net/problem/8905
재밌게 풀었던 백트래킹 문제입니다.
풀이
리트 변환 규칙
문제에서 언급한 리트 표기의 규칙은 다음과 같습니다.
- 각 알파벳을 리트로 나타낼 때, 최대 $K$개의 문자로 나타낼 수 있습니다.
- 한 문자를 리트로 바꾸는 방법은 한 가지입니다.
- d를 앞서
[)로 바꿨다면, 다른 d를|>로 바꾸는 것은 불가능합니다. 앞선 경우와 동일하게[)로 바꿔야만 합니다.
- d를 앞서
- 두 개 이상의 알파벳을 리트로 바꾼 결과가 같을 수도 있습니다.
- 알파벳과 리트가 서로 비슷해 보이지 않아도 상관없다.
주의해야 할 점은, 한 문자는 한 개의 리트에 대응한다는 점입니다. 이전에 변환했던 리트를 기억해, 같은 문자가 두 번 이상 나오는 경우 모두 같은 리트로 변환해야 합니다.
백트래킹 아이디어
먼저, 기존 문자열을 text, 리트 표기를 leet라고 부르겠습니다.
text의 각 문자는 leet 내에서 길이 $1 \cdots K$의 임의의 문자에 대응합니다.
따라서, 현재 탐색에서 비교할 text 내의 문자 인덱스와, leet 내에서 리트 표기로 사용할 수 있는 시작 인덱스를 백트래킹의 매개변수로 사용하면 됩니다.
앞서 리트 변환 규칙에서 언급했듯이, 한 개의 문자는 오직 한 가지 리트로만 변환될 수 있습니다. 따라서, 새로운 문자를 리트로 변환할 때 그 결과를 맵에 저장한 후 같은 문자를 다시 만나게 되면 맵을 참고해 변환을 시도합니다.
백트래킹 과정은 크게 2가지 경우로 나뉩니다.
- 앞서 리트로 바꾸지 않은 새로운 문자를 만날 경우
이 경우, 길이 1~K의 부분 문자열을 현재 리트 문자열의 앞에서 잘라내어 맵에 저장합니다. 이후 다음 문자에 대해 탐색을 계속합니다.
* 인덱스 초과에 유의해야 합니다. - 기존에 리트로 바꿨던 문자를 다시 만날 경우
해당 문자의 리트 표기를 리트 문자열의 앞에서 찾을 수 있다면 탐색을 계속합니다.
그러지 못할 경우, 잘못된 분기이므로 즉시 종료합니다.
백트래킹의 종료 조건은 다음과 같습니다.
- 기존 문자열(text)의 끝에 도달했을 때
- 리트 문자열도 정확히 끝에 도달했다면, 정답입니다.
- 그렇지 않다면 오답입니다.
- 1이 아니지만, 리트 문자열(leet)의 끝에 도달했을 때
- 이 경우는 반드시 잘못된 분기이므로 오답입니다.
전체 코드
input = open(0).readline
K = 0 # 1 ~ 3 사이의 정수. 한 글자가 최대 몇 글자의 리트로 표현될 수 있는지를 정의한다.
text = ""
leet = ""
leet_map = {} # 각 알파벳 소문자의 리트 표기를 저장하는 맵.
def backtrack(text_idx, leet_idx):
# 종료 조건
if text_idx >= len(text): # text를 끝까지 탐색했을 때
return leet_idx == len(leet) # leet 문자열이 아직 다 사용되지 않았다면 잘못된 경우이다.
elif leet_idx >= len(leet): # text는 남아있는데 leet 문자열이 다 사용됬다면 잘못된 경우이다.
return False
text_char = text[text_idx]
# 리트 표기에서, 한 가지 알파벳은 하나의 리트 표기로만 변환된다.
# 따라서, 앞서 변환한 적이 있는 문자라면 이번 문자도 동일한 리트 표기로 변환해보고, 불가능하다면 즉시 False로 반환하고 종료한다.
# 서로 다른 두 문자가 같은 리트 표기로 변환될 수 있다.
# 리트 표기는 기존 알파벳과 비슷해 보이지 않아도 된다
if leet_map.get(text_char, None) is not None:
for d in range(len(leet_map[text_char])):
try:
if leet[leet_idx + d] != leet_map[text_char][d]:
return False
except IndexError:
return False
# 기존 리트 표기로 변형 가능하다면, 이후 문자부터 계속 탐색한다.
return backtrack(text_idx + 1, leet_idx + len(leet_map[text_char]))
else:
# 기존에 리트 표기로 변환한 적 없는 새로운 문자라면, 1 ~ K까지의 길이로 리트 문자열을 읽어들여 리트 표기로 사용하고 이후 문자의 탐색을 진행해본다.
for d in range(1, K + 1):
if leet_idx + d > len(leet):
break
leet_map[text[text_idx]] = leet[leet_idx:leet_idx+d]
res = backtrack(text_idx + 1, leet_idx + d)
if res:
return True
# 다음 백트래킹을 위해 이번 분기의 탐색 정보 정리하기
del leet_map[text[text_idx]]
return False
for _ in range(int(input())):
K = int(input())
text = input().strip()
leet = input().strip()
leet_map.clear()
print("1" if backtrack(0, 0) else "0")
solution.py
개선 방안
이번 풀이에서는 맵을 활용해 각 알파벳 소문자에 대응하는 리트 표기를 관리했는데, 알파벳의 경우 ASCII 테이블 상에서 연속되어 있다는 점을 활용하면 배열을 통해서도 동일한 기능을 구현할 수 있습니다. 메모리 측면이나 속도 측면이나 배열을 활용하는 풀이가 더 좋아 보입니다.