20260513 오늘의 학습: 4회차 풀타임 모의고사와 복기

Yesol Lee·2026년 5월 13일

COS Python

목록 보기
28/30

지난 학습 요약

26차(5/12)에서 5차 기출 7문제 스터디 풀이를 복습하고 오답 총복습을 진행했다. DP 마지막 동작 패턴과 range 역순 3가지 방식을 정리했고, 90도 회전·스택·값 기반 중복 체크 함정의 정착을 재확인했다. D-4 기준 주요 약점 없이 90분 풀타임 모의고사 단계로 진입한 상태였다.

오늘 수업 계획

구름EDU 기출 4회차를 90분 풀타임 모의고사로 진행한 뒤, 통과했지만 찝찝한 문제와 못 푼 2문제를 검산 모드로 복기했다. 시험 환경을 흉내내기 위해 검색·AI 도움 없이 90분 안에 풀어내는 형태로 진행했다.

COS Pro 1급 Python / Phase 3: 실전 대비


모의고사 결과

항목결과
소요 시간73분 (90분 내)
통과 문제8/10
못 푼 문제6번 자아도취 수(빈칸), 8번 n번째 작은 수(함수 작성)
추정 점수800점+ (합격선 600 한참 위)

못 푼 2개에 시간을 더 매달리지 않고 마무리한 것이 시험장 판단력 측면에서 의미가 있었다. 실제 시험에서도 모르는 문제에 30분 매달리는 것보다 손절하고 다른 문제로 넘어가는 것이 합격선 확보에 유리하다.


학습 내용 정리

1번: 재귀 — 함수가 자기 자신을 호출하는 형태

A, E, I, O, U 다섯 알파벳으로 만들 수 있는 길이 1~5의 모든 단어를 사전 순으로 나열했을 때, 주어진 word의 위치를 return하는 문제다. 한 줄을 수정하는 디버깅 유형이다.

def create_words(lev, s):
    global words
    VOWELS = ['A', 'E', 'I', 'O', 'U']
    words.append(s)
    for i in range(0, 5):
        if lev < 5:
            create_words(lev, s + VOWELS[i])   # 버그: lev 그대로 0

버그: lev가 계속 0이라 lev < 5가 영원히 True → 무한 재귀가 발생한다.

수정: create_words(lev + 1, s + VOWELS[i]) — 다음 호출에 깊이 한 단계 더한 값을 인자로 넘긴다.

수업 중 질문: 함수 안에서 자기 자신을 호출하는 형태 자체가 어렵다고 했다. lev += 1을 다른 줄에 넣어봤더니 결과가 달라서 이해되지 않았다고 했다.

재귀의 핵심 멘탈 모델

각 호출은 자기만의 독립된 lev, s를 가진다. create_words(lev+1, ...)새로운 호출에 새 값을 인자로 넘기는 것이지, 위쪽 호출의 lev를 바꾸는 게 아니다.

create_words(0, '')        ← 이 호출의 lev는 0, 영원히 0
   ↓ 새 호출 만듦
   create_words(1, 'A')    ← 이 호출의 lev는 1, 위의 0과 별개
      ↓
      create_words(2, 'AA') ← lev=2, 또 별개

Java로 비유하면 return sum(n-1) + n 같이 새 값을 인자로 넘기는 형태와 같다. 위쪽 호출의 n을 바꾸는 게 아니다.

lev += 1을 다른 줄에 넣으면 어떻게 되는가

def create_words(lev, s):
    words.append(s)
    lev += 1                    # 여기 넣으면 길이 5짜리가 누락된다
    for i in range(0, 5):
        if lev < 5:
            create_words(lev, s + VOWELS[i])

lev=4로 진입(s='AAAA')하면 lev += 1로 lev=5가 되고 if lev < 5가 False가 된다. 자식 호출이 0번이라 'AAAAA' ~ 'AAAAU' 다섯 단어가 누락된다. 그래서 결과가 달랐던 것이다.

재귀 빈칸/디버깅 체크리스트

  1. 종료 조건이 있는가 — 여기선 if lev < 5
  2. 호출할 때 값이 진행하는가lev+1, s + 글자처럼 뭔가 한 칸 나아가야 한다. 안 그러면 무한 루프다
  3. 각 호출은 독립 — 인자 새로 넘기는 거지 바깥 호출 변수를 바꾸는 게 아니다

9번: 시계 시침과 분침의 각도 — 외우는 공식

hour:minute일 때 아날로그 시계의 시침과 분침이 이루는 각도를 구하는 문제다. 본인이 제출한 답:

def solution(hour, minute):
    h_angle = hour / 12 * 360 % 360
    m_angle = (60-minute) / 60 * 360 % 360
    answer = max(h_angle, m_angle) - min(h_angle, m_angle)
    return "{:.1f}".format(answer)

제출은 통과했지만 식에 버그가 3개 있다. 테스트가 약해서 우연히 통과한 케이스다.

버그 1: 시침의 분 기여도 누락

3:30일 때 시침은 3을 정확히 안 가리키고 3과 4 사이에 있다. 분침이 한 바퀴 돌 때마다 시침도 30° 움직이므로, 시침에 minute * 0.5를 더해야 한다.

3:30 시침
hour / 12 * 36090° (틀림)
hour * 30 + minute * 0.5105° (맞음)

버그 2: 분침이 반대로 돈다

(60-minute)/60 * 360은 분침을 반시계 방향으로 돌린다.

본인 식정답 minute * 6
000
1527090
30180180
4590270

m=0과 m=30에서만 우연 일치한다. 4회차 테스트가 이 두 케이스만 있어서 통과한 것으로 보인다.

버그 3: 큰 각도 처리 누락

9:00일 때 시침 270°, 분침 0°. 보통 "이루는 각"은 작은 쪽(90°)을 답한다. 본인 식은 270을 반환한다. min(diff, 360 - diff)로 마무리해야 안전하다.

정답 코드와 외울 공식

def solution(hour, minute):
    h_angle = hour * 30 + minute * 0.5
    m_angle = minute * 6
    diff = abs(h_angle - m_angle)
    answer = min(diff, 360 - diff)
    return "{:.1f}".format(answer)
공식내용
시침 각도hour * 30 + minute * 0.5
분침 각도minute * 6
두 침 사이 각min(\|h-m\|, 360-\|h-m\|)

이 유형은 공식을 모르면 풀 수 없다. 외워두는 게 답이다.


6번: 자아도취 수 — 자릿수 분해 패턴

세 자리 자아도취 수: 153 = 1³ + 5³ + 3³. 각 자릿수를 k 제곱해서 더한 값이 원래 수와 같은 자연수다. 빈칸 2개를 채우는 문제였는데 풀지 못했다.

while current != 0:
    calculated += power(current % 10, k)   # 빈칸 1
    current //= 10                          # 빈칸 2

핵심 패턴 — % 10 + // 10 한 쌍

연산의미
n % 10마지막 자리 한 개 추출
n // 10마지막 자리 한 개 제거

이 두 개가 짝지어 돌아가야 자릿수 분해가 된다. 153 추적:

currentcurrent % 10calculated 누적current //= 10
15330 → 27 (3³)15
15527 → 1521
11152 → 1530
0(탈출)

수업 중 질문: while 조건이 current > 0이 아니고 != 0이라서 빈칸에 탈출 조건을 만들어야 하는데 어떻게 만들어야 할지 모르겠다고 했다.

이게 가장 중요한 오해 지점이었다. 탈출 조건을 빈칸에 넣는 게 아니다. current //= 10이라는 데이터 변형이 매 반복마다 current를 줄여 결국 0에 도달하게 만들고, 그 결과로 current != 0이 자연히 False가 되는 것이다. 탈출은 데이터 변형의 부산물이지 빈칸의 목적이 아니다.

100부터 시작해도 → 10 → 1 → 0이 되니까 3번 만에 탈출한다. 999부터 시작해도 → 99 → 9 → 0이라 마찬가지로 3번이다.

current % 100을 끝 두 자리(53)로 가져온다는 점도 함께 짚었다. 한 자리만 뽑으려면 무조건 % 10이다.

자주 나오는 응용:

  • 자릿수 합 구하기
  • 자릿수 뒤집기 (12345 → 54321)
  • 한수, 자아도취 수, 회문수
  • 진수 변환 (10진 → 2진 등)
# 외워둘 자릿수 분해 템플릿
while n != 0:
    digit = n % 10
    # digit으로 뭔가 처리
    n //= 10

8번: n번째로 작은 수 — itertools.permutations

숫자 카드 배열로 만들 수 있는 모든 수를 작은 순으로 정렬했을 때, n이 몇 번째인지 구하는 문제다.

본인 시도는 Counter로 카드와 n의 자릿수를 비교해 다르면 -1을 반환하는 부분까지 진행했고, 그 뒤가 막혔다. 핵심 도구는 itertools.permutations다.

from itertools import permutations
from collections import Counter

def solution(card, n):
    digits = [int(c) for c in str(n)]
    if Counter(card) != Counter(digits):
        return -1
    perms = sorted(set(permutations(card)))
    target = tuple(digits)
    return perms.index(target) + 1

combinations vs permutations

도구의미예: [1,2,3]에서 2개
combinations순서 무관 조합(1,2), (1,3), (2,3)
permutations순서 있는 순열(1,2), (1,3), (2,1), (2,3), (3,1), (3,2)

10차에서 배운 combinations의 동생격이다.

set이 필요한 이유: permutations는 위치(인덱스) 기반이라 [1,2,1,3]처럼 같은 숫자가 있으면 동일한 순열이 여러 번 나온다. 첫 번째 1(card[0])과 두 번째 1(card[2])을 다른 카드로 취급하기 때문이다. 값으로 봤을 땐 같은 순열이니 set()으로 묶어야 정확한 등수가 나온다.


응용 연습: 가장 큰 짝수 만들기

permutations를 손에 익히기 위해 출제된 응용 문제다.

from itertools import permutations

def solution(cards):
    digits = []
    for perm in set(permutations(cards)):
        num = int(''.join(map(str, perm)))
        if num % 2 == 0:
            digits.append(num)
    return max(digits, default=-1)

7개 테스트 케이스 모두 통과했다. 첫 학습 직후 응용 문제를 자력으로 푼 경우다.

튜플 → 정수 변환: int(''.join(map(str, perm))) 패턴이다. (3, 1, 2)를 312로 만든다.

max(default=-1): max는 빈 리스트에서 에러를 내는데, default 인자로 빈 시퀀스일 때 반환할 값을 지정할 수 있다.

수업 중 슬림화 포인트: 처음에 sorted(set(permutations(cards)))로 정렬했지만, max()로 최댓값만 뽑을 거면 정렬이 필요 없다. 정렬은 O(n log n) 비용이 들지만 결과엔 영향이 없다.

"이 연산 정말 필요한가? 빼도 결과 같지 않은가?"

함수 작성 후 자기 점검 단계로 들어가야 할 메타 질문이다. 23차에 짚었던 슬림화 의식 영역으로, 27차에 1회 재발했다.


10번: 소수의 세제곱 — "통과해도 식 버그" 패턴

a 이상 b 이하 자연수 중 소수의 제곱수와 세제곱수의 개수의 합을 구하는 문제다. 본인이 제출한 답:

def solution(a, b):
    answer = 0
    nums = [2]                            # 소수 2를 미리 넣음
    for n in range(3, b+1):
        i = 2
        for j in range(i, n):
            if n % j == 0:
                break
            if j == n-1:
                if a<=n*n<=b:
                    nums.append(n)
                if a<=n*n*n<=b:
                    nums.append(n)
    answer = len(nums)
    return answer

수업 중 질문: 소수 2를 미리 넣고 3부터 보기 시작하는 방식이 괜찮은지 물어봤다.

답은 "버그다. 테스트가 약해서 통과한 것"이다. nums = [2]로 2를 무조건 카운트에 포함하지만, 2² (=4) 또는 2³ (=8)이 [a,b] 범위에 있는지 확인하지 않는다.

ab정답본인 코드
6, 3044 (우연 일치)
10, 3023 (2 헛카운트)
50, 10001 (2 헛카운트)

근본 원인은 소수 판별 루프가 range(2, n)이라 n=2에선 inner loop가 비어버려 일반 루프에 못 넣은 것이다. 소수 판별을 함수로 분리하면 자연 처리된다.

수업 중 잘한 부분: 처음에 13라인과 15라인을 or로 연결했다가 답이 달라서 예시(3은 제곱과 세제곱 모두 범위 안)를 보고 분리한 부분이다. or로 묶으면 한 번만 카운트되지만 별도 if로 분리하면 둘 다 만족할 때 두 번 카운트된다. 직접 예시를 추적해서 검증한 것은 24차에 정착된 검증 습관이 지속되고 있는 신호다.

추가 이슈 — 시간복잡도

b는 최대 10⁹까지 가능한데 for n in range(3, b+1)로 루프를 돌면 절대 끝나지 않는다.

n² ≤ b이려면 n ≤ √b면 충분하니까 루프 상한을 √b로 줄일 수 있다.

b=10⁹여도 √b ≈ 31,623으로 한순간이다. 23차에 배운 √n 최적화를 소수 판별 자체에도 적용해야 한다.

정답 코드:

def is_prime(n):
    if n < 2:
        return False
    if n == 2:
        return True
    if n % 2 == 0:
        return False
    for i in range(3, int(n**0.5) + 1, 2):
        if n % i == 0:
            return False
    return True

def solution(a, b):
    answer = 0
    limit = int(b ** 0.5) + 1
    for n in range(2, limit + 1):
        if is_prime(n):
            if a <= n * n <= b:
                answer += 1
            if a <= n * n * n <= b:
                answer += 1
    return answer

메타: "테스트 통과해도 식 자체는 틀릴 수 있다"는 패턴

오늘 4회차에서 두 건 발견했다.

  • 9번 시계: m=0, m=30 케이스에서만 우연 일치
  • 10번 소수: a ≤ 8 또는 작은 b에서만 우연 일치

19차에서 짚었던 "주어진 테스트 전부 통과지만 반례에서 깨짐" 패턴이 이번엔 본인이 통과 처리받은 답에서 재발한 형태다. 실제 시험은 테스트가 더 엄격할 수 있으니, 풀이 후 반례를 직접 상상해보는 습관이 필요하다.

10번 nums=[2]에 대해 "이래도 되는걸까?"라고 자가 의심한 것 자체는 좋은 시작이다. 통과한 답에도 의심 가능한 메타인지가 발현된 것이다. 시험장에서는 시간 제약 때문에 모든 답을 의심할 순 없지만, 핵심 식만큼은 "반례 한 개 떠올려보기"를 의식적으로 끼워 넣자.


오늘의 결과

구름EDU 4회차 풀타임 모의고사 73분에 8/10 통과, 추정 800점대로 합격선 한참 위였다.
신규 학습 5건(재귀 멘탈 모델, 시계 각도 공식, 자릿수 분해 패턴, itertools.permutations, max default 인자)을 정리했다.
내일 6회차 풀타임 모의고사 예정이다.

profile
문서화를 좋아하는 개발자

0개의 댓글