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

문제를 풀면서 정리한 코딩테스트 접근법

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

문제를 많이 풀었다기보다 문제를 어떤 식으로 해석하고 표현해야 하는지를 더 많이 배운 하루였다.

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

그런데 오늘은 그렇게 들어갔다가 오히려 더 많이 꼬였다.

그래서 중간부터는

“이걸 어떤 방식으로 바꿔서 봐야 덜 복잡해질까?”

이 질문을 계속 하게 됐다.

오늘 다룬 문제는 다음 세 가지였다.

  • 파일명 정렬
  • 완주하지 못한 선수
  • 신고 결과 받기

겉으로 보기에는 전혀 다른 문제처럼 보였지만,

풀고 나서 보니 공통점이 있었다.

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

내가 다룰 수 있는 구조로 바꾸면 그제서야 풀린다.

오늘은 정답만 정리하기보다,

내가 어떤 식으로 접근했고 어디서 막혔는지,

그리고 그 과정에서 무엇을 새로 이해했는지를 중심으로 남겨보려고 한다.


1. 파일명 정렬

🔗 문제 링크

프로그래머스 17686 - 파일명 정렬

처음에는 그냥 문자열 정렬 문제라고 생각했다

처음 문제를 봤을 때는

파일명을 그냥 정렬하면 되는 문제라고 생각했다.

그런데 조금만 보니 그게 아니었다.

예를 들어 img2와 img10은 문자열 그대로 비교할 때와 숫자 크기 기준으로 비교할 때 결과가 다르다.

즉, 이 문제는 단순히 “정렬”이 아니라

파일명을 어떤 기준으로 나눠서 볼 것인가가 먼저인 문제였다.


처음 시도: 숫자만 따로 뽑아서 처리하려고 했다

처음에는 정규식을 쓰지 않고,

파일명 안에 있는 숫자만 따로 뽑아서 해결해보려고 했다.

# 숫자가 나오면 슬라이싱 해서 끊기
# . 나오면 또 끊기
# 앞의 0은 제거하고 숫자 순으로 정렬
# 다시 붙여서 출력

dic = {}

for i in range(len(files)):
    name = files[i]

    numbers = [int(num) for num in name if num.isdigit()]

    cutnum = list(''.join(map(str, numbers)))

    for i in range(len(cutnum)):
        if cutnum[i] == 0:
            cutnum.remove()
        else:
            break

지금 다시 보면

문제를 너무 직접적으로, 그리고 힘들게 풀려고 했던 코드였다.

이때의 생각은 대충 이런 흐름이었다.

  • 파일명에서 숫자를 찾는다
  • 숫자 앞뒤를 자른다
  • 앞의 0을 제거한다
  • 다시 정렬한다

그런데 이 방식은

문제의 핵심인 HEAD / NUMBER / TAIL 분리를 전혀 반영하지 못했다.


⚠️ 여기서 헤맨 부분

이 문제에서 막힌 건 알고리즘보다도

문자열 처리와 자료형 이해 쪽이었다.

특히 이런 부분에서 많이 꼬였다.

  • 숫자와 문자를 정확히 어떻게 나눠야 하는지 감이 안 왔다
  • remove()를 리스트를 돌면서 바로 쓰려고 했다
  • 앞의 0을 직접 지우려고 했다
  • 숫자 부분을 계속 문자열처럼 다루려고 했다

지금 보면 0010 같은 값은

굳이 앞의 0을 손으로 지울 필요가 없었다.

그냥 int("0010")으로 바꾸면 끝나는 문제였다.

💡 여기서 느낀 건 아주 단순했다.

내가 하나하나 손으로 처리하려고 할수록 더 꼬인다.

파이썬이 이미 해주는 걸 먼저 떠올리는 게 중요하다.


정규식을 배우고 나서 문제 구조가 조금 보이기 시작했다

강의 내용을 듣고 나서야

이 문제는 파일명 전체를 보는 게 아니라,

파일명을 HEAD / NUMBER / TAIL로 나눠서 봐야 한다는 게 보였다.

오늘 정리한 정규식 핵심은 이 정도였다.

  • \d : 숫자
  • [^\d]+ : 숫자가 아닌 문자들
  • \d+ : 연속된 숫자 덩어리

예를 들어 "img0010.jpg"를 보면

  • HEAD → "img"
  • NUMBER → "0010"
  • TAIL → ".jpg"

이런 식으로 나눠볼 수 있다.

정규식이 전에는 되게 어렵게 느껴졌는데,

오늘은 적어도 문자열을 규칙대로 분리하는 도구라는 감각은 생긴 것 같다.


두 번째 시도: 방향은 맞았지만 아직 헷갈렸다

정규식을 배우고 다시 시도한 코드는 이랬다.

import re

def solution(files):
    num = []

    for i in range(len(files)):
        cut = files.index(re.findall(r"[\\d]+", files[i])[0])
        num.append([files[:cut], int(files[cut:])])

    sort.num[1]

    answer = num

이 코드는 처음 시도보다는 방향이 조금 맞아졌지만,

여전히 크게 헷갈린 부분이 많았다.

예를 들면,

  • files.index()는 리스트 안에서 원소 위치를 찾는 함수인데, 문자열 내부 위치 찾기처럼 썼다
  • files[:cut]도 파일명 하나가 아니라 전체 리스트를 자르고 있었다
  • sort.num[1] 같은 문법도 맞지 않는다

즉, 정규식을 배웠다고 해서 바로 풀리는 건 아니었고,

아직 내가 다루는 대상이 문자열인지 리스트인지조차 명확히 정리되지 않은 상태였다.


💡 오늘 파일명 정렬에서 정리된 것

1) 숫자는 문자열이 아니라 숫자로 비교해야 한다

0010은 "0010"이 아니라 10으로 비교해야 한다.

즉, int()로 변환하는 게 핵심이다.

2) 정렬은 “무엇을 기준으로 나눌지”가 먼저다

이 문제는 정렬 함수보다 먼저

HEAD / NUMBER / TAIL로 분리하는 관점이 필요했다.

3) 정규식은 멋있어 보이기 위한 기술이 아니라 분리 도구다

문자열 문제에서 정규식은

문제를 코드로 옮기기 쉽게 만드는 도구라는 걸 느꼈다.


2. 완주하지 못한 선수

🔗 문제 링크

프로그래머스 42576 - 완주하지 못한 선수

처음에는 차집합으로 끝날 줄 알았다

이 문제를 처음 봤을 때는

참가자 목록과 완주자 목록의 차이를 구하면 바로 끝날 줄 알았다.

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

def solution(participant, completion):
    answer = ''

    regist = []
    goal = []

    for i in participant:
        regist.append(i)

    for i in completion:
        goal.append(i)

    answer = list(set(regist).difference(set(goal)))

    return answer[0]

처음엔 그럴듯해 보였다.

실제로 이름이 모두 다르면 어느 정도 맞는 것처럼 보이기도 한다.

그런데 이 문제의 핵심 조건은 동명이인이었다.


⚠️ 시행착오: set은 편했지만 문제 조건을 지워버렸다

이 문제에서 가장 크게 놓친 건

set이 중복을 제거한다는 점이었다.

즉,

  • 참가자에 같은 이름이 여러 번 들어올 수 있고
  • 완주자에도 같은 이름이 여러 번 들어올 수 있는데

set으로 바꾸는 순간

그 정보 자체가 사라져버린다.

그래서 이 문제는 단순 차집합 문제가 아니라

결국 이름별 등장 횟수를 세는 문제였다.

여기서 다시 느낀 건 이거였다.

자료구조는 편해서 쓰는 게 아니라,

문제 조건을 보존할 수 있을 때 써야 한다.


두 번째 시도: 딕셔너리로 풀려고 했지만 아직 불안정했다

중간에는 딕셔너리로 풀어보려고도 했다.

def solution(participant, completion):
    dict = {}
    key = 0

    for i in participant:
        dict[i] = key + 1

    for i in completion:
        dict[i] = key - 1

    for i in range(len(dict))
    if dict.value != 0:
        answer = dict.key

    return answer

이 코드는 “이름별로 숫자를 세야 한다”는 방향 자체는 맞았지만,

딕셔너리 사용이 아직 정확하지 않았다.

이때 내가 헷갈렸던 부분은 이런 것들이었다.

  • 값이 누적되지 않고 계속 덮어써졌다
  • dict.value, dict.key 같은 문법은 파이썬에 없다
  • 반복문과 조건문 구조도 제대로 정리되지 않았다

즉, 문제를 카운팅 문제로 바꾸는 데까지는 왔지만,

그걸 딕셔너리로 정확하게 구현하는 건 아직 익숙하지 않았던 상태였다.


완성 코드: 결국 카운팅 문제였다

정리된 코드는 이쪽이었다.

def solution(participant, completion):
    dic = {}

    for name in participant:
        if name in dic:
            dic[name] += 1
        else:
            dic[name] = 1

    for name in completion:
        dic[name] -= 1

    for name in dic:
        if dic[name] != 0:
            return name

이 코드는 흐름이 명확하다.

  • 참가자는 +1
  • 완주자는 -1
  • 마지막에 0이 아닌 사람을 반환

이렇게 보니까 문제 구조가 훨씬 분명해졌다.

즉, 이 문제는 참가자 명단 비교 문제가 아니라

이름 등장 횟수를 세는 딕셔너리 카운팅 문제였다.


정렬 풀이도 같이 배웠다

이 문제는 정렬로도 풀 수 있었다.

def solution(participant, completion):
    participant.sort()
    completion.sort()

    for i in range(len(completion)):
        if participant[i] != completion[i]:
            return participant[i]

    return participant[-1]

이 풀이는 굉장히 직관적이었다.

  • 두 리스트를 정렬한다
  • 앞에서부터 비교한다
  • 다른 순간이 나오면 그 사람이 정답이다

끝까지 다 같으면 마지막 참가자가 완주하지 못한 선수가 된다.

이걸 보면서 다시 느낀 건,

정렬은 단순히 순서를 맞추는 기능이 아니라

비교를 쉽게 만드는 도구라는 점이었다.


3. 신고 결과 받기

🔗 문제 링크

프로그래머스 92334 - 신고 결과 받기

오늘 제일 어려웠던 건 구현보다 문제 이해였다

오늘 세 문제 중에서

가장 오래 붙잡고도 가장 답답했던 문제는 이 문제였다.

사실 코드 자체가 어려웠다기보다,

문제를 머릿속에서 구조화하는 데 시간이 너무 오래 걸렸다.

처음에는 이렇게 메모하면서 구조를 잡으려고 했다.

# 1. id_list 받아서 dict의 키값으로 배치
# 2. report는 공백 기준으로 분리
# 3. [신고자, 신고당한 사람] 형태로 저장
# 4. 중복 신고는 set으로 제거
# 5. 신고당한 사람 횟수 세기
# 6. k번 이상이면 정지
# 7. 그 사람을 신고한 사람에게 메일 횟수 반영

문장으로 쓰면 단순해 보이는데,

막상 구현하려고 하면 계속 꼬였다.


처음 코드 시도: 한 번에 처리하려다가 무너졌다

def solution(id_list, report, k):
    mail = {}
    sinfo = []

    report = list(set(report))

    for name in id_list:
        mail[name] = ''

    for i in report:
        a, b = i.split()
        mail[a].append(b)

    for name in mail:
        if counter(mail.values(name)) > k:
            cut += name

지금 보면 이 코드는 거의

“생각의 흔적”에 가까운 코드였다.

여기서 헷갈렸던 포인트는 아주 많았다.

  • mail[name] = ''로 문자열을 넣고 append() 하려고 했다
  • counter() 같은 함수도 정확히 모르고 감으로 썼다
  • mail.values(name) 같은 문법도 존재하지 않는다
  • 여러 단계를 한 번에 처리하려다 보니 자료구조가 계속 꼬였다

이 문제는 특히

“말은 쉬운데, 왜 코드로 옮기면 이렇게 안 되지?”

이 느낌이 강했다.


💡 이 문제에서 중요한 건 중간 구조였다

이 문제는 결과를 바로 만들려고 하면 안 되고,

중간 구조를 먼저 나눠서 생각해야 했다.

내가 오늘 정리한 흐름은 이 정도다.

1) 중복 신고 제거

같은 사람이 같은 사람을 여러 번 신고해도 1번으로 처리되니까

먼저 set(report)로 중복을 제거해야 한다.

2) 신고 관계 정리

"muzi frodo" 같은 문자열을 나눠서

누가 누구를 신고했는지 저장해야 한다.

3) 신고당한 횟수 세기

신고당한 사람 기준으로 카운트를 올려서

정지 대상인지 판단해야 한다.

4) 메일 수 계산

정지된 사람을 신고한 사람에게만

메일 횟수를 반영하면 된다.

이렇게 나누고 보니,

이 문제는 단순 구현 문제가 아니라

관계 정보를 자료구조로 어떻게 표현할지가 핵심인 문제처럼 느껴졌다.


4. 오늘 느낀 점

오늘 문제들을 다시 보면 공통점이 있었다.

파일명 정렬

문자열 전체를 보지 말고

HEAD / NUMBER / TAIL로 나눠야 했던 문제

완주하지 못한 선수

리스트 비교가 아니라

이름 카운팅 문제로 바꿔야 했던 문제

신고 결과 받기

결과를 바로 만들기보다

중간 관계 구조를 먼저 설계해야 했던 문제

결국 오늘 세 문제를 관통한 핵심은 하나였다.

문제를 어떻게 표현하느냐가 풀이를 결정한다는 것.

오늘은 문법을 엄청 많이 외운 날은 아니었다.

그런데 문제를 바라보는 방식은 확실히 조금 바뀐 것 같다.

이제는 문제를 보면 바로 코드부터 치기보다

먼저 이런 질문을 해봐야겠다고 느꼈다.

  • 이 문제는 문자열을 나눠서 봐야 하나?
  • 이 문제는 카운팅 문제로 바꿀 수 있나?
  • 중간 자료구조를 먼저 만들어야 하나?
  • 지금 내가 문제를 너무 복잡하게 보고 있지는 않나?

오늘 제일 크게 남은 한 줄은 이거다.

코딩테스트는 정답을 빨리 쓰는 싸움이 아니라,

문제를 덜 꼬이게 바꾸는 연습에 더 가깝다.