문제 보기 · Level 2 · 2020 KAKAO BLIND RECRUITMENT · 정답률 49% · 푼 사람 21,857명 (2026-09-30 기준)1
문제
’(‘와 ‘)’의 개수가 같은 문자열 p를 문제가 정한 재귀 절차로 짝이 맞는 괄호 문자열로 바꾼다.
- 빈 문자열이면 빈 문자열을 돌려준다.
- p를 더 쪼갤 수 없는 균형 문자열 u와 나머지 v로 나눈다.
- u가 짝이 맞으면 u 뒤에 v의 변환 결과를 붙여 돌려준다.
- 짝이 맞지 않으면 ‘(‘ + v의 변환 결과 + ‘)’를 만들고, 그 뒤에 u의 양 끝을 떼고 나머지 괄호 방향을 뒤집은 것을 붙여 돌려준다.
- p 길이 2~1,000, 짝수
풀이
막히면 한 단계씩 펼친다.
필요한 개념
- 재귀와 백트래킹: 문제의 절차가 이미 재귀다. 그대로 옮긴다.
- 스택: 짝이 맞는지 확인하기. 여기서는 스택 대신 “지금까지 연 괄호 수” 하나로 센다.
- 재귀적 정의와 구조적 귀납법: 올바른 문자열은 그 문서의 ‘균형 괄호’(빈 문자열, (s), st 세 규칙으로 만든 것)다. 이 문서의 균형 문자열(개수만 같음)과는 뜻이 다르다. 변환 결과가 늘 올바른 것도 길이에 대한 귀납법으로 보인다. 변환(v)가 올바르다고 두면, u가 올바를 때 결과는 규칙 st다. 아니면 u는 ‘)’로 시작한다. ‘(‘를 +1, ‘)’를 −1로 센 값은 끝에서야 0이 되므로, 가운데에서는 내내 음수다. 그래서 양 끝을 떼고 뒤집은 가운데는 올바르다. ‘(‘ + 변환(v) + ‘)’는 규칙 (s)이고, 그 뒤에 이것을 붙인 결과도 규칙 st다.
1단계 · 작은 예로 손으로 풀기
용어부터 정리한다. 균형 문자열은 ‘(‘와 ‘)’의 개수만 같은 것, 올바른 문자열은 짝까지 맞는 것이다.
p = "))(("
- 앞에서부터 세며 처음으로 개수가 같아지는 곳에서 자른다: ) → −1, ) → −2, ( → −1, ( → 0. 끝까지 가서야 0이니 u =
"))((", v ="" - u는 올바르지 않다(처음부터 ‘)’)
- ’(‘ + 변환(v) + ‘)’ =
"()" - u의 양 끝을 떼면
")(", 방향을 뒤집으면"()" - 합치면
"()()"
“뒤집기”는 글자 순서를 거꾸로 하는 게 아니라, 글자마다 ( ↔ )를 바꾸는 것이다.
2단계 · 가장 단순한 방법과 그 속도
절차를 그대로 따르는 것이 곧 답이다. 한 번 부를 때마다 u를 찾느라 문자열을 훑고, 부를 때마다 v가 적어도 두 글자 짧아진다. 길이 1,000이면 호출이 많아야 500번, 전체 훑기는 50만 번 정도다. 재귀 깊이도 500 이하라 파이썬 한도(1,000) 안이다.
3단계 · 핵심 아이디어
도우미 둘만 만들면 절차를 한 줄씩 옮길 수 있다.
- u, v 나누기: 앞에서부터 ‘(‘면 +1, ‘)’면 −1을 더한다. 처음으로 0이 되는 자리까지가 u다. 그보다 앞에서 0이 된 적이 없으니 “더 쪼갤 수 없는” 균형 문자열이다.
- 올바른지 확인: 같은 방식으로 더하다가 한 번이라도 음수가 되면 올바르지 않다. 닫는 괄호가 여는 괄호보다 먼저 많아진 것이다.
4단계 · 코드 흐름
- p가 비었으면 빈 문자열.
- 개수를 세며 u, v로 나눈다.
- u가 올바르면 u + 변환(v).
- 아니면 “(“ + 변환(v) + “)” + (u의 양 끝을 뗀 뒤 글자마다 뒤집은 것).
5단계 · 파이썬 코드
def is_correct(s):
bal = 0
for ch in s:
bal += 1 if ch == "(" else -1
if bal < 0: # 닫는 괄호가 먼저 많아졌다
return False
return bal == 0
def solution(p):
if p == "": # 1
return ""
bal = 0
for i, ch in enumerate(p): # 2. 처음 균형이 맞는 곳에서 자른다
bal += 1 if ch == "(" else -1
if bal == 0:
u, v = p[:i + 1], p[i + 1:]
break
if is_correct(u): # 3
return u + solution(v)
flipped = "".join("(" if ch == ")" else ")" for ch in u[1:-1])
return "(" + solution(v) + ")" + flipped # 4
1 if ch == "(" else -1: 여는 괄호면 1, 닫는 괄호면 −1이다.u[1:-1]: 첫 글자와 마지막 글자를 뗀다."".join(… for ch in …): 글자마다 바꾼 뒤 이어 붙인다.solution(v): 문제의 “1단계부터 다시 수행”이 곧 재귀 호출이다.
6단계 · 실행 추적
p = "()))((()"
| 호출 | u | v | u가 올바른가 | 결과 |
|---|---|---|---|---|
| f(“()))((()”) | ”()” | ”))((()” | 예 | ”()” + f(“))((()”) |
| f(“))((()”) | ”))((“ | ”()” | 아니오 | ”(“ + f(“()”) + “)” + 뒤집기(“)(“) |
| f(“()”) | ”()” | ”” | 예 | ”()” + f(“”) = “()” |
가운데 줄: “(“ + “()” + “)” + “()” = “(())()”. 맨 위: “()” + “(())()” = "()(())()"
7단계 · 복잡도와 흔한 실수
호출마다 문자열을 훑으니 \(O(n^2)\) 이하다(n ≤ 1,000).
- “뒤집기”를 글자 순서 거꾸로(
u[1:-1][::-1])로 한다. 글자마다 ( ↔ )를 바꿔야 한다. - u를 “가장 짧은” 균형 문자열이 아니라 더 길게 자른다.
- 4단계에서 v를 변환하지 않고 그대로 붙인다.
검증: 문제의 예시 3개를 통과했다. 길이 12 이하의 모든 균형 문자열(1,274개)에서 결과가 늘 올바른 괄호 문자열이고, 입력이 이미 올바르면 그대로 돌아오는 것을 확인했다 — 17_pg60058_p1.py
-
프로그래머스 스쿨, “괄호 변환” (https://school.programmers.co.kr/learn/courses/30/lessons/60058), 2026-09-30 조회. ↩