20260514 오늘의 학습: 6회차 풀타임 모의고사와 7문제 식 검증

Yesol Lee·2026년 5월 14일

COS Python

목록 보기
29/30

지난 학습 요약

27차(5/13)에서 구름EDU 기출 4회차를 73분에 풀어 8/10 통과했고, 통과한 답에서도 식 버그가 2건 발견된 "테스트 통과 but 식 버그" 패턴을 확인했다. 재귀 멘탈 모델·시계 각도 공식·자릿수 분해 패턴·itertools.permutations·max(default=-1) 다섯 가지를 신규 학습했다. 함수 작성 6세션 연속 자력 정답 추세를 유지한 상태였다.

오늘 수업 계획

구름EDU 기출 6회차를 90분 풀타임 모의고사로 진행한 뒤, 자력으로 통과한 문제까지 포함해 7문제를 검산 모드로 복기했다. 어제 발견한 "테스트 통과 but 식 버그" 패턴을 한 번 더 의식하면서 본인이 통과처럼 보이는 답에 자가 의심을 끼워 넣는 흐름으로 진행했다.

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


모의고사 결과

항목결과
소요 시간약 77분 (90분 중 13분 여유)
자력 풀이9/10 (1번 꽃피는 봄에서 막힘)
추정 점수800점+ 안정 (합격선 600 한참 위)

자력 통과 5문제(4·5·8·9·10), 자가 의심 후 자력 수정 3문제(2·3·7), 힌트 후 자력 해결 2문제(1·6)로 정리됐다. 막힌 1번에 시간을 더 끌지 않고 8·9·10번을 자력으로 마무리한 시험장 판단력은 4회차와 일관됐다.


학습 내용 정리

1번: 꽃피는 봄 — 시뮬레이션 동기성 함정

n×n 격자에서 핀 꽃(1)이 매일 상하좌우 4방향으로 한 칸씩 퍼진다. 모든 칸이 필 때까지 며칠이 걸리는지 구한다.

처음 작성한 코드:

def solution(n, garden):
    answer = 0
    bloom = garden[::]                    # 얕은 복사
    dr = [-1, 0, 1, 0]
    dc = [0, 1, 0, -1]

    while any(any(v == 0 for v in row) for row in bloom):
        for i in range(n):
            for j in range(n):
                if bloom[i][j]:
                    for d in range(4):
                        ni, nj = i + dr[d], j + dc[d]
                        if 0 <= ni < n and 0 <= nj < n:
                            bloom[ni][nj] = 1     # 같은 bloom을 읽으면서 수정
        answer += 1
    return answer

핵심 버그: bloom을 읽으면서 같은 bloom을 수정하기 때문에, 같은 날 안에서 갓 핀 꽃이 또 다음 칸을 피우는 연쇄가 일어난다. 예시 1을 한 번 따라가보면 다음과 같다.

초기:        1일째 끝나야 할 모습:
0 0 0        0 1 0
0 1 0   →    1 1 1
0 0 0        0 1 0

(1,1)만 1이니 4방향 4칸만 1로 바뀌어야 한다. 그런데 위 코드는 루프가 (1,1)을 처리한 뒤 (1,2)에 도착했을 때 방금 1로 바뀐 (1,2)를 또 보고 (0,2), (2,2)까지 1로 만들어버린다. 결국 한 번의 while 반복으로 거의 모든 칸이 피어버린다.

해결 — 오늘 시점 스냅샷과 내일 결과를 쓸 곳을 분리한다.

def solution(n, garden):
    answer = 0
    bloom = [row[:] for row in garden]      # 2차원 깊은 복사
    dr = [-1, 0, 1, 0]
    dc = [0, 1, 0, -1]

    while any(any(v == 0 for v in row) for row in bloom):
        today = [row[:] for row in bloom]   # 오늘의 스냅샷
        for i in range(n):
            for j in range(n):
                if today[i][j]:             # 읽기는 today
                    for d in range(4):
                        ni, nj = i + dr[d], j + dc[d]
                        if 0 <= ni < n and 0 <= nj < n:
                            bloom[ni][nj] = 1   # 쓰기는 bloom
        answer += 1
    return answer

2차원 깊은 복사 패턴

코드결과
bloom = garden같은 객체 참조 (수정 동기화)
bloom = garden[::] 또는 garden[:]외부 리스트만 복사, 내부 행은 공유 (얕은 복사)
bloom = [row[:] for row in garden]행까지 새로 복사 (깊은 복사)

2차원 배열을 안전하게 복사하려면 무조건 행 단위 컴프리헨션 패턴을 써야 한다. 시뮬레이션 문제에서 매우 자주 나오니 외워두면 좋다.

슬림화 포인트

처음에 if all(all(v == 1 for v in row) for row in garden): return 0 같은 초기 분기를 넣어뒀었는데, 아래의 while any(any(v == 0 ...))가 이미 이 케이스를 처리한다. 전부 1이면 while 진입을 안 하고 answer = 0 그대로 반환한다.

"이 분기, 빼도 답 같지 않은가?"

23차에 짚었던 슬림화 자기 점검 질문이다. 이번엔 피드백 1회 후 곧장 자력 삭제로 정리됐다.


2번: 단어 순서대로 적기 — 공백을 두 번 세지 않기

한 줄에 K자를 적을 수 있는 메모장에 영어 단어를 순서대로 적는다. 단어 사이는 공백 1칸으로 구분하며, 줄 끝에 단어가 잘리지 않도록 다음 줄로 넘긴다. 단어 배열을 모두 적었을 때 몇 줄이 되는지 구한다.

처음 작성한 코드:

def solution(K, words):
    answer = 0
    current = 0
    for word in words:
        if (current + len(word) + 1) // K > 0:    # 줄바꿈 판단
            answer += 1
            current = len(word)
        else:
            current += len(word) + 1
    return answer

수업 중 질문: 글자 사이 공백을 잘 고려했는지 모르겠다고 했다.

자가 의심이 정확했다. 통과 답에 식 버그가 있다.

버그 1 — 마지막 줄 카운트 누락

answer는 줄바꿈이 일어날 때만 증가한다. 마지막 단어가 같은 줄에 들어가면(else) 그 줄은 세지 않고 끝난다.

solution(10, ["nice"])
# 정답: 1줄, 코드 결과: 0

버그 2 — 공백을 두 번 카운트

첫 단어 nice를 처리할 때 current = 0 + 4 + 1 = 5로 저장한다. nice 뒤에 공백 한 칸을 미리 붙인 형태다. 그런데 다음 단어 c를 추가할 때 또 current + len(word) + 1로 공백을 더한다.

K=5, ["ab", "c", "d"]로 따라가보면:

단계current판단코드 결과사람 직관
ab 추가0 → 3같은 줄"ab_" 저장"ab"
c 추가(3+1+1)//5=1 → 줄바꿈c 새 줄"ab_" 끝 / "c""ab c" 같은 줄 (4칸)
d 추가(1+1+1)//5=0 → 같은 줄"cd"정답 1줄? 2줄?

정답은 2줄 (ab_c 4칸 한 줄, d 한 줄)인데 코드는 1을 반환한다. 공백이 한 번 들어가야 할 자리에 두 번 카운트돼서 발생한 어긋남이다.

수정 코드

def solution(K, words):
    answer = 1
    current = 0
    for word in words:
        if current == 0:                              # 첫 단어
            current = len(word)
        elif current + len(word) + 1 > K:             # 줄바꿈 필요
            answer += 1
            current = len(word)
        else:
            current += len(word) + 1                  # 같은 줄에 공백 + 단어
    return answer

세 가지가 동시에 해결된다.

  1. answer = 1로 시작 — 단어가 있으면 줄은 무조건 1개 이상
  2. 첫 단어 분기 — 공백 없이 단어만 더함
  3. <= K 대신 > K로 명시적 비교 — // 같은 트릭 없이 직관적

3번: 큰수와 작은수의 차이 — combinations은 함정이다

자연수 배열 arr에서 K개를 골라 (가장 큰 수 - 가장 작은 수) 차이가 최소가 되도록 한다. 처음 작성한 코드:

from itertools import combinations

def solution(arr, K):
    coms = set(combinations(arr, K))
    diff = 9999
    for tp in coms:
        diff = min(max(tp) - min(tp), diff)
    return diff

수업 중 질문: 숫자 4개를 순서 상관없이 뽑아야 해서 combinations을 떠올렸는데, arr_len이 최대 1000까지 될 수 있다는 제약을 보고 시간복잡도가 괜찮을지 걱정된다고 했다.

자가 의심이 정확했다. C(1000, 50) ≈ 9 × 10⁸⁵는 우주의 원자 수(약 10⁸⁰)보다 많다. 메모리도, 시간도 절대 못 버틴다.

핵심 인사이트 — 정렬된 배열에서 K개 뽑을 때 차이 최소화는 무조건 연속된 K개다

예: 정렬된 [4, 6, 9, 9, 11, 19]에서 K=4

뽑은 4개max - min
[4, 6, 9, 9] (연속)5
[6, 9, 9, 11] (연속)5
[9, 9, 11, 19] (연속)10
[4, 6, 9, 11] (비연속)7

비연속으로 뽑으면 어딘가 더 먼 수가 끼게 되니 차이가 같거나 더 커진다. 그래서 정렬 + 슬라이딩 윈도우로 풀면 된다.

수정 코드

def solution(arr, K):
    arr_tmp = sorted(arr)                                 # 원본 보존
    answer = arr_tmp[K-1] - arr_tmp[0]                    # 첫 윈도우 초기값
    for i in range(1, len(arr_tmp) - K + 1):              # 둘째 윈도우부터
        diff = arr_tmp[i + K - 1] - arr_tmp[i]
        answer = min(answer, diff)
    return answer

시간복잡도 O(n log n). 정렬 비용이 전부다.

한 코드에 세 가지 정착 패턴이 동시 발현

패턴정착 회차본 코드에서
슬라이딩 윈도우 첫 윈도우 미리 계산20차 자기 진단 약점answer = arr_tmp[K-1] - arr_tmp[0] 자력 작성
range 시작값을 1로 (둘째 윈도우부터)14차 약점range(1, ...) 자력 작성
원본 보존 (sorted 사용, sort()는 원본 변경)16차 정착sorted(arr) 자력 선택

4번: 카드 섞기 — 인덱스 디버깅 한 줄

1부터 n까지 적힌 카드 뭉치를 반으로 나눠 교대로 섞는 과정을 mix번 반복한 뒤, 아래에서 k번째 카드를 구한다. 디버깅 유형(한 줄 수정)이고 본인이 자력으로 정답을 찾았다.

원본 버그:

card_a, card_b = [0 for _ in range(n//2)], [0 for _ in range(n//2)]
for i in range(0, n):
    if i < n//2:
        card_a[i] = card[i]
    else:
        card_b[i] = card[i]   # 버그: card_b 크기는 n//2인데 i가 n//2~n-1 들어감

본인 진단: "후반 1/2를 저장하는 배열 b에 인덱스를 원본 배열 그대로 넣어서 에러가 나는 것으로 보임."

card_b의 크기는 n//2(=3)인데 i가 3, 4, 5일 때 card_b[i]로 접근하면 IndexError가 난다.

수정: card_b[i - (n//2)] = card[i]

i=3 → card_b[0], i=4 → card_b[1], i=5 → card_b[2]로 후반부를 0부터 다시 채운다. 한 라인 안에서 두 토큰(i와 괄호 보정)을 동시에 바꾼 디버깅이지만 "한 줄 수정" 제약은 코드 라인 단위라 그대로 통과한다.


5번: 코인 획득 — DP 자력 발현

4×4 격자의 가장 왼쪽 위에서 가장 오른쪽 아래까지 오른쪽 또는 아래로만 이동하면서 코인을 최대로 모은다. 디버깅 한 줄 유형이고 본인이 자력 정답을 찾았다.

원본 버그:

else:
    coins[i][j] = board[i][j] + max(coins[i][j], coins[i-1][j-1])
    # max 안: 자기 자신(0) vs 대각선(도달 불가)

수정: max(coins[i-1][j], coins[i][j-1])

오른쪽 또는 아래로만 이동하니까 (i, j)에 도달하기 직전 위치는 위 (i-1, j) 또는 왼쪽 (i, j-1)이다. 그중 큰 누적값을 골라 현재 칸 값에 더한다.

coins[0]: [6, 13, 14, 16]
coins[1]: [9, 18, 21, 30]
coins[2]: [15, 22, 27, 32]
coins[3]: [22, 25, 29, 38]   ← 정답 38

26차에 학습한 DP 마지막 동작 패턴이 자력 발동한 형태다. 26차에선 힌트가 필요했는데 이번엔 디버깅 문제에서 의도를 스스로 짚어냈다.


6번: 만남 최대 — 한 라인 안 인덱스 4토큰 보정

4×4 종이를 가로 또는 세로축에 평행한 격자선을 따라 한 번 접었을 때, 만나는 두 칸의 합이 최대가 되도록 한다. 디버깅 한 줄 유형이다.

원본 코드:

for i in range(4):
    for j in range(4):
        for k in range(j + 1, 4, 2):    # step 2
            answer = max(answer,
                         max(grid[i][j] + grid[j][k],
                             grid[i][j] + grid[k][i]))   # 9번 라인

수업 중 질문: 맨 안쪽 for의 step 2가 처음엔 이해되지 않아 숫자를 대입해보면서 의미를 파악하려고 했다고 했다.

step 2의 의미 — (j, k) 쌍은 한 번 접으면 만날 수 있는 두 인덱스 쌍을 만든다. 합이 홀수일 때만 가능하다.

jrange(j+1, 4, 2)(j, k) 후보j+k
0[1, 3](0,1), (0,3)1, 3
1[2](1,2)3
2[3](2,3)5
3[]——

j+k가 짝수이면 접는 선이 칸 한가운데에 떨어져 물리적으로 접을 수 없다. step 2가 이 제약을 자동으로 걸러낸다.

9번 라인의 진짜 버그 — 인덱스가 어긋남

(j, k) 쌍을 받은 다음, 어떻게 활용해야 만나는 두 칸이 되는지 정리하면 다음 두 경우다.

접는 방향두 칸 좌표공통 조건
세로 접기 (같은 행에서)(i, j) ↔ (i, k)행 같음
가로 접기 (같은 열에서)(j, i) ↔ (k, i)열 같음

원본 코드는:

  • grid[i][j] + grid[j][k] — (i, j)와 (j, k)는 같은 행도 같은 열도 아니다. 행이 어긋남
  • grid[i][j] + grid[k][i] — (i, j)와 (k, i)도 마찬가지로 열이 어긋남

수정: grid[i][j] + grid[i][k], grid[j][i] + grid[k][i]

한 라인 안에서 4개 토큰(j→i, j→i, i→j, i→j)을 동시에 바꿔야 한다. 4번 카드 섞기에 이어 한 라인 안 다중 토큰 보정이 2회 연속 성공한 형태다.


7번: UP AND DOWN — "처음 가설과 max를 합쳤어야 했다"

출제자가 1~K 중 하나의 자연수를 정한다. 참가자가 수를 부르면 출제자는 자기 수보다 작을 때 "UP", 클 때 "DOWN", 같을 때 "RIGHT"이라고 한다. 주어진 게임 진행에서 현재 정답이 될 수 있는 숫자가 몇 개인지 구하는 빈칸 채우기 문제다.

본인이 채운 풀이:

def solution(K, numbers, up_down):
    left = 1
    right = K
    for num, word in zip(numbers, up_down):
        if word == "UP":
            left = max(num, left)
        elif word == "DOWN":
            right = min(num, right)
        elif word == "RIGHT":
            return 1
    return right - left - 1

본인의 풀이 흐름 — 처음엔 left = num + 1, right = num - 1로 정확히 1씩 보정하려 했다. 그런데 예시 2번이 "2 UP" 다음 "1 UP"으로 들어와서 단순 대입을 했더니 left가 3에서 2로 회귀하면서 망가졌다고 했다. 그래서 +1을 빼고 max(num, left)로 바꾼 뒤, 마지막에 right - left - 1로 양 끝을 빼는 방식으로 우회했다고 했다.

자가 분석이 정확한 방향이었지만 검증 시 한 가지가 더 어긋난다.

반례

K = 10
numbers = [5]
up_down = ["DOWN"]
# 정답: 1, 2, 3, 4 → 4개
# 코드: right = 5, left = 1 → 5 - 1 - 1 = 3 (틀림)

right - left - 1은 left와 right가 둘 다 후보 바깥 경계값이어야 의미가 맞는 공식인데, 초기값 left=1, right=K는 본인이 후보가 될 수 있는 값들이다. left가 한 번도 안 바뀌면 1이, right가 한 번도 안 바뀌면 K가 후보에서 빠져버린다.

해법 — 빈칸 채우기라 초기값을 못 바꾸므로 max에 +1을 다시 끼워 넣는다

if word == "UP":
    left = max(num + 1, left)            # +1 다시 추가
elif word == "DOWN":
    right = min(num - 1, right)          # -1 다시 추가
elif word == "RIGHT":
    return 1
return right - left + 1                  # 양 끝 포함

max(num + 1, left)로 감싸면 "2 UP 다음 1 UP" 같은 약한 정보도 자연히 무시된다. left가 3에서 1+1=2로 후퇴할 일이 없다 (max로 보호되니까).

검산:

solution(10, [5], ["DOWN"])
# right = min(4, 10) = 4
# left = 1
# 4 - 1 + 1 = 4  ✓

수업 중 메타 정리 (본인 발화): "처음 생각한 거랑 max 아이디어 합쳤어야 했네."

이게 시험장 디버깅의 핵심 사고다. 첫 가설 +1/-1이 틀린 게 아니라 단순 대입 때문에 깨졌던 것이고, max 도입이 그 깨짐의 보호막이었다. 가설을 통째로 버릴 게 아니라 깨진 원인만 찾아 합치면 답이 나온다.


"테스트 통과 but 식 버그" 누적 3건

27차에 2건(시계, 소수), 오늘 7번 UP/DOWN으로 누적 3건이 됐다. 모두 본인이 통과 처리받은 답에 식 자체의 버그가 숨어있던 케이스다.

회차문제어떤 입력에서 깨지나
27차 9번시계 각도m=0과 m=30 외 모든 분
27차 10번소수 세제곱a > 8 또는 b 큰 경우
28차 7번UP/DOWN한 방향만 들어오는 입력 ([5], DOWN 등)

오늘 의미 있게 달라진 건, 2번·3번·7번에서 본인이 통과한 답에 자가 의심을 자발적으로 발화했다는 점이다.

  • 2번: "공백을 잘 고려했는지 모르겠음"
  • 3번: "arr_len 1000인데 시간복잡도 괜찮을까?"
  • 7번: 풀이 흐름 메타 정리

자가 의심이 자발적으로 작동하기 시작하면, 같은 함정이 시험장에서도 잡힌다. 풀이 후 한 박자만 두고 극단 입력 하나만 상상하는 습관이 D-2 환기 포인트다.

상상해볼 극단 입력잡히는 함정
빈 입력 / 1개짜리 입력마지막 줄 카운트 누락, 초기값 모순
한 방향만 들어오는 시퀀스한쪽 변수가 업데이트 안 되는 경우
최대 크기 입력TLE, 메모리 초과
경계값 (a=b, K=1, 단일 원소)off-by-one, 빈 슬라이스

오늘의 결과

구름EDU 6회차 풀타임 모의고사 77분에 9문제 자력 풀이, 추정 800점대 안정으로 합격선 한참 위였다.
시뮬레이션 동기성 패턴(2차원 깊은 복사 포함)을 신규 학습했고, 슬라이딩 윈도우·DP·슬림화·한 라인 다중 토큰 보정 4가지의 자력 발현/정착을 확인했다.
내일은 5차 기출 미해결 구현 3개와 구현 집중 연습을 진행할 예정이다.

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

0개의 댓글