[SK플래닛] ASAC 빅데이터전문가 11기 | 05일차

코테는 결국 “어떻게 표현할 거냐” 싸움인 것 같다

오늘은 코딩테스트 문제를 몇 개 풀었는데,

문제를 많이 풀었다기보다 문제를 어떤 식으로 바꿔서 봐야 하는지를 더 많이 배운 날이었다.

예전에는 문제를 보면 일단 바로 코드를 치려고 했다.

근데 오늘은 그렇게 들어갔다가 계속 꼬였다.

그래서 중간부터는 “이걸 대체 어떤 방식으로 표현해야 덜 꼬이지?”를 계속 생각하게 됐다.

오늘 풀어본 건

  • L자 이동
  • 카카오 키패드
  • 실패율

이렇게 세 문제였는데,

셋 다 겉으로는 다른 문제 같아도 결국은 비슷했다.

문제를 있는 그대로 붙잡고 있으면 점점 복잡해지고,

내가 다룰 수 있는 방식으로 바꾸면 그제서야 풀린다.

오늘은 정답 정리보다,

내가 어디서 막혔고 왜 갈아엎었는지 위주로 적어보려고 한다.


1. L자 이동: 체스판을 만들고 있던 나, 문제를 잘못 보고 있었다

처음엔 이 문제를 보자마자 체스판부터 떠올렸다.

그래서 괜히 뭔가 판 전체를 만들어야 할 것처럼 접근했다.

내 처음 코드는 이런 느낌이었다.

N=[]
#i가 행, j가 열
for i in range(1,9):
    N.append([1, i])
print(N)
#Knight
#움직임의 경우의수 : UUR, UUL, DDR, DDL, RRU, RRD, LLU, LLD
move = [(2,1),(2,-1),(-2,1),(-2,-1),(2,1),(2,-1),(-2,1),(-2,-1)]
count =0
for i in range(8):
  for j in range(8):
    x,y=N[i]
    z,t=move[j]
    if x+z>=0 and y+z>0:
      count+=1

print(count)

지금 다시 보면 문제를 너무 크게 잡았다.

일단 쓸데없이 N이라는 리스트로 체스판 비슷한 걸 만들고 있었고,

심지어 이동 리스트도 복붙하다가 뒤 숫자를 안 바꿔서 같은 값이 들어가 있었다.

그리고 결정적으로 이 문제는 체스판 전체를 다루는 문제가 아니라

현재 위치에서 나이트가 갈 수 있는 8가지 이동을 체크하는 문제였다.

이걸 늦게 봤다.

정리하고 나서 바뀐 코드는 이쪽이었다.

now=input("현재 위치는?: ")
col=ord(now[0])-ord('a')+1
row=int(now[1])

#Knight
#움직임의 경우의수 : UUR, UUL, DDR, DDL, RRU, RRD, LLU, LLD
move = [(2,1),(2,-1),(-2,1),(-2,-1),(1,2),(1,-2),(-1,2),(-1,-2)]
count =0

for i in range(8):
    x,y=col,row
    z,t=move[i]
    if 1 <= x+z <= 8 and 1 <= y+t <= 8:
      count+=1

print(count)

풀고 나서 든 생각은 진짜 단순했다.

아, 문제에서 필요한 것만 보면 되는구나.

괜히 배경 전체를 만들고 있었네.


2. 이 문제에서 막힌 건 알고리즘보다 실수였다

L자 이동 문제는 풀고 나서 보니까

어려운 알고리즘 때문에 막힌 게 아니었다.

거의 이런 것들이었다.

  • ord()를 몰라서 열 문자를 숫자로 바꾸는 걸 바로 못 떠올림
  • 자꾸 0,0 아래로 벗어나는 것만 생각하고 8,8 넘어가는 건 놓침
  • x, y를 계속 쓰니까 나중에 내가 행을 보는 건지 열을 보는 건지 헷갈림
  • 이동 리스트 복붙하고 숫자 안 바꿈
  • 문제를 잘못 읽어서 입력받는 부분을 놓침

이걸 보면서 느낀 건

구현 문제는 진짜 “어려운 걸 푸는 능력”도 중요하지만,

그보다 사소한 걸 안 놓치는 힘이 더 중요하다는 거였다.

특히 변수명도 생각보다 중요했다.

처음엔 그냥 x, y로 막 썼는데,

나중엔 내가 뭘 기준으로 보고 있는지 헷갈렸다.

이런 건 차라리 row, col처럼 의미가 보이는 이름으로 쓰는 게 덜 꼬인다.


3. 키패드: 숫자로 해결하려다가 스스로 문제를 꼬아버림

키패드 문제는 처음에 숫자 자체로 풀려고 했다.

왼손 위치, 오른손 위치를 숫자로 저장하고

눌러야 하는 숫자와의 차이를 절댓값으로 비교하면 되지 않을까 싶었다.

처음 머릿속은 이런 식이었다.

# 1,4,7일때
if numbers[i]==1 or numbers[i]==4 or numbers[i]==7:
    answer+='L'
    Llocation= numbers[i]

# 3,6,9일때
elif numbers[i]==3 or numbers[i]==6 or numbers[i]==9:
    answer+='R'
    Rlocation= numbers[i]

# 2,5,8,0일때
elif numbers[i]==2 or numbers[i]==5 or numbers[i]==8 or numbers[i]==0:

    if abs(Rlocation-numbers[i])<abs(Llocation-numbers[i]):
        answer+='R'
        Rlocation= numbers[i]

    elif abs(Rlocation-numbers[i])>abs(Llocation-numbers[i]):
        answer+='L'
        Llocation= numbers[i]

처음엔 얼핏 그럴듯해 보였는데, 하다 보니까 바로 막혔다.

왜냐면 이 방식으로는

  • 위아래 이동이 제대로 설명이 안 되고
  • 숫자 차이가 실제 키패드 거리랑 안 맞고
  • 0 처리도 애매하고
  • 같은 거리일 때도 점점 꼬이기 시작했다

그러니까 문제를 잘못 표현한 상태에서 계속 버티고 있었던 거다.

그때 진짜 든 생각이

“아… 이거 숫자로 보면 안 되겠는데?”였다.

결국 좌표로 바꾸기로 했다.

그리고 거의 처음 짠 흐름을 싹 엎었다.

솔직히 그 순간엔 좀 짜증났다.

지금까지 쓴 걸 거의 다시 해야 했으니까.

근데 지나고 보니, 그때 갈아엎은 게 맞았다.


4. 키패드는 좌표로 바꾸는 순간부터 정리가 됐다

다시 짠 코드는 이거였다.

def solution(numbers, hand):
    answer = ''
    Rlocation=[3,2]
    Llocation=[3,0]

    keypad={
        1:[0,0],
        2:[0,1],
        3:[0,2],

        4:[1,0],
        5:[1,1],
        6:[1,2],

        7:[2,0],
        8:[2,1],
        9:[2,2],

        0:[3,1],
    }

    # numbers에 있는 숫자를 keypad에 있는 좌표로 불러오기
    # 좌표 거리 계산 후 오른손 왼손 결정
    # 이때 거리가 같을경우, 자기손을 따라감
    # 현재 내 손의 좌표 위치 저장

    for num in numbers:
        location = keypad[num]

        # 1,4,7
        if num in [1,4,7]:
            answer += 'L'
            Llocation = location

        # 3,6,9
        elif num == 3 or num == 6 or num == 9:
            answer += 'R'
            Rlocation = location

        # 2,5,8,0
        else:
            left_dist = abs(Llocation[0] - location[0]) + abs(Llocation[1] - location[1])
            right_dist = abs(Rlocation[0] - location[0]) + abs(Rlocation[1] - location[1])

            if left_dist<right_dist:
                answer+='L'
                Llocation = location

            elif left_dist>right_dist:
                answer+='R'
                Rlocation = location

            #거리가 같을때 손 위치 따라가는거
            elif left_dist==right_dist:
                if hand=='right':
                    answer+='R'
                    Rlocation = location
                elif hand=='left':
                    answer+='L'
                    Llocation = location
                else : pass

            else: pass

    return answer

이 문제는 좌표로 바꾸고 나니까 흐름이 훨씬 또렷해졌다.

  • 1, 4, 7은 무조건 왼손
  • 3, 6, 9는 무조건 오른손
  • 2, 5, 8, 0은 거리 비교

이렇게 나누고 나니까

그제서야 문제 설명이 코드로 옮겨지는 느낌이 들었다.

오늘 키패드에서 제일 크게 남은 건 이거였다.

표현을 잘못 잡으면 계속 안 풀린다.

문제를 푸는 것보다 먼저, 문제를 어떻게 표현할지부터 맞춰야 한다.


5. 맨해튼 거리, 오늘 제대로 기억남

키패드 문제 풀면서 맨해튼 거리도 제대로 남았다.

left_dist = abs(Llocation[0] - location[0]) + abs(Llocation[1] - location[1])
right_dist = abs(Rlocation[0] - location[0]) + abs(Rlocation[1] - location[1])

대각선은 무시하고 상하좌우만 보는 거리.

전에는 그냥 “아 그런 게 있구나” 정도였는데,

이번엔 왜 필요한지 알 것 같았다.

숫자로 거리 계산하려다가 계속 꼬였던 문제가

좌표 + 맨해튼 거리로 바뀌니까 너무 자연스러워졌다.

그리고 이런 것도 다시 눈에 들어왔다.

if num in [1,4,7]:

예전엔 이런 걸 자꾸

if num == 1 or num == 4 or num == 7:

이렇게 길게 썼는데,

막상 문제 풀다 보니까 in 쓰는 게 훨씬 낫다.

눈에도 잘 들어오고 덜 복잡하다.

또 이것도.

for num in numbers:
    location = keypad[num]

굳이 인덱스를 잡고 numbers[i]로 가는 것보다

요소를 바로 받는 방식이 훨씬 편했다.

이런 사소한 문법이 실제 문제 풀 때는 꽤 차이를 만든다.


6. 실패율: 논리는 맞았는데 시간초과를 맞았다

실패율 문제는 처음엔

“각 스테이지마다 도달한 사람 수, 실패한 사람 수 세면 끝 아닌가?”

이렇게 생각했다.

그래서 코드는 이렇게 갔다.

def solution(N, stages):

    answer = [] #최종출력
    result =[] #중간출력

    for now in range(1,N+1):
        reach=0
        fail=0
        for i in range(len(stages)): #총 사람만큼 반복

            if stages[i]>=now :
                reach+=1
            if stages[i]==now:
                fail+=1
        if reach==0:
            result.append((now,0))
        else:
            result.append((now,fail/reach))

    result = sorted(result, key=lambda x: (-x[1], x[0]))
    answer = [x[0] for x in result]

    return answer

처음엔 진짜 이게 맞는 줄 알았다.

  • 각 스테이지마다 확인하고
  • 도달자 세고
  • 실패자 세고
  • 실패율 구해서
  • 정렬

논리만 보면 틀린 것 같지 않았다.

근데 결과는 시간초과.

그때 솔직히 좀 어이없었다.

“아니, 맞잖아…” 이 생각이 먼저 들었다.

근데 조금만 보니까 바로 이유가 보였다.

이중 for문 때문이었다.


7. 여기서 처음 제대로 체감한 것: 맞는 코드랑 통과하는 코드는 다르다

오늘 실패율 문제에서 가장 크게 남은 건 이거다.

맞는 코드와 통과하는 코드는 다를 수 있다.

이전엔 출력만 맞으면 된다고 생각한 적도 많았는데,

코테는 그게 아니었다.

특히 입력 크기가 커지면

이중 반복문은 바로 부담이 된다.

그러니까 이제는 문제를 풀 때

  • 이 로직이 맞는가
  • 이 코드가 얼마나 많이 도는가

이걸 같이 봐야 한다는 걸 확실히 느꼈다.

아직 최적화까지 완벽하게 한 건 아니지만,

적어도 왜 시간초과가 나는지는 이번에 진짜 보였다.

그것만으로도 오늘 꽤 많이 배운 느낌이다.


8. 실패율 문제는 정렬 기준을 코드로 바꾸는 것도 중요했다

실패율 문제는 단순 계산 문제만은 아니었다.

정렬 기준도 정확해야 했다.

result = sorted(result, key=lambda x: (-x[1], x[0]))

이 한 줄이 뜻하는 건

  • 실패율 높은 순
  • 실패율이 같으면 스테이지 번호 낮은 순

이다.

이걸 보면서 느낀 건

코테는 결국 문장을 코드로 번역하는 일이라는 거였다.

문제에서 말로 주어진 조건을

정확하게 정렬 기준으로 바꾸는 힘이 필요하다.

이런 건 그냥 넘어가기 쉬운데,

실제로는 이런 한 줄이 정답을 갈라버린다.


9. 오늘 세 문제를 관통한 공통점

오늘 푼 세 문제를 다시 보면 공통점이 있었다.

L자 이동은

→ 체스판 전체를 만들지 말고, 현재 위치와 이동 규칙만 보면 되는 문제였고

키패드는

→ 숫자로 보면 꼬이고, 좌표로 바꾸면 풀리는 문제였고

실패율은

→ 그냥 세면 맞아도, 시간초과 때문에 다시 봐야 하는 문제였다

결국 다

문제를 어떻게 표현하느냐가 핵심이었다.

오늘 문법을 엄청 새로 배운 건 아닌데,

문제를 보는 방식은 조금 바뀐 것 같다.

이제는 문제를 보면 바로 코드 치기 전에

먼저 이걸 생각해야겠다.

  • 이거 좌표로 바꾸면 편한가?
  • 케이스를 먼저 나눠야 하나?
  • 지금 내가 문제를 이상하게 표현하고 있진 않나?
  • 이중 반복문 돌리면 시간 터지지 않나?

이걸 먼저 보고 들어가야 덜 망할 것 같다.


오늘의 한 줄

코딩테스트는 정답을 빨리 쓰는 게 아니라, 문제를 덜 꼬이게 바꾸는 연습에 더 가까운 것 같다.


마무리

오늘은 깔끔하게 푼 날은 아니었다.

문제를 잘못 읽기도 했고,

숫자로 붙잡고 있다가 좌표로 갈아엎기도 했고,

맞는 줄 알았는데 시간초과도 났다.

근데 이상하게 이런 날이 더 많이 남는다.

잘 풀린 문제보다

왜 안 풀렸는지 알게 된 문제가 더 오래 기억에 남는 느낌이다.

그래서 오늘은 정답 정리보다

내가 어디서 막혔는지를 남겨두는 게 더 의미 있는 것 같다.

다음에 비슷한 문제를 만나면

오늘처럼 괜히 멀리 돌아가지 않았으면 좋겠다.