오늘은 백준 31458번, "팩토리얼과 논리 반전"이라는 흥미로운 문제와 씨름했다. !!0!!
같은 수식을 계산하는 문제였는데, 처음엔 간단해 보였다. 하지만 문제는 연산 순서였다. 나는 !!0!!
를 !(!((0!)!))
순서로, 즉 안쪽부터 차례대로 계산하는 줄 알았다.
첫 번째 삽질: 복잡함 속으로
이 잘못된 이해를 바탕으로 코드를 짜기 시작했다. 숫자를 기준으로 앞뒤 느낌표를 각각 true
, false
값의 배열로 만들었다. 그리고 changeValue
라는 함수로 이 값들을 처리하려고 했다.
func changeValue(_ num: Int, isFront: Bool) -> Int {
return num == 0 ? 1 : (isFront ? 0 : 1)
}
func splitValue(_ value: String) {
let numIndex = value.firstIndex(where: { $0 != "!" })!
var frontArray = [Bool]()
var backArray = [Bool]()
for i in value[..<numIndex] {
if i == "!" {
frontArray.append(true)
}
}
for i in value[value.index(after: numIndex)...] {
if i == "!" {
backArray.append(false)
}
}
var result = Int(String(value[numIndex])) ?? 0
while !frontArray.isEmpty || !backArray.isEmpty {
if !backArray.isEmpty {
let f = backArray.removeFirst()
result = changeValue(result, isFront: f)
}
if !frontArray.isEmpty {
let t = frontArray.removeFirst()
result = changeValue(result, isFront: t)
}
}
print(result)
}
결과는? 당연히 "출력 오류"와 "틀렸습니다"의 반복이었다. 논리도 복잡했고, 결정적으로 문제의 핵심인 연산 순서를 완전히 잘못 파악하고 있었다.
구원 요청과 첫 번째 깨달음: 분리가 답이다
막다른 길에 다다른 나는 결국 ChatGPT에게 도움을 청했다. 여기서 첫 번째 중요한 점을 배웠다. 복잡한 연산은 분리해야 한다는 것! 팩토리얼(factorial
)과 논리 반전(logicalNot
) 함수를 따로 만드는 것이 훨씬 깔끔하고 관리하기 좋다는 조언을 받았다.
두 번째 삽질과 진짜 깨달음: 문제를 다시 읽자!
함수를 분리했지만, 여전히 뭔가 맞지 않았다. 그때서야 문제 설명을 다시 꼼꼼히 읽었다. 아뿔싸! "팩토리얼을 먼저 계산한다"는 명백한 규칙을 놓치고 있었다. !!0!!
의 올바른 계산 순서는 모든 팩토리얼(!
)을 먼저 처리하고, 남은 논리 반전(!
)을 처리하는 것이었다. 즉, !!(0!)!
-> !!1
-> !0
-> 1
이었던 것이다.
개선된 접근: 단순함의 미학
깨달음 뒤에는 길이 보였다. 더 이상 복잡한 불린 배열은 필요 없었다. 앞뒤 느낌표의 개수만 세면 충분했다. 숫자를 찾고, distance(from:to:)
를 이용해 앞뒤 느낌표 개수를 구했다.
// 팩토리얼 계산 함수
func factorial(_ n: Int) -> Int {
return n <= 1 ? 1 : n * factorial(n - 1)
}
// 논리 반전 함수
func logicalNot(_ n: Int) -> Int {
return n == 0 ? 1 : 0
}
// 수식 계산 함수
func calculateExpression(_ expression: String) -> Int {
// 숫자의 위치 찾기
guard let numberIndex = expression.firstIndex(where: { $0.isNumber }) else { return 0 }
// 앞쪽 느낌표 개수
let frontExclamationCount = expression.distance(from: expression.startIndex, to: numberIndex)
// 뒤쪽 느낌표 개수
let backExclamationCount = expression.distance(from: expression.index(after: numberIndex),
to: expression.endIndex)
// 가운데 숫자 파싱
var result = Int(String(expression[numberIndex]))!
// 1. 뒤쪽 느낌표(팩토리얼) 먼저 계산
for _ in 0..<backExclamationCount {
result = factorial(result)
}
// 2. 앞쪽 느낌표(논리 반전) 계산
for _ in 0..<frontExclamationCount {
result = logicalNot(result)
}
return result
}
// 메인 실행
func solution() {
let T = Int(readLine()!)!
for _ in 0..<T {
let expression = readLine()!
let result = calculateExpression(expression)
print(result)
}
}
이 과정에서 Swift의 distance(from:to:)
메서드와 컬렉션의 endIndex
가 마지막 요소 다음을 가리킨다는 점도 새롭게 알게 되었다. endIndex
에 직접 접근하면 오류가 나고, 마지막 요소는 index(before: endIndex)
로 접근해야 한다는 점도 흥미로운 발견이었다.
오늘의 교훈
오늘의 삽질은 값진 교훈을 남겼다.
1. 문제를 꼼꼼히 읽자. 가장 기본이지만 가장 중요하다.
2. 복잡한 문제는 분리하자. 작은 함수들로 나누면 이해하기 쉽고 오류도 줄어든다.
3. Swift의 디테일을 알자. distance
나 endIndex
같은 도구를 잘 활용하면 코드가 간결해진다.
비록 많은 시간을 헤맸지만, 문제 해결 과정에서 많은 것을 배우고 느낄 수 있었던 하루였다.