프로그래머스 파이썬 풀이 노트
과일 장수, 소수 만들기, 오픈채팅방, 개미 군단, 컨트롤 제트 풀이 정리
이번 글은 Google 문서에 정리해 둔 문제 풀이 기록을 블로그용으로 다시 다듬은 글입니다. 한 가지 주제만 다루는 Day 글은 아니고, 그리디, 조합, 해시, 스택을 한 번에 복습하는 문제 묶음입니다.
원문 흐름을 빠뜨리지 않기 위해 처음 떠올린 생각, 실수한 코드, 왜 틀렸는지, 수정 풀이, 더 짧은 풀이, 초보자용 설명까지 함께 정리했습니다.
문서에서 다룬 프로그래머스 문제를 먼저 표로 정리합니다.
사용자가 작성한 코드의 의도와 틀린 지점을 함께 봅니다.
기본 풀이를 먼저 이해한 뒤, 실전에서 쓸 수 있는 짧은 풀이까지 정리합니다.
예제 결과와 실수 방지 포인트를 마지막에 모아 봅니다.
1. 문제 및 프로그래머스 공식 링크
문서에서 다룬 문제는 총 5개입니다. 난이도와 주제는 섞여 있지만, 각각 코딩테스트에서 자주 만나는 핵심 도구를 하나씩 연습하게 해 줍니다.
| 문제 | 공식 링크 | 핵심 개념 |
|---|---|---|
| 과일 장수 | 문제 보기 | 그리디, 정렬, 슬라이싱, 나머지 |
| 소수 만들기 | 문제 보기 | 조합, 소수 판별 |
| 오픈채팅방 | 문제 보기 | 해시, 딕셔너리, 투 패스 |
| 개미 군단 | 문제 보기 | 그리디, 몫과 나머지, divmod |
| 컨트롤 제트 | 문제 보기 | split, 스택, append, pop |
2. 과일 장수: 비싼 사과부터 묶고 최저 점수만 계산하기
문제 요약
사과 점수 배열 score가 주어지고, 한 상자에 m개씩 담아 팔아야 합니다. 한 상자의 가격은 그 상자 안에서 가장 낮은 점수 p에 m을 곱한 값입니다. 남는 사과는 팔 수 없고 버립니다.
처음 떠올린 힌트
최대 이익을 내려면 점수가 높은 사과끼리 먼저 묶는 것이 유리합니다. 그래서 sorted(score, reverse=True)로 내림차순 정렬합니다.
내림차순으로 정렬한 뒤 m개씩 묶으면, 각 묶음의 마지막 위치가 그 상자의 가격을 결정하는 최저 점수입니다.
처음 작성한 코드
def solution(k, m, score):
answer = 0
new_score = sorted(score, reverse=True)
for i in range(0, len(score) + 1, m):
answer += i * m
return answer
잘못된 점
i는 사과 점수가 아니라 인덱스다
range(0, len(score) + 1, m)에서 i는 0, m, 2m처럼 위치 번호입니다. 실제 사과 점수를 꺼내려면 new_score[i]처럼 배열에 접근해야 합니다.
new_score[i + m - 1]이다
i는 상자의 시작 위치입니다. 가격을 결정하는 사과는 상자의 마지막 사과이므로 i + m - 1 위치를 봐야 합니다.
len(score) + 1까지 반복하면 상자를 채우지 못하는 사과까지 보려고 하다가 인덱스 에러가 날 수 있습니다. 온전한 상자만 만들려면 range(0, len(score) - m + 1, m)이 안전합니다.
기본 수정 풀이
def solution(k, m, score):
answer = 0
new_score = sorted(score, reverse=True)
for i in range(0, len(score) - m + 1, m):
lowest_apple = new_score[i + m - 1]
answer += lowest_apple * m
return answer
내림차순 슬라이싱 풀이
def solution(k, m, score):
return sum(sorted(score, reverse=True)[m-1::m]) * m
sorted(score, reverse=True)로 비싼 사과부터 줄 세운 뒤, [m-1::m]으로 첫 번째 상자의 마지막 사과, 두 번째 상자의 마지막 사과를 차례로 뽑습니다. 이 값들이 각 상자의 최저 점수입니다.
문서에 나온 빠른 풀이: 오름차순과 나머지 활용
def solution(k, m, score):
return sum(sorted(score)[len(score) % m::m]) * m
이 풀이는 오름차순으로 정렬합니다. 가장 싼 사과가 앞에 오기 때문에 len(score) % m개만큼 앞에서 건너뛰면, 상자를 채우지 못하고 버려야 할 가장 싼 사과들을 먼저 제외할 수 있습니다. 그다음 ::m으로 각 상자의 첫 번째 사과만 뽑는데, 오름차순 묶음에서는 이 첫 번째 사과가 그 상자의 최저 점수입니다.
sorted(score)는 싼 사과부터 줄 세우기, len(score) % m은 버릴 사과 개수 찾기, [시작점::m]은 각 상자의 최저 점수만 건너뛰며 뽑기, sum(...) * m은 총 이익 계산입니다.
3. 소수 만들기: 3개씩 고르고 합이 소수인지 확인하기
문제 요약
nums에서 서로 다른 숫자 3개를 고른 뒤, 그 합이 소수인 경우의 개수를 구합니다. 문서에서 짚은 것처럼 핵심은 “3개씩 묶기”와 “소수 정의로 판별하기”입니다.
풀이 방향
직접 3중 반복문을 써도 되지만, 파이썬에서는 itertools.combinations(nums, 3)가 더 명확합니다.
소수는 1과 자기 자신만으로 나누어떨어지는 수입니다. 2부터 제곱근까지 나누어 보며 확인하면 충분합니다.
기본 풀이
from itertools import combinations
def is_prime(n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
def solution(nums):
answer = 0
for selected in combinations(nums, 3):
if is_prime(sum(selected)):
answer += 1
return answer
더 짧게 쓰기
from itertools import combinations
def is_prime(n):
return n > 1 and all(n % i != 0 for i in range(2, int(n ** 0.5) + 1))
def solution(nums):
return sum(1 for selected in combinations(nums, 3) if is_prime(sum(selected)))
문서의 질문처럼 이 문제는 “3개씩 묶고, 소수 정의로 확인하면 되는 문제”가 맞습니다. 다만 모든 3개 조합을 빠짐없이 만들어야 하므로 조합 도구를 쓰는 것이 실수를 줄여 줍니다.
4. 오픈채팅방: 최종 닉네임 장부를 먼저 만든다
문제 요약
record에는 입장, 퇴장, 닉네임 변경 기록이 들어 있습니다. 최종 출력 메시지에는 과거 메시지에도 최신 닉네임이 반영되어야 합니다. 그래서 한 번에 메시지를 만들지 말고, 먼저 아이디별 최종 닉네임 장부를 만든 뒤 메시지를 찍어내야 합니다.
핵심 아이디어: 투 패스
Enter와 Change 기록을 보며 uid → 최신 닉네임 딕셔너리를 완성합니다.
Enter와 Leave 기록만 메시지로 바꾸고, 닉네임은 첫 번째 순회에서 만든 장부에서 꺼냅니다.
실수한 코드
def solution(record):
answer = []
namespace = {}
printer = {'Enter': '님이 들어왔습니다.', 'Leave': '님이 나갔습니다.'}
for r in record:
rr = r.split(' ')
if rr[0] in ['Enter', 'Change']:
namespace[rr[1]] == rr[2]
for r in record:
rr = r.split(' ')
action = rr[0]
uid = rr[1]
if action != "Change":
answer.append(namespace[uid] + printer[action])
return answer
잘못된 점: ==와 =
namespace[rr[1]] == rr[2]는 “둘이 같니?”라고 비교만 하는 코드입니다. 딕셔너리에 값을 저장하려면 namespace[rr[1]] = rr[2]처럼 대입 연산자 =를 써야 합니다. 이 한 글자 때문에 장부가 비어 있고, 두 번째 반복문에서 KeyError가 날 수 있습니다.
수정한 최종 코드
def solution(record):
answer = []
namespace = {}
printer = {'Enter': '님이 들어왔습니다.', 'Leave': '님이 나갔습니다.'}
for r in record:
rr = r.split(' ')
if rr[0] in ['Enter', 'Change']:
namespace[rr[1]] = rr[2]
for r in record:
rr = r.split(' ')
action = rr[0]
uid = rr[1]
if action != 'Change':
answer.append(namespace[uid] + printer[action])
return answer
{}는 가능할까?
가능합니다. {}는 빈 딕셔너리를 만들 때 쓰고, {1, 2, 3}처럼 값을 넣으면 집합이 됩니다. 문자열 앞에 f를 붙인 f-string에서는 f"{name}"처럼 변수 값을 문자열 안에 넣을 때도 중괄호를 씁니다.
문서에 나온 최적화 포인트
두 번째 반복문에서 r.split(' ')을 여러 번 반복해서 쓰면 매번 문자열을 새로 쪼개야 합니다. 그래서 문서에서처럼 rr = r.split(' ')로 한 번만 쪼개고 action, uid 변수에 담아 재사용하는 편이 좋습니다.
5. 개미 군단: 가장 강한 개미부터 최대한 쓰기
문제 요약
장군개미는 공격력 5, 병정개미는 공격력 3, 일개미는 공격력 1입니다. 사냥감의 체력 hp를 맞추기 위해 필요한 최소 개미 수를 구합니다.
내 풀이
def solution(hp):
return hp // 5 + (hp % 5 // 3) + ((hp % 5) % 3)
초보자용 3단계 설명
hp // 5
가장 강한 장군개미를 몇 마리 쓸 수 있는지 먼저 구합니다. //는 몫입니다.
(hp % 5) // 3
장군개미가 처리하고 남은 체력을 병정개미로 얼마나 처리할 수 있는지 구합니다. %는 나머지입니다.
(hp % 5) % 3
장군개미와 병정개미를 쓰고도 남은 체력은 일개미가 처리합니다. 일개미 공격력은 1이므로 남은 체력 자체가 필요한 일개미 수입니다.
divmod()를 활용한 풀이
def solution(hp):
general, hp = divmod(hp, 5)
soldier, hp = divmod(hp, 3)
return general + soldier + hp
divmod(a, b)는 a를 b로 나눈 몫과 나머지를 한 번에 반환합니다. 그래서 몫은 개미 수로, 나머지는 다음 단계의 남은 체력으로 이어서 사용할 수 있습니다.
6. 컨트롤 제트: 방금 더한 값을 취소하려면 스택
문제 요약
문자열 s에는 숫자와 "Z"가 공백으로 구분되어 있습니다. 숫자는 더하고, "Z"가 나오면 바로 전에 더했던 숫자를 취소합니다.
처음 작성한 코드
def solution(s):
answer = 0
for i in s:
if i == is_int:
answer += i
else:
answer -= s[i-1]
return answer
힌트와 문제점
s.split()이 먼저다
for i in s를 쓰면 공백까지 포함해서 한 글자씩 순회합니다. "10 20 Z"를 제대로 다루려면 s.split()으로 ["10", "20", "Z"]처럼 나눠야 합니다.
is_int라는 내장 명령어는 없다
이 문제는 숫자인지 판별하기보다 i == "Z"인지 먼저 확인하는 쪽이 더 쉽습니다. 숫자를 더할 때는 int(i)로 바꿔야 합니다.
s[i-1]로는 취소를 안정적으로 처리하기 어렵다
i가 인덱스가 아니라 값이면 s[i-1] 자체가 불가능하고, 설령 인덱스로 바꿔도 이전 값이 "Z"일 수 있습니다. 이런 “방금 한 일을 취소”하는 문제는 스택이 맞습니다.
인덱스로 다시 시도한 코드
def solution(s):
answer = 0
for i in range(len(s)):
if s[i] != "Z":
answer += s[i]
else:
answer -= s[i-1]
return answer
이 코드도 그대로는 틀립니다. s를 글자 단위로 돌기 때문에 "10"이 "1"과 "0"으로 쪼개지고, answer는 숫자인데 s[i]는 문자열이라 더할 수 없습니다. 또한 "Z"가 나오면 직전에 실제로 더한 숫자를 취소해야 하므로 스택이 더 안정적입니다.
정석 풀이: 스택
def solution(s):
stack = []
for i in s.split():
if i != 'Z':
stack.append(int(i))
else:
stack.pop()
return sum(stack)
숫자면 append()로 넣고, "Z"면 pop()으로 가장 최근에 넣은 숫자를 꺼내 버립니다. 이 흐름이 바로 스택입니다.
map은 왜 안 될까?
sum(map(int, s.split()))처럼 쓰고 싶을 수 있지만, 이 문제에서는 적합하지 않습니다. 첫째, "Z"는 int("Z")로 바꿀 수 없어 에러가 납니다. 둘째, map은 각 값을 독립적으로 변환할 뿐, 직전에 넣은 값을 기억했다가 취소하는 기능이 없습니다. 이 문제처럼 과거 기록을 지워야 할 때는 스택을 써야 합니다.
7. 입출력 예로 빠르게 검산하기
| 문제 | 예시 입력 | 기대 결과 |
|---|---|---|
| 과일 장수 | k=3, m=4, score=[1,2,3,1,2,3,1] |
8 |
| 소수 만들기 | nums=[1,2,7,6,4] |
4 |
| 오픈채팅방 | ["Enter uid1234 Muzi", "Enter uid4567 Prodo", ...] |
["Prodo님이 들어왔습니다.", "Ryan님이 들어왔습니다.", ...] |
| 개미 군단 | hp=23 |
5 |
| 컨트롤 제트 | "1 2 Z 3" |
4 |
8. 실수 방지 체크리스트
i는 위치 번호입니다. 배열 안의 값을 쓰려면 arr[i]처럼 접근해야 합니다.
과일 장수에서는 상자를 채우지 못한 사과를 버립니다. 반복 범위를 넉넉하게 잡으면 오답이나 에러가 날 수 있습니다.
=와 ==를 구분한다
딕셔너리에 값을 저장할 때는 대입 연산자 =를 써야 합니다.
split()할지 확인한다
공백으로 구분된 숫자 문자열은 글자 단위가 아니라 토큰 단위로 다뤄야 합니다.
append()로 넣고 pop()으로 빼는 패턴은 컨트롤 제트 같은 문제에 잘 맞습니다.
9. 같이 보면 좋은 글
마무리
이번 문서는 한 가지 문법만 반복하는 글이 아니라, 여러 문제를 통해 자료구조 선택을 연습하는 기록이었습니다. 과일 장수와 개미 군단은 그리디, 소수 만들기는 조합과 소수 판별, 오픈채팅방은 딕셔너리, 컨트롤 제트는 스택이 핵심이었습니다.
특히 “내가 지금 다루는 값이 인덱스인지 실제 값인지”, “방금 처리한 기록을 나중에 다시 취소해야 하는지”, “최종 상태를 먼저 만들어야 하는지”를 구분하는 감각이 중요했습니다. 이 감각이 쌓이면 문제를 봤을 때 어떤 도구를 꺼내야 할지 훨씬 빠르게 보입니다.
'AI > Coding_Test' 카테고리의 다른 글
| Day 9) 프로그래머스 문자열 문제: 리스트 컴프리헨션, zip, 슬라이싱, 접미사 풀이 정리 (0) | 2026.07.14 |
|---|---|
| Day 8) 프로그래머스 조건문·문자열: 논리 연산, 주사위 게임 3, 문자열 만들기, 9로 나눈 나머지 (0) | 2026.07.13 |
| Day 7) 프로그래머스 반복문: 구간 쿼리, 배열 만들기, 콜라츠, 스택 풀이 정리 (0) | 2026.07.12 |
| Day 6)Programmers Coding Test (0) | 2026.07.11 |
| Day5) Programmers Coding Test (0) | 2026.07.11 |