오늘은 좌표 평면 위의 점들로 만들 수 있는 x축 또는 y축 평행선 개수를 세는 문제를 풀었다. 문제는 간단해 보였는데, 정답까지 가는 길은 생각보다 험난했다. 당연하다고 생각했던 문제 이해가 발목을 잡을 줄이야... 오늘의 삽질은 정말 값진 교훈을 남겼다.
문제를 처음 읽었을 때, "두 개 이상의 점을 지나면서 x축 또는 y축에 평행한 직선이 몇 개인지" 라는 문구를 보고, 자연스럽게 '같은 x좌표를 가진 점들이 N개 있다면, 그 점들 중 2개를 골라 만들 수 있는 모든 y축 평행선'을 세어야 한다고 생각했다. y축 평행선도 마찬가지였다.
그래서 각 x좌표와 y좌표의 빈도수를 센 다음, 빈도수가 N인 경우 C(N, 2), 즉 N * (N - 1) / 2
(등차수열의 합과 유사한 형태)를 계산해서 모두 더하면 되겠다고 판단했다. 코드는 금방 작성했지만... 결과는 계속 "틀렸습니다" 였다.
머릿속은 복잡해졌다.
Int64
로 바꿔야 하나?"!
강제 언래핑이 문제인가? guard let
을 써야 하나?"온갖 문법적, 기술적 측면만 파고들며 시간을 보냈다. 정작 가장 중요한 '문제 이해'를 의심하지 못했던 것이다.
계속된 실패에 지쳐 혹시나 하는 마음에 문제 설명을 다시 꼼꼼히 읽어보았다.
"평면에 n개의 점이 있다. 그중 두 개 이상의 점을 지나면서 x축 또는 y축에 평행한 직선이 몇 개인지 알아내는 프로그램을 작성하시오."
순간 머리를 한 대 맞은 듯한 느낌이 들었다. 문제는 "점들의 쌍" 이나 "만들 수 있는 모든 선분" 의 개수를 묻는 것이 아니었다. 점이 아무리 많아도, 예를 들어 x=5 위에 점이 100개가 있어도, 그 점들을 지나는 y축 평행선은 오직 x=5
하나 뿐이다. 즉, "고유한 직선" 의 개수를 세라는 의미였다!
그동안 C(N, 2)를 계산했던 것은 완전히 잘못된 방향이었다. 허탈함과 동시에 '아!' 하는 탄식이 나왔다.
나의 첫 번째 코드는 다음과 같았다. 같은 좌표를 공유하는 점들로 만들 수 있는 모든 직선(실제로는 점 쌍)의 개수를 세려고 했다.
// 초기 잘못된 코드 (제출했던 코드)
import Foundation
let count = Int(readLine()!)!
var xPoints = [Int: Int]()
var yPoints = [Int: Int]()
for _ in 0..<count {
var dot = readLine()!.split(separator: " ").compactMap { Int($0) }
// y좌표와 x좌표 순서로 읽도록 수정 (이전 질문 기반)
let yCoord = dot.removeLast()
let xCoord = dot.removeLast()
yPoints[yCoord, default: 0] += 1
xPoints[xCoord, default: 0] += 1
}
// 점 N개로 만들 수 있는 선분(쌍)의 개수 계산 함수 (오해의 근원)
func calculateParrellaLines(_ value: Int) -> Int {
guard value > 1 else { return 0 }
return Int(((value - 1)*value)/2) // C(value, 2)
}
// 각 좌표 그룹별 쌍의 개수를 모두 합산
let xCount = xPoints.values.reduce(0, { $0 + calculateParrellaLines($1) })
let yCount = yPoints.values.reduce(0, { $0 + calculateParrellaLines($1) })
print(xCount + yCount)
왜 틀렸는가: calculateParrellaLines
함수는 점 N개로 만들 수 있는 직선의 '쌍'의 개수를 계산한다. 하지만 문제는 '고유한' 직선의 개수를 원했다. 예를 들어 x=0 위에 점이 3개 있으면, 위 코드는 3을 결과에 더하지만, 정답은 1만 더해야 했다.
문제의 요구사항을 정확히 파악한 후 코드를 수정하는 것은 매우 간단했다. 조합 계산은 전혀 필요 없었다.
핵심 아이디어:
// 최종 올바른 코드
import Foundation
let n = Int(readLine()!)!
var xPoints = [Int: Int]()
var yPoints = [Int: Int]()
for _ in 0..<n {
let coordinates = readLine()!.split(separator: " ").compactMap { Int($0) }
if coordinates.count == 2 {
let xCoord = coordinates[0]
let yCoord = coordinates[1]
// ✨ 오늘의 이득: default 서브스크립트로 간결하게 빈도수 증가!
xPoints[xCoord, default: 0] += 1
yPoints[yCoord, default: 0] += 1
}
}
// x좌표가 2번 이상 등장한 '고유한' x좌표의 개수 (수직선 개수)
let verticalLines = xPoints.values.filter { $0 >= 2 }.count
// y좌표가 2번 이상 등장한 '고유한' y좌표의 개수 (수평선 개수)
let horizontalLines = yPoints.values.filter { $0 >= 2 }.count
print(verticalLines + horizontalLines)
왜 맞는가: filter { $0 >= 2 }.count
를 사용하여, 단순히 같은 좌표를 공유하는 점이 2개 이상인 경우를 딱 1번만 카운트한다. 이것이 문제에서 요구한 "고유한 직선의 개수"와 정확히 일치한다. 정말 어이없을 정도로 간단한 수정이었다!
default:
서브스크립트이번 삽질 과정에서 그나마 얻은 작은 수확은 Swift 딕셔너리의 default:
서브스크립트를 제대로 활용해본 것이다. 이전에는 if let
등으로 키 존재 여부를 확인하고 값을 업데이트했지만,
xPoints[xCoord, default: 0] += 1
이렇게 한 줄로 쓰니 코드가 훨씬 간결해졌다. xCoord
키가 딕셔너리에 없으면 default
값인 0을 사용하고, 거기에 1을 더해서 저장해준다. 키가 이미 있다면 기존 값에 1을 더한다. 빈도수 계산할 때 정말 유용하다!
default:
서브스크립트는 빈도수 계산 등에 매우 유용하고 코드를 간결하게 만들어준다.