<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>lee_yesol421.log</title>
        <link>https://velog.io/</link>
        <description>문서화를 좋아하는 개발자</description>
        <lastBuildDate>Fri, 15 May 2026 12:54:31 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>lee_yesol421.log</title>
            <url>https://images.velog.io/images/lee_yesol421/profile/f87aef0b-d0a8-40c7-80ae-2911f85279fb/social.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. lee_yesol421.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/lee_yesol421" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[20260515 오늘의 학습: 시험 전날 공식 총정리]]></title>
            <link>https://velog.io/@lee_yesol421/20260515-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-%EC%8B%9C%ED%97%98-%EC%A0%84%EB%82%A0-%EA%B3%B5%EC%8B%9D-%EC%B4%9D%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@lee_yesol421/20260515-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-%EC%8B%9C%ED%97%98-%EC%A0%84%EB%82%A0-%EA%B3%B5%EC%8B%9D-%EC%B4%9D%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Fri, 15 May 2026 12:54:31 GMT</pubDate>
            <description><![CDATA[<h2 id="지난-학습-요약">지난 학습 요약</h2>
<p>28차(5/13)와 29차(5/14)에 구름EDU 기출 4회차와 6회차를 풀타임 모의고사로 풀었고, 두 번 모두 추정 800점+ 안정으로 합격선 600을 한참 위에서 통과했다. 함수 작성 6세션 연속 자력 정답, &quot;테스트 통과 but 식 버그&quot; 패턴에 대한 자가 의심도 자발적으로 발동되는 단계까지 진입했다. 모의고사 컨디션이 충분히 올라온 상태였다.</p>
<h2 id="오늘-수업-계획">오늘 수업 계획</h2>
<p>새 문제 풀이로 머리를 더 쓰는 대신, <strong>이미 아는 공식을 인덱싱하는 정리 모드</strong>로 전환했다. 5차 기출 중 스터디에서 미해결로 남은 3문제(5번 몬스터 잡기 / 6번 진법 변환 / 9번 연산 횟수)를 빠르게 분석해서 외울 가치를 분류하고, 학습 전체 이력에서 시험 직전 마지막 한 번 훑기에 적합한 공식을 한 페이지로 압축했다.</p>
<blockquote>
<p>COS Pro 1급 Python / Phase 4: 마무리</p>
</blockquote>
<hr>
<h2 id="5차-미해결-3문제-빠른-분석">5차 미해결 3문제 빠른 분석</h2>
<h3 id="5번--몬스터-잡기-두-배열-매칭-그리디">5번 — 몬스터 잡기 (두 배열 매칭 그리디)</h3>
<p>스스로 떠올린 아이디어는 &quot;두 배열을 정렬한 뒤 큰 값부터 비교, 이기면 카운트&quot;였고 방향은 정확했다. <strong>누락된 디테일은 &quot;못 이겼을 때 어떻게 할지&quot;</strong> 한 가지였다.</p>
<pre><code class="language-python">def solution(enemies, armies):
    enemies = sorted(enemies)
    armies = sorted(armies)
    count = 0
    while enemies and armies:
        if armies[-1] &gt;= enemies[-1]:
            armies.pop()
            enemies.pop()
            count += 1
        else:
            enemies.pop()   # 가장 강한 캐릭터도 못 이기는 몬스터 → 영구 포기
    return count</code></pre>
<p>가장 강한 캐릭터로도 못 이기는 몬스터는 약한 캐릭터들도 못 이기니까 그 몬스터만 버리고 캐릭터는 남긴다. <strong>이 문제에서 외울 공식은 없고, 외워야 할 한 줄 패턴만 남는다</strong>: &quot;두 묶음을 짝지어야 하는 문제는 둘 다 정렬부터, 못 매칭되는 쪽만 한 칸씩 버린다.&quot;</p>
<h3 id="6번--p진법-→-q진법-변환">6번 — p진법 → q진법 변환</h3>
<p>처음 떠올린 직관은 &quot;각 자릿수별로 더해서 n 이상이면 다음 자리에 +1&quot;이었다. <strong>같은 진법끼리 더하는 받아올림 알고리즘은 맞지만 p≠q일 때 안 통한다</strong>. 이 문제는 무조건 10진법을 거쳐가는 게 정석이다.</p>
<pre><code class="language-python">def solution(s1, s2, p, q):
    total = int(s1, p) + int(s2, p)        # ① p진법 → 10진법
    if total == 0:
        return &quot;0&quot;
    digits = []
    while total &gt; 0:
        digits.append(str(total % q))      # ② 10진법 → q진법
        total //= q
    return &#39;&#39;.join(reversed(digits))</code></pre>
<p><strong>수업 중 질문: digit 문자열 생성할 때 문자열을 붙이지 않고 리스트로 만든 후 join하는 이유가 있어?</strong></p>
<p>Python 문자열은 <strong>immutable(불변)</strong>이라서 <code>result += str(...)</code>로 반복해서 붙이면 매번 새 문자열 객체를 통째로 새로 만든다. 자릿수가 많으면 O(n²)로 느려진다. 리스트는 mutable이라 <code>append</code>가 O(1)이고, 마지막에 <code>&#39;&#39;.join()</code>으로 한 번만 합치니까 전체가 O(n)이다. 이 문제 크기에서는 체감 차이가 없어서 세 방식 모두 정답으로 인정되지만, 시험 빈칸/디버깅 답안은 보통 출제자 스타일인 <strong>리스트 + join 패턴</strong>에 맞추는 게 안전하다.</p>
<p><strong>수업 중 질문: reversed() 파라미터로 리스트 말고 가능한 거 뭐 있어? q_digit[::-1]과 비교하면 원본 리스트를 반대로 뒤집는 건가?</strong></p>
<p><code>reversed()</code>는 <code>len()</code>과 인덱스 접근(<code>[i]</code>)이 되는 시퀀스 타입에 다 쓸 수 있다. list, tuple, str, range, dict(Python 3.8+) 모두 가능하지만 set, zip, map, filter 같이 한 방향으로만 흐르는 iterator는 불가능하다. 그리고 <strong><code>reversed()</code>도 <code>[::-1]</code>도 원본은 그대로 둔다</strong>. 원본을 진짜 뒤집는 건 <code>lst.reverse()</code> 메서드(in-place, 반환값 None)이다.</p>
<table>
<thead>
<tr>
<th>표현</th>
<th>반환값</th>
<th>원본 변경</th>
</tr>
</thead>
<tbody><tr>
<td><code>reversed(lst)</code></td>
<td>iterator (lazy, 한 번만 소비 가능)</td>
<td>❌</td>
</tr>
<tr>
<td><code>lst[::-1]</code></td>
<td>새 리스트 (복사)</td>
<td>❌</td>
</tr>
<tr>
<td><code>lst.reverse()</code></td>
<td>None</td>
<td>✅ in-place</td>
</tr>
</tbody></table>
<p>16차에 정착시킨 &quot;원본 보존&quot; 의식의 연장선상에서 보면, <code>reverse()</code>/<code>sort()</code>처럼 <strong>반환값이 None인 메서드는 함정용 디버깅으로 자주 등장</strong>한다.</p>
<h3 id="9번--number를-target으로-만드는-최소-연산-횟수">9번 — number를 target으로 만드는 최소 연산 횟수</h3>
<p><code>+1</code>, <code>-1</code>, <code>×2</code> 세 연산으로 number를 target으로 만드는 최소 횟수를 구한다. <strong>그리디로는 불가능하고 BFS(너비 우선 탐색) 최단 거리로만 풀린다</strong>. 학습 초기에 합격 기댓값(부분형 7 + 완성형 1 = 600점+) 기준으로 BFS/DFS는 <strong>의도적으로 제외 결정</strong>한 영역이라, 시험에서 함수 작성으로 나오면 손절이 정답이다.</p>
<p>다만 빈칸 채우기로 나올 때를 대비해 <strong>BFS 골격을 알아보는 키워드</strong>만 인지하면 된다.</p>
<pre><code class="language-python">from collections import deque

def solution(number, target):
    visited = {number}
    queue = deque([(number, 0)])     # (현재값, 횟수)
    while queue:
        cur, cnt = queue.popleft()   # ← popleft가 보이면 BFS
        if cur == target:
            return cnt
        for nxt in (cur + 1, cur - 1, cur * 2):
            if nxt not in visited:
                visited.add(nxt)
                queue.append((nxt, cnt + 1))</code></pre>
<p><code>deque</code> / <code>popleft()</code> / <code>visited</code> set / <code>(상태, 횟수)</code> 튜플 — <strong>이 네 개가 보이면 BFS 유형</strong>이라고 즉시 판단하면 된다.</p>
<hr>
<h2 id="시험-전날-공식-총정리--마지막-한-번-훑기용">시험 전날 공식 총정리 — 마지막 한 번 훑기용</h2>
<h3 id="🔥-무조건-외울-공식-그-자리에서-못-만드는-것">🔥 무조건 외울 공식 (그 자리에서 못 만드는 것)</h3>
<p><strong>1. 진법 변환</strong></p>
<pre><code class="language-python">int(s, p)                                  # p진법 문자열 → 10진법 정수
# 10진법 → q진법
digits = []
while n &gt; 0:
    digits.append(str(n % q))
    n //= q
return &#39;&#39;.join(reversed(digits))</code></pre>
<p><strong>2. 자릿수 분해</strong> — 진법 변환의 q=10 특수 케이스</p>
<pre><code class="language-python">while n &gt; 0:
    digit = n % 10            # 마지막 자리
    n //= 10                  # 한 자리 떨구기</code></pre>
<p><strong>3. 시계 각도</strong> — 외워야 풀린다</p>
<pre><code class="language-python">hour_angle = h * 30 + m * 0.5    # 시침 (분 기여도 +0.5도/분)
min_angle  = m * 6               # 분침 (6도/분)
diff = abs(hour_angle - min_angle)
answer = min(diff, 360 - diff)   # 큰 각도 처리</code></pre>
<p><strong>4. 소수 판별 √n 최적화</strong></p>
<pre><code class="language-python">def is_prime(n):
    if n &lt; 2: return False
    for i in range(2, int(n**0.5) + 1):
        if n % i == 0:
            return False
    return True</code></pre>
<p><strong>5. GCD / LCM</strong></p>
<pre><code class="language-python">def gcd(a, b):
    while b:
        a, b = b, a % b
    return a

def lcm(a, b):
    return a // gcd(a, b) * b    # 오버플로우 안전 형태</code></pre>
<p><strong>6. 90도 회전</strong></p>
<pre><code class="language-python"># 시계방향 90도
rotated[i][j] = arr[n-1-j][i]                    # 인덱스 공식
rotated = list(map(list, zip(*arr[::-1])))       # zip 패턴 (먼저 뒤집고 전치)

# 반시계방향 90도
rotated[i][j] = arr[j][n-1-i]                    # 인덱스 공식
rotated = list(map(list, zip(*arr)))[::-1]       # zip 패턴 (전치 후 행 뒤집기)</code></pre>
<p><strong>7. dx/dy 좌표 이동</strong></p>
<pre><code class="language-python">dx = [-1, 1, 0, 0]    # 상 하 좌 우 (행 기준)
dy = [0, 0, -1, 1]
if 0 &lt;= nx &lt; n and 0 &lt;= ny &lt; m:    # 양쪽 차원 다 체크
    ...</code></pre>
<p><strong>8. 이진 탐색 템플릿</strong></p>
<pre><code class="language-python">left, right = 0, len(arr) - 1
while left &lt;= right:
    mid = (left + right) // 2      # ← // 정수 나눗셈 (디버깅 빈출)
    if arr[mid] == target: return mid
    elif arr[mid] &lt; target: left = mid + 1
    else: right = mid - 1
return -1</code></pre>
<h3 id="⚡-반사-신경-패턴-보면-바로-떠올라야">⚡ 반사 신경 패턴 (보면 바로 떠올라야)</h3>
<table>
<thead>
<tr>
<th>키워드 / 상황</th>
<th>떠올릴 도구</th>
</tr>
</thead>
<tbody><tr>
<td>빈도 세기</td>
<td><code>Counter</code> 또는 <code>dict.get(k, 0) + 1</code></td>
</tr>
<tr>
<td>중복 제거 + 순서 유지</td>
<td><code>dict.fromkeys()</code></td>
</tr>
<tr>
<td>좌표 방문 체크</td>
<td><code>visited = set()</code> + <code>visited.add((x, y))</code></td>
</tr>
<tr>
<td>순열 / 조합</td>
<td><code>itertools.permutations</code> / <code>combinations</code></td>
</tr>
<tr>
<td>큐 (앞에서 꺼냄)</td>
<td><code>from collections import deque</code> + <code>popleft()</code></td>
</tr>
<tr>
<td>정렬 기준 커스텀</td>
<td><code>sorted(arr, key=lambda x: (x[1], -x[0]))</code></td>
</tr>
<tr>
<td>빈 리스트 max 방지</td>
<td><code>max(arr, default=-1)</code></td>
</tr>
<tr>
<td>문자열 순환</td>
<td><code>text * 2</code> 슬라이싱</td>
</tr>
<tr>
<td>슬라이딩 윈도우</td>
<td>초기 윈도우 세팅 → <code>range(k, len)</code> 시작</td>
</tr>
<tr>
<td>시뮬레이션 동기성</td>
<td><code>today = [row[:] for row in grid]</code> 스냅샷</td>
</tr>
</tbody></table>
<h3 id="🚨-함정-체크리스트-시험-직전-환기">🚨 함정 체크리스트 (시험 직전 환기)</h3>
<ol>
<li><strong><code>reverse()</code> vs <code>reversed()</code> vs <code>[::-1]</code></strong> — 반환값 / 원본 변경 여부 다름</li>
<li><strong>내장함수명 변수 사용 금지</strong> — <code>sum</code>, <code>dict</code>, <code>type</code>, <code>str</code>, <code>list</code>, <code>map</code>, <code>filter</code></li>
<li><strong><code>/</code> vs <code>//</code></strong> — 인덱스 / 정수 계산엔 무조건 <code>//</code></li>
<li><strong>빈 컨테이너 체크 누락</strong> — <code>while stack</code> / <code>if not arr</code></li>
<li><strong>값 기반 중복 체크 함정</strong> — 좌표 / 인덱스 기반 <code>visited</code>로 전환</li>
<li><strong>데이터 변환 후 원본 소실</strong> — 변환용 따로, 반환용 원본</li>
<li><strong>&quot;테스트 통과 but 식 버그&quot;</strong> — 풀이 후 <strong>극단 입력 상상</strong>(빈 입력, 한 개짜리, 한쪽만)</li>
</ol>
<h3 id="🎯-시험장-판단-매뉴얼">🎯 시험장 판단 매뉴얼</h3>
<table>
<thead>
<tr>
<th>마주친 유형</th>
<th>대응</th>
</tr>
</thead>
<tbody><tr>
<td>BFS / DFS 함수 작성</td>
<td><strong>즉시 포기</strong></td>
</tr>
<tr>
<td>Union-Find / 어려운 DP 함수 작성</td>
<td>포기 후보 (시간 5분 룰)</td>
</tr>
<tr>
<td>BFS / DFS 빈칸</td>
<td>시도 (<code>deque</code>, <code>popleft</code>, <code>visited</code> 키워드로 인지)</td>
</tr>
<tr>
<td>DP 빈칸 / 디버깅</td>
<td>시도 — 학습 분량 내</td>
</tr>
<tr>
<td>클래스 상속</td>
<td><strong>무조건 정답</strong> (완전 정착 영역)</td>
</tr>
<tr>
<td>시뮬레이션 / 구현</td>
<td>침착하게 추적 — 강점 영역</td>
</tr>
</tbody></table>
<hr>
<h2 id="오늘의-결과">오늘의 결과</h2>
<p>5차 미해결 3문제 모두 분석 완료, 그중 5번은 누락 디테일 한 줄 보강만으로 자력 가능, 6번은 진법 변환 공식 두 개로 압축, 9번은 손절 영역으로 분류했다. 학습 이력 전체에서 외울 가치 있는 공식 8개와 반사 신경 패턴 10개, 함정 체크리스트 7개, 시험장 판단 매뉴얼 6개를 한 페이지로 정리했다. 다음은 시험 당일 — 이 글 한 장만 시험 직전에 다시 훑고 들어가는 것으로 마무리한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[20260514 오늘의 학습: 6회차 풀타임 모의고사와 7문제 식 검증]]></title>
            <link>https://velog.io/@lee_yesol421/20260514-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-6%ED%9A%8C%EC%B0%A8-%ED%92%80%ED%83%80%EC%9E%84-%EB%AA%A8%EC%9D%98%EA%B3%A0%EC%82%AC%EC%99%80-7%EB%AC%B8%EC%A0%9C-%EC%8B%9D-%EA%B2%80%EC%A6%9D</link>
            <guid>https://velog.io/@lee_yesol421/20260514-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-6%ED%9A%8C%EC%B0%A8-%ED%92%80%ED%83%80%EC%9E%84-%EB%AA%A8%EC%9D%98%EA%B3%A0%EC%82%AC%EC%99%80-7%EB%AC%B8%EC%A0%9C-%EC%8B%9D-%EA%B2%80%EC%A6%9D</guid>
            <pubDate>Thu, 14 May 2026 15:02:55 GMT</pubDate>
            <description><![CDATA[<h2 id="지난-학습-요약">지난 학습 요약</h2>
<p>27차(5/13)에서 구름EDU 기출 4회차를 73분에 풀어 8/10 통과했고, 통과한 답에서도 식 버그가 2건 발견된 &quot;테스트 통과 but 식 버그&quot; 패턴을 확인했다. 재귀 멘탈 모델·시계 각도 공식·자릿수 분해 패턴·<code>itertools.permutations</code>·<code>max(default=-1)</code> 다섯 가지를 신규 학습했다. 함수 작성 6세션 연속 자력 정답 추세를 유지한 상태였다.</p>
<h2 id="오늘-수업-계획">오늘 수업 계획</h2>
<p>구름EDU 기출 6회차를 90분 풀타임 모의고사로 진행한 뒤, 자력으로 통과한 문제까지 포함해 7문제를 검산 모드로 복기했다. 어제 발견한 &quot;테스트 통과 but 식 버그&quot; 패턴을 한 번 더 의식하면서 본인이 통과처럼 보이는 답에 자가 의심을 끼워 넣는 흐름으로 진행했다.</p>
<blockquote>
<p>COS Pro 1급 Python / Phase 3: 실전 대비</p>
</blockquote>
<hr>
<h2 id="모의고사-결과">모의고사 결과</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>소요 시간</td>
<td>약 77분 (90분 중 13분 여유)</td>
</tr>
<tr>
<td>자력 풀이</td>
<td>9/10 (1번 꽃피는 봄에서 막힘)</td>
</tr>
<tr>
<td>추정 점수</td>
<td>800점+ 안정 (합격선 600 한참 위)</td>
</tr>
</tbody></table>
<p>자력 통과 5문제(4·5·8·9·10), 자가 의심 후 자력 수정 3문제(2·3·7), 힌트 후 자력 해결 2문제(1·6)로 정리됐다. 막힌 1번에 시간을 더 끌지 않고 8·9·10번을 자력으로 마무리한 시험장 판단력은 4회차와 일관됐다.</p>
<hr>
<h2 id="학습-내용-정리">학습 내용 정리</h2>
<h3 id="1번-꽃피는-봄--시뮬레이션-동기성-함정">1번: 꽃피는 봄 — 시뮬레이션 동기성 함정</h3>
<p>n×n 격자에서 핀 꽃(1)이 매일 상하좌우 4방향으로 한 칸씩 퍼진다. 모든 칸이 필 때까지 며칠이 걸리는지 구한다.</p>
<p>처음 작성한 코드:</p>
<pre><code class="language-python">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 &lt;= ni &lt; n and 0 &lt;= nj &lt; n:
                            bloom[ni][nj] = 1     # 같은 bloom을 읽으면서 수정
        answer += 1
    return answer</code></pre>
<p><strong>핵심 버그</strong>: <code>bloom</code>을 읽으면서 같은 <code>bloom</code>을 수정하기 때문에, <strong>같은 날 안에서 갓 핀 꽃이 또 다음 칸을 피우는 연쇄</strong>가 일어난다. 예시 1을 한 번 따라가보면 다음과 같다.</p>
<pre><code>초기:        1일째 끝나야 할 모습:
0 0 0        0 1 0
0 1 0   →    1 1 1
0 0 0        0 1 0</code></pre><p>(1,1)만 1이니 4방향 4칸만 1로 바뀌어야 한다. 그런데 위 코드는 루프가 (1,1)을 처리한 뒤 (1,2)에 도착했을 때 방금 1로 바뀐 (1,2)를 또 보고 (0,2), (2,2)까지 1로 만들어버린다. 결국 한 번의 while 반복으로 거의 모든 칸이 피어버린다.</p>
<p><strong>해결 — 오늘 시점 스냅샷과 내일 결과를 쓸 곳을 분리한다.</strong></p>
<pre><code class="language-python">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 &lt;= ni &lt; n and 0 &lt;= nj &lt; n:
                            bloom[ni][nj] = 1   # 쓰기는 bloom
        answer += 1
    return answer</code></pre>
<p><strong>2차원 깊은 복사 패턴</strong></p>
<table>
<thead>
<tr>
<th>코드</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td><code>bloom = garden</code></td>
<td>같은 객체 참조 (수정 동기화)</td>
</tr>
<tr>
<td><code>bloom = garden[::]</code> 또는 <code>garden[:]</code></td>
<td>외부 리스트만 복사, 내부 행은 공유 (얕은 복사)</td>
</tr>
<tr>
<td><code>bloom = [row[:] for row in garden]</code></td>
<td>행까지 새로 복사 (깊은 복사)</td>
</tr>
</tbody></table>
<p>2차원 배열을 안전하게 복사하려면 무조건 행 단위 컴프리헨션 패턴을 써야 한다. 시뮬레이션 문제에서 매우 자주 나오니 외워두면 좋다.</p>
<p><strong>슬림화 포인트</strong></p>
<p>처음에 <code>if all(all(v == 1 for v in row) for row in garden): return 0</code> 같은 초기 분기를 넣어뒀었는데, 아래의 <code>while any(any(v == 0 ...))</code>가 이미 이 케이스를 처리한다. 전부 1이면 while 진입을 안 하고 <code>answer = 0</code> 그대로 반환한다.</p>
<blockquote>
<p>&quot;이 분기, 빼도 답 같지 않은가?&quot;</p>
</blockquote>
<p>23차에 짚었던 슬림화 자기 점검 질문이다. 이번엔 피드백 1회 후 곧장 자력 삭제로 정리됐다.</p>
<hr>
<h3 id="2번-단어-순서대로-적기--공백을-두-번-세지-않기">2번: 단어 순서대로 적기 — 공백을 두 번 세지 않기</h3>
<p>한 줄에 K자를 적을 수 있는 메모장에 영어 단어를 순서대로 적는다. 단어 사이는 공백 1칸으로 구분하며, 줄 끝에 단어가 잘리지 않도록 다음 줄로 넘긴다. 단어 배열을 모두 적었을 때 몇 줄이 되는지 구한다.</p>
<p>처음 작성한 코드:</p>
<pre><code class="language-python">def solution(K, words):
    answer = 0
    current = 0
    for word in words:
        if (current + len(word) + 1) // K &gt; 0:    # 줄바꿈 판단
            answer += 1
            current = len(word)
        else:
            current += len(word) + 1
    return answer</code></pre>
<p><strong>수업 중 질문</strong>: 글자 사이 공백을 잘 고려했는지 모르겠다고 했다.</p>
<p>자가 의심이 정확했다. 통과 답에 식 버그가 있다.</p>
<p><strong>버그 1 — 마지막 줄 카운트 누락</strong></p>
<p><code>answer</code>는 줄바꿈이 일어날 때만 증가한다. 마지막 단어가 같은 줄에 들어가면(else) 그 줄은 세지 않고 끝난다.</p>
<pre><code class="language-python">solution(10, [&quot;nice&quot;])
# 정답: 1줄, 코드 결과: 0</code></pre>
<p><strong>버그 2 — 공백을 두 번 카운트</strong></p>
<p>첫 단어 <code>nice</code>를 처리할 때 <code>current = 0 + 4 + 1 = 5</code>로 저장한다. nice 뒤에 공백 한 칸을 미리 붙인 형태다. 그런데 다음 단어 <code>c</code>를 추가할 때 또 <code>current + len(word) + 1</code>로 공백을 더한다.</p>
<p>K=5, <code>[&quot;ab&quot;, &quot;c&quot;, &quot;d&quot;]</code>로 따라가보면:</p>
<table>
<thead>
<tr>
<th>단계</th>
<th>current</th>
<th>판단</th>
<th>코드 결과</th>
<th>사람 직관</th>
</tr>
</thead>
<tbody><tr>
<td>ab 추가</td>
<td>0 → 3</td>
<td>같은 줄</td>
<td>&quot;ab_&quot; 저장</td>
<td>&quot;ab&quot;</td>
</tr>
<tr>
<td>c 추가</td>
<td>(3+1+1)//5=1 → 줄바꿈</td>
<td>c 새 줄</td>
<td>&quot;ab_&quot; 끝 / &quot;c&quot;</td>
<td>&quot;ab c&quot; 같은 줄 (4칸)</td>
</tr>
<tr>
<td>d 추가</td>
<td>(1+1+1)//5=0 → 같은 줄</td>
<td>&quot;c_d_&quot;</td>
<td>정답 1줄? 2줄?</td>
<td></td>
</tr>
</tbody></table>
<p>정답은 2줄 (<code>ab_c</code> 4칸 한 줄, <code>d</code> 한 줄)인데 코드는 1을 반환한다. 공백이 한 번 들어가야 할 자리에 두 번 카운트돼서 발생한 어긋남이다.</p>
<p><strong>수정 코드</strong></p>
<pre><code class="language-python">def solution(K, words):
    answer = 1
    current = 0
    for word in words:
        if current == 0:                              # 첫 단어
            current = len(word)
        elif current + len(word) + 1 &gt; K:             # 줄바꿈 필요
            answer += 1
            current = len(word)
        else:
            current += len(word) + 1                  # 같은 줄에 공백 + 단어
    return answer</code></pre>
<p>세 가지가 동시에 해결된다.</p>
<ol>
<li><code>answer = 1</code>로 시작 — 단어가 있으면 줄은 무조건 1개 이상</li>
<li>첫 단어 분기 — 공백 없이 단어만 더함</li>
<li><code>&lt;= K</code> 대신 <code>&gt; K</code>로 명시적 비교 — <code>//</code> 같은 트릭 없이 직관적</li>
</ol>
<hr>
<h3 id="3번-큰수와-작은수의-차이--combinations은-함정이다">3번: 큰수와 작은수의 차이 — combinations은 함정이다</h3>
<p>자연수 배열 arr에서 K개를 골라 (가장 큰 수 - 가장 작은 수) 차이가 최소가 되도록 한다. 처음 작성한 코드:</p>
<pre><code class="language-python">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</code></pre>
<p><strong>수업 중 질문</strong>: 숫자 4개를 순서 상관없이 뽑아야 해서 combinations을 떠올렸는데, arr_len이 최대 1000까지 될 수 있다는 제약을 보고 시간복잡도가 괜찮을지 걱정된다고 했다.</p>
<p>자가 의심이 정확했다. <strong>C(1000, 50) ≈ 9 × 10⁸⁵</strong>는 우주의 원자 수(약 10⁸⁰)보다 많다. 메모리도, 시간도 절대 못 버틴다.</p>
<p><strong>핵심 인사이트 — 정렬된 배열에서 K개 뽑을 때 차이 최소화는 무조건 연속된 K개다</strong></p>
<p>예: 정렬된 <code>[4, 6, 9, 9, 11, 19]</code>에서 K=4</p>
<table>
<thead>
<tr>
<th>뽑은 4개</th>
<th>max - min</th>
</tr>
</thead>
<tbody><tr>
<td><code>[4, 6, 9, 9]</code> (연속)</td>
<td>5</td>
</tr>
<tr>
<td><code>[6, 9, 9, 11]</code> (연속)</td>
<td>5</td>
</tr>
<tr>
<td><code>[9, 9, 11, 19]</code> (연속)</td>
<td>10</td>
</tr>
<tr>
<td><code>[4, 6, 9, 11]</code> (비연속)</td>
<td>7</td>
</tr>
</tbody></table>
<p>비연속으로 뽑으면 어딘가 더 먼 수가 끼게 되니 차이가 같거나 더 커진다. 그래서 정렬 + 슬라이딩 윈도우로 풀면 된다.</p>
<p><strong>수정 코드</strong></p>
<pre><code class="language-python">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</code></pre>
<p>시간복잡도 O(n log n). 정렬 비용이 전부다.</p>
<p><strong>한 코드에 세 가지 정착 패턴이 동시 발현</strong></p>
<table>
<thead>
<tr>
<th>패턴</th>
<th>정착 회차</th>
<th>본 코드에서</th>
</tr>
</thead>
<tbody><tr>
<td>슬라이딩 윈도우 첫 윈도우 미리 계산</td>
<td>20차 자기 진단 약점</td>
<td><code>answer = arr_tmp[K-1] - arr_tmp[0]</code> 자력 작성</td>
</tr>
<tr>
<td>range 시작값을 1로 (둘째 윈도우부터)</td>
<td>14차 약점</td>
<td><code>range(1, ...)</code> 자력 작성</td>
</tr>
<tr>
<td>원본 보존 (sorted 사용, sort()는 원본 변경)</td>
<td>16차 정착</td>
<td><code>sorted(arr)</code> 자력 선택</td>
</tr>
</tbody></table>
<hr>
<h3 id="4번-카드-섞기--인덱스-디버깅-한-줄">4번: 카드 섞기 — 인덱스 디버깅 한 줄</h3>
<p>1부터 n까지 적힌 카드 뭉치를 반으로 나눠 교대로 섞는 과정을 mix번 반복한 뒤, 아래에서 k번째 카드를 구한다. 디버깅 유형(한 줄 수정)이고 본인이 자력으로 정답을 찾았다.</p>
<p>원본 버그:</p>
<pre><code class="language-python">card_a, card_b = [0 for _ in range(n//2)], [0 for _ in range(n//2)]
for i in range(0, n):
    if i &lt; n//2:
        card_a[i] = card[i]
    else:
        card_b[i] = card[i]   # 버그: card_b 크기는 n//2인데 i가 n//2~n-1 들어감</code></pre>
<p><strong>본인 진단</strong>: &quot;후반 1/2를 저장하는 배열 b에 인덱스를 원본 배열 그대로 넣어서 에러가 나는 것으로 보임.&quot;</p>
<p><code>card_b</code>의 크기는 <code>n//2</code>(=3)인데 i가 3, 4, 5일 때 <code>card_b[i]</code>로 접근하면 IndexError가 난다.</p>
<p><strong>수정</strong>: <code>card_b[i - (n//2)] = card[i]</code></p>
<p>i=3 → card_b[0], i=4 → card_b[1], i=5 → card_b[2]로 후반부를 0부터 다시 채운다. 한 라인 안에서 두 토큰(i와 괄호 보정)을 동시에 바꾼 디버깅이지만 &quot;한 줄 수정&quot; 제약은 코드 라인 단위라 그대로 통과한다.</p>
<hr>
<h3 id="5번-코인-획득--dp-자력-발현">5번: 코인 획득 — DP 자력 발현</h3>
<p>4×4 격자의 가장 왼쪽 위에서 가장 오른쪽 아래까지 오른쪽 또는 아래로만 이동하면서 코인을 최대로 모은다. 디버깅 한 줄 유형이고 본인이 자력 정답을 찾았다.</p>
<p>원본 버그:</p>
<pre><code class="language-python">else:
    coins[i][j] = board[i][j] + max(coins[i][j], coins[i-1][j-1])
    # max 안: 자기 자신(0) vs 대각선(도달 불가)</code></pre>
<p><strong>수정</strong>: <code>max(coins[i-1][j], coins[i][j-1])</code></p>
<p>오른쪽 또는 아래로만 이동하니까 (i, j)에 도달하기 직전 위치는 위 (i-1, j) 또는 왼쪽 (i, j-1)이다. 그중 큰 누적값을 골라 현재 칸 값에 더한다.</p>
<pre><code>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</code></pre><p>26차에 학습한 DP 마지막 동작 패턴이 자력 발동한 형태다. 26차에선 힌트가 필요했는데 이번엔 디버깅 문제에서 의도를 스스로 짚어냈다.</p>
<hr>
<h3 id="6번-만남-최대--한-라인-안-인덱스-4토큰-보정">6번: 만남 최대 — 한 라인 안 인덱스 4토큰 보정</h3>
<p>4×4 종이를 가로 또는 세로축에 평행한 격자선을 따라 한 번 접었을 때, 만나는 두 칸의 합이 최대가 되도록 한다. 디버깅 한 줄 유형이다.</p>
<p>원본 코드:</p>
<pre><code class="language-python">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번 라인</code></pre>
<p><strong>수업 중 질문</strong>: 맨 안쪽 for의 step 2가 처음엔 이해되지 않아 숫자를 대입해보면서 의미를 파악하려고 했다고 했다.</p>
<p><strong>step 2의 의미</strong> — <code>(j, k)</code> 쌍은 <strong>한 번 접으면 만날 수 있는 두 인덱스 쌍</strong>을 만든다. 합이 홀수일 때만 가능하다.</p>
<table>
<thead>
<tr>
<th>j</th>
<th>range(j+1, 4, 2)</th>
<th>(j, k) 후보</th>
<th>j+k</th>
</tr>
</thead>
<tbody><tr>
<td>0</td>
<td>[1, 3]</td>
<td>(0,1), (0,3)</td>
<td>1, 3</td>
</tr>
<tr>
<td>1</td>
<td>[2]</td>
<td>(1,2)</td>
<td>3</td>
</tr>
<tr>
<td>2</td>
<td>[3]</td>
<td>(2,3)</td>
<td>5</td>
</tr>
<tr>
<td>3</td>
<td>[]</td>
<td>—</td>
<td>—</td>
</tr>
</tbody></table>
<p><code>j+k</code>가 짝수이면 접는 선이 칸 한가운데에 떨어져 물리적으로 접을 수 없다. step 2가 이 제약을 자동으로 걸러낸다.</p>
<p><strong>9번 라인의 진짜 버그 — 인덱스가 어긋남</strong></p>
<p><code>(j, k)</code> 쌍을 받은 다음, 어떻게 활용해야 만나는 두 칸이 되는지 정리하면 다음 두 경우다.</p>
<table>
<thead>
<tr>
<th>접는 방향</th>
<th>두 칸 좌표</th>
<th>공통 조건</th>
</tr>
</thead>
<tbody><tr>
<td>세로 접기 (같은 행에서)</td>
<td>(i, j) ↔ (i, k)</td>
<td>행 같음</td>
</tr>
<tr>
<td>가로 접기 (같은 열에서)</td>
<td>(j, i) ↔ (k, i)</td>
<td>열 같음</td>
</tr>
</tbody></table>
<p>원본 코드는:</p>
<ul>
<li><code>grid[i][j] + grid[j][k]</code> — (i, j)와 (j, k)는 같은 행도 같은 열도 아니다. 행이 어긋남</li>
<li><code>grid[i][j] + grid[k][i]</code> — (i, j)와 (k, i)도 마찬가지로 열이 어긋남</li>
</ul>
<p><strong>수정</strong>: <code>grid[i][j] + grid[i][k]</code>, <code>grid[j][i] + grid[k][i]</code></p>
<p>한 라인 안에서 4개 토큰(j→i, j→i, i→j, i→j)을 동시에 바꿔야 한다. 4번 카드 섞기에 이어 한 라인 안 다중 토큰 보정이 2회 연속 성공한 형태다.</p>
<hr>
<h3 id="7번-up-and-down--처음-가설과-max를-합쳤어야-했다">7번: UP AND DOWN — &quot;처음 가설과 max를 합쳤어야 했다&quot;</h3>
<p>출제자가 1~K 중 하나의 자연수를 정한다. 참가자가 수를 부르면 출제자는 자기 수보다 작을 때 &quot;UP&quot;, 클 때 &quot;DOWN&quot;, 같을 때 &quot;RIGHT&quot;이라고 한다. 주어진 게임 진행에서 현재 정답이 될 수 있는 숫자가 몇 개인지 구하는 빈칸 채우기 문제다.</p>
<p>본인이 채운 풀이:</p>
<pre><code class="language-python">def solution(K, numbers, up_down):
    left = 1
    right = K
    for num, word in zip(numbers, up_down):
        if word == &quot;UP&quot;:
            left = max(num, left)
        elif word == &quot;DOWN&quot;:
            right = min(num, right)
        elif word == &quot;RIGHT&quot;:
            return 1
    return right - left - 1</code></pre>
<p><strong>본인의 풀이 흐름</strong> — 처음엔 <code>left = num + 1</code>, <code>right = num - 1</code>로 정확히 1씩 보정하려 했다. 그런데 예시 2번이 &quot;2 UP&quot; 다음 &quot;1 UP&quot;으로 들어와서 단순 대입을 했더니 left가 3에서 2로 회귀하면서 망가졌다고 했다. 그래서 <code>+1</code>을 빼고 <code>max(num, left)</code>로 바꾼 뒤, 마지막에 <code>right - left - 1</code>로 양 끝을 빼는 방식으로 우회했다고 했다.</p>
<p>자가 분석이 정확한 방향이었지만 검증 시 한 가지가 더 어긋난다.</p>
<p><strong>반례</strong></p>
<pre><code class="language-python">K = 10
numbers = [5]
up_down = [&quot;DOWN&quot;]
# 정답: 1, 2, 3, 4 → 4개
# 코드: right = 5, left = 1 → 5 - 1 - 1 = 3 (틀림)</code></pre>
<p><code>right - left - 1</code>은 left와 right가 둘 다 <strong>후보 바깥 경계값</strong>이어야 의미가 맞는 공식인데, 초기값 <code>left=1, right=K</code>는 본인이 후보가 될 수 있는 값들이다. left가 한 번도 안 바뀌면 1이, right가 한 번도 안 바뀌면 K가 후보에서 빠져버린다.</p>
<p><strong>해법 — 빈칸 채우기라 초기값을 못 바꾸므로 max에 +1을 다시 끼워 넣는다</strong></p>
<pre><code class="language-python">if word == &quot;UP&quot;:
    left = max(num + 1, left)            # +1 다시 추가
elif word == &quot;DOWN&quot;:
    right = min(num - 1, right)          # -1 다시 추가
elif word == &quot;RIGHT&quot;:
    return 1
return right - left + 1                  # 양 끝 포함</code></pre>
<p><code>max(num + 1, left)</code>로 감싸면 &quot;2 UP 다음 1 UP&quot; 같은 약한 정보도 자연히 무시된다. left가 3에서 1+1=2로 후퇴할 일이 없다 (max로 보호되니까).</p>
<p>검산:</p>
<pre><code class="language-python">solution(10, [5], [&quot;DOWN&quot;])
# right = min(4, 10) = 4
# left = 1
# 4 - 1 + 1 = 4  ✓</code></pre>
<p><strong>수업 중 메타 정리 (본인 발화)</strong>: &quot;처음 생각한 거랑 max 아이디어 합쳤어야 했네.&quot;</p>
<p>이게 시험장 디버깅의 핵심 사고다. 첫 가설 <code>+1</code>/<code>-1</code>이 틀린 게 아니라 <strong>단순 대입 때문에 깨졌던 것</strong>이고, max 도입이 그 깨짐의 보호막이었다. 가설을 통째로 버릴 게 아니라 <strong>깨진 원인만 찾아 합치면</strong> 답이 나온다.</p>
<hr>
<h3 id="테스트-통과-but-식-버그-누적-3건">&quot;테스트 통과 but 식 버그&quot; 누적 3건</h3>
<p>27차에 2건(시계, 소수), 오늘 7번 UP/DOWN으로 누적 3건이 됐다. 모두 본인이 통과 처리받은 답에 식 자체의 버그가 숨어있던 케이스다.</p>
<table>
<thead>
<tr>
<th>회차</th>
<th>문제</th>
<th>어떤 입력에서 깨지나</th>
</tr>
</thead>
<tbody><tr>
<td>27차 9번</td>
<td>시계 각도</td>
<td>m=0과 m=30 외 모든 분</td>
</tr>
<tr>
<td>27차 10번</td>
<td>소수 세제곱</td>
<td>a &gt; 8 또는 b 큰 경우</td>
</tr>
<tr>
<td>28차 7번</td>
<td>UP/DOWN</td>
<td>한 방향만 들어오는 입력 (<code>[5], DOWN</code> 등)</td>
</tr>
</tbody></table>
<p>오늘 의미 있게 달라진 건, 2번·3번·7번에서 본인이 <strong>통과한 답에 자가 의심을 자발적으로 발화</strong>했다는 점이다.</p>
<ul>
<li>2번: &quot;공백을 잘 고려했는지 모르겠음&quot;</li>
<li>3번: &quot;arr_len 1000인데 시간복잡도 괜찮을까?&quot;</li>
<li>7번: 풀이 흐름 메타 정리</li>
</ul>
<p>자가 의심이 자발적으로 작동하기 시작하면, 같은 함정이 시험장에서도 잡힌다. 풀이 후 한 박자만 두고 <strong>극단 입력 하나만 상상</strong>하는 습관이 D-2 환기 포인트다.</p>
<table>
<thead>
<tr>
<th>상상해볼 극단 입력</th>
<th>잡히는 함정</th>
</tr>
</thead>
<tbody><tr>
<td>빈 입력 / 1개짜리 입력</td>
<td>마지막 줄 카운트 누락, 초기값 모순</td>
</tr>
<tr>
<td>한 방향만 들어오는 시퀀스</td>
<td>한쪽 변수가 업데이트 안 되는 경우</td>
</tr>
<tr>
<td>최대 크기 입력</td>
<td>TLE, 메모리 초과</td>
</tr>
<tr>
<td>경계값 (a=b, K=1, 단일 원소)</td>
<td>off-by-one, 빈 슬라이스</td>
</tr>
</tbody></table>
<hr>
<h2 id="오늘의-결과">오늘의 결과</h2>
<p>구름EDU 6회차 풀타임 모의고사 77분에 9문제 자력 풀이, 추정 800점대 안정으로 합격선 한참 위였다.
시뮬레이션 동기성 패턴(2차원 깊은 복사 포함)을 신규 학습했고, 슬라이딩 윈도우·DP·슬림화·한 라인 다중 토큰 보정 4가지의 자력 발현/정착을 확인했다.
내일은 5차 기출 미해결 구현 3개와 구현 집중 연습을 진행할 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[20260513 오늘의 학습: 4회차 풀타임 모의고사와 복기]]></title>
            <link>https://velog.io/@lee_yesol421/20260513-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-4%ED%9A%8C%EC%B0%A8-%ED%92%80%ED%83%80%EC%9E%84-%EB%AA%A8%EC%9D%98%EA%B3%A0%EC%82%AC%EC%99%80-%EB%B3%B5%EA%B8%B0</link>
            <guid>https://velog.io/@lee_yesol421/20260513-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-4%ED%9A%8C%EC%B0%A8-%ED%92%80%ED%83%80%EC%9E%84-%EB%AA%A8%EC%9D%98%EA%B3%A0%EC%82%AC%EC%99%80-%EB%B3%B5%EA%B8%B0</guid>
            <pubDate>Wed, 13 May 2026 14:18:03 GMT</pubDate>
            <description><![CDATA[<h2 id="지난-학습-요약">지난 학습 요약</h2>
<p>26차(5/12)에서 5차 기출 7문제 스터디 풀이를 복습하고 오답 총복습을 진행했다. DP 마지막 동작 패턴과 range 역순 3가지 방식을 정리했고, 90도 회전·스택·값 기반 중복 체크 함정의 정착을 재확인했다. D-4 기준 주요 약점 없이 90분 풀타임 모의고사 단계로 진입한 상태였다.</p>
<h2 id="오늘-수업-계획">오늘 수업 계획</h2>
<p>구름EDU 기출 4회차를 90분 풀타임 모의고사로 진행한 뒤, 통과했지만 찝찝한 문제와 못 푼 2문제를 검산 모드로 복기했다. 시험 환경을 흉내내기 위해 검색·AI 도움 없이 90분 안에 풀어내는 형태로 진행했다.</p>
<blockquote>
<p>COS Pro 1급 Python / Phase 3: 실전 대비</p>
</blockquote>
<hr>
<h2 id="모의고사-결과">모의고사 결과</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>소요 시간</td>
<td>73분 (90분 내)</td>
</tr>
<tr>
<td>통과 문제</td>
<td>8/10</td>
</tr>
<tr>
<td>못 푼 문제</td>
<td>6번 자아도취 수(빈칸), 8번 n번째 작은 수(함수 작성)</td>
</tr>
<tr>
<td>추정 점수</td>
<td>800점+ (합격선 600 한참 위)</td>
</tr>
</tbody></table>
<p>못 푼 2개에 시간을 더 매달리지 않고 마무리한 것이 시험장 판단력 측면에서 의미가 있었다. 실제 시험에서도 모르는 문제에 30분 매달리는 것보다 손절하고 다른 문제로 넘어가는 것이 합격선 확보에 유리하다.</p>
<hr>
<h2 id="학습-내용-정리">학습 내용 정리</h2>
<h3 id="1번-재귀--함수가-자기-자신을-호출하는-형태">1번: 재귀 — 함수가 자기 자신을 호출하는 형태</h3>
<p>A, E, I, O, U 다섯 알파벳으로 만들 수 있는 길이 1~5의 모든 단어를 사전 순으로 나열했을 때, 주어진 word의 위치를 return하는 문제다. 한 줄을 수정하는 디버깅 유형이다.</p>
<pre><code class="language-python">def create_words(lev, s):
    global words
    VOWELS = [&#39;A&#39;, &#39;E&#39;, &#39;I&#39;, &#39;O&#39;, &#39;U&#39;]
    words.append(s)
    for i in range(0, 5):
        if lev &lt; 5:
            create_words(lev, s + VOWELS[i])   # 버그: lev 그대로 0</code></pre>
<p><strong>버그</strong>: <code>lev</code>가 계속 0이라 <code>lev &lt; 5</code>가 영원히 True → 무한 재귀가 발생한다.</p>
<p><strong>수정</strong>: <code>create_words(lev + 1, s + VOWELS[i])</code> — 다음 호출에 깊이 한 단계 더한 값을 인자로 넘긴다.</p>
<p><strong>수업 중 질문</strong>: 함수 안에서 자기 자신을 호출하는 형태 자체가 어렵다고 했다. <code>lev += 1</code>을 다른 줄에 넣어봤더니 결과가 달라서 이해되지 않았다고 했다.</p>
<p><strong>재귀의 핵심 멘탈 모델</strong></p>
<p>각 호출은 자기만의 독립된 <code>lev</code>, <code>s</code>를 가진다. <code>create_words(lev+1, ...)</code>은 <strong>새로운 호출에 새 값을 인자로 넘기는 것</strong>이지, 위쪽 호출의 lev를 바꾸는 게 아니다.</p>
<pre><code>create_words(0, &#39;&#39;)        ← 이 호출의 lev는 0, 영원히 0
   ↓ 새 호출 만듦
   create_words(1, &#39;A&#39;)    ← 이 호출의 lev는 1, 위의 0과 별개
      ↓
      create_words(2, &#39;AA&#39;) ← lev=2, 또 별개</code></pre><p>Java로 비유하면 <code>return sum(n-1) + n</code> 같이 새 값을 인자로 넘기는 형태와 같다. 위쪽 호출의 <code>n</code>을 바꾸는 게 아니다.</p>
<p><strong><code>lev += 1</code>을 다른 줄에 넣으면 어떻게 되는가</strong></p>
<pre><code class="language-python">def create_words(lev, s):
    words.append(s)
    lev += 1                    # 여기 넣으면 길이 5짜리가 누락된다
    for i in range(0, 5):
        if lev &lt; 5:
            create_words(lev, s + VOWELS[i])</code></pre>
<p>lev=4로 진입(s=&#39;AAAA&#39;)하면 <code>lev += 1</code>로 lev=5가 되고 <code>if lev &lt; 5</code>가 False가 된다. 자식 호출이 0번이라 &#39;AAAAA&#39; ~ &#39;AAAAU&#39; 다섯 단어가 누락된다. 그래서 결과가 달랐던 것이다.</p>
<p><strong>재귀 빈칸/디버깅 체크리스트</strong></p>
<ol>
<li><strong>종료 조건이 있는가</strong> — 여기선 <code>if lev &lt; 5</code></li>
<li><strong>호출할 때 값이 진행하는가</strong> — <code>lev+1</code>, <code>s + 글자</code>처럼 <em>뭔가</em> 한 칸 나아가야 한다. 안 그러면 무한 루프다</li>
<li><strong>각 호출은 독립</strong> — 인자 새로 넘기는 거지 바깥 호출 변수를 바꾸는 게 아니다</li>
</ol>
<hr>
<h3 id="9번-시계-시침과-분침의-각도--외우는-공식">9번: 시계 시침과 분침의 각도 — 외우는 공식</h3>
<p>hour:minute일 때 아날로그 시계의 시침과 분침이 이루는 각도를 구하는 문제다. 본인이 제출한 답:</p>
<pre><code class="language-python">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 &quot;{:.1f}&quot;.format(answer)</code></pre>
<p>제출은 통과했지만 식에 버그가 3개 있다. 테스트가 약해서 우연히 통과한 케이스다.</p>
<p><strong>버그 1: 시침의 분 기여도 누락</strong></p>
<p>3:30일 때 시침은 3을 정확히 안 가리키고 3과 4 사이에 있다. 분침이 한 바퀴 돌 때마다 시침도 30° 움직이므로, 시침에 <code>minute * 0.5</code>를 더해야 한다.</p>
<table>
<thead>
<tr>
<th>식</th>
<th>3:30 시침</th>
</tr>
</thead>
<tbody><tr>
<td><code>hour / 12 * 360</code></td>
<td>90° (틀림)</td>
</tr>
<tr>
<td><code>hour * 30 + minute * 0.5</code></td>
<td>105° (맞음)</td>
</tr>
</tbody></table>
<p><strong>버그 2: 분침이 반대로 돈다</strong></p>
<p><code>(60-minute)/60 * 360</code>은 분침을 반시계 방향으로 돌린다.</p>
<table>
<thead>
<tr>
<th>분</th>
<th>본인 식</th>
<th>정답 <code>minute * 6</code></th>
</tr>
</thead>
<tbody><tr>
<td>0</td>
<td>0</td>
<td>0</td>
</tr>
<tr>
<td>15</td>
<td>270</td>
<td>90</td>
</tr>
<tr>
<td>30</td>
<td>180</td>
<td>180</td>
</tr>
<tr>
<td>45</td>
<td>90</td>
<td>270</td>
</tr>
</tbody></table>
<p>m=0과 m=30에서만 우연 일치한다. 4회차 테스트가 이 두 케이스만 있어서 통과한 것으로 보인다.</p>
<p><strong>버그 3: 큰 각도 처리 누락</strong></p>
<p>9:00일 때 시침 270°, 분침 0°. 보통 &quot;이루는 각&quot;은 작은 쪽(90°)을 답한다. 본인 식은 270을 반환한다. <code>min(diff, 360 - diff)</code>로 마무리해야 안전하다.</p>
<p><strong>정답 코드와 외울 공식</strong></p>
<pre><code class="language-python">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 &quot;{:.1f}&quot;.format(answer)</code></pre>
<table>
<thead>
<tr>
<th>공식</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>시침 각도</td>
<td><code>hour * 30 + minute * 0.5</code></td>
</tr>
<tr>
<td>분침 각도</td>
<td><code>minute * 6</code></td>
</tr>
<tr>
<td>두 침 사이 각</td>
<td><code>min(|h-m|, 360-|h-m|)</code></td>
</tr>
</tbody></table>
<p>이 유형은 공식을 모르면 풀 수 없다. 외워두는 게 답이다.</p>
<hr>
<h3 id="6번-자아도취-수--자릿수-분해-패턴">6번: 자아도취 수 — 자릿수 분해 패턴</h3>
<p>세 자리 자아도취 수: <code>153 = 1³ + 5³ + 3³</code>. 각 자릿수를 k 제곱해서 더한 값이 원래 수와 같은 자연수다. 빈칸 2개를 채우는 문제였는데 풀지 못했다.</p>
<pre><code class="language-python">while current != 0:
    calculated += power(current % 10, k)   # 빈칸 1
    current //= 10                          # 빈칸 2</code></pre>
<p><strong>핵심 패턴 — <code>% 10</code> + <code>// 10</code> 한 쌍</strong></p>
<table>
<thead>
<tr>
<th>연산</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>n % 10</code></td>
<td>마지막 자리 한 개 추출</td>
</tr>
<tr>
<td><code>n // 10</code></td>
<td>마지막 자리 한 개 제거</td>
</tr>
</tbody></table>
<p>이 두 개가 짝지어 돌아가야 자릿수 분해가 된다. 153 추적:</p>
<table>
<thead>
<tr>
<th>current</th>
<th>current % 10</th>
<th>calculated 누적</th>
<th>current //= 10</th>
</tr>
</thead>
<tbody><tr>
<td>153</td>
<td>3</td>
<td>0 → 27 (3³)</td>
<td>15</td>
</tr>
<tr>
<td>15</td>
<td>5</td>
<td>27 → 152</td>
<td>1</td>
</tr>
<tr>
<td>1</td>
<td>1</td>
<td>152 → 153</td>
<td>0</td>
</tr>
<tr>
<td>0</td>
<td>(탈출)</td>
<td></td>
<td></td>
</tr>
</tbody></table>
<p><strong>수업 중 질문</strong>: while 조건이 <code>current &gt; 0</code>이 아니고 <code>!= 0</code>이라서 빈칸에 탈출 조건을 만들어야 하는데 어떻게 만들어야 할지 모르겠다고 했다.</p>
<p>이게 가장 중요한 오해 지점이었다. <strong>탈출 조건을 빈칸에 넣는 게 아니다.</strong> <code>current //= 10</code>이라는 데이터 변형이 매 반복마다 current를 줄여 결국 0에 도달하게 만들고, 그 결과로 <code>current != 0</code>이 자연히 False가 되는 것이다. 탈출은 <strong>데이터 변형의 부산물</strong>이지 빈칸의 목적이 아니다.</p>
<p>100부터 시작해도 → 10 → 1 → 0이 되니까 3번 만에 탈출한다. 999부터 시작해도 → 99 → 9 → 0이라 마찬가지로 3번이다.</p>
<p>또 <code>current % 100</code>을 끝 두 자리(53)로 가져온다는 점도 함께 짚었다. 한 자리만 뽑으려면 무조건 <code>% 10</code>이다.</p>
<p><strong>자주 나오는 응용</strong>:</p>
<ul>
<li>자릿수 합 구하기</li>
<li>자릿수 뒤집기 (12345 → 54321)</li>
<li>한수, 자아도취 수, 회문수</li>
<li>진수 변환 (10진 → 2진 등)</li>
</ul>
<pre><code class="language-python"># 외워둘 자릿수 분해 템플릿
while n != 0:
    digit = n % 10
    # digit으로 뭔가 처리
    n //= 10</code></pre>
<hr>
<h3 id="8번-n번째로-작은-수--itertoolspermutations">8번: n번째로 작은 수 — itertools.permutations</h3>
<p>숫자 카드 배열로 만들 수 있는 모든 수를 작은 순으로 정렬했을 때, n이 몇 번째인지 구하는 문제다.</p>
<p>본인 시도는 Counter로 카드와 n의 자릿수를 비교해 다르면 -1을 반환하는 부분까지 진행했고, 그 뒤가 막혔다. 핵심 도구는 <strong>itertools.permutations</strong>다.</p>
<pre><code class="language-python">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</code></pre>
<p><strong>combinations vs permutations</strong></p>
<table>
<thead>
<tr>
<th>도구</th>
<th>의미</th>
<th>예: [1,2,3]에서 2개</th>
</tr>
</thead>
<tbody><tr>
<td><code>combinations</code></td>
<td>순서 무관 조합</td>
<td>(1,2), (1,3), (2,3)</td>
</tr>
<tr>
<td><code>permutations</code></td>
<td>순서 있는 순열</td>
<td>(1,2), (1,3), (2,1), (2,3), (3,1), (3,2)</td>
</tr>
</tbody></table>
<p>10차에서 배운 combinations의 동생격이다.</p>
<p><strong>set이 필요한 이유</strong>: permutations는 위치(인덱스) 기반이라 [1,2,1,3]처럼 같은 숫자가 있으면 동일한 순열이 여러 번 나온다. 첫 번째 1(card[0])과 두 번째 1(card[2])을 다른 카드로 취급하기 때문이다. 값으로 봤을 땐 같은 순열이니 <code>set()</code>으로 묶어야 정확한 등수가 나온다.</p>
<hr>
<h3 id="응용-연습-가장-큰-짝수-만들기">응용 연습: 가장 큰 짝수 만들기</h3>
<p>permutations를 손에 익히기 위해 출제된 응용 문제다.</p>
<pre><code class="language-python">from itertools import permutations

def solution(cards):
    digits = []
    for perm in set(permutations(cards)):
        num = int(&#39;&#39;.join(map(str, perm)))
        if num % 2 == 0:
            digits.append(num)
    return max(digits, default=-1)</code></pre>
<p>7개 테스트 케이스 모두 통과했다. 첫 학습 직후 응용 문제를 자력으로 푼 경우다.</p>
<p><strong>튜플 → 정수 변환</strong>: <code>int(&#39;&#39;.join(map(str, perm)))</code> 패턴이다. (3, 1, 2)를 312로 만든다.</p>
<p><strong><code>max(default=-1)</code></strong>: max는 빈 리스트에서 에러를 내는데, <code>default</code> 인자로 빈 시퀀스일 때 반환할 값을 지정할 수 있다.</p>
<p><strong>수업 중 슬림화 포인트</strong>: 처음에 <code>sorted(set(permutations(cards)))</code>로 정렬했지만, <code>max()</code>로 최댓값만 뽑을 거면 정렬이 필요 없다. 정렬은 O(n log n) 비용이 들지만 결과엔 영향이 없다.</p>
<blockquote>
<p>&quot;이 연산 정말 필요한가? 빼도 결과 같지 않은가?&quot;</p>
</blockquote>
<p>함수 작성 후 자기 점검 단계로 들어가야 할 메타 질문이다. 23차에 짚었던 슬림화 의식 영역으로, 27차에 1회 재발했다.</p>
<hr>
<h3 id="10번-소수의-세제곱--통과해도-식-버그-패턴">10번: 소수의 세제곱 — &quot;통과해도 식 버그&quot; 패턴</h3>
<p>a 이상 b 이하 자연수 중 소수의 제곱수와 세제곱수의 개수의 합을 구하는 문제다. 본인이 제출한 답:</p>
<pre><code class="language-python">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&lt;=n*n&lt;=b:
                    nums.append(n)
                if a&lt;=n*n*n&lt;=b:
                    nums.append(n)
    answer = len(nums)
    return answer</code></pre>
<p><strong>수업 중 질문</strong>: 소수 2를 미리 넣고 3부터 보기 시작하는 방식이 괜찮은지 물어봤다.</p>
<p>답은 &quot;버그다. 테스트가 약해서 통과한 것&quot;이다. <code>nums = [2]</code>로 2를 무조건 카운트에 포함하지만, 2² (=4) 또는 2³ (=8)이 [a,b] 범위에 있는지 확인하지 않는다.</p>
<table>
<thead>
<tr>
<th>a</th>
<th>b</th>
<th>정답</th>
<th>본인 코드</th>
</tr>
</thead>
<tbody><tr>
<td>6, 30</td>
<td>4</td>
<td>4 (우연 일치)</td>
<td></td>
</tr>
<tr>
<td>10, 30</td>
<td>2</td>
<td>3 (2 헛카운트)</td>
<td></td>
</tr>
<tr>
<td>50, 100</td>
<td>0</td>
<td>1 (2 헛카운트)</td>
<td></td>
</tr>
</tbody></table>
<p>근본 원인은 소수 판별 루프가 <code>range(2, n)</code>이라 n=2에선 inner loop가 비어버려 일반 루프에 못 넣은 것이다. 소수 판별을 함수로 분리하면 자연 처리된다.</p>
<p><strong>수업 중 잘한 부분</strong>: 처음에 13라인과 15라인을 or로 연결했다가 답이 달라서 예시(3은 제곱과 세제곱 모두 범위 안)를 보고 분리한 부분이다. or로 묶으면 한 번만 카운트되지만 별도 if로 분리하면 둘 다 만족할 때 두 번 카운트된다. 직접 예시를 추적해서 검증한 것은 24차에 정착된 검증 습관이 지속되고 있는 신호다.</p>
<p><strong>추가 이슈 — 시간복잡도</strong></p>
<p>b는 최대 10⁹까지 가능한데 <code>for n in range(3, b+1)</code>로 루프를 돌면 절대 끝나지 않는다.</p>
<blockquote>
<p>n² ≤ b이려면 n ≤ √b면 충분하니까 루프 상한을 √b로 줄일 수 있다.</p>
</blockquote>
<p>b=10⁹여도 √b ≈ 31,623으로 한순간이다. 23차에 배운 √n 최적화를 소수 판별 자체에도 적용해야 한다.</p>
<p><strong>정답 코드</strong>:</p>
<pre><code class="language-python">def is_prime(n):
    if n &lt; 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 &lt;= n * n &lt;= b:
                answer += 1
            if a &lt;= n * n * n &lt;= b:
                answer += 1
    return answer</code></pre>
<hr>
<h3 id="메타-테스트-통과해도-식-자체는-틀릴-수-있다는-패턴">메타: &quot;테스트 통과해도 식 자체는 틀릴 수 있다&quot;는 패턴</h3>
<p>오늘 4회차에서 두 건 발견했다.</p>
<ul>
<li><strong>9번 시계</strong>: m=0, m=30 케이스에서만 우연 일치</li>
<li><strong>10번 소수</strong>: a ≤ 8 또는 작은 b에서만 우연 일치</li>
</ul>
<p>19차에서 짚었던 &quot;주어진 테스트 전부 통과지만 반례에서 깨짐&quot; 패턴이 이번엔 본인이 통과 처리받은 답에서 재발한 형태다. 실제 시험은 테스트가 더 엄격할 수 있으니, <strong>풀이 후 반례를 직접 상상해보는 습관</strong>이 필요하다.</p>
<p>10번 nums=[2]에 대해 &quot;이래도 되는걸까?&quot;라고 자가 의심한 것 자체는 좋은 시작이다. 통과한 답에도 의심 가능한 메타인지가 발현된 것이다. 시험장에서는 시간 제약 때문에 모든 답을 의심할 순 없지만, 핵심 식만큼은 &quot;반례 한 개 떠올려보기&quot;를 의식적으로 끼워 넣자.</p>
<hr>
<h2 id="오늘의-결과">오늘의 결과</h2>
<p>구름EDU 4회차 풀타임 모의고사 73분에 8/10 통과, 추정 800점대로 합격선 한참 위였다.
신규 학습 5건(재귀 멘탈 모델, 시계 각도 공식, 자릿수 분해 패턴, itertools.permutations, max default 인자)을 정리했다.
내일 6회차 풀타임 모의고사 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[20260512 오늘의 학습: 5차 기출 복습 + 오답 총정리]]></title>
            <link>https://velog.io/@lee_yesol421/20260512-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-5%EC%B0%A8-%EA%B8%B0%EC%B6%9C-%EB%B3%B5%EC%8A%B5-%EC%98%A4%EB%8B%B5-%EC%B4%9D%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@lee_yesol421/20260512-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-5%EC%B0%A8-%EA%B8%B0%EC%B6%9C-%EB%B3%B5%EC%8A%B5-%EC%98%A4%EB%8B%B5-%EC%B4%9D%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 12 May 2026 14:57:50 GMT</pubDate>
            <description><![CDATA[<h2 id="지난-학습-요약">지난 학습 요약</h2>
<p>25차(5/8)에서 10일 공백 후 전 영역 워밍업 11문제를 진행했고 11/11 1차 정답으로 컨디션 회복을 확인했다.
90도 회전 공식, 스택 패턴, 이진 탐색, 소수 판별 등 전 영역이 공백에도 안정적으로 유지됐다.
90분 타이머 모의고사 진입 가능 신호가 확정된 상태로 실전 대비를 이어가고 있다.</p>
<h2 id="오늘-수업-계획">오늘 수업 계획</h2>
<p>20260511 파이썬 스터디에서 구름EDU 5차 기출 7문제(구현형 제외)를 풀었다.
오늘은 전날 풀이를 함께 복습하고 부족한 개념을 보충했으며, 이후 D-4 기준 오답 총복습을 진행했다.</p>
<blockquote>
<p>COS Pro 1급 Python / Phase 3: 실전 대비</p>
</blockquote>
<hr>
<h2 id="학습-내용-정리">학습 내용 정리</h2>
<h3 id="5차-기출-7문제-복습">5차 기출 7문제 복습</h3>
<table>
<thead>
<tr>
<th>문제</th>
<th>유형</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>1번 계단 오르기</td>
<td>빈칸</td>
<td>힌트 후 정답</td>
</tr>
<tr>
<td>2번 물 담기</td>
<td>디버깅</td>
<td>혼자 정답</td>
</tr>
<tr>
<td>3번 배열 사전순 정렬</td>
<td>디버깅</td>
<td>혼자 정답</td>
</tr>
<tr>
<td>4번 숫자 읽기</td>
<td>디버깅</td>
<td>검색 후 정답</td>
</tr>
<tr>
<td>7번 그래프 사이클</td>
<td>빈칸</td>
<td>AI 도움</td>
</tr>
<tr>
<td>8번 공약수 구하기</td>
<td>빈칸</td>
<td>혼자 정답</td>
</tr>
<tr>
<td>10번 계산기 만들기</td>
<td>빈칸</td>
<td>혼자 정답</td>
</tr>
</tbody></table>
<hr>
<h3 id="1번-dp--마지막-동작으로-경우-나누기">1번: DP — 마지막 동작으로 경우 나누기</h3>
<p>계단을 1칸, 2칸, 3칸씩 오를 수 있을 때, n번째 계단에 오르는 경우의 수를 구하는 문제다.</p>
<pre><code class="language-python">def solution(n):
    steps = [0 for _ in range(n+1)]
    steps[1] = 1
    steps[2] = 2
    steps[3] = 4
    for i in range(4, n+1):
        steps[i] = steps[i-1] + steps[i-2] + steps[i-3]
    return steps[n]</code></pre>
<p><strong>핵심 사고방식:</strong> &quot;n번째에 어떻게 도착했는가&quot;로 경우를 나눈다.</p>
<pre><code>n번째 계단에 도달하는 방법:
  1. (n-1)번째에서 1칸 → steps[n-1]가지
  2. (n-2)번째에서 2칸 → steps[n-2]가지
  3. (n-3)번째에서 3칸 → steps[n-3]가지
→ steps[n] = steps[n-1] + steps[n-2] + steps[n-3]</code></pre><table>
<thead>
<tr>
<th>마지막 동작</th>
<th>점화식</th>
</tr>
</thead>
<tbody><tr>
<td>1칸 또는 2칸 (타일)</td>
<td><code>f(n) = f(n-1) + f(n-2)</code></td>
</tr>
<tr>
<td>1/2/3칸 (계단)</td>
<td><code>f(n) = f(n-1) + f(n-2) + f(n-3)</code></td>
</tr>
</tbody></table>
<p>수업 중 질문: 타일 채우기 연습 문제를 요청함.</p>
<pre><code class="language-python"># 타일 채우기 연습 — 1×n 타일을 1칸/2칸으로 채우는 방법의 수
def solution(n):
    tile = [0] * (n + 1)
    tile[1] = 1
    tile[2] = 2
    for i in range(3, n + 1):
        tile[i] = tile[i-1] + tile[i-2]  # 마지막 타일이 1칸/2칸
    return tile[n]</code></pre>
<hr>
<h3 id="2번-디버깅--부등호-방향">2번: 디버깅 — 부등호 방향</h3>
<p>두 벽 사이에 담을 수 있는 물의 양은 <strong>낮은 벽 기준</strong>으로 계산해야 한다.</p>
<pre><code class="language-python"># 버그: walls[i][1] &gt; walls[j][1] → i가 높으면 i 기준 (높은 쪽으로 계산)
# 수정: walls[i][1] &lt; walls[j][1] → i가 낮으면 i 기준 (낮은 쪽으로 계산)
if walls[i][1] &lt; walls[j][1]:
    area = walls[i][1] * (walls[j][0] - walls[i][0])</code></pre>
<p>부등호 방향 실수는 디버깅 문제에서 자주 나오는 패턴이다.</p>
<hr>
<h3 id="3번-디버깅--right-범위">3번: 디버깅 — right 범위</h3>
<p>배열을 앞쪽은 오름차순, 뒤쪽은 내림차순으로 정렬하는 문제다. 중앙(mid)에 최대값을 배치하고 뒤쪽을 뒤집는 방식이다.</p>
<pre><code class="language-python">mid = (len(numbers) - 1) // 2
numbers[mid], numbers[len(numbers)-1] = numbers[len(numbers)-1], numbers[mid]
left = mid + 1
right = len(numbers) - 2  # 버그 수정: -1이 아닌 -2 (이미 swap된 마지막 인덱스 제외)</code></pre>
<p>8번 줄에서 <code>numbers[-1]</code>이 이미 mid와 swap됐기 때문에 <code>right</code>는 <code>len-2</code>부터 시작해야 한다.</p>
<hr>
<h3 id="4번-디버깅--range-역순">4번: 디버깅 — range 역순</h3>
<p>숫자를 큰 자릿수부터 읽기 위해 역순으로 순회해야 한다.</p>
<pre><code class="language-python"># 버그: range(10) → 0,1,...9 순서 → &quot;122134&quot; 출력
# 수정: range(9, -1, -1) → 9,8,...0 순서 → &quot;413221&quot; 출력
for i in range(9, -1, -1):
    if number_count[i] != 0:
        answer += (str(i) + str(number_count[i]))</code></pre>
<p>수업 중 질문: range 역순 말고 다른 방법이 있냐고 질문했다.</p>
<pre><code class="language-python"># 방법 1: range(9, -1, -1)
# 방법 2: reversed(range(10))  ← 인덱스 역순에 가장 가독성 좋음
# 방법 3: my_list[::-1]         ← 리스트 자체를 뒤집을 때

# reversed()는 이터레이터를 반환
result = reversed([1, 2, 3])
print(list(result))  # [3, 2, 1] — list()로 변환해야 리스트</code></pre>
<hr>
<h3 id="7번-union-find-그래프-사이클-탐지">7번: Union-Find (그래프 사이클 탐지)</h3>
<p>그래프에서 두 노드를 연결할 때 이미 같은 그룹이면 사이클이 생긴다.</p>
<pre><code class="language-python">def find(parent, u):        # u의 최상위 대표(루트) 찾기
    if u == parent[u]:
        return u
    parent[u] = find(parent, parent[u])  # 경로 압축
    return parent[u]

def merge(parent, u, v):    # 두 노드를 같은 그룹으로 합치기
    u = find(parent, u)
    v = find(parent, v)
    if u == v:
        return True         # 이미 같은 그룹 → 사이클!
    parent[u] = v
    return False

def solution(n, connections):
    parent = list(range(n + 1))  # [0,1,2,...,n]: 처음엔 자기가 자기 대표
    for i, connection in enumerate(connections):
        if merge(parent, connection[0], connection[1]):
            return i + 1
    return answer</code></pre>
<p>수업 중 질문: parent가 왜 n+1개인지 질문했다.</p>
<pre><code>노드 번호가 1,2,3이면 인덱스도 1,2,3으로 맞추기 위해 n+1 크기로 만든다.
parent[0]은 사용하지 않고 버린다. 그래야 노드 번호 = 인덱스가 돼서 직관적이다.

parent = list(range(n+1))       # 간결한 표현
parent = [i for i in range(n+1)]  # 같은 결과지만 변환이 없으면 list(range())가 더 명확</code></pre><p>시험 전략: Union-Find는 코드 전체가 주어지고 빈칸 2개를 채우는 형태로 나온다. &quot;사이클 = 이미 같은 그룹&quot;과 빈칸 패턴(<code>find(parent, parent[u])</code>, <code>parent[u] = v</code>)만 기억해두면 충분하다.</p>
<hr>
<h3 id="8번-공약수-구하기--함수-역할-파악-전략">8번: 공약수 구하기 — 함수 역할 파악 전략</h3>
<p>세 함수의 역할을 각각의 return을 보고 먼저 파악한 뒤 solution을 채웠다.</p>
<pre><code class="language-python"># func_a: GCD (유클리드 호제법)
# func_b: 약수 개수 세기
# func_c: p가 q의 약수인지 여부

def solution(a, b, c):
    gcd = func_a(func_a(a, b), c)  # 세 수의 GCD: 두 번 연쇄 적용
    answer = func_b(gcd)            # gcd의 약수 개수
    return answer</code></pre>
<p>23차에서 배운 LCM 누적 패턴(<code>lcm(lcm(a,b), c)</code>)과 완전히 같은 구조다.</p>
<hr>
<h3 id="10번-클래스-상속--계산기-만들기">10번: 클래스 상속 — 계산기 만들기</h3>
<p>클래스 구조도를 보고 상속 관계를 파악해 빈칸을 채웠다.</p>
<pre><code class="language-python">class PartTimeJob(Job):    # Job 상속
    def get_salary(self):  # 오버라이드

class SalesJob(Job):       # Job 상속
    def get_salary(self):  # 오버라이드</code></pre>
<hr>
<h3 id="오답-총복습">오답 총복습</h3>
<p><strong>값 기반 중복 체크 함정 (20차 약점)</strong></p>
<p>경계 순회에서 <code>arr[x][y] not in result</code>로 중복을 체크하면 테두리에 같은 값이 여러 번 나올 때 누락이 생긴다.</p>
<pre><code class="language-python"># 값 기반 (위험)
if arr[0][j] not in result:
    result.append(arr[0][j])

# 좌표 기반 (안전)
if (0, j) not in visited:
    result.append(arr[0][j])
    visited.add((0, j))</code></pre>
<p>경계 순회 → 무조건 좌표 기반 visited로 전환.</p>
<p>수업 중 질문: visited set에 좌표를 저장하는 방법을 물어봤다.</p>
<pre><code class="language-python">visited.add(0, j)    # ❌ TypeError: add()는 인자 하나만 받음
visited.add((0, j))  # ✅ 튜플을 하나의 인자로 전달</code></pre>
<p><strong>90도 회전 공식</strong></p>
<pre><code class="language-python">rotated[i][j] = arr[n-1-j][i]</code></pre>
<p>즉시 정답. 정착 재확인.</p>
<p><strong>스택 괄호 매칭</strong></p>
<pre><code class="language-python">def is_valid(s):
    stack = []
    for ch in s:
        if ch == &#39;(&#39;:
            stack.append(ch)
        else:
            if not stack:      # 빈 스택 = 짝 없는 )
                return False
            stack.pop()        # 짝 맞추고 제거
    return len(stack) == 0     # 남은 게 없어야 완전 매칭</code></pre>
<p>즉시 정답. 정착 재확인.</p>
<p><strong>내장함수명 변수 사용 주의</strong></p>
<pre><code class="language-python">str = &quot;&quot;    # ❌ 내장함수 str()이 덮어씌워짐
str(num)    # → TypeError 발생

# 변수명 블랙리스트: str, sum, list, dict, type, len, max, min
# 대체: result, total, items, data ...</code></pre>
<hr>
<h2 id="오늘의-결과">오늘의 결과</h2>
<p>5차 기출 7문제(스터디 풀이) 복습 완료, 부족했던 DP 패턴과 range 역순 개념 보충했다.
오답 총복습에서 90도 회전, 스택, 중복 체크 패턴 전 항목 정착 재확인했다.
D-4 기준 주요 약점 없음. 내일부터 90분 풀타임 모의고사 2회 예정.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[20260508 오늘의 학습: 전 영역 워밍업 11문제]]></title>
            <link>https://velog.io/@lee_yesol421/20260508-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-%EC%A0%84-%EC%98%81%EC%97%AD-%EC%9B%8C%EB%B0%8D%EC%97%85-11%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@lee_yesol421/20260508-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-%EC%A0%84-%EC%98%81%EC%97%AD-%EC%9B%8C%EB%B0%8D%EC%97%85-11%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Fri, 08 May 2026 13:41:26 GMT</pubDate>
            <description><![CDATA[<h2 id="지난-학습-요약">지난 학습 요약</h2>
<p>지난 세션(4/28)에서는 90도 회전 공식과 약수 함수 슬림화로 3문제 전부 1차 정답을 받았다. 8일 공백에도 <code>arr[n-1-j][i]</code> 공식을 정확히 떠올리며 정착 신호 확인. 함수 작성 4세션 연속 자력 정답으로 추세를 굳혔다.</p>
<h2 id="오늘-수업-계획">오늘 수업 계획</h2>
<p>시험까지 일주일 남은 시점에서 약 10일 만에 다시 책상 앞에 앉았다. 바로 90분 타이머 모의고사로 들어가는 건 무리라 판단해, <strong>전 영역을 한 문제씩 가볍게 노출</strong>하는 워밍업 트랙을 짰다. 총 11문제. 자료형/조건/반복 → 리스트/문자열/딕셔너리/집합 → 정렬/함수/예외 → 스택/이진탐색/수학/구현 순서로, 지금까지 배운 모든 개념을 자연스럽게 한 번씩 점검한다. 컨디션 회복이 1차 목표, 90분 타이머 진입 가능 여부 진단이 2차 목표.</p>
<hr>
<h2 id="학습-내용-정리">학습 내용 정리</h2>
<h3 id="1-반복문--조건-sum_multiples_of_3">1. 반복문 + 조건 (sum_multiples_of_3)</h3>
<p>a부터 b까지(둘 다 포함) 정수 중 3의 배수의 합을 반환. 음수 입력도 처리해야 한다.</p>
<pre><code class="language-python">def sum_multiples_of_3(a, b):
    total = 0
    for i in range(a, b+1):
        if i % 3 == 0:
            total += i
    return total</code></pre>
<p><strong>한 줄 컴프리헨션 형태도 익혀두자</strong> — 시험 빈칸으로 자주 나오는 형태.</p>
<pre><code class="language-python">return sum(i for i in range(a, b+1) if i % 3 == 0)</code></pre>
<blockquote>
<p><strong>수업 중 질문</strong>: 테스트케이스 <code>(-5, 5)</code>의 기대값이 왜 <code>3</code>이지? <code>-3 + 0 + 3</code>이면 <code>0</code> 아닌가?</p>
<p>학습자가 첫 문제부터 출제자 검산을 시작했다. 정확한 지적이고 내 계산 실수였다. 정정: <code>-3 + 0 + 3 = 0</code>이 맞다. 참고로 Python에서 <code>-3 % 3 == 0</code>, <code>0 % 3 == 0</code> 둘 다 성립하므로 <code>n % 3 == 0</code> 조건 하나로 음수까지 잘 잡힌다. 검증 습관이 10일 공백에도 살아있다는 좋은 신호.</p>
</blockquote>
<hr>
<h3 id="2-리스트-슬라이싱--컴프리헨션-filter_reverse">2. 리스트 슬라이싱 + 컴프리헨션 (filter_reverse)</h3>
<p>리스트를 뒤집은 후 k 이상인 값만 필터링.</p>
<pre><code class="language-python">def filter_reverse(nums, k):
    return [i for i in nums if i &gt;= k][::-1]</code></pre>
<h4 id="인사이트-filter와-reverse는-교환-가능">인사이트: filter와 reverse는 교환 가능</h4>
<p>문제는 &quot;뒤집은 후 필터&quot;였는데 풀이는 &quot;필터한 후 뒤집기&quot;다. 결과는 같다 — <strong>둘 다 상대 순서를 보존하는 연산</strong>이라 교환 법칙이 성립하기 때문. 그리고 필터로 먼저 줄인 뒤 뒤집는 게 작업량이 더 적어서 살짝 효율적이기까지 하다.</p>
<pre><code class="language-python">[i for i in nums if i &gt;= k][::-1]   # 효율적
[i for i in nums[::-1] if i &gt;= k]   # 문제 그대로</code></pre>
<p>→ <strong>메모해둘 패턴</strong>: 두 연산의 순서를 바꿔도 결과가 같은지 자문하는 습관. 시험에서 빈칸 답이 두 형태 모두 정답인 경우가 종종 있다.</p>
<hr>
<h3 id="3-문자열-회문-판별-is_palindrome">3. 문자열 회문 판별 (is_palindrome)</h3>
<p>대소문자와 공백을 무시하고 회문 판별.</p>
<pre><code class="language-python">def is_palindrome(s):
    word = s.replace(&quot; &quot;, &quot;&quot;).lower()
    return word == word[::-1]</code></pre>
<p><strong>2단계 공식</strong>: (1) 정규화(공백 제거 + 소문자), (2) <code>word == word[::-1]</code> 비교 한 줄. 회문 패턴은 손에 박힌 상태.</p>
<blockquote>
<p><strong>사이드 노트</strong>: 만약 숫자나 구두점도 무시해야 하는 강한 회문 문제라면 <code>re.sub(r&#39;[^a-z0-9]&#39;, &#39;&#39;, word)</code> 패턴이 필요하다. 오늘은 워밍업이라 안 갔지만 응용 문제에서 떠올릴 수 있게 익혀둔다.</p>
</blockquote>
<hr>
<h3 id="4-딕셔너리--counter--가장-많이-등장한-글자-most_char">4. 딕셔너리 / Counter — 가장 많이 등장한 글자 (most_char)</h3>
<p>가장 많이 등장한 글자와 횟수를 튜플로. 동률이면 먼저 나온 글자 우선.</p>
<h4 id="풀이-a--counter-한-줄">풀이 A — Counter 한 줄</h4>
<pre><code class="language-python">from collections import Counter

def most_char(s):
    return Counter(s).most_common(1)[0]</code></pre>
<h4 id="동률-처리가-자동으로-되는-이유">동률 처리가 자동으로 되는 이유</h4>
<ul>
<li><code>Counter</code>는 <code>dict</code> 상속이라 <strong>삽입 순서를 보존</strong> (Python 3.7+)</li>
<li><code>most_common(n)</code>은 내부적으로 <code>heapq.nlargest</code>를 쓰는데, 이건 <strong>동률일 때 입력 순서를 유지</strong>하는 안정 정렬</li>
<li>그래서 &quot;먼저 등장한 글자&quot;가 자동으로 앞에 옴</li>
</ul>
<p>→ <code>most_common(1)[0]</code>은 거의 공식처럼 외워도 된다.</p>
<blockquote>
<p><strong>수업 중 질문</strong>: <code>dict.get()</code> 방식으로도 풀어보다가 막혔다. 이 코드는 왜 에러나?</p>
<pre><code class="language-python">tmp_dict = {}
for c in s:
    tmp_dict[c] = tmp_dict.get(c, 0) + 1
return max(tmp_dict, key=lambda x: x[1])   # IndexError 발생</code></pre>
<p>원인은 <strong><code>max(tmp_dict, ...)</code>가 딕셔너리에서 key만 꺼낸다</strong>는 것. 그래서 <code>lambda x: x[1]</code>의 <code>x</code>는 글자 한 개(<code>&#39;a&#39;</code>, <code>&#39;p&#39;</code> 등)가 되고, 한 글자짜리 문자열에 <code>x[1]</code>을 하면 IndexError가 난다.</p>
<p><strong>수정</strong>: <code>.items()</code>를 넘긴다.</p>
<pre><code class="language-python">return max(tmp_dict.items(), key=lambda x: x[1])</code></pre>
<p>이렇게 하면 <code>x</code>가 <code>(&#39;a&#39;, 1)</code> 같은 튜플이 되고 <code>x[1]</code>이 횟수가 된다. 그리고 동률 처리는 <code>Counter</code> 버전과 동일 — <code>dict.items()</code>가 삽입 순서로 순회하고 <code>max</code>는 먼저 본 max를 반환하므로 &quot;먼저 등장한 글자&quot;가 자동으로 우선된다.</p>
</blockquote>
<h4 id="자료구조와-순회-패턴-정리">자료구조와 순회 패턴 정리</h4>
<table>
<thead>
<tr>
<th>표현</th>
<th>순회 시 꺼내는 것</th>
</tr>
</thead>
<tbody><tr>
<td><code>for x in dict</code></td>
<td><strong>key만</strong></td>
</tr>
<tr>
<td><code>for x in dict.values()</code></td>
<td>value만</td>
</tr>
<tr>
<td><code>for x in dict.items()</code></td>
<td><strong>(key, value) 튜플</strong></td>
</tr>
</tbody></table>
<p><code>max</code> / <code>min</code> / <code>sorted</code> / <code>for</code> 모두 같은 규칙. 한 번 의식해두면 비슷한 에러 다시 안 난다.</p>
<hr>
<h3 id="5-집합set-교집합-common_chars">5. 집합(set) 교집합 (common_chars)</h3>
<p>두 문자열에 모두 등장하는 글자를 알파벳순으로.</p>
<pre><code class="language-python">def common_chars(a, b):
    return sorted(set(a) &amp; set(b))</code></pre>
<h4 id="집합-연산자-4가지-시험-빈출">집합 연산자 4가지 (시험 빈출)</h4>
<table>
<thead>
<tr>
<th>기호</th>
<th>의미</th>
<th>예시 (<code>A={1,2,3}</code>, <code>B={2,3,4}</code>)</th>
</tr>
</thead>
<tbody><tr>
<td><code>A &amp; B</code></td>
<td>교집합 (intersection)</td>
<td><code>{2, 3}</code></td>
</tr>
<tr>
<td><code>A | B</code></td>
<td>합집합 (union)</td>
<td><code>{1, 2, 3, 4}</code></td>
</tr>
<tr>
<td><code>A - B</code></td>
<td>차집합 (difference)</td>
<td><code>{1}</code></td>
</tr>
<tr>
<td><code>A ^ B</code></td>
<td>대칭차 (symmetric difference)</td>
<td><code>{1, 4}</code></td>
</tr>
</tbody></table>
<h4 id="슬림화-체크--신규-약점-자연-노출">슬림화 체크 — 신규 약점 자연 노출</h4>
<p>처음 작성한 풀이는 <code>sorted(list(set(a) &amp; set(b)))</code> 였다. <strong><code>list()</code> 호출이 불필요</strong>하다. <code>sorted()</code>는 어떤 iterable이든 받아서 항상 list를 반환하기 때문.</p>
<p>이게 4/27에 새로 잡힌 약점인 <strong>&quot;불필요 변환/분기 추가&quot;</strong> 패턴이다. 자연 노출에서 1회 발생. 의식하면 다음번엔 안 나올 가능성 높음.</p>
<p>함수 작성 후 자기 점검 한 마디:</p>
<blockquote>
<p>&quot;이 변환/분기 정말 필요한가? 자료구조나 호출 결과를 그대로 넘기면 사라지지 않나?&quot;</p>
</blockquote>
<hr>
<h3 id="6-다중-정렬-sort_students">6. 다중 정렬 (sort_students)</h3>
<p><code>(이름, 점수)</code> 튜플 리스트를 점수 내림차순, 동점이면 이름 오름차순으로 정렬.</p>
<pre><code class="language-python">def sort_students(students):
    return sorted(students, key=lambda x: (-x[1], x[0]))</code></pre>
<h4 id="튜플-key의-동작-원리">튜플 key의 동작 원리</h4>
<pre><code>key=lambda x: (-x[1], x[0])
        ┌─────┐  ┌────┐
        점수    이름
        내림    오름</code></pre><ol>
<li>첫 원소 <code>-x[1]</code>로 먼저 비교 → 음수로 만들어서 <strong>큰 점수가 앞에</strong></li>
<li>첫 원소가 같으면 둘째 원소 <code>x[0]</code>(이름)으로 오름차순 비교</li>
<li><strong>튜플은 lexicographic 비교</strong>라 자동으로 1차 → 2차 정렬이 작동</li>
</ol>
<p>→ 음수 트릭으로 내림차순 + 두 번째 키로 동률 처리. <strong>COS Pro 빈출 1순위 패턴</strong>.</p>
<hr>
<h3 id="7-함수-기본값--예외-처리-safe_get">7. 함수 (기본값) + 예외 처리 (safe_get)</h3>
<p><code>nums[idx]</code>를 안전하게 반환. 범위를 벗어나면 default 반환.</p>
<pre><code class="language-python">def safe_get(nums, idx, default=None):
    try:
        return nums[idx]
    except IndexError:
        return default</code></pre>
<blockquote>
<p><strong>수업 중 질문</strong>: try/except 구문이 바로 안 떠올라서 옛날 자료를 찾아봤다. 나도 모르게 try-catch로 생각하고 있더라.</p>
<p>Java 출신이라 자연스러운 슬립이고, 7차 학습 이후 한 달 반 만의 자연 노출이라 더 그렇다. 로직은 완전히 손에 있는데 문법 키워드만 슬쩍 미끄러진 상태. 다시 굳히는 차원에서 매핑 정리.</p>
</blockquote>
<h4 id="java-→-python-tryexcept-매핑">Java → Python try/except 매핑</h4>
<pre><code class="language-python">try:
    risky()
except IndexError as e:    # catch → except, (X) → as
    handle(e)
except (KeyError, ValueError):   # 여러 개 잡기 (튜플)
    handle()
else:                       # try가 예외 없이 끝났을 때만 (Java엔 없음)
    print(&quot;성공&quot;)
finally:
    cleanup()</code></pre>
<table>
<thead>
<tr>
<th>Java</th>
<th>Python</th>
<th>메모</th>
</tr>
</thead>
<tbody><tr>
<td><code>try</code></td>
<td><code>try</code></td>
<td>동일</td>
</tr>
<tr>
<td><code>catch (Exception e)</code></td>
<td><strong><code>except Exception as e</code></strong></td>
<td>키워드/문법 다름</td>
</tr>
<tr>
<td><code>finally</code></td>
<td><code>finally</code></td>
<td>동일</td>
</tr>
<tr>
<td>❌ 없음</td>
<td><strong><code>else</code></strong></td>
<td>try가 성공했을 때만 실행 (보너스 절)</td>
</tr>
</tbody></table>
<p><strong>시험 함정 포인트</strong>:</p>
<ul>
<li><code>return</code>이 try에 있어도 <strong><code>finally</code>는 반드시 실행</strong>된다</li>
<li><code>else</code>는 <code>try</code>가 예외 없이 끝났을 때만, <strong><code>except</code>보다 먼저</strong> 위치</li>
</ul>
<h4 id="음수-인덱스가-자동-처리되는-점">음수 인덱스가 자동 처리되는 점</h4>
<p><code>nums[-1]</code>은 마지막 원소를 정상 반환하고, <strong>범위를 벗어난 음수</strong>(<code>nums[-10]</code> for <code>len=3</code>)에서만 <code>IndexError</code>를 던진다. C/Java라면 음수 인덱스 자체에서 별도 분기를 짜야 했을 텐데 Python은 한 줄에 처리됨.</p>
<hr>
<h3 id="8-스택--다종-괄호-매칭-is_balanced">8. 스택 — 다종 괄호 매칭 (is_balanced)</h3>
<p><code>( ) [ ] { }</code> 6가지 괄호의 짝과 순서를 모두 검사.</p>
<pre><code class="language-python">def is_balanced(s):
    pairs = {&quot;)&quot;: &quot;(&quot;, &quot;]&quot;: &quot;[&quot;, &quot;}&quot;: &quot;{&quot;}
    stack = []
    for c in s:
        if c in pairs.values():
            stack.append(c)
        elif c in pairs:
            if not stack or stack[-1] != pairs[c]:
                return False
            stack.pop()
    return not stack</code></pre>
<h4 id="정착-약점-두-개가-자연스럽게-등장">정착 약점 두 개가 자연스럽게 등장</h4>
<p>19차에서 학습한 두 가지가 다시 자연 재노출에서 안정적으로 나왔다.</p>
<ul>
<li>✅ <strong>무엇 저장</strong>: 여는 괄호를 스택에 쌓는다</li>
<li>✅ <strong>빈 스택 체크</strong>: <code>not stack</code> 단락평가로 <code>stack[-1]</code> 호출을 보호</li>
</ul>
<p>3가지 False 조건도 모두 처리:</p>
<ol>
<li>빈 스택에서 닫는 괄호 등장 (<code>not stack</code>)</li>
<li>짝이 안 맞음 (<code>stack[-1] != pairs[c]</code>)</li>
<li>끝났는데 스택에 남은 여는 괄호 (<code>return not stack</code>)</li>
</ol>
<h4 id="마이크로-최적화-시험엔-영향-없음">마이크로 최적화 (시험엔 영향 없음)</h4>
<p><code>c in pairs.values()</code>는 매번 values 순회라 O(n)이고, <code>c in pairs</code>는 dict 해시라 O(1). 문자열 상수로 꺼내두면 더 빠르고 시험 답안 같은 느낌:</p>
<pre><code class="language-python">opens = &quot;([{&quot;
pairs = {&quot;)&quot;: &quot;(&quot;, &quot;]&quot;: &quot;[&quot;, &quot;}&quot;: &quot;{&quot;}
for c in s:
    if c in opens:
        stack.append(c)
    elif c in pairs:
        if not stack or stack[-1] != pairs[c]:
            return False
        stack.pop()</code></pre>
<hr>
<h3 id="9-이진-탐색--기본-템플릿-binary_search">9. 이진 탐색 — 기본 템플릿 (binary_search)</h3>
<p>정렬된 리스트에서 target 인덱스 반환. 없으면 -1.</p>
<pre><code class="language-python">def binary_search(arr, target):
    left, right = 0, len(arr) - 1
    while left &lt;= right:
        mid = (left + right) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] &lt; target:
            left = mid + 1
        else:
            right = mid - 1
    return -1</code></pre>
<h4 id="디테일-4가지--시험-빈출-함정-포인트">디테일 4가지 — 시험 빈출 함정 포인트</h4>
<ul>
<li><code>while left &lt;= right</code> (등호 포함, 원소 1개 케이스 처리)</li>
<li><code>(left + right) // 2</code> (정수 나눗셈, <code>/</code>면 float이 되어 인덱싱 에러)</li>
<li><code>mid + 1</code> / <code>mid - 1</code> (무한루프 방지)</li>
<li>빈 리스트 자동 처리 (<code>right = -1</code>이라 루프 안 들어감)</li>
</ul>
<p><strong><code>/</code> vs <code>//</code></strong> 디버깅 함정은 22차에서 직접 잡아본 문제. 그 경험이 자산이 됐다.</p>
<hr>
<h3 id="10-수학--소수-판별-√n-최적화-is_prime">10. 수학 — 소수 판별 √n 최적화 (is_prime)</h3>
<pre><code class="language-python">def is_prime(n):
    if n &lt;= 1:
        return False
    i = 2
    while i * i &lt;= n:
        if n % i == 0:
            return False
        i += 1
    return True</code></pre>
<blockquote>
<p><strong>수업 중 질문</strong>: <code>range(2, int(n**0.5) + 1)</code> 이 형태는 뭐지?</p>
<p>while 형태와 <strong>완전히 같은 범위를 도는 for 형태</strong>다. 메커니즘만 다르다.</p>
</blockquote>
<h4 id="두-형태-비교">두 형태 비교</h4>
<pre><code class="language-python"># 정수 연산만 사용 (안전)
i = 2
while i * i &lt;= n:
    ...
    i += 1

# 동등한 for 형태 (sqrt 직접 계산)
for i in range(2, int(n**0.5) + 1):
    ...</code></pre>
<p>조각조각 뜯어보면 (n=25 기준):</p>
<table>
<thead>
<tr>
<th>표현</th>
<th>값</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>n ** 0.5</code></td>
<td><code>5.0</code></td>
<td>√n 계산 (float 반환)</td>
</tr>
<tr>
<td><code>int(...)</code></td>
<td><code>5</code></td>
<td>소수점 버림 (양수에선 floor)</td>
</tr>
<tr>
<td><code>+ 1</code></td>
<td><code>6</code></td>
<td>range 끝값은 미포함이라 +1</td>
</tr>
<tr>
<td><code>range(2, 6)</code></td>
<td><code>2, 3, 4, 5</code></td>
<td>i가 2~5까지 순회</td>
</tr>
</tbody></table>
<h4 id="어느-쪽이-좋은가">어느 쪽이 좋은가</h4>
<table>
<thead>
<tr>
<th>형태</th>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td><code>while i*i &lt;= n</code></td>
<td><strong>정수 연산만</strong> 사용 (안전)</td>
<td>for문 한 줄로 못 끝냄</td>
</tr>
<tr>
<td><code>range(2, int(n**0.5)+1)</code></td>
<td>for문 한 줄로 깔끔</td>
<td>float 거치므로 <strong>아주 큰 수</strong>에서 정밀도 함정 가능</td>
</tr>
</tbody></table>
<p>→ COS Pro 입력 크기에서는 둘 다 정답. 빈칸으로 어느 쪽이 나와도 헷갈리지 않게 둘 다 알아둔다.</p>
<hr>
<h3 id="11-구현--90도-시계방향-회전-rotate">11. 구현 — 90도 시계방향 회전 (rotate)</h3>
<p>N×N 행렬을 시계방향으로 90도 돌린 새 리스트를 반환.</p>
<pre><code class="language-python">def rotate(arr):
    n = len(arr)
    rotated = [[0] * n for _ in range(n)]
    for i in range(n):
        for j in range(n):
            rotated[j][n - 1 - i] = arr[i][j]
    return rotated</code></pre>
<h4 id="두-가지-매핑-스타일-둘-다-정답">두 가지 매핑 스타일 (둘 다 정답)</h4>
<pre><code class="language-python">rotated[j][n-1-i] = arr[i][j]   # 원본 → 회전 매핑 (이번 풀이)
rotated[i][j] = arr[n-1-j][i]   # 회전 → 원본 매핑 (4/28 학습)</code></pre>
<p>루프를 도는 관점이 다를 뿐, 결과는 동일.</p>
<blockquote>
<p><strong>수업 중 질문</strong>: 이건 시계방향이지? 만약 반대로(반시계) 돌린다면?</p>
</blockquote>
<h4 id="회전-4종-매핑-정리">회전 4종 매핑 정리</h4>
<table>
<thead>
<tr>
<th>방향</th>
<th>원본 → 회전 매핑</th>
<th>회전 → 원본 매핑</th>
<th>zip 한 줄형</th>
</tr>
</thead>
<tbody><tr>
<td><strong>CW (시계)</strong></td>
<td><code>rotated[j][n-1-i] = arr[i][j]</code></td>
<td><code>rotated[i][j] = arr[n-1-j][i]</code></td>
<td><code>zip(*arr[::-1])</code></td>
</tr>
<tr>
<td><strong>CCW (반시계)</strong></td>
<td><code>rotated[n-1-j][i] = arr[i][j]</code></td>
<td><code>rotated[i][j] = arr[j][n-1-i]</code></td>
<td><code>list(zip(*arr))[::-1]</code></td>
</tr>
</tbody></table>
<h4 id="시각적-기억법--외우는-것보다-안-까먹음">시각적 기억법 — 외우는 것보다 안 까먹음</h4>
<pre><code>    원본            CW (시계)       CCW (반시계)
   1 2 3            7 4 1           3 6 9
   4 5 6     →      8 5 2           2 5 8
   7 8 9            9 6 3           1 4 7</code></pre><ul>
<li><strong>CW</strong>: 첫 행(1,2,3)이 → <strong>마지막 열</strong>로 (왼쪽 위 → 오른쪽 위)</li>
<li><strong>CCW</strong>: 첫 행(1,2,3)이 → <strong>첫 열을 거꾸로</strong> (왼쪽 위 → 왼쪽 아래)</li>
</ul>
<h4 id="보너스--회전-패밀리-4종">보너스 — 회전 패밀리 4종</h4>
<ul>
<li><strong>시계 90도</strong>: <code>arr[n-1-j][i]</code></li>
<li><strong>반시계 90도</strong>: <code>arr[j][n-1-i]</code></li>
<li><strong>180도</strong>: <code>rotated[i][j] = arr[n-1-i][n-1-j]</code> (양쪽 다 뒤집힘)</li>
<li><strong>전치(transpose, 대각선 대칭)</strong>: <code>rotated[i][j] = arr[j][i]</code> 또는 <code>zip(*arr)</code></li>
</ul>
<p>CW 두 번 = 180도 = CCW 두 번. 회전 패밀리는 이 4가지가 전부다.</p>
<hr>
<h2 id="오늘의-결과">오늘의 결과</h2>
<p>11문제 1차 정답률 <strong>11/11 (100%)</strong> — 약 10일 공백 후 컨디션 회복용 워밍업 완료. 자연 노출 약점 1건(불필요한 <code>list()</code> 변환)만 발생, 의식 후 다음 노출 정착 판정 예정. <strong>90분 타이머 모의고사 진입 가능 신호</strong> 확인 — 다음 세션은 구름EDU 기출 4회차 풀타임 풀이.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[20260428 오늘의 학습: 2차원 회전 복습 + 약수 함수 슬림화]]></title>
            <link>https://velog.io/@lee_yesol421/20260428-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-2%EC%B0%A8%EC%9B%90-%ED%9A%8C%EC%A0%84-%EB%B3%B5%EC%8A%B5-%EC%95%BD%EC%88%98-%ED%95%A8%EC%88%98-%EC%8A%AC%EB%A6%BC%ED%99%94</link>
            <guid>https://velog.io/@lee_yesol421/20260428-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-2%EC%B0%A8%EC%9B%90-%ED%9A%8C%EC%A0%84-%EB%B3%B5%EC%8A%B5-%EC%95%BD%EC%88%98-%ED%95%A8%EC%88%98-%EC%8A%AC%EB%A6%BC%ED%99%94</guid>
            <pubDate>Tue, 28 Apr 2026 12:34:21 GMT</pubDate>
            <description><![CDATA[<h2 id="지난-학습-요약">지난 학습 요약</h2>
<p>지난 세션(4/27)에서는 수학 파트(소수 판별, 약수, GCD, LCM)를 한 번에 정리하며 4문제 전부 1차 정답으로 끝냈다. 다만 함수 작성에서 <strong>같은 값을 가진 변수 2개</strong>, <strong>불필요한 엣지 분기</strong> 같은 비-슬림한 패턴이 나와서 &quot;코드 슬림화 체크리스트 3가지&quot;를 신규 약점으로 정리했다. 함수 작성 자력 정답은 21차 → 22차 → 23차로 3세션 연속 진행 중.</p>
<h2 id="오늘-수업-계획">오늘 수업 계획</h2>
<p>오늘은 가벼운 보강 세션. 두 가지 트랙으로 갔다. 먼저 4/20에 학습한 <strong>90도 회전 공식</strong>이 이번엔 8일 공백이라 망각 점검 — 인덱스 직접 / zip 패턴 두 형태로 빈칸과 디버깅 한 문제씩. 그다음 어제 신규 약점인 <strong>슬림화 의식</strong>을 보강하기 위해 약수 함수 작성 후 직접 작성한 풀이를 슬림화 체크리스트로 자기 점검.</p>
<hr>
<h2 id="학습-내용-정리">학습 내용 정리</h2>
<h3 id="1-90도-시계방향-회전--인덱스-직접-매핑">1. 90도 시계방향 회전 — 인덱스 직접 매핑</h3>
<p>n × n 2차원 배열을 시계 방향으로 90도 돌리는 가장 직관적인 풀이. 핵심 공식 한 줄을 외우면 끝난다.</p>
<blockquote>
<p><strong><code>rotated[i][j] = arr[n-1-j][i]</code></strong></p>
</blockquote>
<h4 id="왜-이렇게-매핑되는가">왜 이렇게 매핑되는가?</h4>
<p>회전 후 i행은 <strong>원본 i열을 아래에서 위로 읽은 값</strong>으로 채워진다. 3×3 예시로 추적해보면 명확해진다.</p>
<p>원본:</p>
<pre><code>[1, 2, 3]
[4, 5, 6]
[7, 8, 9]</code></pre><p>시계 방향 90도 회전 후 (기대):</p>
<pre><code>[7, 4, 1]
[8, 5, 2]
[9, 6, 3]</code></pre><p>회전 후 0행 <code>[7, 4, 1]</code>은 <strong>원본의 0열 <code>[1, 4, 7]</code>을 거꾸로</strong> 읽은 값이다. 그래서:</p>
<table>
<thead>
<tr>
<th>위치 (i, j)</th>
<th>값</th>
<th>매핑</th>
</tr>
</thead>
<tbody><tr>
<td>rotated[0][0] = 7</td>
<td>arr[2][0]</td>
<td><code>arr[n-1-j][i]</code> = arr[2][0] ✓</td>
</tr>
<tr>
<td>rotated[0][1] = 4</td>
<td>arr[1][0]</td>
<td><code>arr[n-1-j][i]</code> = arr[1][0] ✓</td>
</tr>
<tr>
<td>rotated[0][2] = 1</td>
<td>arr[0][0]</td>
<td><code>arr[n-1-j][i]</code> = arr[0][0] ✓</td>
</tr>
</tbody></table>
<p>→ <strong>행 인덱스에는 <code>n-1-j</code>(거꾸로), 열 인덱스에는 <code>i</code>(원본 열)</strong> 가 들어간다.</p>
<h4 id="빈칸-풀이">빈칸 풀이</h4>
<pre><code class="language-python">def rotate_90(arr):
    n = len(arr)
    rotated = [[0] * n for _ in range(n)]
    for i in range(n):
        for j in range(n):
            rotated[i][j] = arr[n - 1 - j][i]   # ← 핵심
    return rotated</code></pre>
<blockquote>
<p>⚠️ <strong>함정 후보</strong>: <code>arr[n-1-i][j]</code>로 쓰면 <strong>반시계 방향</strong>이 된다. 시험 디버깅 문제로 충분히 등장 가능. i와 j 자리 바뀌는 거 항상 주의.</p>
</blockquote>
<hr>
<h3 id="2-90도-회전--zip-패턴-뒤집기--전치">2. 90도 회전 — zip 패턴 (뒤집기 + 전치)</h3>
<p>같은 회전을 Python 답게 한 줄로 쓰는 방법도 있다. 이게 시험 디버깅에 자주 등장.</p>
<pre><code class="language-python">[list(row) for row in zip(*arr[::-1])]</code></pre>
<p>이 한 줄을 <strong>두 단계로 분해</strong>해서 이해하면 외우기 쉬워진다.</p>
<h4 id="단계-1-arr-1--행-순서-뒤집기">단계 1: <code>arr[::-1]</code> — 행 순서 뒤집기</h4>
<pre><code>원본:           뒤집기 후:
[1, 2, 3]      [7, 8, 9]
[4, 5, 6]  →   [4, 5, 6]
[7, 8, 9]      [1, 2, 3]</code></pre><h4 id="단계-2-zip뒤집힌_배열--전치transpose">단계 2: <code>zip(*뒤집힌_배열)</code> — 전치(transpose)</h4>
<p><code>zip</code>은 <strong>여러 시퀀스에서 i번째 원소를 묶어주는</strong> 함수. <code>*</code> 언패킹으로 각 행을 인자로 펼쳐 넣으면 열 단위로 묶이는 효과가 난다.</p>
<pre><code class="language-python">zip([7,8,9], [4,5,6], [1,2,3])
→ (7,4,1), (8,5,2), (9,6,3)</code></pre>
<p>list로 감싸면 회전 결과 완성. 인덱스를 직접 쓰지 않고 끝낸다.</p>
<h4 id="디버깅-문제--어디가-버그">디버깅 문제 — 어디가 버그?</h4>
<pre><code class="language-python">def rotate_90(arr):
    flipped = arr[::]                              # ⓐ
    return [list(row) for row in zip(*flipped)]    # ⓑ</code></pre>
<p>ⓐ에 버그가 있다. <code>arr[::]</code>은 단순 복사(슬라이싱으로 새 리스트 만들기)일 뿐, <strong>순서를 뒤집지 않는다</strong>. 그래서 zip만 호출되면 그냥 전치(transpose)가 되어버려 회전이 아닌 전혀 다른 결과가 나온다.</p>
<p><strong>한 줄 수정</strong>:</p>
<pre><code class="language-python">flipped = arr[::-1]</code></pre>
<h4 id="디버깅-풀이-흐름-직접-설명">디버깅 풀이 흐름 (직접 설명)</h4>
<blockquote>
<p>&quot;zip 부분이 마지막 행에서부터 하나씩 뽑아야 되니 마지막 행이 처음 오도록 뒤집어야 함&quot;</p>
</blockquote>
<p>zip의 동작이 <strong>첫 행부터 차례로 열 단위로 묶는</strong> 거라는 점을 이해하면, 시계 방향 회전 결과의 0행이 &quot;원본 마지막 행의 0번째 값들&quot;이 되려면 마지막 행이 맨 앞에 와야 한다는 게 자연스럽게 따라온다. 4/20에 학습한 &quot;뒤집기 + 전치&quot; 분해가 도구로 자리잡은 형태.</p>
<hr>
<h3 id="3-약수-구하기--직접-작성--슬림화-비교">3. 약수 구하기 — 직접 작성 + 슬림화 비교</h3>
<p>어제(4/27)에 √n 최적화 패턴으로 약수 짝꿍 구조를 다뤘다면, 오늘은 그걸 <strong>함수 작성</strong>으로 응용하면서 슬림화 의식을 점검.</p>
<h4 id="문제">문제</h4>
<p>자연수 n의 모든 약수를 오름차순 리스트로 반환. 시간 복잡도 O(√n).</p>
<pre><code class="language-python">divisors(12)  # [1, 2, 3, 4, 6, 12]
divisors(36)  # [1, 2, 3, 4, 6, 9, 12, 18, 36]   ← 제곱수
divisors(7)   # [1, 7]                            ← 소수
divisors(1)   # [1]                               ← 엣지</code></pre>
<h4 id="직접-작성한-풀이">직접 작성한 풀이</h4>
<pre><code class="language-python">def divisors(n):
    small = []
    large = []
    i = 1
    while i * i &lt;= n:
        if n % i == 0:                  # 나누어떨어짐(약수)
            small.append(i)
            if n // i != i:              # i로 n 나눈 수가 i 아니면 두 번째도 추가
                large.append(n // i)
        i += 1
    return small + large[::-1]           # large는 큰 수부터 들어가있으니 역순</code></pre>
<p><strong>1차 정답</strong> — 5개 테스트 전부 통과.</p>
<p>이 풀이의 좋은 점:</p>
<ul>
<li>제곱수 함정(<code>if n // i != i</code>)을 정확히 처리</li>
<li><code>n == 1</code> 같은 안전망 엣지 분기를 <strong>추가하지 않음</strong> (메인 while 루프가 자연 처리)</li>
<li>같은 값 변수 중복 없음</li>
</ul>
<p>→ <strong>어제(23차) 신규 약점이던 &quot;안전망 분기 추가&quot; 패턴이 1회 노출에서 발생하지 않음</strong>. 어제 학습한 슬림화 체크리스트 3가지가 자기 점검으로 작동한 신호.</p>
<h4 id="슬림화-체크--한-발-더-짧게-가는-길">슬림화 체크 — 한 발 더 짧게 가는 길</h4>
<p>학습자 풀이는 <strong>시간 복잡도 측면에서 살짝 우위</strong>다 (reverse는 O(d), sorted는 O(d log d)). 의식적인 성능 선택이라면 그대로 둬도 좋다. 다만 <strong>코드 길이/가독성</strong> 관점에서 보면 더 짧은 형태가 있다.</p>
<p><strong>대안 1: 한 리스트 + sorted</strong></p>
<pre><code class="language-python">def divisors(n):
    result = []
    i = 1
    while i * i &lt;= n:
        if n % i == 0:
            result.append(i)
            if i != n // i:
                result.append(n // i)
        i += 1
    return sorted(result)</code></pre>
<p>리스트 분리 없이 한곳에 다 넣고 마지막에 정렬. 코드 짧아짐.</p>
<p><strong>대안 2: set + sorted (가장 슬림)</strong></p>
<pre><code class="language-python">def divisors(n):
    result = set()
    i = 1
    while i * i &lt;= n:
        if n % i == 0:
            result.add(i)
            result.add(n // i)
        i += 1
    return sorted(result)</code></pre>
<p>여기선 <strong>제곱수 분기(<code>if i != n // i</code>) 자체가 사라진다.</strong> set이 중복을 자동으로 제거해주니까. 메인 자료구조를 바꾸는 것만으로 분기 한 줄이 통째로 없어지는 패턴.</p>
<h4 id="메타-포인트--자료구조-선택을-통한-슬림화">메타 포인트 — 자료구조 선택을 통한 슬림화</h4>
<table>
<thead>
<tr>
<th>풀이</th>
<th>분기 줄</th>
<th>코드 길이</th>
<th>시간복잡도</th>
</tr>
</thead>
<tbody><tr>
<td>small/large 분리 + reverse</td>
<td>1줄 (제곱수 체크)</td>
<td>가장 김</td>
<td>O(√n + d)</td>
</tr>
<tr>
<td>한 리스트 + sorted</td>
<td>1줄 (제곱수 체크)</td>
<td>중간</td>
<td>O(√n + d log d)</td>
</tr>
<tr>
<td><strong>set + sorted</strong></td>
<td><strong>0줄</strong></td>
<td><strong>가장 짧음</strong></td>
<td>O(√n + d log d)</td>
</tr>
</tbody></table>
<p>세 가지 모두 정답이고 trade-off가 있다. 다만 어제 학습한 슬림화 체크리스트에 <strong>항목 하나가 추가</strong>된다.</p>
<blockquote>
<p>💡 <strong>슬림화 자기 점검 — 추가 메타 질문</strong>
4. <strong>이 분기 정말 필요한가? 자료구조 바꾸면 사라지지 않나?</strong></p>
<p>&quot;리스트 + 중복 체크 분기&quot; 자리에 <strong>&quot;set으로 자동 중복 제거&quot;</strong>가 거의 항상 한 번 더 짧게 만들어준다. 약수 문제뿐 아니라 경계 순회(20차 좌표 visited), 부분합 중복 등에도 동일.</p>
</blockquote>
<h4 id="java-vs-python--자료구조-선택의-무게">Java vs Python — 자료구조 선택의 무게</h4>
<table>
<thead>
<tr>
<th>측면</th>
<th>Java</th>
<th>Python</th>
</tr>
</thead>
<tbody><tr>
<td>HashSet 사용</td>
<td><code>Set&lt;Integer&gt; set = new HashSet&lt;&gt;();</code></td>
<td><code>result = set()</code></td>
</tr>
<tr>
<td>선언 무게</td>
<td>타입 명시 + 제네릭</td>
<td>한 단어</td>
</tr>
<tr>
<td>변환 비용</td>
<td><code>new ArrayList&lt;&gt;(set)</code> + sort</td>
<td><code>sorted(result)</code> 한 번에</td>
</tr>
</tbody></table>
<p>Java에서는 자료구조 바꾸는 게 선언/변환 비용 때문에 <strong>약간의 결심</strong>이 필요한 행동인데, Python은 그 비용이 거의 없다. 그래서 Python에선 <strong>&quot;분기를 자료구조로 흡수&quot;</strong>하는 게 더 자연스러운 선택지가 된다.</p>
<hr>
<h3 id="4-공식-재노출-주기-관리--8일-공백-결과">4. 공식 재노출 주기 관리 — 8일 공백 결과</h3>
<p>4/20(20차)에서 90도 회전 공식을 5일 공백으로 망각했던 적이 있어, 그 후 재노출 주기를 의식하고 있었다. 오늘은 4/20 이후 <strong>8일 공백</strong> 상태에서 첫 재노출.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>인덱스 형태 (<code>arr[n-1-j][i]</code>)</td>
<td>정확히 떠올림 ✓</td>
</tr>
<tr>
<td>zip 패턴 (<code>arr[::-1]</code>)</td>
<td>디버깅 정답 + 자체 설명 ✓</td>
</tr>
</tbody></table>
<p><strong>8일 공백에도 두 형태 모두 안정</strong> → 4/20 망각 사례 이후 첫 재노출에서 정착 신호. 다음 자연 재노출은 5/12 전후 오답 총복습 기간으로 예정.</p>
<blockquote>
<p>📌 <strong>공백 주기 관리 메타</strong> — &quot;공식은 망각 사이클을 의식하고 다시 만지기&quot;라는 학습 운영이 한 번 검증됨. 새 공식을 외울 때마다 다음 재노출 시점을 머릿속에 한 번 박아두는 게 효과적.</p>
</blockquote>
<hr>
<h2 id="오늘의-결과">오늘의 결과</h2>
<p>오늘 푼 문제는 3문제(빈칸 1 + 디버깅 1 + 함수 작성 1), <strong>1차 정답률 3/3 (100%)</strong>. 함수 작성 자력 정답이 21차 → 22차 → 23차 → 24차로 <strong>4세션 연속</strong>, 어제 신규 약점이던 슬림화 의식도 1회 노출에서 안전망 분기 추가 없이 통과. 다음 학습은 4/29(수)부터 BFS/DFS 시작 — 그래프 탐색 본격 진입.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[20260427 오늘의 학습: 수학 기초 (소수 판별, 약수, GCD, LCM)]]></title>
            <link>https://velog.io/@lee_yesol421/20260427-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-%EC%88%98%ED%95%99-%EA%B8%B0%EC%B4%88-%EC%86%8C%EC%88%98-%ED%8C%90%EB%B3%84-%EC%95%BD%EC%88%98-GCD-LCM</link>
            <guid>https://velog.io/@lee_yesol421/20260427-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-%EC%88%98%ED%95%99-%EA%B8%B0%EC%B4%88-%EC%86%8C%EC%88%98-%ED%8C%90%EB%B3%84-%EC%95%BD%EC%88%98-GCD-LCM</guid>
            <pubDate>Mon, 27 Apr 2026 11:38:16 GMT</pubDate>
            <description><![CDATA[<h2 id="지난-학습-요약">지난 학습 요약</h2>
<p>지난 세션(4/24)에서는 이진 탐색을 처음 학습하며 빈칸/디버깅/함수 작성 4문제 전부 1차 정답으로 끝냈다. 코드 추적 습관이 강화돼 <code>print(f&quot;left=... mid=...&quot;)</code>로 실행을 시뮬레이션하며 풀이하는 흐름이 자리잡았다. bisect 라이브러리는 완성형에서만 쓰고 빈칸은 직접 구현이 정석이라는 실전 전략도 같이 정리.</p>
<h2 id="오늘-수업-계획">오늘 수업 계획</h2>
<p>시험 출제 범위 중 자주 등장하는 수학 파트를 한 번에 정리한다. 소수 판별의 √n 최적화부터 시작해서, 같은 원리로 풀리는 약수 구하기, 그리고 외우면 끝나는 공식 두 개(유클리드 호제법, LCM 공식)까지. 마지막은 GCD를 이용해 N개 수의 LCM을 구하는 함수 작성 문제로 마무리.</p>
<hr>
<h2 id="학습-내용-정리">학습 내용 정리</h2>
<h3 id="1-소수-판별--√n-최적화">1. 소수 판별 — √n 최적화</h3>
<p>소수는 1과 자기 자신만으로 나누어떨어지는 자연수(1 제외). 가장 단순한 풀이는 2부터 n-1까지 다 나눠보는 것이다.</p>
<pre><code class="language-python">def is_prime(n):
    if n &lt; 2:
        return False
    for i in range(2, n):
        if n % i == 0:
            return False
    return True</code></pre>
<p>문제는 n이 1,000,000 같은 큰 수면 백만 번 다 돌아야 한다는 점. 그래서 <strong>√n까지만 보면 충분</strong>하다는 최적화가 핵심이다.</p>
<h4 id="왜-√n까지면-충분한가-시각적-설명">왜 √n까지면 충분한가? (시각적 설명)</h4>
<p>36의 약수 쌍을 적어보면 √36 = 6을 기준으로 거울처럼 대칭이다.</p>
<pre><code>1 × 36 = 36
2 × 18 = 36
3 × 12 = 36
4 × 9  = 36
6 × 6  = 36   ← √36 (중간점)
9 × 4  = 36   ← 위와 대칭
12 × 3 = 36
...</code></pre><p>즉 √n 이하에서 약수를 못 찾으면 √n 초과에서도 못 찾는다(대칭이니까). 그래서 검사 범위를 √n까지로 줄이면 시간복잡도가 O(n) → O(√n)으로 떨어진다.</p>
<pre><code class="language-python">def is_prime(n):
    if n &lt; 2:
        return False
    i = 2
    while i * i &lt;= n:        # i*i &lt;= n 은 i &lt;= √n 과 같음
        if n % i == 0:
            return False
        i += 1
    return True</code></pre>
<blockquote>
<p>⚠️ <strong>포인트</strong>: <code>i * i &lt;= n</code>은 제곱근 함수(<code>math.sqrt</code>)를 쓰지 않고도 같은 효과를 낸다. 실수 오차도 없고 정수 연산이라 빠르다. 이 패턴은 시험에 그대로 등장한다.</p>
</blockquote>
<h4 id="java-vs-python--정수-연산-비교">Java vs Python — 정수 연산 비교</h4>
<table>
<thead>
<tr>
<th>연산</th>
<th>Java</th>
<th>Python</th>
</tr>
</thead>
<tbody><tr>
<td>정수 나눗셈</td>
<td><code>5 / 2 = 2</code> (자동)</td>
<td><code>5 / 2 = 2.5</code> (실수), <code>5 // 2 = 2</code></td>
</tr>
<tr>
<td>나머지</td>
<td><code>%</code></td>
<td><code>%</code></td>
</tr>
<tr>
<td>거듭제곱</td>
<td><code>Math.pow(a, b)</code> (실수 반환)</td>
<td><code>a ** b</code> (정수 반환)</td>
</tr>
</tbody></table>
<p>Java에서 <code>int / int = int</code> 로 자동 정수화되는 게, Python에서는 명시적으로 <code>//</code>로 적어줘야 한다. <strong>22차 이진 탐색에서도 이 함정이 디버깅 문제로 나왔는데</strong>, 오늘 LCM 공식에서도 같은 패턴이 또 등장. 정수 결과가 필요한 곳에선 무조건 <code>//</code> — 외우자.</p>
<hr>
<h3 id="2-약수-구하기--짝꿍-패턴">2. 약수 구하기 — 짝꿍 패턴</h3>
<p>약수 구하기도 √n 트릭을 그대로 쓴다. 다만 핵심 아이디어가 하나 더 추가된다.</p>
<blockquote>
<p><strong>i가 약수면 <code>n // i</code>도 자동으로 약수다.</strong> 두 개를 동시에 수집하면 √n까지만 돌면 된다.</p>
</blockquote>
<pre><code class="language-python">def get_divisors(n):
    small = []
    large = []
    i = 1
    while i * i &lt;= n:
        if n % i == 0:
            small.append(i)
            if i != n // i:           # 제곱수 함정 처리
                large.append(n // i)
        i += 1
    return small + large[::-1]</code></pre>
<h4 id="⚠️-제곱수-함정">⚠️ 제곱수 함정</h4>
<p>36의 경우 i=6일 때 짝꿍 <code>n//i = 6</code> — 같은 수다. 이 체크를 빼먹으면 6이 두 번 들어가 <code>[1,2,3,4,6,6,9,...]</code>이 된다. <strong>이게 시험 디버깅 문제의 단골 함정.</strong></p>
<h4 id="디버깅-문제">디버깅 문제</h4>
<pre><code class="language-python">def count_divisors(n):
    count = 0
    i = 1
    while i * i &lt;= n:
        if n % i == 0:
            count += 2          # 버그: 제곱수에서 2번 셈
        i += 1
    return count

# count_divisors(36) → 기대 9, 실제 10 (i=6에서 2번 셈)
# count_divisors(49) → 기대 3, 실제 4</code></pre>
<p><strong>한 줄 수정</strong>:</p>
<pre><code class="language-python">count += 1 if i == n // i else 2</code></pre>
<h4 id="수업-중-질문-i--n--i-이-조건-i--i--n-이렇게-써도-되지-않나-저렇게-쓴-이유가-있나해서">수업 중 질문: <code>i == n // i</code> 이 조건 <code>i * i == n</code> 이렇게 써도 되지 않나? 저렇게 쓴 이유가 있나해서</h4>
<p>좋은 질문이다. <strong>둘 다 정답</strong>이고 우리 맥락에선 결과가 같다. 다만 미묘한 차이가 있다.</p>
<table>
<thead>
<tr>
<th>표현</th>
<th>의미</th>
<th>안전성</th>
</tr>
</thead>
<tbody><tr>
<td><code>i == n // i</code></td>
<td>&quot;i와 짝꿍이 같은 값?&quot; (약수 쌍 관점)</td>
<td><code>n % i == 0</code> 조건 안에서만 안전</td>
</tr>
<tr>
<td><code>i * i == n</code></td>
<td>&quot;i가 정확히 √n?&quot; (제곱근 관점)</td>
<td><strong>항상 안전</strong> — 정수 곱셈, 오차 없음</td>
</tr>
</tbody></table>
<p><strong>왜 차이가 생기나?</strong> 만약 <code>if n % i == 0:</code> 조건이 없는 곳에서 비교한다고 하면:</p>
<pre><code class="language-python">n = 10, i = 3
i * i == n   →  9 == 10  →  False  ✅ (정확)
i == n // i  →  3 == 10 // 3 → 3 == 3 → True  ❌ (잘못된 판정!)</code></pre>
<p>정수 나눗셈 <code>//</code>은 나머지를 버려버리니까 i가 약수가 아닌데도 결과가 맞아떨어진 것처럼 보일 수 있다. 우리 코드는 <code>if n % i == 0:</code> 안에서 쓰니까 둘 다 안전하지만, <strong>습관적으로는 <code>i * i == n</code>이 robust한 디폴트.</strong> 의도 표현 면에서는 짝꿍 변수와 같이 쓸 때 <code>i == n // i</code>가 가독성이 좋다 — 상황에 맞게 쓰면 된다.</p>
<hr>
<h3 id="3-최대공약수-gcd-greatest-common-divisor--유클리드-호제법">3. 최대공약수 (GCD, Greatest Common Divisor) — 유클리드 호제법</h3>
<p>두 수의 공통 약수 중 가장 큰 수. 외우면 끝나는 공식 1개:</p>
<blockquote>
<p><strong><code>gcd(a, b) = gcd(b, a % b)</code> 를 반복. b가 0이 되면 a가 답.</strong></p>
</blockquote>
<h4 id="추적해보기">추적해보기</h4>
<pre><code>gcd(48, 18)
= gcd(18, 48 % 18) = gcd(18, 12)
= gcd(12, 18 % 12) = gcd(12, 6)
= gcd(6,  12 % 6)  = gcd(6, 0)
                              ↑ b=0 → a=6이 정답</code></pre><h4 id="코드-반복문--시험에-더-흔함">코드 (반복문 — 시험에 더 흔함)</h4>
<pre><code class="language-python">def gcd(a, b):
    while b != 0:
        a, b = b, a % b
    return a</code></pre>
<blockquote>
<p>🐍 <strong>Python 튜플 언패킹</strong>: <code>a, b = b, a % b</code>는 <strong>오른쪽 전체를 먼저 계산한 다음 왼쪽에 동시 대입</strong>한다. Java라면 임시변수가 필요한 패턴인데 Python은 한 줄에 끝.</p>
<pre><code class="language-java">// Java
int temp = a;
a = b;
b = temp % b;</code></pre>
<pre><code class="language-python"># Python
a, b = b, a % b</code></pre>
</blockquote>
<h4 id="수업-중-질문-a-b-파라미터-조건-있어-둘-중에-작은-수를-기준값으로-해야할-것-같은데-그런-설명이-없더라고">수업 중 질문: a, b 파라미터 조건 있어? 둘 중에 작은 수를 기준값으로 해야할 것 같은데 그런 설명이 없더라고</h4>
<p>핵심 질문이다. <strong><code>a &lt; b</code>여도 자동으로 작동한다.</strong> 추적해보면 보인다:</p>
<p><code>gcd(5, 12)</code> — 작은 수를 a에 넣어봤다.</p>
<table>
<thead>
<tr>
<th>반복</th>
<th>a</th>
<th>b</th>
<th>a % b</th>
<th>다음 (a, b)</th>
</tr>
</thead>
<tbody><tr>
<td>시작</td>
<td>5</td>
<td>12</td>
<td>5 % 12 = <strong>5</strong></td>
<td>(12, 5) ← <strong>자동 swap!</strong></td>
</tr>
<tr>
<td>1</td>
<td>12</td>
<td>5</td>
<td>2</td>
<td>(5, 2)</td>
</tr>
<tr>
<td>2</td>
<td>5</td>
<td>2</td>
<td>1</td>
<td>(2, 1)</td>
</tr>
<tr>
<td>3</td>
<td>2</td>
<td>1</td>
<td>0</td>
<td>(1, 0)</td>
</tr>
<tr>
<td>4</td>
<td>1</td>
<td>0</td>
<td>종료</td>
<td>return <strong>1</strong></td>
</tr>
</tbody></table>
<p><strong>왜 자동 swap이 되냐면</strong>, <code>a &lt; b</code>이면 <code>a % b = a</code>가 항상 성립한다(5를 12로 나눈 나머지는 5 그대로). 그래서 첫 줄 <code>a, b = b, a % b</code>가 → <code>a, b = b, a</code>로 변신. 한 번 헛돌고 큰 수가 자연스럽게 앞으로 온다.</p>
<p>→ <strong>매개변수 조건 따로 없다. 어느 쪽이 크든 알아서 됨.</strong> 그래서 시험에서 빈칸/디버깅 문제에 <code>if a &lt; b: a, b = b, a</code> 같은 줄이 있다면 그건 <strong>있어도 없어도 결과 같은 줄</strong>(함정 후보).</p>
<hr>
<h3 id="4-최소공배수-lcm-least-common-multiple--황금-공식">4. 최소공배수 (LCM, Least Common Multiple) — 황금 공식</h3>
<p>두 수의 공통 배수 중 가장 작은 수. <strong>공식 한 줄.</strong></p>
<p>$$
\text{lcm}(a, b) = \frac{a \times b}{\gcd(a, b)}
$$</p>
<pre><code class="language-python">lcm = (a * b) // gcd(a, b)
# 또는 (오버플로우 안전 버전)
lcm = a // gcd(a, b) * b</code></pre>
<p><strong>왜 성립?</strong> 직관적으로:</p>
<ul>
<li><code>a × b</code>는 공통 배수 중 하나(최소는 아닐 수 있음)</li>
<li>거기서 <strong>공통으로 겹치는 부분(GCD)만큼 한 번 빼주면</strong> 최소가 됨</li>
</ul>
<p>확인:</p>
<pre><code>lcm(4, 6) = (4 * 6) // gcd(4, 6) = 24 // 2 = 12 ✅
lcm(12, 18) = (12 * 18) // gcd(12, 18) = 216 // 6 = 36 ✅</code></pre><blockquote>
<p>⚠️ <strong><code>/</code> vs <code>//</code> 함정 재등장</strong>: 22차 이진 탐색에서도 나왔던 함정. 정수 결과 필요한 곳에선 무조건 <code>//</code>. <code>(4*6) / gcd(4,6)</code>은 <code>12.0</code>(실수)가 나와서 시험 답으로 오답 처리 가능.</p>
</blockquote>
<hr>
<h3 id="5-n개-수의-lcm--누적-패턴-함수-작성">5. N개 수의 LCM — 누적 패턴 함수 작성</h3>
<p>세 수 이상의 LCM은 어떻게? <strong><code>lcm(a, b, c) = lcm(lcm(a, b), c)</code></strong> — 둘씩 누적해서 합쳐가면 된다.</p>
<h4 id="수업-중-질문-빈-리스트가-들어올-수도-있어">수업 중 질문: 빈 리스트가 들어올 수도 있어?</h4>
<p>엣지케이스 사고력 좋은 질문. 이번 문제는 <strong><code>len(numbers) &gt;= 1</code></strong> 가정. COS Pro는 보통 입력 제약이 명시되고, 빈 리스트가 들어오는 경우는 드물다. 다만 <strong>실전 코딩테스트에서 입력 제약을 먼저 확인하는 습관</strong>은 함정 회피에 직접적으로 도움이 된다 — 계속 유지할 것.</p>
<h4 id="직접-작성한-풀이">직접 작성한 풀이</h4>
<pre><code class="language-python">def lcm_all(numbers):
    if len(numbers) == 1:
        return numbers[0]

    a = numbers[0]
    lcm = numbers[0]

    for i in range(1, len(numbers)):
        b = numbers[i]
        lcm = a // gcd(a, b) * b
        a = lcm

    return lcm</code></pre>
<p>테스트 5개 전부 통과. 1차 정답.</p>
<h4 id="피드백-코드-슬림화-체크리스트">피드백: 코드 슬림화 체크리스트</h4>
<p>잘 동작하지만 <strong><code>a</code>와 <code>lcm</code> 두 변수가 항상 같은 값</strong>이다. 추적해보면:</p>
<pre><code>i=1 직후:  a = lcm = 12
i=2 직후:  a = lcm = 24</code></pre><p>또 <code>if len == 1</code> 분기도 <strong>불필요</strong>하다 — 리스트가 1개면 for 루프가 한 번도 안 돌고 <code>lcm = numbers[0]</code> 그대로 반환된다.</p>
<p><strong>슬림화된 형태</strong>:</p>
<pre><code class="language-python">def lcm_all(numbers):
    lcm = numbers[0]
    for n in numbers[1:]:                    # 첫 번째 빼고 값 순회
        lcm = lcm // gcd(lcm, n) * n
    return lcm</code></pre>
<blockquote>
<p>💡 <strong>코드 슬림화 체크리스트</strong> (앞으로 함수 작성 후 한 번씩 자기 점검)</p>
<ol>
<li><strong>같은 값을 가진 변수 2개</strong>가 있나? → 하나로 합치기</li>
<li><strong>불필요한 엣지 분기</strong>가 있나? → 메인 로직이 자연스럽게 처리하면 제거</li>
<li><strong>인덱스가 꼭 필요한가?</strong> → 값 자체로 충분하면 <code>for x in arr</code></li>
</ol>
</blockquote>
<p><code>for n in numbers[1:]</code>는 <strong>첫 번째 원소 빼고 값으로 순회</strong>하는 Python 패턴이다. 인덱스 안 쓰고 값 자체로 도는 게 더 깔끔. 누적 패턴에선 슬라이싱 + 값 순회가 더 흔하다.</p>
<hr>
<h2 id="오늘의-결과">오늘의 결과</h2>
<p>오늘 푼 문제는 4문제(빈칸 2 + 디버깅 1 + 함수 작성 1), <strong>1차 정답률 4/4 (100%)</strong>. 함수 작성 자력 정답이 21차→22차→23차로 3세션 연속 이어지며 과거 약점이 강점으로 전환되는 추세. 다음 학습은 4/29(수)부터 BFS/DFS 시작.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[20260424 오늘의 학습: 이진 탐색]]></title>
            <link>https://velog.io/@lee_yesol421/20260424-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-%EC%9D%B4%EC%A7%84-%ED%83%90%EC%83%89</link>
            <guid>https://velog.io/@lee_yesol421/20260424-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-%EC%9D%B4%EC%A7%84-%ED%83%90%EC%83%89</guid>
            <pubDate>Fri, 24 Apr 2026 14:06:37 GMT</pubDate>
            <description><![CDATA[<h2 id="지난-학습-요약">지난 학습 요약</h2>
<p>4/23 21차 세션에서는 20차 자기 진단에서 나온 두 약점(초기값 세팅 누락 / 인덱스+값 병렬 처리 미숙)을 겨냥해 <code>enumerate</code> → <code>zip</code> → 튜플 정렬 세 빈칸을 가볍게 풀었다. <code>zip</code> 문제에서 <code>top_score = scores[0]</code>, <code>top_name = names[0]</code> 초기값 세팅을 힌트 없이 스스로 써서 개선 신호를 확인했고, 부호 뒤집기 트릭의 경계 질문에서 stable sort 기반 2단계 정렬까지 확장했다. 세션 마지막에는 이미 풀어본 프린터 우선순위 문제를 매개로 <strong>그리디 알고리즘 개념</strong>을 명시적으로 정립했다.</p>
<h2 id="오늘-수업-계획">오늘 수업 계획</h2>
<p>오늘은 <strong>이진 탐색(Binary Search)</strong> 을 처음부터 배운다. 개념 설명 → 기본 템플릿 빈칸 → lower bound 변형 빈칸 → 디버깅 → 함수 작성(개수 세기 응용) 순서로 진행한다. COS Pro 1급에 빈출되는 유형이자 아직 안 배운 영역이라 기초를 탄탄히 쌓는 데 집중한다. 마지막에 학습자가 제기한 <strong>&quot;시험에서 bisect 써도 되나?&quot;</strong> 질문에서 완성형/부분형별 활용 전략까지 정리한다.</p>
<hr>
<h2 id="학습-내용-정리">학습 내용 정리</h2>
<h3 id="1-이진-탐색--언제-왜-쓰는가">1) 이진 탐색 — 언제, 왜 쓰는가?</h3>
<p><strong>전제 조건</strong>: 리스트가 <strong>정렬되어 있어야</strong> 한다. 정렬되지 않은 데이터에는 못 쓴다.</p>
<p><strong>속도 비교</strong>:</p>
<table>
<thead>
<tr>
<th>탐색 방법</th>
<th>시간복잡도</th>
<th>크기 100만 리스트에서 최악 비교 횟수</th>
</tr>
</thead>
<tbody><tr>
<td>선형 탐색</td>
<td>O(n)</td>
<td>약 1,000,000번</td>
</tr>
<tr>
<td><strong>이진 탐색</strong></td>
<td><strong>O(log n)</strong></td>
<td><strong>약 20번</strong> (2²⁰ ≈ 100만)</td>
</tr>
</tbody></table>
<p>절반씩 버려가며 탐색하기 때문에, 데이터가 커질수록 격차가 기하급수적으로 벌어진다.</p>
<p><strong>동작 원리</strong>: <code>[1, 3, 5, 7, 9, 11, 13]</code> 에서 <code>11</code> 찾기</p>
<ol>
<li>가운데(인덱스 3) 값은 <code>7</code>. 찾는 값 11보다 작으니 <strong>왼쪽 반 전부 버림</strong> → <code>left = mid + 1</code></li>
<li>남은 <code>[9, 11, 13]</code> 의 가운데(인덱스 5) 값은 <code>11</code>. <strong>찾음, 반환 5</strong></li>
</ol>
<p>핵심 3줄로 요약하면:</p>
<pre><code class="language-python">mid = (left + right) // 2
if arr[mid] == target: 찾음
elif arr[mid] &lt; target: left = mid + 1   # target은 오른쪽에 있음
else: right = mid - 1                     # target은 왼쪽에 있음</code></pre>
<hr>
<h3 id="2-기본-이진-탐색-템플릿-빈칸-1">2) 기본 이진 탐색 템플릿 (빈칸 1)</h3>
<p><strong>문제</strong>: 정렬된 <code>arr</code> 에서 <code>target</code> 의 인덱스를 반환, 없으면 <code>-1</code>. ㉠, ㉡ 을 채우시오.</p>
<pre><code class="language-python">def solution(arr, target):
    left, right = 0, len(arr) - 1
    while left &lt;= right:
        mid = (left + right) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] &lt; target: # 7 &lt; 11
            left = mid + 1      # ㉠
        else:
            right = mid - 1     # ㉡
    return -1</code></pre>
<p><strong>왜 <code>+1</code>, <code>-1</code> 을 붙이나?</strong></p>
<p><code>arr[mid]</code> 는 이미 target이 아닌 걸 확인했으니 mid 자체는 다시 볼 필요가 없다. 그래서 <code>±1</code> 로 <strong>mid를 제외</strong>하고 범위를 줄여야 한다.</p>
<p>만약 <code>left = mid</code> 로 쓰면? <code>[1, 3]</code> 에서 3 찾을 때 <code>left=0, right=1</code> → <code>mid=0</code> → <code>arr[0]=1 &lt; 3</code> → <code>left=0</code> 그대로 → <strong>무한 루프</strong> 💥</p>
<p><strong>while 조건 <code>&lt;=</code> 에 등호가 꼭 필요한 이유</strong>: 원소가 한 개 남았을 때(<code>left == right</code>) 그 한 개도 검사해야 하기 때문. 등호를 빼면 마지막 남은 원소를 놓친다.</p>
<hr>
<h3 id="3-lower-bound-패턴--target-이상-첫-위치-빈칸-2">3) lower bound 패턴 — &quot;target 이상 첫 위치&quot; (빈칸 2)</h3>
<p>기본 이진 탐색만으로는 부족한 경우가 많다. 예: &quot;6 이상의 값이 처음 나오는 위치&quot; 같은 <strong>경계값 찾기</strong>. COS Pro에서도 종종 이 변형으로 출제된다.</p>
<pre><code class="language-python"># 정렬된 arr에서 target 이상인 값이 처음 등장하는 위치를 반환
# 모든 값이 target보다 작으면 len(arr) 반환
def solution(arr, target):
    left, right = 0, len(arr)     # ← right가 len-1이 아닌 len
    while left &lt; right:            # ← 등호 없음
        mid = (left + right) // 2
        if arr[mid] &gt;= target:
            right = mid            # ← mid도 답 후보이므로 제외하지 않음
        else:
            left = mid + 1
    return left

# 테스트
print(solution([1,3,5,7,9], 6))   # 3  (6 이상 첫 값은 7, 인덱스 3)
print(solution([1,3,5,7,9], 10))  # 5  (없음 → len 반환)</code></pre>
<p><strong>기본 이진 탐색과 3가지 차이점</strong>:</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>기본 이진 탐색</th>
<th>lower bound</th>
</tr>
</thead>
<tbody><tr>
<td>초기 <code>right</code></td>
<td><code>len(arr) - 1</code></td>
<td><code>len(arr)</code></td>
</tr>
<tr>
<td>while 조건</td>
<td><code>left &lt;= right</code></td>
<td><code>left &lt; right</code></td>
</tr>
<tr>
<td>범위 좁히기</td>
<td><code>right = mid - 1</code></td>
<td><code>right = mid</code></td>
</tr>
</tbody></table>
<p><strong>왜 <code>right = mid</code> 이고 <code>mid - 1</code> 이 아닌가?</strong></p>
<ul>
<li>기본 버전: <code>arr[mid] == target</code> 이면 바로 반환이라 mid는 이후 볼 필요 없음 → 제외 가능 → <code>mid ± 1</code></li>
<li>lower bound: <code>arr[mid] &gt;= target</code> 이면 <strong>mid도 답 후보</strong> 로 남겨야 함 → 제외 금지 → <code>right = mid</code></li>
</ul>
<p><strong>왜 <code>&lt;</code> 로 등호를 뺐나?</strong></p>
<ul>
<li>만약 <code>&lt;=</code> 이면 <code>left == right == mid</code> 에서 <code>right = mid</code> 로 범위가 그대로 → 무한 루프</li>
<li><code>&lt;</code> 면 <code>left == right</code> 에서 루프 종료하고 그 값이 답</li>
</ul>
<p>풀이 중에는 <code>print(f&quot;left={left} right={right} mid={mid} arr[mid]={arr[mid]}&quot;)</code> 로 실행을 직접 추적하며 ㉠에 <code>right = mid</code>, ㉡에 <code>left = mid + 1</code> 을 채웠다. 이진 탐색처럼 조건이 헷갈릴 때는 한 번 찍어보고 가는 게 확실하다.</p>
<hr>
<h3 id="4-디버깅---vs--함정">4) 디버깅 — <code>/</code> vs <code>//</code> 함정</h3>
<p><strong>문제</strong>: 한 줄만 고쳐라.</p>
<pre><code class="language-python">def solution(arr, target):
    left, right = 0, len(arr) - 1
    while left &lt;= right:
        mid = (left + right) / 2    # ← 버그!
        if arr[mid] == target:
            return mid
        ...</code></pre>
<p><strong>답</strong>: <code>mid = (left + right) // 2</code> (정수 나눗셈으로 교체)</p>
<p><strong>이유</strong>:</p>
<ul>
<li>Python의 <code>/</code> 는 항상 <strong>실수(float)</strong> 를 반환한다. <code>(0 + 6) / 2</code> 는 <code>3.0</code> 이다.</li>
<li><code>arr[3.0]</code> 은 <code>TypeError: list indices must be integers</code> 에러가 난다. 리스트 인덱스는 반드시 정수여야 한다.</li>
<li>따라서 <strong>정수 나눗셈 <code>//</code></strong> 를 써야 한다.</li>
</ul>
<p><strong>Java와 비교</strong>:</p>
<table>
<thead>
<tr>
<th>언어</th>
<th><code>(0 + 6) / 2</code> 결과</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Java</strong> (<code>int / int</code>)</td>
<td><code>3</code> (자동으로 정수 나눗셈)</td>
</tr>
<tr>
<td><strong>Python</strong></td>
<td><code>3.0</code> (항상 실수)</td>
</tr>
<tr>
<td><strong>Python 정수 나눗셈</strong></td>
<td><code>(0 + 6) // 2</code> → <code>3</code></td>
</tr>
</tbody></table>
<p>Java 경험이 있으면 오히려 이 함정에 걸리기 쉽다. <code>/</code> 가 Java에서처럼 자동으로 정수로 떨어질 거라 기대하기 때문. <strong>이진 탐색 디버깅 단골 함정</strong>이므로 반드시 기억해두자.</p>
<hr>
<h3 id="5-함수-작성--target-등장-횟수-lower--upper-bound-조합">5) 함수 작성 — target 등장 횟수 (lower + upper bound 조합)</h3>
<p><strong>문제</strong>: 정렬된 리스트 <code>arr</code> 에서 <code>target</code> 이 몇 번 등장하는지 반환. 중복 가능.</p>
<p><strong>아이디어</strong>: 두 번의 이진 탐색으로 경계를 찾아 뺀다.</p>
<ul>
<li><code>target</code> 이상 첫 위치 = <code>left_idx</code></li>
<li><code>target + 1</code> 이상 첫 위치 = <code>right_idx</code> (= target 초과 첫 위치)</li>
<li>개수 = <code>right_idx - left_idx</code></li>
</ul>
<p><strong>예</strong>: <code>[1, 2, 2, 2, 3]</code>, target=2</p>
<ul>
<li>2 이상 첫 위치 = 1</li>
<li>3 이상 첫 위치 = 4</li>
<li>개수 = 4 − 1 = <strong>3</strong> ✓</li>
</ul>
<p><strong>풀이</strong>:</p>
<pre><code class="language-python">def solution(arr, target):
    # right_idx 찾기 (target+1 이상 첫 위치)
    left, right = 0, len(arr)
    while left &lt; right:
        mid = (left + right) // 2
        if arr[mid] &gt;= target + 1:
            right = mid
        else:
            left = mid + 1
    right_idx = left

    # left_idx 찾기 (target 이상 첫 위치)
    left, right = 0, right_idx  # 위에서 이미 찾은 범위만 뒤지면 충분
    while left &lt; right:
        mid = (left + right) // 2
        if arr[mid] &gt;= target:
            right = mid
        else:
            left = mid + 1
    left_idx = left

    return right_idx - left_idx</code></pre>
<p>테스트 <code>[1,2,2,2,3,4,5]</code> target=2 → 3, <code>[1,1,1,1,1]</code> target=1 → 5, 빈 리스트 <code>[]</code> → 0 (while 루프 자체에 진입 안 해서 자연스럽게 0 반환) 까지 <strong>전부 통과</strong>.</p>
<p>빈 리스트 대응을 위해 별도 방어 코드를 쓰지 않아도 <code>left=0, right=0</code> 이면 <code>while left &lt; right</code> 가 바로 거짓이 되어 빠져나간다. 이진 탐색 템플릿의 숨겨진 장점 중 하나.</p>
<h4 id="더-간결한-방법-학습자-질문">더 간결한 방법 (학습자 질문)</h4>
<pre><code>수업 중 질문 (풀이 직후): &quot;이진 탐색 로직을 두 번 썼는데 더 간결한 방법도 있을까?&quot;</code></pre><p>크게 두 가지 대안이 있다.</p>
<p><strong>방법 1: <code>bisect</code> 표준 라이브러리 활용</strong></p>
<pre><code class="language-python">from bisect import bisect_left, bisect_right

def solution(arr, target):
    return bisect_right(arr, target) - bisect_left(arr, target)</code></pre>
<ul>
<li><code>bisect_left(arr, x)</code> = x 이상 첫 위치 (= <code>left_idx</code>)</li>
<li><code>bisect_right(arr, x)</code> = x 초과 첫 위치 (= <code>right_idx</code>)</li>
<li>위에서 손으로 짠 두 이진 탐색을 한 줄로 대체</li>
</ul>
<p><strong>방법 2: 헬퍼 함수로 중복 제거</strong></p>
<pre><code class="language-python">def solution(arr, target):
    def lower_bound(x):
        left, right = 0, len(arr)
        while left &lt; right:
            mid = (left + right) // 2
            if arr[mid] &gt;= x:
                right = mid
            else:
                left = mid + 1
        return left
    return lower_bound(target + 1) - lower_bound(target)</code></pre>
<p>이진 탐색 로직이 <strong>한 곳</strong>에만 있어 중복이 제거된다. 바깥 변수 <code>arr</code> 는 클로저로 자연스럽게 참조된다.</p>
<p><strong>결론</strong>: 중복처럼 보여도 <strong>if 조건이 다르기 때문에</strong> 직접 두 번 쓰는 것도 완전히 정답. 리팩터링은 취향.</p>
<hr>
<h3 id="6-시험-전략--bisect-를-시험에서-그대로-써도-되나">6) 시험 전략 — bisect 를 시험에서 그대로 써도 되나?</h3>
<pre><code>수업 중 질문: &quot;시험에서 bisect를 그대로 써도 되는거야?&quot;</code></pre><p>좋은 질문이다. 정답은 <strong>&quot;쓸 수 있다, 하지만 뉘앙스 주의&quot;</strong>.</p>
<p><strong>COS Pro 1급에서 허용되는 표준 라이브러리</strong>:</p>
<pre><code class="language-python">import bisect                          # 이진 탐색
from collections import Counter, deque  # 빈도 / 큐
from itertools import combinations      # 조합
import heapq                            # 우선순위 큐
import math                             # gcd, sqrt</code></pre>
<p><strong>하지만 유형별로 활용 가능 범위가 다르다</strong>:</p>
<table>
<thead>
<tr>
<th>유형</th>
<th>bisect 사용 가능?</th>
<th>실전 팁</th>
</tr>
</thead>
<tbody><tr>
<td><strong>완성형 (함수 작성, 3문제)</strong></td>
<td>O, 자유롭게</td>
<td>bisect 쓰면 코드가 훨씬 짧아져 시간 절약</td>
</tr>
<tr>
<td><strong>빈칸 채우기</strong></td>
<td>출제자가 정해둔 코드를 따라야 함</td>
<td><strong>직접 구현 코드</strong>가 제시되면 그 흐름대로 빈칸 채움</td>
</tr>
<tr>
<td><strong>디버깅</strong></td>
<td>출제자 코드 구조 그대로</td>
<td>bisect가 이미 쓰여있으면 그 안에서 버그 찾기</td>
</tr>
</tbody></table>
<p><strong>왜 빈칸에선 직접 구현이 더 많이 나오나?</strong>
출제자 입장에서 <code>bisect_left(arr, target)</code> 한 줄을 빈칸으로 내면 시험이 너무 쉬워진다. 그래서 <strong>이진 탐색 로직을 풀어쓴 코드</strong>에서 <code>left = mid + 1</code> 같은 부분을 빈칸으로 내는 게 일반적이다.</p>
<p><strong>결론적인 학습 전략</strong>:</p>
<ol>
<li><strong>직접 구현 원리를 확실히 이해</strong> — 빈칸/디버깅 대비 (부분형 7문제 = 700점, 훨씬 큰 비중)</li>
<li><strong>bisect 사용법도 알아두기</strong> — 완성형에서 시간 단축 무기</li>
<li>완성형에서 남은 시간이 빠듯하면 bisect 한 줄로 처리하고 다음 문제로 넘어가는 판단도 가능</li>
</ol>
<p>오늘 세션에서 빈칸 2개 + 디버깅 1개 + 함수 작성 1개 모두 직접 구현으로 풀었으니, 부분형 대비의 기초는 탄탄히 깔린 셈이다.</p>
<hr>
<h2 id="오늘의-결과">오늘의 결과</h2>
<ul>
<li>푼 문제 4문제 (빈칸 2 + 디버깅 1 + 함수 작성 1), 1차 정답률 <strong>100% (4/4)</strong></li>
<li>이진 탐색 기본 / lower bound 변형 / 개수 세기 응용까지 한 세션에 체화</li>
<li>다음 학습: 수학 (소수 판별 / GCD / LCM) 진입 예정</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[20260423 오늘의 학습: 인덱스+값 병렬 패턴과 그리디 개념]]></title>
            <link>https://velog.io/@lee_yesol421/20260423-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-%EC%9D%B8%EB%8D%B1%EC%8A%A4%EA%B0%92-%EB%B3%91%EB%A0%AC-%ED%8C%A8%ED%84%B4%EA%B3%BC-%EA%B7%B8%EB%A6%AC%EB%94%94-%EA%B0%9C%EB%85%90</link>
            <guid>https://velog.io/@lee_yesol421/20260423-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-%EC%9D%B8%EB%8D%B1%EC%8A%A4%EA%B0%92-%EB%B3%91%EB%A0%AC-%ED%8C%A8%ED%84%B4%EA%B3%BC-%EA%B7%B8%EB%A6%AC%EB%94%94-%EA%B0%9C%EB%85%90</guid>
            <pubDate>Thu, 23 Apr 2026 13:13:58 GMT</pubDate>
            <description><![CDATA[<h2 id="지난-학습-요약">지난 학습 요약</h2>
<p>4/20 20차 세션에서 구현/시뮬레이션 범위를 복습하며 18차 약점이던 y 차원 경계 체크(<code>0 &lt;= ny &lt; n</code>)를 힌트 없이 재현하여 정착을 확인했다. 반면 2차원 90도 회전 공식 <code>rotated[i][j]=arr[n-1-j][i]</code> 은 재노출 공백으로 잠시 날아갔고, 경계 순회 문제에선 <code>arr[x][y] not in result</code> 같은 값 기반 중복 체크가 중복 값 반례에서 깨져 좌표 기반 <code>visited = {(0,0)}</code> 방식으로 교정했다. 세션 말미에 스스로 <strong>&quot;루프 전 초기 값 세팅을 자주 깜빡한다&quot;</strong>, <strong>&quot;인덱스+값을 병렬로 다루는 감각이 아직 약하다&quot;</strong> 두 약점을 정확히 진단했고, 오늘은 그 연장선에서 가볍게 워밍업한다.</p>
<h2 id="오늘-수업-계획">오늘 수업 계획</h2>
<p>20차 자기 진단에서 나온 두 약점(초기값 세팅 누락 / 인덱스+값 병렬 처리 미숙)을 겨냥해 <strong><code>enumerate</code> → <code>zip</code> → 튜플 정렬</strong> 순서로 빈칸 세 문제만 가볍게 푼다. 이 세 도구는 겉보기엔 다르지만 &quot;두 종류의 데이터를 같은 시점에 묶어 다룬다&quot;는 같은 범주에 속한다. 풀이 중 학습자가 제기한 &quot;부호 뒤집기 트릭의 경계 조건&quot; 질문에서 stable sort 기반 2단계 정렬 트릭까지 확장하고, 마지막에 <strong>그리디 알고리즘 개념</strong>을 이미 푼 프린터 우선순위 문제를 매개로 정립한다.</p>
<hr>
<h2 id="학습-내용-정리">학습 내용 정리</h2>
<h3 id="0-세션-시작-전-질문-알고리즘-용어-점검">0) 세션 시작 전 질문: 알고리즘 용어 점검</h3>
<pre><code>수업 중 질문: &quot;최근에 1급 딴 다른 사람 후기 봤는데 DFS BFS DP 그리디 이런
알고리즘 용어가 나오더라고? 잘 몰랐는데 이미 내가 푼 적도 있는거야?&quot;</code></pre><p>현 커리큘럼 기준으로 정리하면 다음과 같다.</p>
<table>
<thead>
<tr>
<th>용어</th>
<th>뜻</th>
<th>학습 상태</th>
</tr>
</thead>
<tbody><tr>
<td><strong>DFS</strong> (깊이 우선 탐색)</td>
<td>한 갈래 끝까지 가보고 막히면 돌아와서 다른 갈래</td>
<td>미학습, 4월 말 진입 예정</td>
</tr>
<tr>
<td><strong>BFS</strong> (너비 우선 탐색)</td>
<td>가까운 곳부터 한 겹씩 퍼져나가며 탐색</td>
<td>미학습, 4월 말 진입 예정</td>
</tr>
<tr>
<td><strong>DP</strong> (동적 프로그래밍)</td>
<td>작은 문제 답을 저장해두고 큰 문제에 재활용</td>
<td>커리큘럼 제외 (합격 전략상 불필요)</td>
</tr>
<tr>
<td><strong>그리디</strong></td>
<td>매 순간 가장 좋아 보이는 선택을 하는 전략</td>
<td><strong>이미 푼 적 있음</strong> (용어만 몰랐을 뿐)</td>
</tr>
</tbody></table>
<p>특히 그리디의 경우 이미 17차/19차에서 푼 <strong>프린터 우선순위</strong> 문제가 정통 그리디 유형이었다는 점을 확인하고, 오늘 세션 마지막에 개념을 명시적으로 정립하기로 했다.</p>
<hr>
<h3 id="1-enumerate--인덱스와-값을-같이-순회">1) enumerate — 인덱스와 값을 같이 순회</h3>
<p><strong>문제</strong>: 숫자 리스트에서 가장 큰 값이 있는 위치(인덱스)를 반환하는 함수를 작성하라. <code>max()</code>, <code>index()</code> 사용 금지.</p>
<pre><code class="language-python">def find_max_index(arr):
    max_idx = 0
    max_val = arr[0]
    for i, v in enumerate(arr):
        if v &gt; max_val:
            max_val = v
            max_idx = i
    return max_idx</code></pre>
<p><strong>핵심</strong>:</p>
<ul>
<li><code>enumerate(arr)</code> 은 <code>(인덱스, 값)</code> 튜플을 하나씩 내어주는 반복자. 이걸 <code>for i, v in ...</code> 로 언패킹하면 한 번의 순회로 두 정보를 동시에 쓸 수 있다.</li>
<li><code>&gt;</code> 부등호(등호 없음) 때문에 동점이면 <strong>먼저 나온 인덱스가 유지</strong>된다. <code>[5, 5, 5]</code> 입력에서 <code>0</code> 이 나오는 이유.</li>
</ul>
<pre><code>수업 중 질문 (풀이 직후): &quot;초기값 세팅은 내가 한 거 아니야.&quot;</code></pre><p>정확한 지적이었다. 이 문제에서 <code>max_idx = 0</code> / <code>max_val = arr[0]</code> 두 줄은 빈칸 바깥에 제시된 코드라 학습자가 직접 쓴 부분은 아니었다. 초기값 세팅 약점 개선은 다음 문제에서 진짜로 확인하기로 했다.</p>
<hr>
<h3 id="2-zip--두-리스트를-값끼리-병렬-순회">2) zip — 두 리스트를 값끼리 병렬 순회</h3>
<p><strong>문제</strong>: 학생 이름 리스트와 점수 리스트가 있을 때, 가장 높은 점수를 받은 학생의 이름을 반환하라.</p>
<pre><code class="language-python">def top_student(names, scores):
    top_score = scores[0]
    top_name = names[0]
    for name, score in zip(names, scores):
        if score &gt; top_score:
            top_score = score
            top_name = name
    return top_name</code></pre>
<p><strong>핵심</strong>:</p>
<ul>
<li><code>zip(names, scores)</code> 는 같은 위치의 두 원소를 <strong>튜플로 묶어</strong> 내어주는 반복자. 길이가 다르면 <strong>짧은 쪽에 맞춰 멈춘다</strong> (그래서 짧은 리스트의 길이까지만 순회됨).</li>
<li><code>enumerate</code> 가 &quot;(인덱스, 값)&quot; 을 묶었다면 <code>zip</code> 은 &quot;(값, 값)&quot; 을 묶는다. 묶는 대상이 다를 뿐 <strong>&quot;두 데이터를 병렬로 쓴다&quot;</strong> 는 목적은 같다.</li>
</ul>
<p>이번 문제에서는 <code>top_score = scores[0]</code>, <code>top_name = names[0]</code> 초기값 세팅 두 줄을 힌트 없이 직접 썼다. 20차 때 스스로 진단했던 <strong>&quot;루프 전 초기값을 자주 깜빡한다&quot;</strong> 약점이 <strong>이번엔 작동하지 않았다</strong> — 한 번의 실전 노출로 개선 신호를 확인한 셈.</p>
<p><strong>enumerate ↔ zip 비교표</strong>:</p>
<table>
<thead>
<tr>
<th>상황</th>
<th>도구</th>
<th>언패킹 예</th>
</tr>
</thead>
<tbody><tr>
<td>한 리스트의 인덱스와 값이 같이 필요</td>
<td><code>enumerate(arr)</code></td>
<td><code>for i, v in ...</code></td>
</tr>
<tr>
<td>두 리스트를 같은 위치끼리 비교</td>
<td><code>zip(a, b)</code></td>
<td><code>for x, y in ...</code></td>
</tr>
<tr>
<td>세 리스트 이상</td>
<td><code>zip(a, b, c)</code></td>
<td><code>for x, y, z in ...</code></td>
</tr>
<tr>
<td>인덱스도 있고 두 리스트도 같이</td>
<td><code>enumerate(zip(a, b))</code></td>
<td><code>for i, (x, y) in ...</code></td>
</tr>
</tbody></table>
<hr>
<h3 id="3-튜플-정렬--다중-key--부호-뒤집기-트릭">3) 튜플 정렬 — 다중 key + 부호 뒤집기 트릭</h3>
<p><strong>문제</strong>: <code>[(이름, 점수), ...]</code> 리스트를 <strong>점수 내림차순</strong>, <strong>동점이면 이름 가나다순</strong>으로 정렬하라.</p>
<pre><code class="language-python">def sort_students(students):
    return sorted(students, key=lambda x: (-x[1], x[0]))</code></pre>
<p><strong>핵심 아이디어</strong>:</p>
<ul>
<li><code>sorted()</code> 의 key 함수가 <strong>튜플을 반환</strong>하면 Python은 튜플을 <strong>앞에서부터 순서대로</strong> 비교한다. 1차 기준이 같을 때만 2차 기준을 본다.</li>
<li>문제의 함정: <strong>점수는 내림차, 이름은 오름차</strong> — 방향이 섞였다. <code>reverse=True</code> 는 튜플 전체를 뒤집기 때문에 둘 다 내림차가 되어 버린다.</li>
<li>해법: <strong>한 쪽만 부호를 뒤집는다</strong>. 점수 <code>-x[1]</code> 로 넣으면 정렬은 오름차로 하면서 실제 결과는 점수 내림차가 된다.</li>
</ul>
<p><strong>Java와 비교</strong>:</p>
<table>
<thead>
<tr>
<th>언어</th>
<th>점수 내림 + 이름 오름 정렬</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Java</strong></td>
<td><code>list.sort(Comparator.comparingInt(Student::score).reversed().thenComparing(Student::name))</code></td>
</tr>
<tr>
<td><strong>Python</strong></td>
<td><code>sorted(list, key=lambda x: (-x[1], x[0]))</code></td>
</tr>
</tbody></table>
<p>Java의 <code>.reversed().thenComparing()</code> 체이닝과 Python의 부호 뒤집기 트릭은 목표는 같지만 표현이 다르다. Python 쪽이 짧은 대신 &quot;숫자에만 먹힌다&quot;는 제약이 있다.</p>
<hr>
<h3 id="4-부호-뒤집기-트릭의-경계-문자열에-적용하려면">4) 부호 뒤집기 트릭의 경계: 문자열에 적용하려면?</h3>
<pre><code>수업 중 질문: &quot;지금은 내림차순 정렬하는 값이 숫자라서 가능한 거지?
만약 이름을 내림차순(사전 반대방향)으로 정렬한다고 하면
어쩔 수 없이 reverse=True 사용해야되겠지?&quot;</code></pre><p>매우 정확한 경계 질문이다. 답을 정리하면:</p>
<pre><code class="language-python">&gt;&gt;&gt; -&quot;abc&quot;
TypeError: bad operand type for unary -: &#39;str&#39;</code></pre>
<p>부호 뒤집기는 <strong>숫자에만</strong> 먹힌다. 그렇다고 <code>reverse=True</code> 만이 유일한 답은 아니다. <strong>Python의 <code>sorted()</code> 가 stable(안정 정렬)</strong> 이라는 성질을 이용하면 문자열에도 &quot;방향이 섞인 다중 정렬&quot;이 가능하다.</p>
<p><strong>Stable sort 의 정의</strong>: 같은 key 값을 가진 원소들의 <strong>상대 순서가 정렬 전후에 유지</strong>된다.</p>
<p><strong>활용법 — 2단계 정렬</strong>: &quot;덜 중요한 기준을 먼저, 더 중요한 기준을 나중에&quot; 두 번 정렬한다.</p>
<pre><code class="language-python"># 예: 점수 오름차 + 이름 내림차 (사전 반대 방향)
students = [(&quot;철수&quot;, 80), (&quot;영희&quot;, 80), (&quot;민수&quot;, 70)]

# 1단계: 덜 중요한 기준 먼저 — 이름 내림차
temp = sorted(students, key=lambda x: x[0], reverse=True)
# → [(&#39;철수&#39;, 80), (&#39;영희&#39;, 80), (&#39;민수&#39;, 70)]

# 2단계: 더 중요한 기준 나중 — 점수 오름차
result = sorted(temp, key=lambda x: x[1])
# → [(&#39;민수&#39;, 70), (&#39;철수&#39;, 80), (&#39;영희&#39;, 80)]
#    점수 오름차 ✓, 동점은 이름 내림차(철 &gt; 영) ✓</code></pre>
<p><strong>정리표</strong>:</p>
<table>
<thead>
<tr>
<th>상황</th>
<th>쓸 수 있는 방법</th>
</tr>
</thead>
<tbody><tr>
<td>모든 기준이 같은 방향</td>
<td><code>reverse=True</code> 하나로 해결</td>
</tr>
<tr>
<td>숫자 기준 방향이 섞임</td>
<td><code>-x</code> 부호 뒤집기 (오늘 푼 문제)</td>
</tr>
<tr>
<td>문자열 기준 방향이 섞임</td>
<td><strong>2단계 정렬</strong> (stable sort 활용)</td>
</tr>
</tbody></table>
<p>COS Pro 1급에서 문자열 내림차 + 다른 기준 조합은 빈도가 낮다. 실전 빈출은 <strong>점수 내림 + 이름 오름</strong> 쪽이라 오늘 푼 <code>(-x[1], x[0])</code> 패턴이 훨씬 자주 나온다. 2단계 정렬은 &quot;이런 카드도 있다&quot; 정도로 알아두면 충분하다.</p>
<hr>
<h3 id="5-그리디-greedy-개념-정립">5) 그리디 (Greedy) 개념 정립</h3>
<pre><code>수업 중 질문: &quot;끝내기 전에 이미 내가 한 적 있다던 그리디에 대해
정리하고 가자. 문제 풀 건 아니고 개념 → 예시 문제 → 바로 풀이법 설명까지.
내가 푼 적 있는 문제면 더 좋고.&quot;</code></pre><p><strong>한 문장 정의</strong>:</p>
<blockquote>
<p>&quot;매 순간, 지금 당장 가장 좋아 보이는 선택을 한다.&quot;</p>
</blockquote>
<ul>
<li>전체 경로를 미리 고민하지 않고 <strong>지역 최적(local optimal)</strong> 을 고른다.</li>
<li>이 선택들을 쌓은 결과가 <strong>전체 최적(global optimal)</strong> 과 일치하는 문제에서만 그리디로 풀 수 있다.</li>
</ul>
<h4 id="이미-푼-예시-프린터-우선순위">이미 푼 예시: 프린터 우선순위</h4>
<p>문제 요약: 대기열에 있는 문서들에 각자 우선순위가 있다. 큐 맨 앞 문서를 꺼냈을 때, 뒤에 더 높은 우선순위가 남아있으면 그 문서를 <strong>맨 뒤로 보낸다</strong>. 없으면 <strong>인쇄한다</strong>.</p>
<p>입력 예: <code>priorities = [2, 1, 3, 2]</code>, 내 문서 위치 = 2 → 출력: <code>1</code></p>
<p><strong>왜 이게 그리디냐</strong>:</p>
<blockquote>
<p>&quot;현재 대기열에서 우선순위가 가장 높은 문서부터 먼저 인쇄한다.&quot;</p>
</blockquote>
<p>지금 남아있는 것 중 <strong>제일 큰 것</strong>을 처리하면 된다 — 이게 곧 탐욕 규칙.</p>
<pre><code class="language-python">from collections import deque

def solution(priorities, location):
    queue = deque((i, p) for i, p in enumerate(priorities))
    count = 0
    while queue:
        idx, pri = queue.popleft()
        if queue and max(p for _, p in queue) &gt; pri:
            queue.append((idx, pri))   # 더 큰 거 있으니 뒤로
        else:
            count += 1
            if idx == location:
                return count</code></pre>
<p><code>max(...) &gt; pri</code> 로 &quot;지금 가장 높은 게 뭐냐&quot; 를 묻는 부분이 그리디의 핵심이다.</p>
<h4 id="그리디가-항상-맞는-건-아니다-중요">그리디가 항상 맞는 건 아니다 (중요)</h4>
<p>반례: 동전 <code>[1원, 4원, 5원]</code> 으로 8원 만들기</p>
<table>
<thead>
<tr>
<th>전략</th>
<th>선택</th>
<th>동전 개수</th>
</tr>
</thead>
<tbody><tr>
<td>그리디 (큰 거부터)</td>
<td>5 + 1 + 1 + 1</td>
<td>4개</td>
</tr>
<tr>
<td>최적</td>
<td>4 + 4</td>
<td><strong>2개</strong></td>
</tr>
</tbody></table>
<p>그래서 그리디를 선택하기 전에 &quot;이 전략이 정말 전체 최적과 일치하는가?&quot; 를 한 번 점검해야 한다. 애매하면 DP 영역으로 넘어가야 한다.</p>
<p>다행히 <strong>COS Pro 1급 수준의 그리디는 대부분 &quot;정렬 후 순서대로 집어먹기&quot;</strong> 패턴으로 해결되어 복잡한 증명 없이 접근 가능하다. 그래서 그리디 문제에는 거의 항상 <code>sorted()</code> 가 등장한다.</p>
<h4 id="대표-유형-이름만-기억">대표 유형 (이름만 기억)</h4>
<table>
<thead>
<tr>
<th>유형</th>
<th>한 줄 설명</th>
</tr>
</thead>
<tbody><tr>
<td>거스름돈 문제</td>
<td>큰 동전부터 최대한 사용</td>
</tr>
<tr>
<td>회의실 배정</td>
<td>끝나는 시간 빠른 것부터 선택</td>
</tr>
<tr>
<td>구명보트 문제</td>
<td>가장 무거운 사람 + 가장 가벼운 사람 짝짓기</td>
</tr>
</tbody></table>
<h4 id="이미-가진-그리디-감각">이미 가진 그리디 감각</h4>
<ul>
<li>17차·19차 프린터 우선순위 — 19차에선 <code>(인덱스, 우선순위)</code> 튜플을 힌트 없이 스스로 설계</li>
<li>20차 점수 순 정렬 — &quot;가장 좋은 것부터&quot; 사고방식은 그리디와 결이 같음</li>
</ul>
<p>용어만 몰랐을 뿐 감각은 이미 있다. 앞으로 문제에서 <strong>&quot;가장 ~한 것부터&quot;</strong> 라는 키워드가 보이면 <strong>&quot;그리디 + 정렬&quot;</strong> 을 먼저 떠올리면 된다.</p>
<hr>
<h2 id="오늘의-결과">오늘의 결과</h2>
<ul>
<li>푼 문제 3문제 (enumerate / zip / 튜플 정렬 빈칸 채우기), 1차 정답률 100%</li>
<li>20차 자기 진단 약점 &quot;초기값 세팅 깜빡&quot; — 문제 2에서 힌트 없이 직접 세팅하여 개선 확인</li>
<li>다음 학습: 이진 탐색 + 수학(소수/GCD/LCM) 진입</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[20260420 오늘의 학습: 구현/시뮬레이션 복습과 병렬 리스트 패턴]]></title>
            <link>https://velog.io/@lee_yesol421/20260420-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-%EA%B5%AC%ED%98%84%EC%8B%9C%EB%AE%AC%EB%A0%88%EC%9D%B4%EC%85%98-%EB%B3%B5%EC%8A%B5%EA%B3%BC-%EB%B3%91%EB%A0%AC-%EB%A6%AC%EC%8A%A4%ED%8A%B8-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@lee_yesol421/20260420-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-%EA%B5%AC%ED%98%84%EC%8B%9C%EB%AE%AC%EB%A0%88%EC%9D%B4%EC%85%98-%EB%B3%B5%EC%8A%B5%EA%B3%BC-%EB%B3%91%EB%A0%AC-%EB%A6%AC%EC%8A%A4%ED%8A%B8-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Mon, 20 Apr 2026 12:27:47 GMT</pubDate>
            <description><![CDATA[<h2 id="지난-학습-요약">지난 학습 요약</h2>
<p>4/17 19차 세션에서 17차 약점이었던 스택/큐 두 가지 — <strong>스택에 무엇을 저장할지 판단</strong>과 <strong>빈 스택 체크</strong> — 를 힌트 없이 재현하며 완전 정착을 확인했다. 프린터 우선순위 문제도 17차와 달리 <code>(인덱스, 우선순위)</code> 튜플을 스스로 설계했다. 다만 괄호 중첩 깊이 문제에서 &quot;깊이&quot;를 &quot;완성된 쌍 수&quot;로 해석해 <code>((())())</code> 반례에서 깨지는 풀이를 낸 적이 있다.</p>
<h2 id="오늘-수업-계획">오늘 수업 계획</h2>
<p>학습 공백이 좀 있었기 때문에 감각 복구 목적으로 18차 범위(구현/시뮬레이션 기초)를 복습한다. dx/dy 좌표 이동, 2차원 배열 90도 회전, 주기성 활용을 빈칸 → 디버깅 → 함수 작성 순서로 점진적으로 끌어올리고, 마지막에 응용 함수 작성 문제로 현재 실력을 진단한다.</p>
<hr>
<h2 id="학습-내용-정리">학습 내용 정리</h2>
<h3 id="1-dxdy-이동--경계-체크--행-열-규약-복습">1) dx/dy 이동 + 경계 체크 — (행, 열) 규약 복습</h3>
<p>로봇이 N×N 격자에서 상하좌우 명령을 받아 이동하는 문제. 격자를 벗어나는 이동은 무시한다.</p>
<pre><code class="language-python">def solution(n, commands):
    direction = {
        &#39;U&#39;: (-1, 0),   # 위 = 행 감소
        &#39;D&#39;: (1, 0),
        &#39;L&#39;: (0, -1),
        &#39;R&#39;: (0, 1)
    }
    x, y = 0, 0
    for cmd in commands:
        nx = x + direction[cmd][0]
        ny = y + direction[cmd][1]
        if 0 &lt;= nx &lt; n and 0 &lt;= ny &lt; n:   # ← 18차 약점이었던 y 차원 체크
            x, y = nx, ny
    return (x, y)</code></pre>
<p><strong>18차 약점 재확인</strong>: 경계 체크 시 x 차원만 검사하고 y 차원을 빠뜨린 실수가 있었다. 오늘은 힌트 없이 <code>0 &lt;= ny &lt; n</code>까지 스스로 썼다 → <strong>완전 정착</strong>.</p>
<p><strong>좌표 규약 핵심</strong>: (행, 열) 규약에서</p>
<ul>
<li>&#39;위&#39;로 가면 <strong>행이 감소</strong> (인덱스 값 작은 쪽 위)</li>
<li>&#39;왼쪽&#39;으로 가면 <strong>열이 감소</strong></li>
</ul>
<hr>
<h3 id="2-2차원-배열-90도-회전--공식-재암기">2) 2차원 배열 90도 회전 — 공식 재암기</h3>
<pre><code>수업 중 질문: &quot;공식 가르쳐줬었는데 기억 안나네&quot;</code></pre><p>시간이 좀 지나니 18차에 배운 회전 공식이 가물가물해졌다. 개념부터 재구축.</p>
<h4 id="직관적-규칙">직관적 규칙</h4>
<pre><code>원본:              시계 90도:
1 2 3              7 4 1
4 5 6       →      8 5 2
7 8 9              9 6 3</code></pre><p><strong>눈에 보이는 규칙</strong>: 원본의 <strong>첫 번째 열</strong> <code>[1, 4, 7]</code>이 회전 후 <strong>맨 윗줄에 뒤집혀서</strong> <code>[7, 4, 1]</code>로 들어간다.</p>
<h4 id="공식">공식</h4>
<pre><code class="language-python">rotated[i][j] = arr[n - 1 - j][i]</code></pre>
<h4 id="왜-이렇게-되는가--인덱스-추적">왜 이렇게 되는가 — 인덱스 추적</h4>
<table>
<thead>
<tr>
<th>회전 후</th>
<th>값</th>
<th>원본 위치</th>
<th>공식 확인</th>
</tr>
</thead>
<tbody><tr>
<td><code>rotated[0][0]</code></td>
<td>7</td>
<td><code>arr[2][0]</code></td>
<td><code>arr[n-1-0][0]</code> ✓</td>
</tr>
<tr>
<td><code>rotated[0][1]</code></td>
<td>4</td>
<td><code>arr[1][0]</code></td>
<td><code>arr[n-1-1][0]</code> ✓</td>
</tr>
<tr>
<td><code>rotated[0][2]</code></td>
<td>1</td>
<td><code>arr[0][0]</code></td>
<td><code>arr[n-1-2][0]</code> ✓</td>
</tr>
</tbody></table>
<h4 id="외우는-팁-18차-때와-동일한-분해">외우는 팁 (18차 때와 동일한 분해)</h4>
<pre><code>i → 원본의 &quot;열&quot;로 간다
j → 원본의 &quot;행&quot;으로 뒤집혀서 간다 (n-1-j)</code></pre><p>행/열이 <strong>서로 바뀌고(transpose)</strong> + 새 행 쪽이 <strong>뒤집힌다(reverse)</strong> → 그래서 <code>zip(*arr[::-1])</code> 한 줄 풀이도 가능했던 것.</p>
<p><strong>교훈</strong>: 한동안 안 보면 공식은 날아간다. 주기적으로 다시 노출할 것.</p>
<hr>
<h3 id="3-k번-회전--주기성">3) K번 회전 + 주기성</h3>
<p>K가 10억이어도 빠르게 동작해야 한다 → 주기성 활용.</p>
<pre><code class="language-python">def solution(arr, k):
    n = len(arr)
    k = k % 4          # ① 90도 회전 4번이면 원상복구 = 주기 4

    for _ in range(k): # ② 나머지 횟수만큼만 회전
        rotated = [[0] * n for _ in range(n)]
        for i in range(n):
            for j in range(n):
                rotated[i][j] = arr[n - 1 - j][i]
        arr = rotated
    return arr</code></pre>
<p>18차에 배운 <strong>K % 주기</strong> 패턴이 새 문제에서도 즉시 적용됐다. 스택/큐에서 &quot;무엇을 저장할지&quot; 판단이 정착한 것처럼, 주기성 감각도 정착 단계.</p>
<p><strong>보너스 — 테스트 시 <code>[row[:] for row in arr]</code>의 의미</strong>: 2차원 배열의 깊은 복사. 바깥 리스트만 복사하는 <code>arr[:]</code>은 내부 행이 공유되어 원본 오염 가능. 각 행까지 복사해야 안전. 16차에 정착한 <strong>원본 보존</strong> 패턴의 연장선.</p>
<hr>
<h3 id="4-2차원-경계-순회--값-기반-중복-체크의-함정">4) 2차원 경계 순회 — &quot;값 기반 중복 체크&quot;의 함정</h3>
<p>N×N 배열의 경계를 시계 방향으로 순회해 값 리스트를 반환하는 함수. 처음 작성한 풀이:</p>
<pre><code class="language-python">def solution(arr):
    dx = [0, 1, 0, -1]   # 우 하 좌 상
    dy = [1, 0, -1, 0]
    n = len(arr)
    d = 0
    x, y = 0, 0
    result = []

    while d &lt; 4:
        if arr[x][y] not in result:   # ← 여기가 문제
            result.append(arr[x][y])
        nx, ny = x + dx[d], y + dy[d]
        if 0 &lt;= nx &lt; n and 0 &lt;= ny &lt; n:
            x, y = nx, ny
        else:
            d += 1
    return result</code></pre>
<p>주어진 테스트 4개(<code>3x3</code>, <code>4x4</code>, <code>1x1</code>, <code>2x2</code>)는 전부 통과. 하지만 <strong>반례가 있었다</strong>.</p>
<h4 id="반례">반례</h4>
<pre><code class="language-python">arr = [[1, 2, 1],
       [3, 4, 5],
       [1, 6, 7]]
# 기대: [1, 2, 1, 5, 7, 6, 1, 3]
# 실제: [1, 2, 5, 7, 6, 3]     ← 중복 값이 전부 사라짐</code></pre>
<h4 id="원인">원인</h4>
<p><code>result</code>에 <strong>값</strong>을 저장하고 <strong>값</strong>으로 중복 체크 → 경계에 같은 값이 여러 번 나오면 하나로 취급된다. <code>arr[x][y] not in result</code> 가드는 &quot;코너에서 방향 바뀔 때 같은 칸을 두 번 기록하는 것&quot;을 막으려는 의도였으나, 값으로 판단해서 다른 위치의 동일 값까지 막아버린다.</p>
<h4 id="올바른-방법--좌표위치-기반-체크">올바른 방법 — 좌표(위치) 기반 체크</h4>
<pre><code class="language-python">def solution(arr):
    dx = [0, 1, 0, -1]
    dy = [1, 0, -1, 0]
    n = len(arr)
    d = 0
    x, y = 0, 0
    result = [arr[0][0]]       # ← 첫 값을 루프 전에 세팅
    visited = {(0, 0)}          # ← 좌표 set으로 방문 체크

    while d &lt; 4:
        nx, ny = x + dx[d], y + dy[d]
        if 0 &lt;= nx &lt; n and 0 &lt;= ny &lt; n and (nx, ny) not in visited:
            x, y = nx, ny
            visited.add((x, y))
            result.append(arr[x][y])
        else:
            d += 1
    return result</code></pre>
<p><strong>핵심 변화</strong>: 방문 체크 대상이 <strong>값 → 좌표</strong>로 바뀌었다. 이것이 인덱스+값 병렬 처리 패턴의 전형.</p>
<h4 id="반복되는-학습-패턴--테스트-통과-≠-정답">반복되는 학습 패턴 — &quot;테스트 통과 ≠ 정답&quot;</h4>
<table>
<thead>
<tr>
<th>차수</th>
<th>사건</th>
</tr>
</thead>
<tbody><tr>
<td>19차</td>
<td>괄호 깊이 문제 — 7개 테스트 통과했지만 <code>((())())</code>에서 깨짐</td>
</tr>
<tr>
<td>20차 (오늘)</td>
<td>경계 순회 — 4개 테스트 통과했지만 중복 값 케이스에서 깨짐</td>
</tr>
</tbody></table>
<p><strong>앞으로 지킬 습관</strong>: 풀이를 내기 전 &quot;이 풀이가 깨지는 입력이 뭘까?&quot; 반례를 상상해보기. 특히 <strong>&quot;값 기반 체크&quot;는 중복을 의심</strong>.</p>
<hr>
<h3 id="5-점수-순-학생-정렬--zip--sorted--key-컴비네이션">5) 점수 순 학생 정렬 — zip + sorted + key 컴비네이션</h3>
<p><code>names[i]</code> 학생이 <code>scores[i]</code>점일 때, 점수 내림차순 이름 반환 (동점은 입력 순서 유지).</p>
<pre><code class="language-python">def solution(names, scores):
    # 단계별 버전
    result = list(zip(names, scores))
    result2 = sorted(result, key=lambda x: x[1], reverse=True)
    result3 = [name for name, score in result2]
    return result3


def solution(names, scores):
    # 한 줄 버전
    return [name for name, _ in sorted(zip(names, scores),
                                       key=lambda x: x[1],
                                       reverse=True)]</code></pre>
<h4 id="세-가지-핵심-도구">세 가지 핵심 도구</h4>
<pre><code class="language-python"># ① zip — 두 리스트를 같은 인덱스로 묶기
list(zip([&quot;A&quot;,&quot;B&quot;,&quot;C&quot;], [1, 2, 3]))
# [(&#39;A&#39;, 1), (&#39;B&#39;, 2), (&#39;C&#39;, 3)]

# ② sorted + key + reverse — 기준과 방향 지정
sorted([(&#39;A&#39;, 85), (&#39;B&#39;, 92)], key=lambda x: x[1], reverse=True)

# ③ enumerate — 값 + 원본 인덱스
list(enumerate([&quot;철수&quot;,&quot;영희&quot;]))
# [(0, &#39;철수&#39;), (1, &#39;영희&#39;)]</code></pre>
<h4 id="동점-시-원래-순서-유지--python-sorted의-stable-sort-특성">동점 시 원래 순서 유지 — Python <code>sorted</code>의 stable sort 특성</h4>
<p>Python <code>sorted()</code>는 <strong>안정 정렬(stable sort)</strong> 이다. 같은 키 값을 가진 원소들은 입력 순서를 보존한다. 그래서 동점 <code>92점</code>인 영희와 지영 중 영희가 먼저 나왔다.</p>
<blockquote>
<p>시험에 &quot;동점 시 원래 순서 유지&quot; 문구가 나오면 별도 처리 없이 <code>sorted</code> 그냥 쓰면 된다.</p>
</blockquote>
<hr>
<h3 id="6-오늘의-자기-진단--다음-학습-주제-선정">6) 오늘의 자기 진단 — 다음 학습 주제 선정</h3>
<pre><code>수업 중 질문: &quot;이해했어. 최초 값 미리 세팅해두는 걸 자꾸 깜빡하네.
             그리고 파이썬에서 인덱스(위치)랑 값을 별도 리스트에 저장한 다음
             같은 인덱스로 돌면서 처리하는 게 많이 나오는데 아직 익숙하지 않은듯&quot;</code></pre><p>정확한 자기 진단이다. 두 포인트가 다음 단계 핵심:</p>
<h4 id="약점-1-첫-값-미리-세팅을-깜빡함">약점 1: 첫 값 미리 세팅을 깜빡함</h4>
<p>루프 안에서 &quot;이동 후 기록&quot; 구조를 쓰면, 시작 위치는 이동 없이 거기 있으니 <strong>루프 전에 먼저 넣어줘야 한다</strong>.</p>
<ul>
<li><code>result = [arr[0][0]]</code>, <code>visited = {(0,0)}</code> 같은 초기화</li>
<li>슬라이딩 윈도우에서 <strong>첫 윈도우 합은 루프 전에</strong> 계산하는 패턴과 동일 (15차)</li>
</ul>
<h4 id="약점-2-인덱스값-병렬-관리-패턴-미숙">약점 2: 인덱스+값 병렬 관리 패턴 미숙</h4>
<p>이미 경험한 문제들을 관통하는 공통 패턴:</p>
<table>
<thead>
<tr>
<th>문제</th>
<th>병렬 관리 방법</th>
</tr>
</thead>
<tbody><tr>
<td>17차 프린터 우선순위</td>
<td><code>(우선순위, 인덱스)</code> 튜플</td>
</tr>
<tr>
<td>19차 프린터 재도전</td>
<td><code>(인덱스, 우선순위)</code> 튜플 스스로 설계</td>
</tr>
<tr>
<td>오늘 경계 순회 (개선)</td>
<td><code>(x, y)</code> 좌표 set</td>
</tr>
<tr>
<td>오늘 점수 정렬</td>
<td><code>zip(names, scores)</code> 튜플 리스트</td>
</tr>
<tr>
<td>dx/dy 이동</td>
<td>방향 리스트 + <code>d</code> 인덱스</td>
</tr>
</tbody></table>
<p>공통 도구: <strong><code>enumerate</code></strong>, <strong><code>zip</code></strong>, <strong>튜플 리스트 + sort</strong>, <strong>좌표 키 set/dict</strong>.</p>
<p>이 다섯 문제를 하나의 범주로 묶어 이해하는 시점이 왔다 → 다음 세션에서 집중 훈련.</p>
<hr>
<h2 id="오늘의-결과">오늘의 결과</h2>
<p>총 5문제 풀이, 1차 정답 3/5 (60%) — 감각 복귀 구간치고는 양호. 18차 약점(y 차원 경계 체크)은 힌트 없이 해결하며 정착 확인. 다음 세션은 오늘 스스로 진단한 <strong>인덱스+값 병렬 패턴</strong>을 핵심 주제로 잡고 <code>enumerate</code>/<code>zip</code>/튜플 정렬을 집중 훈련한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[20260417 오늘의 학습: 스택/큐 복습과 괄호 문제]]></title>
            <link>https://velog.io/@lee_yesol421/20260417-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-%EC%8A%A4%ED%83%9D%ED%81%90-%EB%B3%B5%EC%8A%B5%EA%B3%BC-%EA%B4%84%ED%98%B8-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@lee_yesol421/20260417-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-%EC%8A%A4%ED%83%9D%ED%81%90-%EB%B3%B5%EC%8A%B5%EA%B3%BC-%EA%B4%84%ED%98%B8-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Fri, 17 Apr 2026 05:59:52 GMT</pubDate>
            <description><![CDATA[<h2 id="지난-학습-요약">지난 학습 요약</h2>
<p>4/15 18차 세션에서 구현/시뮬레이션 기초로 dx/dy 좌표 이동과 2차원 배열 90도 회전(<code>zip(*arr[::-1])</code>)을 학습했다. 빈칸 3칸 전부 1차 정답, 함수 작성 로직도 완벽했고 (행,열) 좌표 규약도 시각적 분해로 이해했다. 4/13 17차에서 처음 접한 스택/큐는 1차 정답률 50%로, 그때 <strong>&quot;스택에 무엇을 저장할지&quot;</strong> 판단과 <strong>빈 스택 체크</strong> 두 가지가 약점으로 남아있었다.</p>
<h2 id="오늘-수업-계획">오늘 수업 계획</h2>
<p>17차 스택/큐 약점 2가지를 스스로 극복할 수 있는지 확인하는 것이 오늘의 주요 목표. 괄호 문자열을 재료로 5문제를 풀며 점진적으로 난이도를 올렸다.</p>
<hr>
<h2 id="학습-내용-정리">학습 내용 정리</h2>
<h3 id="1-스택에-무엇을-저장할지-판단--인덱스-저장-패턴">1) 스택에 &quot;무엇을 저장할지&quot; 판단 — 인덱스 저장 패턴</h3>
<p>짝이 맞지 않는 여는 괄호의 <strong>위치</strong>를 반환하는 문제였다. 17차 약점이 바로 이 부분이었는데, 이번엔 스스로 판단했다.</p>
<p><strong>핵심 규칙</strong>: 스택에 저장할 값은 <strong>&quot;나중에 뭘 반환해야 하는지&quot;</strong> 에서 역산한다.</p>
<ul>
<li>위치(인덱스)를 반환해야 함 → 스택에 <code>i</code>를 저장</li>
<li>그냥 괄호 자체(<code>(</code>)를 저장하면 나중에 위치 복원 불가</li>
</ul>
<pre><code class="language-python">def solution(s):
    stack = []
    for i in range(len(s)):
        if s[i] == &#39;(&#39;:
            stack.append(i)      # ← 위치 저장
        else:  # &#39;)&#39;
            if not stack:
                return i         # 닫는 괄호가 먼저 → 그 위치
            stack.pop()
    if stack:
        return stack[0]          # &quot;가장 먼저 나온&quot; → 첫 번째
    return -1</code></pre>
<p><strong>포인트</strong>: <code>stack[0]</code> vs <code>stack[-1]</code> 구분.</p>
<ul>
<li>&quot;가장 먼저 나온 것&quot; = 제일 먼저 append된 것 = <code>stack[0]</code></li>
<li>&quot;가장 나중&quot; = <code>stack[-1]</code></li>
</ul>
<p>스택은 단순 LIFO 자료구조가 아니라 <strong>순서 정보를 담은 기록 장치</strong>로 활용하는 감각이 생겼다.</p>
<hr>
<h3 id="2-빈-스택-체크--단락평가short-circuit-패턴">2) 빈 스택 체크 — 단락평가(short-circuit) 패턴</h3>
<p>연속된 같은 문자를 제거하는 &quot;쌍쌍 지우기&quot; 디버깅 문제였다.</p>
<p><strong>버그가 있는 원본</strong>:</p>
<pre><code class="language-python">if stack[-1] == ch:
    stack.pop()</code></pre>
<p>첫 글자에서 스택이 비어있으면 <code>IndexError</code> 발생.</p>
<p><strong>수정</strong>:</p>
<pre><code class="language-python">if stack and stack[-1] == ch:
    stack.pop()</code></pre>
<p>Python의 <code>and</code>는 <strong>단락평가</strong>를 한다. 앞이 False면 뒤는 아예 평가하지 않음.</p>
<ul>
<li><code>stack</code>이 빈 리스트(<code>[]</code>) → Falsy → <code>stack and ...</code> 전체가 False → <code>stack[-1]</code> 아예 접근하지 않음</li>
<li><code>stack</code>이 비어있지 않음 → Truthy → 뒤 조건 평가</li>
</ul>
<p>이 패턴은 <strong><code>stack[-1]</code>, <code>queue[0]</code>, <code>lst[i]</code>, <code>dict[key]</code></strong> 접근 전 어디든 쓸 수 있는 관용구다. <code>not stack</code> 체크 후 return/continue하는 방식과 동등하지만 한 줄로 처리된다.</p>
<hr>
<h3 id="3-큐--인덱스-우선순위-튜플-추적--17차-힌트-유형">3) 큐 + (인덱스, 우선순위) 튜플 추적 — 17차 힌트 유형</h3>
<p>&quot;프린터 우선순위&quot; 유형. 17차에서는 힌트가 필요했지만 이번엔 스스로 설계했다.</p>
<pre><code class="language-python">from collections import deque

def solution(priorities, target):
    queue = deque((idx, p) for idx, p in enumerate(priorities))
    count = 0

    while queue:
        idx, p = queue.popleft()
        if queue and p &lt; max(p for _, p in queue):
            queue.append((idx, p))
        else:
            count += 1
            if idx == target:
                return count</code></pre>
<p><strong>핵심 설계 결정</strong>:</p>
<ul>
<li>큐에 단순 우선순위 값만 넣으면 <strong>원래 위치를 잃어버림</strong> → 튜플로 <code>(인덱스, 우선순위)</code> 함께 저장</li>
<li><code>_</code>로 인덱스 무시: <code>max(p for _, p in queue)</code> — 언패킹 관용구</li>
<li><code>queue and</code> 단락평가로 마지막 하나 남은 경우 <code>max()</code> 에러 방지 → 위 2번에서 배운 패턴을 여기에도 적용</li>
</ul>
<p>17차에서 힌트 받았던 &quot;스택에 무엇을 저장할지&quot;가 큐 문제에서도 같은 원리로 작동한다는 것을 체감했다.</p>
<hr>
<h3 id="4-여러-종류-괄호-매칭--딕셔너리-매핑-활용">4) 여러 종류 괄호 매칭 — 딕셔너리 매핑 활용</h3>
<p><code>(</code>, <code>{</code>, <code>[</code> 세 종류가 섞인 문자열의 올바름을 판단하는 문제.</p>
<pre><code class="language-python">def solution(s):
    brackets = {&quot;[&quot;:&quot;]&quot;, &quot;(&quot;:&quot;)&quot;, &quot;{&quot;:&quot;}&quot;}
    stack = []

    for ch in s:
        if ch in brackets:
            stack.append(ch)
        else:
            if stack and ch == brackets[stack[-1]]:
                stack.pop()
            else:
                return False
    return not stack  # 남은 여는 괄호 없으면 True</code></pre>
<p><strong>핵심 설계</strong>:</p>
<ul>
<li>딕셔너리로 여는→닫는 매핑을 선언 → if/elif 지옥 회피</li>
<li><code>ch in brackets</code> → dict의 키 존재 체크, 여는 괄호 판별로 활용</li>
<li><code>brackets[stack[-1]]</code> → 스택 맨 위 여는 괄호의 <strong>짝(닫는 괄호)</strong> 을 조회</li>
<li><code>return not stack</code> → 마지막에 남은 여는 괄호 없으면 True (Falsy 체크)</li>
</ul>
<hr>
<h3 id="5-중첩-깊이-최댓값--해석-오류와-교정">5) 중첩 깊이 최댓값 — 해석 오류와 교정</h3>
<p>여기서 오늘의 <strong>실수</strong>가 나왔다. &quot;괄호 중첩 깊이&quot;를 <strong>완성된 괄호 쌍의 수</strong>로 해석해 버렸다. 그래서 <strong>연속된 <code>)</code>의 개수</strong>를 추적하는 창의적인(?) 풀이를 만들었다.</p>
<pre><code class="language-python"># 잘못된 풀이 (핵심 부분)
if ch == &quot;(&quot;:
    if count &gt; 0:
        count = 0          # )가 나오다가 (가 나오면 리셋
    stack.append(ch)
elif ch == &quot;)&quot;:
    stack.pop()
    count += 1             # 연속 ) 개수 카운트
    max_count = max(max_count, count)</code></pre>
<p>주어진 7개 테스트는 <strong>전부 통과</strong>했다. 하지만 반례 하나에서 무너졌다.</p>
<p><strong>반례: <code>&quot;((())())&quot;</code></strong></p>
<pre><code>( ( ( ) )  ( ) )
1 2 3 2 1  2 1 0   ← 실제 깊이: 최대 3</code></pre><p>최대 깊이 3을 찍고 2만 닫은 뒤 다시 열기 때문에 &quot;연속 <code>)</code> 개수&quot;는 2밖에 안 된다. 내 풀이는 2를 반환. 정답은 3.</p>
<p><strong>개념 교정</strong>:</p>
<ul>
<li>중첩 깊이 = &quot;그 시점에 <strong>동시에 열려있는</strong> 괄호의 개수&quot; = 스택 크기</li>
<li>완성 여부는 상관 없음. 닫기 전이라도 &quot;열려있으면 카운트&quot;</li>
</ul>
<p>비유하자면 바다 수영에서 깊이 3m는 &quot;지금 발 밑에 물이 3m 있다&quot;이지, &quot;3m까지 내려갔다가 수면까지 다시 올라온 횟수&quot;가 아니다.</p>
<p><strong>교정 후 풀이</strong>:</p>
<pre><code class="language-python">def solution(s):
    max_depth = 0
    depth = 0
    for ch in s:
        if ch == &quot;(&quot;:
            depth += 1
            max_depth = max(max_depth, depth)
        elif ch == &quot;)&quot;:
            depth -= 1
    return max_depth</code></pre>
<p>이 문제에서는 스택이 필요 없다. <strong>지금 몇 개 열려있는가</strong>만 알면 되기 때문에 단순 카운터로 충분하다. <code>depth</code>는 사실상 &quot;스택 크기&quot;와 같은 값을 추적하는 것이다.</p>
<p><strong>도구 선택 감각</strong>: 스택은 &quot;LIFO 순서나 저장했다가 나중에 꺼낼 정보&quot;가 있을 때 빛난다. 이 문제는 그런 요구가 없어서 카운터가 더 직관적이다.</p>
<hr>
<h3 id="오늘의-교훈--반례를-스스로-떠올려보기">오늘의 교훈 — 반례를 스스로 떠올려보기</h3>
<p>주어진 테스트케이스 7개를 전부 통과해도, <strong>출제자가 놓쳤을 법한 패턴</strong>을 스스로 떠올려보는 검증 습관이 필요하다. 특히 <strong>&quot;일반적이지 않은 접근&quot;</strong> 으로 풀었다면 더욱 그렇다. 오늘처럼 &quot;연속 <code>)</code> 개수 추적&quot; 같은 간접 지표로 푸는 경우엔 그 지표가 정말 원래 목표값과 동치인지 반례로 검증해야 한다.</p>
<hr>
<h2 id="오늘의-결과">오늘의 결과</h2>
<p>5문제 해결, 1차 정답 4/5(80%). 17차 약점 2가지(<strong>스택에 무엇을 저장할지 / 빈 스택 체크</strong>)는 <strong>완전 정착</strong> 판정.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[20260415 오늘의 학습: 구현/시뮬레이션 기초]]></title>
            <link>https://velog.io/@lee_yesol421/20260415-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-%EA%B5%AC%ED%98%84%EC%8B%9C%EB%AE%AC%EB%A0%88%EC%9D%B4%EC%85%98-%EA%B8%B0%EC%B4%88</link>
            <guid>https://velog.io/@lee_yesol421/20260415-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-%EA%B5%AC%ED%98%84%EC%8B%9C%EB%AE%AC%EB%A0%88%EC%9D%B4%EC%85%98-%EA%B8%B0%EC%B4%88</guid>
            <pubDate>Wed, 15 Apr 2026 15:10:41 GMT</pubDate>
            <description><![CDATA[<h2 id="지난-학습-요약">지난 학습 요약</h2>
<ul>
<li>스택과 큐 개념 학습: 리스트 기반 스택, <code>collections.deque</code> 기반 큐, 괄호 매칭/요세푸스/프린터 우선순위 등 6문제 풀이</li>
<li>스택 활용 시 &quot;무엇을 저장할지&quot; 판단하는 부분에서 약간의 혼동이 있었으나 힌트 후 해결</li>
<li>이어서 미학습 유형인 <strong>구현/시뮬레이션</strong>으로 넘어감</li>
</ul>
<h2 id="오늘-수업-계획">오늘 수업 계획</h2>
<p>COS Pro 단골 유형인 <strong>구현/시뮬레이션 기초</strong>를 다룬다. 좌표 이동 패턴(dx/dy), 2차원 배열 90도 회전을 핵심 개념으로 잡고 빈칸 2문제 + 함수 작성 1문제를 푼다.</p>
<hr>
<h2 id="학습-내용-정리">학습 내용 정리</h2>
<h3 id="1-좌표-이동-dxdy-패턴">1. 좌표 이동 dx/dy 패턴</h3>
<p>시뮬레이션 문제의 출발점. 상하좌우를 일일이 if-else로 쓰지 않고, <strong>방향 배열 두 개</strong>로 한 번에 처리한다.</p>
<pre><code class="language-python"># 북, 동, 남, 서 (시계방향)
dx = [-1, 0, 1, 0]
dy = [0, 1, 0, -1]

for i in range(4):
    nx = x + dx[i]
    ny = y + dy[i]</code></pre>
<p><strong>핵심</strong>: <code>dx</code>, <code>dy</code>의 배열 순서는 방향 순서(N/E/S/W 또는 U/R/D/L)에 따라 달라진다. 문제마다 방향 규약을 먼저 확인해야 한다.</p>
<blockquote>
<p>수업 중 질문: &quot;dx dy에 방향값 넣어두는건 방향 순서에 따라 달라지겠네?&quot;
→ 정확한 일반화. 이전에 푼 소용돌이 문제에서 방향이 우→하→좌→상 순서로 돌았던 것을 떠올리면, 그 문제의 dx/dy와 이번 문제의 dx/dy가 왜 다른지 자연스럽게 연결된다.</p>
</blockquote>
<h3 id="2-경계-체크-out-of-bound-방어">2. 경계 체크 (Out-of-bound 방어)</h3>
<p>좌표 이동 후 <strong>새 좌표가 격자 안에 있는지</strong> 확인하는 패턴.</p>
<pre><code class="language-python">if 0 &lt;= nx &lt; N and 0 &lt;= ny &lt; N:
    x, y = nx, ny   # 유효하면 이동
# 아니면 그대로 (명령 무시)</code></pre>
<p><strong>피드백 포인트</strong>: 경계 체크를 <code>0 &lt;= x &lt; N</code>처럼 <strong>이동 전 좌표</strong>로 쓰거나, <strong>y 차원을 빠뜨리는</strong> 실수가 잦다.</p>
<ul>
<li>체크 대상은 반드시 <strong>이동 후 좌표(<code>nx</code>, <code>ny</code>)</strong></li>
<li>2차원이면 x, y <strong>둘 다</strong> 검사해야 함 (<code>and</code>로 연결)</li>
</ul>
<h3 id="3-2차원-배열-90도-시계-회전">3. 2차원 배열 90도 시계 회전</h3>
<p>공식 버전:</p>
<pre><code class="language-python">def rotate_once(arr):
    N = len(arr)
    rotated = [[0]*N for _ in range(N)]
    for i in range(N):
        for j in range(N):
            rotated[j][N-1-i] = arr[i][j]
    return rotated</code></pre>
<p>Pythonic 버전:</p>
<pre><code class="language-python">rotated = [list(row) for row in zip(*arr[::-1])]</code></pre>
<blockquote>
<p>수업 중 질문: &quot;zip 사용한 부분이 왜 90도 회전이랑 같아지는지 모르겠다.&quot;
→ <strong>시각적 분해</strong>로 이해하면 쉽다.</p>
<ol>
<li><code>arr[::-1]</code> — 위아래 뒤집기 (행 순서 역전)</li>
<li><code>zip(*...)</code> — 전치(transpose), 즉 행↔열 교환</li>
<li>뒤집기 + 전치 = 시계방향 90도 회전</li>
</ol>
<p>수학적 증명보다는 &quot;왜 이게 그렇게 되는지 그림으로 한 번 그려보기&quot;가 기억에 잘 남는다.</p>
</blockquote>
<h3 id="4-주기성-활용--k--4">4. 주기성 활용 — <code>K % 4</code></h3>
<p>같은 회전을 K번 반복할 때, <strong>4번 돌면 원상복귀</strong>하므로 <code>K = K % 4</code>로 불필요한 반복을 제거한다.</p>
<pre><code class="language-python">K = K % 4
for _ in range(K):
    result = rotate_once(result)</code></pre>
<p>이 패턴은 회전뿐 아니라 <strong>순환 구조 전반</strong>에 쓰인다. 요세푸스, 원형 큐, 방향 회전 등.</p>
<h3 id="5-행-열-좌표-규약">5. (행, 열) 좌표 규약</h3>
<p>구현 문제에서 가장 헷갈리는 부분. <strong>리스트 기반 2차원 배열은 수학 좌표계와 다르다.</strong></p>
<table>
<thead>
<tr>
<th>구분</th>
<th>수학 좌표계</th>
<th>리스트 좌표계 (grid[x][y])</th>
</tr>
</thead>
<tbody><tr>
<td>x 증가 방향</td>
<td>오른쪽</td>
<td><strong>아래</strong></td>
</tr>
<tr>
<td>y 증가 방향</td>
<td>위</td>
<td>오른쪽</td>
</tr>
<tr>
<td>북</td>
<td>y+1</td>
<td><strong>x-1</strong></td>
</tr>
<tr>
<td>남</td>
<td>y-1</td>
<td><strong>x+1</strong></td>
</tr>
<tr>
<td>동</td>
<td>x+1</td>
<td>y+1</td>
</tr>
<tr>
<td>서</td>
<td>x-1</td>
<td>y-1</td>
</tr>
</tbody></table>
<p><strong>왜 이렇게 쓰는가?</strong>
<code>grid[x][y]</code>라고 쓰면, <code>grid[x]</code>가 먼저 행을 선택하고, 그 다음 <code>[y]</code>가 열을 고른다. 즉 <strong>x는 행 번호(세로 위치)</strong>, <strong>y는 열 번호(가로 위치)</strong>가 되는 것이 자연스럽다.</p>
<blockquote>
<p>수업 중 질문: &quot;행열 규약이 왜 저런지 아직 이해 못했어.&quot;
→ 수학식으로 이해하려 하지 말고, <strong>리스트 구조가 원래 그렇게 생겼다</strong>고 받아들이는 것이 실전에 빠르다. 문제 풀 때 &quot;북이면 x를 줄인다&quot;만 기억하면 됨.</p>
</blockquote>
<h3 id="6-방향-전환-로직---4">6. 방향 전환 로직 — <code>% 4</code></h3>
<p>L/R 명령으로 방향을 바꿀 때:</p>
<pre><code class="language-python"># 방향: 0(북) 1(동) 2(남) 3(서)
if cmd == &#39;R&#39;:
    d = (d + 1) % 4
elif cmd == &#39;L&#39;:
    d = (d - 1) % 4   # Python은 음수 % 양수도 자동 양수화</code></pre>
<p>Python의 <code>%</code>는 음수에서도 <strong>양수 결과를 반환</strong>하므로 <code>(d-1) % 4</code>가 안전하게 작동한다. (Java/C는 음수 나올 수 있어 주의.)</p>
<h3 id="7-문제에서-예시-답을-의심하는-능력">7. 문제에서 예시 답을 의심하는 능력</h3>
<p><strong>오늘의 중요 발견</strong>: 함수 작성 문제에서 (가로, 세로) 좌표계로 dx/dy를 짠 답과 예시 답이 달랐다. 학습자가 먼저 &quot;예시 답이 정확한지 검증해달라&quot;고 요청 → 확인해보니 <strong>학습자의 로직도 완벽</strong>했고, 단지 좌표 규약이 (가로,세로)냐 (행,열)이냐의 차이였다.</p>
<p><strong>피드백 포인트</strong>:</p>
<ul>
<li>자기 답이 틀렸다고 바로 단정하지 말 것</li>
<li>예시 답과 다르면 <strong>규약 차이</strong>인지 <strong>로직 오류</strong>인지부터 구분해야 함</li>
<li>COS Pro 실전에서는 문제에 명시된 좌표 규약을 <strong>먼저 확인</strong>하고 시작하는 습관이 중요</li>
</ul>
<hr>
<h2 id="오늘의-결과">오늘의 결과</h2>
<ul>
<li>풀이 3문제 (빈칸 2, 함수 작성 1) / 1차 정답률 100% (경계 체크 부분만 힌트 후 수정)</li>
<li>좌표 이동 dx/dy + 90도 회전 + K%4 주기성 — 구현 3대 기초 패턴 습득</li>
<li>다음 학습(4/16)은 구현/시뮬레이션 기초 마무리 (달팽이 배열, 종이접기 등 복합 시뮬레이션 예정)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[20260413 오늘의 학습: 스택과 큐]]></title>
            <link>https://velog.io/@lee_yesol421/20260413-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-%EC%8A%A4%ED%83%9D%EA%B3%BC-%ED%81%90</link>
            <guid>https://velog.io/@lee_yesol421/20260413-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-%EC%8A%A4%ED%83%9D%EA%B3%BC-%ED%81%90</guid>
            <pubDate>Mon, 13 Apr 2026 14:16:47 GMT</pubDate>
            <description><![CDATA[<h2 id="지난-학습-요약">지난 학습 요약</h2>
<ul>
<li>약점 집중 연습 2회차에서 원본 보존 패턴, 순환 순회 범위 모두 정착 판정</li>
<li>주요 약점 전부 해소 → 실력 강화 기간으로 전환 결정</li>
<li>아직 안 다룬 유형: 스택/큐, 그리디, BFS/DFS, 이진 탐색, DP 등</li>
</ul>
<h2 id="오늘-수업-계획">오늘 수업 계획</h2>
<p>COS Pro 출제 범위 중 미학습 유형인 <strong>스택과 큐</strong>를 집중 학습한다. 개념 설명 후 빈칸/디버깅/함수 작성 6문제를 풀며 핵심 패턴을 익힌다.</p>
<hr>
<h2 id="학습-내용-정리">학습 내용 정리</h2>
<h3 id="1-스택-stack--lifo">1. 스택 (Stack) — LIFO</h3>
<p>마지막에 넣은 걸 먼저 꺼내는 구조. Java에서는 <code>Stack</code> 클래스를 쓰지만, Python에서는 <strong>그냥 리스트</strong>로 스택을 구현한다.</p>
<table>
<thead>
<tr>
<th>Java</th>
<th>Python</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>push()</code></td>
<td><code>append()</code></td>
<td>넣기</td>
</tr>
<tr>
<td><code>pop()</code></td>
<td><code>pop()</code></td>
<td>꺼내기 (마지막 원소)</td>
</tr>
<tr>
<td><code>peek()</code></td>
<td><code>[-1]</code></td>
<td>맨 위 보기 (꺼내지 않음)</td>
</tr>
<tr>
<td><code>isEmpty()</code></td>
<td><code>not stack</code></td>
<td>비어있는지 확인</td>
</tr>
</tbody></table>
<h3 id="2-큐-queue--fifo">2. 큐 (Queue) — FIFO</h3>
<p>먼저 넣은 걸 먼저 꺼내는 구조. 리스트의 <code>pop(0)</code>은 O(n)이라 느리므로, <strong><code>collections.deque</code></strong>를 사용한다.</p>
<pre><code class="language-python">from collections import deque

queue = deque()
queue.append(1)        # 뒤에 넣기
front = queue.popleft() # 앞에서 꺼내기 — O(1)</code></pre>
<table>
<thead>
<tr>
<th>연산</th>
<th>리스트</th>
<th>deque</th>
</tr>
</thead>
<tbody><tr>
<td>뒤에 넣기</td>
<td>append() — O(1)</td>
<td>append() — O(1)</td>
</tr>
<tr>
<td>앞에서 꺼내기</td>
<td>pop(0) — O(n)</td>
<td>popleft() — O(1)</td>
</tr>
</tbody></table>
<p><strong>COS Pro 포인트</strong>: 큐 문제가 나오면 <code>deque</code>를 import하는 게 정석이다.</p>
<h3 id="3-빈-컨테이너-체크--not-활용">3. 빈 컨테이너 체크 — <code>not</code> 활용</h3>
<blockquote>
<p>수업 중 질문: &quot;stack이 리스트니까 <code>not list</code>도 되는 거야? dict이나 튜플 같은 다른 자료구조는?&quot;</p>
</blockquote>
<p>Python의 <strong>모든 컨테이너</strong>에 동일하게 적용된다:</p>
<pre><code class="language-python">not []         # True  (빈 리스트)
not {}         # True  (빈 딕셔너리)
not ()         # True  (빈 튜플)
not set()      # True  (빈 집합)
not deque()    # True  (빈 덱)
not &quot;&quot;         # True  (빈 문자열)</code></pre>
<p>Java의 <code>isEmpty()</code>를 모든 자료구조에 통일해서 쓸 수 있는 셈. COS Pro 빈칸에서 <code>not 변수명</code>이 나오면 &quot;비어있는지 확인&quot;이라고 읽으면 된다.</p>
<pre><code class="language-python"># 이 둘은 같은 의미
if len(stack) == 0:    # 명시적
if not stack:          # Pythonic — 시험에서 자주 나오는 형태</code></pre>
<h3 id="4-괄호-매칭--스택의-대표-패턴">4. 괄호 매칭 — 스택의 대표 패턴</h3>
<h4 id="단일-괄호-검사">단일 괄호 검사</h4>
<p>여는 괄호를 만나면 push, 닫는 괄호를 만나면 스택이 비어있는지 확인 후 pop.</p>
<pre><code class="language-python">def is_valid_parentheses(s):
    stack = []
    for char in s:
        if char == &#39;(&#39;:
            stack.append(char)
        elif char == &#39;)&#39;:
            if not stack:       # 짝이 없다
                return False
            stack.pop()
    return not stack            # 남은 괄호 없으면 True</code></pre>
<h4 id="다중-괄호-검사---">다중 괄호 검사 (<code>()</code>, <code>{}</code>, <code>[]</code>)</h4>
<p>딕셔너리로 여는/닫는 괄호의 짝을 매핑한다:</p>
<pre><code class="language-python">def is_valid_brackets(s):
    stack = []
    brackets = {&quot;[&quot;: &quot;]&quot;, &quot;(&quot;: &quot;)&quot;, &quot;{&quot;: &quot;}&quot;}

    for char in s:
        if char in brackets:           # .keys() 생략 가능!
            stack.append(char)
        elif char in brackets.values():
            if not stack or brackets[stack[-1]] != char:
                return False
            stack.pop()
    return not stack</code></pre>
<p><strong>딕셔너리 팁</strong>: <code>&quot;[&quot; in brackets</code>는 기본적으로 key에서 검색한다. <code>.keys()</code> 안 붙여도 된다.</p>
<h3 id="5-브라우저-뒤로가기--스택-활용">5. 브라우저 뒤로가기 — 스택 활용</h3>
<blockquote>
<p>수업 중 질문: &quot;visit과 back 개수가 같으면 &#39;홈&#39;으로 돌아가는 조건이 따로 필요한 거 아니야?&quot;</p>
</blockquote>
<p>&quot;홈&quot;도 visit할 때 스택에 들어가기 때문에 별도 조건 없이 자동으로 복귀된다.</p>
<pre><code class="language-python">def browser_history(actions):
    history = []
    current = &quot;홈&quot;
    for action, value in actions:
        if action == &quot;visit&quot;:
            history.append(current)   # ← 새 페이지가 아니라 &quot;지금 페이지&quot;를 저장!
            current = value
        elif action == &quot;back&quot;:
            if history:
                current = history.pop()
    return current</code></pre>
<p><strong>핵심 포인트</strong>: 스택에 저장하는 건 &quot;어디로 가는지(value)&quot;가 아니라 <strong>&quot;어디서 왔는지(current)&quot;</strong>. 뒤로 가기니까!</p>
<h3 id="6-요세푸스-문제--큐로-원형-순환">6. 요세푸스 문제 — 큐로 원형 순환</h3>
<p>N명이 원형으로 앉아있고 K번째 사람을 순서대로 제거하는 문제. 큐로 원형을 표현하는 트릭: <strong>앞에서 빼서 뒤로 보내면</strong> 원형처럼 동작한다.</p>
<pre><code class="language-python">from collections import deque

def josephus(n, k):
    queue = deque(range(1, n + 1))  # deque는 range를 직접 받을 수 있다
    result = []
    while queue:
        for _ in range(k - 1):         # k-1명 건너뛰기
            queue.append(queue.popleft())  # 앞에서 빼서 뒤로
        result.append(queue.popleft())     # k번째 사람 제거
    return result</code></pre>
<h3 id="7-프린터-우선순위--큐--튜플-추적">7. 프린터 우선순위 — 큐 + 튜플 추적</h3>
<p>우선순위가 높은 문서가 먼저 인쇄되는 프린터. 내 문서가 몇 번째로 인쇄되는지 구하는 문제.</p>
<blockquote>
<p>수업 중 질문: &quot;&#39;deque mutated during iteration&#39; 에러는 뭐야?&quot;</p>
</blockquote>
<p>deque를 <code>for</code>로 순회하는 도중에 내용을 변경하면 발생하는 에러. <code>while</code>로 바꿔서 해결한다.</p>
<pre><code class="language-python">from collections import deque

def print_order(priorities, target):
    queue = deque((p, idx) for idx, p in enumerate(priorities))
    count = 0
    while queue:
        p, idx = queue.popleft()
        if queue and p &lt; max(p for p, _ in queue):
            queue.append((p, idx))    # 우선순위 낮으면 뒤로
        else:
            count += 1
            if idx == target:         # 내 문서면 바로 반환
                return count</code></pre>
<p><strong>핵심</strong>: 큐에 (우선순위, 원래 인덱스) 튜플을 넣어서 내 문서를 추적한다. 인쇄 즉시 카운터로 반환하면 불필요한 순회를 줄일 수 있다.</p>
<hr>
<h2 id="오늘의-결과">오늘의 결과</h2>
<ul>
<li>스택/큐 개념 + 6문제 풀이 (1차 정답 3문제, 최종 전부 정답)</li>
<li>핵심 패턴 정리: 괄호 매칭, 원형 순환, 우선순위 큐</li>
<li>다음 학습: 그리디 알고리즘 개념 및 문제 풀이</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[20260412 오늘의 학습: 약점 집중 연습 2회차]]></title>
            <link>https://velog.io/@lee_yesol421/20260412-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-%EC%95%BD%EC%A0%90-%EC%A7%91%EC%A4%91-%EC%97%B0%EC%8A%B5-2%ED%9A%8C%EC%B0%A8</link>
            <guid>https://velog.io/@lee_yesol421/20260412-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-%EC%95%BD%EC%A0%90-%EC%A7%91%EC%A4%91-%EC%97%B0%EC%8A%B5-2%ED%9A%8C%EC%B0%A8</guid>
            <pubDate>Sun, 12 Apr 2026 05:18:48 GMT</pubDate>
            <description><![CDATA[<h2 id="지난-학습-요약">지난 학습 요약</h2>
<ul>
<li>약점 집중 연습 1회차에서 슬라이딩 윈도우 완전 정착 확인</li>
<li>문자열 순환 text*2 패턴 정착, 순회 범위 구분은 추가 연습 필요</li>
<li>데이터 변환 후 원본 소실 실수 1건 발견 — 반복 노출 필요 판정</li>
</ul>
<h2 id="오늘-수업-계획">오늘 수업 계획</h2>
<p>남은 약점 2가지(원본 보존, 순환 순회 범위)를 집중 연습하고, 두 패턴을 결합한 종합 문제까지 도전한다.</p>
<hr>
<h2 id="학습-내용-정리">학습 내용 정리</h2>
<h3 id="1-원본-보존-패턴--비교는-변환-반환은-원본">1. 원본 보존 패턴 — &quot;비교는 변환, 반환은 원본&quot;</h3>
<p>COS Pro에서 자주 나오는 패턴이다. 대소문자 무시하고 비교하되, 결과는 원래 형태 그대로 돌려줘야 하는 문제.</p>
<p>핵심 원칙은 간단하다:</p>
<pre><code class="language-python">for w in words:
    if w.lower().startswith(prefix_lower):  # 비교할 때만 변환
        result.append(w)                     # 결과엔 원본</code></pre>
<p>주의할 점은 <strong>변환한 리스트를 따로 만들어서 순회하면 안 된다</strong>는 것이다.</p>
<pre><code class="language-python"># 잘못된 패턴
cleaned_words = [w.lower() for w in words]
for word in cleaned_words:  # 여기서 이미 원본을 잃음
    result = word           # 소문자 버전이 저장됨

# 올바른 패턴
for word in words:          # 원본 리스트를 순회
    if word.lower() == ...: # 비교만 변환
        result = word       # 원본 저장</code></pre>
<p>디버깅 문제에서 <code>cleaned_words</code>라는 변수가 만들어져 있었는데, <code>isalpha()</code>로 알파벳만 세는 로직에서는 사실 대소문자 변환 자체가 불필요했다. 이런 <strong>함정용 변수</strong>를 코드에 넣어두고 순회 대상을 바꾸게 만드는 유형이 시험에 나올 수 있으니, 코드 전체를 비판적으로 읽는 습관이 중요하다.</p>
<h3 id="2-중복-제거-순서의-중요성">2. 중복 제거 순서의 중요성</h3>
<p>평균 이상인 학생을 점수 내림차순으로 반환하되, 동명이인(대소문자 무시)은 먼저 나온 학생만 포함하는 함수를 작성했다.</p>
<p>처음에는 <strong>정렬 후 중복 제거</strong>를 했더니, 점수가 높은 쪽이 먼저 처리되어 원래 순서의 이름이 아닌 다른 대소문자 버전이 남았다.</p>
<pre><code class="language-python"># 문제가 되는 순서
students_sorted = sorted(students, key=lambda x: x[1], reverse=True)
# (&quot;ALICE&quot;, 90)이 (&quot;Alice&quot;, 85)보다 먼저 → &quot;ALICE&quot;가 남음

# 올바른 순서: 중복 제거(원본 순서) → 필터 → 정렬</code></pre>
<p><strong>&quot;먼저 나온&quot;이라는 기준은 원본 리스트 순서</strong>를 의미하므로, 정렬 전에 중복 제거를 해야 한다.</p>
<p>수업 중 질문: &quot;코드가 마음에 안 드는데 더 효율적이고 깔끔한 방법 추천해줘&quot;
→ 두 가지 개선 포인트를 배웠다:</p>
<table>
<thead>
<tr>
<th>기존</th>
<th>개선</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>for 루프로 합계 계산</td>
<td><code>sum(s for _, s in students)</code></td>
<td>한 줄로 평균 계산</td>
</tr>
<tr>
<td>리스트 컴프리헨션으로 매번 중복 체크</td>
<td><code>set()</code>으로 체크</td>
<td>O(n) → O(1) 탐색</td>
</tr>
</tbody></table>
<p>특히 <code>set</code>을 이용한 중복 체크는 시험에서도 자주 쓰이는 최적화 패턴이다.</p>
<pre><code class="language-python"># 최종
def above_average_students(students):
    avg = sum(s for _, s in students) / len(students)

    seen = set()
    unique = []
    for name, score in students:
        if name.lower() not in seen and score &gt;= avg:
            seen.add(name.lower())
            unique.append((name, score))

    return [name for name, score in sorted(unique, key=lambda x: x[1], reverse=True)]</code></pre>
<h3 id="3-순환-순회-범위-복습--lentext가-정답">3. 순환 순회 범위 복습 — len(text)가 정답</h3>
<p>원형 문자열에서 길이 k인 부분 문자열을 탐색할 때:</p>
<pre><code class="language-python">extended = text * 2
for i in range(len(text)):  # len(extended)-k 아님!
    sub = extended[i:i+k]</code></pre>
<p><code>text * 2</code>로 확장해도, 원래 문자열의 시작 위치는 <code>len(text)</code>개뿐이다. <code>len(extended) - k</code>를 쓰면 중복 탐색이 발생한다.</p>
<table>
<thead>
<tr>
<th>비교</th>
<th>range</th>
<th>text=&quot;dacb&quot;, k=3</th>
</tr>
</thead>
<tbody><tr>
<td><code>len(text)</code></td>
<td>range(4) → 0,1,2,3</td>
<td>정확히 4개 위치</td>
</tr>
<tr>
<td><code>len(extended)-k</code></td>
<td>range(5) → 0,1,2,3,4</td>
<td>i=4는 &quot;cbd&quot;로 i=2와 중복</td>
</tr>
</tbody></table>
<h3 id="4-종합-문제--순환--원본-보존">4. 종합 문제 — 순환 + 원본 보존</h3>
<p>마지막 문제는 원형 배치된 학생 이름 리스트에서 연속 k명의 이름을 이어붙인 문자열이 사전순으로 가장 빠른 그룹을 찾아 원래 이름 리스트로 반환하는 함수였다.</p>
<p>두 약점을 한 번에 적용해야 하는 종합 문제:</p>
<pre><code class="language-python">def smallest_circular_group(names, k):
    names_extended = names * 2                              # 순환 처리
    names_lowered = [name.lower() for name in names_extended]  # 비교용 변환

    result = &quot;&quot;.join(names_lowered[:k])
    result_idx = 0

    for i in range(1, len(names)):                          # 순회 범위: len(names)
        sub = &quot;&quot;.join(names_lowered[i:k+i])
        if sub &lt; result:
            result = sub
            result_idx = i

    return names_extended[result_idx: result_idx+k]         # 반환은 원본</code></pre>
<ul>
<li><code>names_lowered</code>로 비교하고 <code>names_extended</code>에서 꺼내서 원본 보존</li>
<li>순회 범위는 <code>len(names)</code>로 정확히 제한</li>
<li><code>result_idx</code>로 위치를 추적해서 원본 리스트에서 슬라이싱</li>
</ul>
<p>참고: 디버깅용 <code>print()</code>는 시험 제출 전에 반드시 삭제해야 한다. 감점 요인이 될 수 있다.</p>
<hr>
<h2 id="오늘의-결과">오늘의 결과</h2>
<ul>
<li>약점 연습 5문제 전부 1차 정답 — 원본 보존, 순환 범위 모두 <strong>정착 판정</strong></li>
<li>주요 약점 모두 해소 완료, 내일부터 프로그래머스/백준 문제 풀이로 전환</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[20260411 오늘의 학습: 약점 집중 연습]]></title>
            <link>https://velog.io/@lee_yesol421/20260411-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-%EC%95%BD%EC%A0%90-%EC%A7%91%EC%A4%91-%EC%97%B0%EC%8A%B5</link>
            <guid>https://velog.io/@lee_yesol421/20260411-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-%EC%95%BD%EC%A0%90-%EC%A7%91%EC%A4%91-%EC%97%B0%EC%8A%B5</guid>
            <pubDate>Sat, 11 Apr 2026 04:44:27 GMT</pubDate>
            <description><![CDATA[<h2 id="지난-학습-요약">지난 학습 요약</h2>
<ul>
<li>구름EDU 기출 3회차 전체 완료 (10문제 모두 최종 정답)</li>
<li>슬라이딩 윈도우, 문자열 순환 처리(text*2) 새로 학습</li>
<li>내장함수명 변수 사용, 하드코딩 습관 재발 → 지속 주의 필요</li>
</ul>
<h2 id="오늘-수업-계획">오늘 수업 계획</h2>
<ul>
<li>14차에서 발견된 취약 영역 3가지를 집중 복습</li>
<li>슬라이딩 윈도우 → 문자열 순환 → 함수 작성 종합 순서로 총 6문제</li>
</ul>
<hr>
<h2 id="학습-내용-정리">학습 내용 정리</h2>
<h3 id="1-슬라이딩-윈도우--빈칸-채우기">1. 슬라이딩 윈도우 — 빈칸 채우기</h3>
<p>연속된 k개 원소의 합이 최대인 구간의 합을 구하는 함수.</p>
<pre><code class="language-python">def max_subarray_sum(nums, k):
    if len(nums) &lt; k:
        return 0

    window_sum = sum(nums[:k])       # (a) 첫 윈도우
    max_sum = window_sum

    for i in range(k, len(nums)):    # (b) k부터 시작
        window_sum += nums[i] - nums[i-k]  # (c) 왼쪽 제거
        if window_sum &gt; max_sum:
            max_sum = window_sum

    return max_sum</code></pre>
<h4 id="슬라이딩-윈도우-핵심-구조">슬라이딩 윈도우 핵심 구조</h4>
<ol>
<li><strong>첫 윈도우</strong>: <code>sum(nums[:k])</code>로 초기값 계산</li>
<li><strong>이동 시작</strong>: 첫 윈도우 이미 구했으므로 <code>range(k, len(nums))</code></li>
<li><strong>갱신</strong>: 오른쪽 추가(<code>nums[i]</code>) - 왼쪽 제거(<code>nums[i-k]</code>)</li>
</ol>
<p>14차에서는 range 시작값에서 힌트가 필요했는데, 이번엔 바로 정답!</p>
<h3 id="2-슬라이딩-윈도우--함수-작성">2. 슬라이딩 윈도우 — 함수 작성</h3>
<p>연속된 k개 원소의 <strong>평균이 최대인 구간의 시작 인덱스</strong>를 반환하는 함수.</p>
<pre><code class="language-python">def solution(nums, k):
    std_sum = sum(nums[:k])
    std_idx = 0
    max_sum = std_sum

    for i in range(k, len(nums)):
        std_sum += nums[i] - nums[i-k]
        if std_sum &gt; max_sum:
            max_sum = std_sum
            std_idx = i - k + 1

    return std_idx</code></pre>
<h4 id="포인트">포인트</h4>
<ul>
<li><strong>합이 최대면 평균도 최대</strong> — k가 고정이니까 나누기 불필요. 불필요한 연산을 줄이는 판단이 좋았다는 피드백을 받았다.</li>
<li><strong>시작 인덱스</strong>: <code>i - k + 1</code> (빠진 원소의 다음 인덱스)</li>
<li><strong><code>&gt;</code> vs <code>&gt;=</code></strong>: 같은 합일 때 앞쪽 인덱스를 유지해야 하므로 <code>&gt;</code>(초과) 사용</li>
</ul>
<blockquote>
<p>수업 중 질문: &quot;첫번째 문제처럼 len(nums) &lt; k인 경우는 없어?&quot;
→ COS Pro 실제 시험에서는 보통 입력 조건이 보장되어 있어서 예외 처리 없이 바로 로직에 집중하면 된다.</p>
</blockquote>
<h3 id="3-문자열-순환--빈칸-채우기">3. 문자열 순환 — 빈칸 채우기</h3>
<p>문자열을 원형(순환)으로 간주하여 연속된 k글자 중 사전순으로 가장 앞서는 부분 문자열을 반환하는 함수.</p>
<pre><code class="language-python">def earliest_circular_substring(text, k):
    extended = text * 2            # (a) 순환 처리
    result = extended[:k]

    for i in range(1, len(text)):  # (b) 고유 시작점은 len(text)개
        substring = extended[i:i+k]
        if substring &lt; result:
            result = substring

    return result</code></pre>
<h4 id="순환-처리-핵심">순환 처리 핵심</h4>
<ul>
<li><strong><code>text * 2</code></strong>: 문자열을 두 번 이어붙여서 끝을 넘어가는 케이스 해결</li>
<li><strong>순회 범위는 <code>len(text)</code></strong>: 원형 문자열에서 고유한 시작점은 원본 길이만큼만 존재</li>
</ul>
<p>처음에 <code>len(extended) - k</code>로 답했는데, 동작은 하지만 중복 순회가 발생한다. 시험 빈칸 채우기에서는 <code>len(text)</code>가 정답이므로 오답 처리될 수 있다는 피드백을 받았다.</p>
<table>
<thead>
<tr>
<th>i</th>
<th>&quot;dbca&quot;(k=2)</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>0</td>
<td>db</td>
<td></td>
</tr>
<tr>
<td>1</td>
<td>bc</td>
<td></td>
</tr>
<tr>
<td>2</td>
<td>ca</td>
<td></td>
</tr>
<tr>
<td>3</td>
<td><strong>ad</strong></td>
<td>← 순환으로만 나오는 조합</td>
</tr>
<tr>
<td>4</td>
<td>db</td>
<td>i=0과 동일 (불필요)</td>
</tr>
<tr>
<td>5</td>
<td>bc</td>
<td>i=1과 동일 (불필요)</td>
</tr>
</tbody></table>
<h3 id="4-문자열-순환--함수-작성">4. 문자열 순환 — 함수 작성</h3>
<p>문자열을 순환으로 간주하여 연속된 k글자로 만들 수 있는 <strong>서로 다른 부분 문자열의 개수</strong>를 반환하는 함수.</p>
<pre><code class="language-python">def solution(text, k):
    extended = text * 2
    str_set = set()

    for i in range(0, len(text)):
        sub_str = extended[i:i+k]
        str_set.add(sub_str)

    return len(str_set)</code></pre>
<h4 id="포인트-1">포인트</h4>
<ul>
<li><strong>set()으로 중복 제거</strong> — &quot;서로 다른 개수&quot;에 딱 맞는 자료구조</li>
<li>앞 문제 피드백을 바로 반영하여 <code>len(text)</code> 범위로 정확히 설정</li>
<li>변수명 <code>str_set</code> — <code>str</code>로 지었으면 내장함수를 덮어쓸 뻔했는데 잘 피함</li>
</ul>
<h3 id="5-함수-작성-종합-1--상하위-평균-차이">5. 함수 작성 종합 (1) — 상하위 평균 차이</h3>
<p>학생들의 점수에서 상위 k명과 하위 k명의 평균 차이를 반환하는 함수.</p>
<pre><code class="language-python">def solution(scores, k):
    sorted_scores = sorted(scores)
    high_avg = sum(sorted_scores[-k:]) / k
    low_avg = sum(sorted_scores[:k]) / k

    return high_avg - low_avg</code></pre>
<h4 id="포인트-2">포인트</h4>
<ul>
<li><code>sorted()</code>로 원본 안 건드림 (좋은 습관)</li>
<li><code>[-k:]</code>, <code>[:k]</code> 슬라이싱 정확</li>
<li>변수명 <code>sorted_scores</code> — 내장함수명 안 겹침</li>
</ul>
<blockquote>
<p>수업 중 질문: &quot;abs() 감싼다는 건 마지막 return할 때 <code>abs(high_avg - low_avg)</code> 하라는 거야?&quot;
→ 맞다. 이 문제에서는 정렬 후 상위 &gt;= 하위가 보장되지만, &quot;절댓값으로 반환하라&quot;는 조건이 명시된 문제라면 <code>abs()</code>를 감싸는 게 안전하다.</p>
</blockquote>
<p>풀이 중 출제자가 제시한 예시 답(35.0)이 틀렸다는 것을 직접 계산해서 발견했다 (정답: 40.0). 검증 습관이 좋다는 피드백을 받았다.</p>
<h3 id="6-함수-작성-종합-2--모음-최다-단어">6. 함수 작성 종합 (2) — 모음 최다 단어</h3>
<p>문자열 리스트에서 모음(a, e, i, o, u)이 가장 많은 단어를 반환하는 함수.</p>
<h4 id="첫-번째-시도-버그-있음">첫 번째 시도 (버그 있음)</h4>
<pre><code class="language-python">def solution(words):
    lowered_words = [x.lower() for x in words]  # 여기서 원본 소실!
    chk_list = [&#39;a&#39;, &#39;e&#39;, &#39;i&#39;, &#39;o&#39;, &#39;u&#39;]
    max_count = 0
    result_word = &quot;&quot;

    for w in lowered_words:  # 소문자 버전을 순회
        ...
        result_word = w      # 소문자 버전이 저장됨

    return result_word</code></pre>
<p><strong>문제</strong>: <code>lowered_words</code>를 순회하면서 <code>result_word</code>에 소문자 변환된 값이 저장된다. <code>[&quot;APPLE&quot;]</code> 입력 시 <code>&quot;apple&quot;</code>이 반환됨.</p>
<h4 id="수정-후-정답">수정 후 (정답)</h4>
<pre><code class="language-python">def solution(words):
    chk_list = [&#39;a&#39;, &#39;e&#39;, &#39;i&#39;, &#39;o&#39;, &#39;u&#39;]
    max_count = 0
    result_word = &quot;&quot;

    for w in words:              # 원본을 순회
        cnt = 0
        for ch in w.lower():    # 비교할 때만 소문자로
            if ch in chk_list:
                cnt += 1
        if cnt &gt; max_count:
            max_count = cnt
            result_word = w      # 원본이 저장됨

    return result_word</code></pre>
<h4 id="데이터-변환-시-원본-보존-패턴">데이터 변환 시 원본 보존 패턴</h4>
<table>
<thead>
<tr>
<th>방식</th>
<th>코드</th>
<th>원본 유지</th>
</tr>
</thead>
<tbody><tr>
<td>변환 리스트 순회</td>
<td><code>for w in lowered_words:</code></td>
<td>X</td>
</tr>
<tr>
<td>원본 순회 + 비교만 변환</td>
<td><code>for w in words: ... w.lower()</code></td>
<td>O</td>
</tr>
</tbody></table>
<p><strong>핵심</strong>: 비교/검색에만 변환을 쓰고, 결과에는 원본을 저장할 것!</p>
<p>추가로 <code>chk_list</code> 대신 문자열로도 가능하다는 팁을 받았다: <code>if ch in &quot;aeiou&quot;</code></p>
<hr>
<h2 id="오늘의-결과">오늘의 결과</h2>
<ul>
<li>6문제 풀이, 1차 정답률 67% (4/6) — 함수 작성 3/4로 개선 추세 유지</li>
<li>슬라이딩 윈도우 완전 정착, 내장함수명 변수 사용·하드코딩 미발생</li>
<li>다음 학습: 프로그래머스/백준 문제 풀이로 함수 작성 실전 연습 시작</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[20260409 오늘의 학습: COS Pro 기출 3회차 완료]]></title>
            <link>https://velog.io/@lee_yesol421/20260409-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-COS-Pro-%EA%B8%B0%EC%B6%9C-3%ED%9A%8C%EC%B0%A8-%EC%99%84%EB%A3%8C</link>
            <guid>https://velog.io/@lee_yesol421/20260409-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-COS-Pro-%EA%B8%B0%EC%B6%9C-3%ED%9A%8C%EC%B0%A8-%EC%99%84%EB%A3%8C</guid>
            <pubDate>Thu, 09 Apr 2026 15:04:38 GMT</pubDate>
            <description><![CDATA[<h2 id="지난-학습-요약">지난 학습 요약</h2>
<ul>
<li>구름EDU 기출 3회차 전반부(1, 2, 3, 6, 7번) 5문제 전부 1차 정답</li>
<li>배열 회전, 팰린드롬, 소수 판별, 카프리카 수, 비숍 공격 범위 학습</li>
<li>3회차 나머지 5문제(4, 5, 8, 9, 10번) 풀이 예정이었음</li>
</ul>
<h2 id="오늘-수업-계획">오늘 수업 계획</h2>
<ul>
<li>구름EDU 기출 3회차 나머지 5문제 풀이 (4, 5, 8, 9, 10번)</li>
<li>디버깅 2문제 + 빈칸 1문제 + 함수 작성 2문제</li>
</ul>
<hr>
<h2 id="학습-내용-정리">학습 내용 정리</h2>
<h3 id="1-break-vs-continue-vs-pass--디버깅-문제-8">1. break vs continue vs pass — 디버깅 (문제 8)</h3>
<p>선풍기 대수를 구하는 디버깅 문제. 교실마다 선풍기 4대가 있고, 선풍기 1대당 k명에게 바람을 보낼 때, 추가로 필요한 선풍기 총 대수를 구하는 코드에서 한 줄 수정.</p>
<h4 id="버그-break-→-continue">버그: <code>break</code> → <code>continue</code></h4>
<pre><code class="language-python">for s in student:
    s -= 4 * k
    if s &lt;= 0:
        break       # ❌ 루프 전체 종료 — 뒤 교실 무시
        continue     # ✅ 이 교실 건너뛰고 다음으로
    answer += (s + k - 1) // k</code></pre>
<h4 id="break--continue--pass-차이-정리">break / continue / pass 차이 정리</h4>
<table>
<thead>
<tr>
<th>키워드</th>
<th>동작</th>
</tr>
</thead>
<tbody><tr>
<td><code>break</code></td>
<td>루프 <strong>전체 종료</strong></td>
</tr>
<tr>
<td><code>continue</code></td>
<td>이번 반복 <strong>건너뛰고</strong> 다음 반복으로</td>
</tr>
<tr>
<td><code>pass</code></td>
<td><strong>아무것도 안 함</strong> (다음 줄 실행)</td>
</tr>
</tbody></table>
<p>이 문제에서 <code>pass</code>를 쓰면 <code>if</code> 블록 이후의 <code>answer += ...</code> 줄이 실행되어 0이하 값이 더해질 수 있다. <code>continue</code>가 정확한 답이다.</p>
<hr>
<h3 id="2-슬라이딩-윈도우--디버깅-문제-9">2. 슬라이딩 윈도우 — 디버깅 (문제 9)</h3>
<p>팝업스토어를 열 최적의 날짜를 찾는 문제. n일간의 매출액 중 연속 k일의 합이 최대인 값을 구한다.</p>
<h4 id="슬라이딩-윈도우-패턴">슬라이딩 윈도우 패턴</h4>
<p>연속 k개 합을 구할 때, 매번 sum()을 쓰면 느리다. 대신 &quot;하나 빼고 하나 더하기&quot;로 O(n)에 해결한다.</p>
<pre><code class="language-python"># 첫 윈도우 합
rsum = sum(revenue[0:k])
answer = rsum

# 윈도우를 한 칸씩 밀기
for i in range(k, len(revenue)):    # ← i는 k부터!
    rsum = rsum - revenue[i - k] + revenue[i]
    if answer &lt; rsum:
        answer = rsum</code></pre>
<p><strong>핵심 포인트:</strong></p>
<ul>
<li>첫 윈도우(0~k-1)는 이미 구했으니, for문은 <strong>i=k부터</strong> 시작</li>
<li><code>i</code>는 항상 윈도우의 <strong>오른쪽 끝</strong>을 가리킴</li>
<li><code>revenue[i-k]</code>는 빠지는 왼쪽 값, <code>revenue[i]</code>는 새로 들어오는 오른쪽 값</li>
</ul>
<p>예시 (k=4, <code>[1, 1, 9, 3, 7, 6, 5, 10]</code>):</p>
<pre><code>첫 윈도우: [1, 1, 9, 3]              → sum = 14
i=4:        1 [1, 9, 3, 7]           → 14 - 1 + 7 = 20
i=5:           [9, 3, 7, 6]          → 20 - 1 + 6 = 25
i=6:              [3, 7, 6, 5]       → 25 - 9 + 5 = 21
i=7:                 [7, 6, 5, 10]   → 21 - 3 + 10 = 28 ← 최대!</code></pre><blockquote>
<p>수업 중 질문: &quot;range 시작값으로 0이나 1이 아닌 k를 쓰는 이유?&quot;
→ i가 len을 넘지 않으면서도 i-k가 0부터 시작하게 하려는 것. i가 윈도우의 오른쪽 끝이기 때문에, i=k일 때 첫 번째로 빠지는 값이 revenue[0]이 된다.</p>
</blockquote>
<hr>
<h3 id="3-클래스-상속--메서드-오버라이드--빈칸-채우기-문제-10">3. 클래스 상속 + 메서드 오버라이드 — 빈칸 채우기 (문제 10)</h3>
<p>미용실(HairShop)과 레스토랑(Restaurant)이 Shop을 상속받아 각각 예약 조건을 다르게 구현하는 문제. 빈칸 8개를 채워야 한다.</p>
<h4 id="예약-조건">예약 조건</h4>
<ul>
<li><strong>미용실</strong>: 인원수 1명만, 시간 겹침 불가</li>
<li><strong>레스토랑</strong>: 인원수 2~8명, 같은 시간 최대 2팀까지</li>
</ul>
<pre><code class="language-python">class HairShop(Shop):                        # 상속
    def __init__(self):
        super().__init__()

    def reserve(self, customer):             # 메서드 오버라이드
        if customer.num_of_people != 1:      # 1명 아니면 거절
            return False
        for r in self.reserve_list:
            if r.time == customer.time:      # 시간 겹침 체크
                return False
        self.reserve_list.append(customer)
        return True</code></pre>
<pre><code class="language-python">class Restaurant(Shop):
    def reserve(self, customer):
        if customer.num_of_people &lt; 2 or customer.num_of_people &gt; 8:
            return False
        count = 0
        for r in self.reserve_list:
            if r.time == customer.time:
                count += 1
        if count &gt;= 2:          # for문 밖에서 판단!
            return False
        self.reserve_list.append(customer)
        return True</code></pre>
<p><strong>포인트:</strong> Restaurant의 <code>count &gt;= 2</code> 체크는 for문 <strong>밖</strong>에서 한다. 안에서 하면 아직 다 세지도 않았는데 판단하게 될 수 있다.</p>
<p>이 문제는 10차 세션에서 배운 클래스 상속 개념으로 8개 빈칸 전부 1차 정답!</p>
<hr>
<h3 id="4-문자열-겹침-이어붙이기--함수-작성-문제-4">4. 문자열 겹침 이어붙이기 — 함수 작성 (문제 4)</h3>
<p>두 문자열 s1, s2를 이어붙이되, 한쪽 끝과 다른 쪽 시작이 겹치면 한 번만 쓴다. s1+s2, s2+s1 중 더 짧은 길이를 return.</p>
<h4 id="풀이-핵심-두-방향-겹침-중-최대값-찾기">풀이 핵심: 두 방향 겹침 중 최대값 찾기</h4>
<pre><code class="language-python">def solution(s1, s2):
    answer = 0
    std_len = min(len(s1), len(s2))
    dup_len = 0

    for i in range(1, std_len + 1):    # ← +1 주의!
        # s2 뒤에 s1 붙이는 경우 (s1 시작 = s2 끝)
        if s1[:i] == s2[-i:]:
            dup_len = i
        # s1 뒤에 s2 붙이는 경우 (s1 끝 = s2 시작)
        if s1[-i:] == s2[:i] and i &gt; dup_len:
            dup_len = i

    answer = len(s1) + len(s2) - dup_len
    return answer</code></pre>
<p><strong>주의:</strong> <code>range(1, std_len)</code>이 아니라 <code>range(1, std_len + 1)</code> — 겹침이 짧은 쪽 문자열 전체 길이와 같을 수 있기 때문이다.</p>
<hr>
<h3 id="5-전광판-스크롤--함수-작성-문제-5">5. 전광판 스크롤 — 함수 작성 (문제 5)</h3>
<p>화면에 14자를 표시하는 전광판에서, 문구가 왼쪽으로 1초에 한 칸씩 스크롤된다. 주어진 초(second)에 화면에 보이는 문자열을 return.</p>
<h4 id="풀이-핵심-밑줄-패딩--text2로-순환-처리">풀이 핵심: 밑줄 패딩 + text*2로 순환 처리</h4>
<pre><code class="language-python">def solution(phrases, second):
    answer = &#39;&#39;
    text = phrases.zfill(len(phrases) * 2).replace(&quot;0&quot;, &quot;_&quot;)
    d_text = text * 2
    cal_sc = second % len(text)
    answer = d_text[cal_sc:len(phrases) + cal_sc]
    return answer</code></pre>
<p><strong>단계별 동작:</strong></p>
<ol>
<li><code>zfill(28)</code>: &quot;happy-birthday&quot; → &quot;00000000000000happy-birthday&quot; (14개의 0 + 문구)</li>
<li><code>.replace(&quot;0&quot;, &quot;_&quot;)</code>: &quot;______________happy-birthday&quot; (밑줄 14개 + 문구)</li>
<li><code>text * 2</code>: 문자열을 두 번 이어붙여서 순환 처리</li>
<li><code>second % len(text)</code>: 나머지 연산으로 반복 주기 처리</li>
<li>슬라이싱으로 14글자 추출</li>
</ol>
<blockquote>
<p>수업 중 질문: &quot;왜 text를 두 번 붙이나요?&quot;
→ 슬라이싱이 문자열 끝을 넘어가면 잘린다. 예를 들어 t=20일 때 <code>text[20:34]</code>인데 text는 28자라 8글자만 나온다. text*2로 56자를 만들어두면 어디서 잘라도 항상 14글자가 보장된다.</p>
</blockquote>
<p><strong>주의:</strong> <code>str</code>은 Python 내장함수이므로 변수명으로 사용하면 안 된다! <code>text</code>, <code>display_str</code> 같은 이름을 쓰자.</p>
<hr>
<h3 id="6-음수-인덱스와-슬라이싱-복습">6. 음수 인덱스와 슬라이싱 복습</h3>
<p>문제 4에서 <code>s1[-i:]</code> 같은 표현이 나왔는데, 왜 이게 &quot;뒤에서 i글자&quot;가 되는지 정리.</p>
<h4 id="음수-인덱스--뒤에서부터-세는-위치">음수 인덱스 = 뒤에서부터 세는 위치</h4>
<pre><code class="language-python">s = &quot;hello&quot;
#     h  e  l  l  o
# 양수: 0  1  2  3  4
# 음수:-5 -4 -3 -2 -1</code></pre>
<h4 id="슬라이싱에서-생략된-부분">슬라이싱에서 생략된 부분</h4>
<p><code>s[-1:]</code>은 사실 <code>s[-1:len(s)]</code>와 같다. 뒤쪽을 생략하면 <strong>끝까지</strong>라는 뜻.</p>
<pre><code class="language-python">s[-1:]   # s[-1:5] → &quot;o&quot;       (뒤 1글자)
s[-2:]   # s[-2:5] → &quot;lo&quot;      (뒤 2글자)
s[-3:]   # s[-3:5] → &quot;llo&quot;     (뒤 3글자)</code></pre>
<p>마찬가지로 <code>s[:3]</code>은 <code>s[0:3]</code>과 같다. 앞쪽 생략하면 <strong>처음부터</strong>.</p>
<h4 id="주의-음수-인덱스-≠-역순-출력">주의: 음수 인덱스 ≠ 역순 출력</h4>
<blockquote>
<p>수업 중 질문: &quot;음수 인덱스를 쓰면 거꾸로 출력되나요?&quot;
→ 아니다! 음수는 <strong>시작 위치</strong>만 뒤쪽일 뿐, 출력 순서는 왼→오 그대로다.</p>
</blockquote>
<pre><code class="language-python">s = &quot;hello&quot;
s[-3:]    # &quot;llo&quot; ← 뒤에서 3글자를 순서대로
s[::-1]   # &quot;olleh&quot; ← 이게 진짜 역순! (step이 -1)</code></pre>
<p><strong>역순은 step에 -1을 줘야 한다.</strong> 음수가 나오는 위치가 다르다:</p>
<ul>
<li><strong>인덱스의 음수</strong> → 위치 (어디서부터)</li>
<li><strong>step의 음수</strong> → 방향 (어느 쪽으로)</li>
</ul>
<pre><code class="language-python"># 뒤 3글자를 거꾸로 출력하려면?
s[:-4:-1]   # &quot;oll&quot; ← start 생략(=끝), end=-4(=index 1) 전까지, step=-1</code></pre>
<hr>
<h2 id="오늘의-결과">오늘의 결과</h2>
<ul>
<li>기출 3회차 나머지 5문제 완료 → <strong>3회차 전체 10문제 완료!</strong></li>
<li>1차 정답률 3/5 (60%) — 슬라이딩 윈도우, 전광판 스크롤은 힌트 후 해결</li>
<li>다음 학습: 구름EDU 기출 4회차 (학습용 마지막)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[20260406 오늘의 학습: COS Pro 기출 2회차 완료]]></title>
            <link>https://velog.io/@lee_yesol421/20260406-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-COS-Pro-%EA%B8%B0%EC%B6%9C-2%ED%9A%8C%EC%B0%A8-%EC%99%84%EB%A3%8C</link>
            <guid>https://velog.io/@lee_yesol421/20260406-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-COS-Pro-%EA%B8%B0%EC%B6%9C-2%ED%9A%8C%EC%B0%A8-%EC%99%84%EB%A3%8C</guid>
            <pubDate>Mon, 06 Apr 2026 12:14:44 GMT</pubDate>
            <description><![CDATA[<h2 id="지난-학습-요약">지난 학습 요약</h2>
<ul>
<li>구름EDU 기출 2회차 1~4번, 10번 풀이 완료</li>
<li>클래스 상속/추상 클래스, itertools.combinations 새로 학습</li>
<li>combinations에서 상수/변수 혼동 실수 1건 (힌트 후 교정)</li>
</ul>
<h2 id="오늘-수업-계획">오늘 수업 계획</h2>
<ul>
<li>구름EDU 기출 2회차 나머지 5~9번 마무리</li>
<li>2회차 완료 후 학습 일정 점검</li>
</ul>
<hr>
<h2 id="학습-내용-정리">학습 내용 정리</h2>
<h3 id="1-거스름돈-구하기--빈칸-채우기-문제-7">1. 거스름돈 구하기 — 빈칸 채우기 (문제 7)</h3>
<p>금액을 8가지 화폐(10원~50000원)로 거슬러줄 때 동전+지폐 개수의 합을 최소로 만드는 문제다.</p>
<h4 id="그리디greedy-알고리즘">그리디(Greedy) 알고리즘</h4>
<p>&quot;큰 단위부터 최대한 많이 쓰고, 나머지를 다음 단위로 넘긴다&quot;는 전략이다. 이 문제처럼 화폐 단위가 배수 관계일 때 그리디가 최적해를 보장한다.</p>
<pre><code class="language-python">def solution(money):
    coin = [10, 50, 100, 500, 1000, 5000, 10000, 50000]
    counter = 0
    idx = len(coin) - 1
    while money:
        counter += money // coin[idx]  # 해당 화폐로 몇 개 거슬러줄 수 있는지
        money %= coin[idx]             # 나머지를 다음 화폐로 넘김
        idx -= 1
    return counter</code></pre>
<p>처음에 <code>counter += 1</code>로 작성했다가, 50000원짜리를 0개 써도 카운터가 올라가는 문제를 발견했다. <code>money // coin[idx]</code>로 해당 화폐를 <strong>최대 몇 개</strong> 쓸 수 있는지 계산해야 한다.</p>
<h3 id="2-규칙에-맞는-배열-구하기--디버깅-문제-8">2. 규칙에 맞는 배열 구하기 — 디버깅 (문제 8)</h3>
<p>배열의 앞/뒤에서 번갈아 가져와 새 배열을 만드는 문제다.</p>
<pre><code class="language-python"># 버그 코드
if left % 2 == 0:    # left는 앞에서 가져올 때만 증가

# 수정
if idx % 2 == 0:     # idx는 매 반복마다 증가 → 번갈아 동작</code></pre>
<p><code>left</code>는 앞에서 가져올 때만 증가하므로, 한 번 홀수가 되면 계속 뒤에서만 가져오게 된다. 매 반복마다 1씩 증가하는 <code>idx</code>를 기준으로 짝수/홀수를 판단해야 번갈아 동작한다.</p>
<h3 id="3-비밀번호-검사--디버깅-문제-9">3. 비밀번호 검사 — 디버깅 (문제 9)</h3>
<p>비밀번호에 연속 3자리 이상의 알파벳이나 숫자(abc, 987 등)가 있으면 안전하지 않다고 판단하는 문제다.</p>
<pre><code class="language-python"># 버그 코드
first_check = ord(password[i + 1]) - ord(password[i])
second_check = ord(password[i]) - ord(password[i+1])   # first_check의 부호 반대일 뿐

# 수정
second_check = ord(password[i+2]) - ord(password[i+1])  # 다음 쌍의 차이</code></pre>
<p>3연속을 판단하려면 <strong>연속된 두 쌍</strong>의 차이를 비교해야 한다:</p>
<ul>
<li>i→i+1 차이와 i+1→i+2 차이가 같고, 그 값이 ±1이면 3연속이다.</li>
</ul>
<h3 id="4-언제까지-오르막길이야--함수-작성-문제-5">4. 언제까지 오르막길이야 — 함수 작성 (문제 5)</h3>
<p>배열에서 숫자가 연속해서 증가하는 가장 긴 구간의 길이를 구하는 문제다. 같은 값은 증가로 치지 않는다.</p>
<pre><code class="language-python">def solution(arr):
    answer = 0
    tmp_answer = 1
    std_num = arr[0]

    for i in range(1, len(arr)):
        if arr[i] &gt; std_num:
            tmp_answer += 1
        else:
            tmp_answer = 1
        answer = max(answer, tmp_answer)  # 매 반복마다 갱신!
        std_num = arr[i]

    return answer</code></pre>
<p>수업 중 질문: 처음에 <code>answer = max(answer, tmp_answer)</code>를 <code>else</code> 블록 안에만 넣었더니, 배열 끝까지 증가하는 경우 answer가 갱신되지 않는 문제가 있었다. → <code>for</code> 루프 안, <code>if/else</code> 바깥으로 옮겨서 <strong>매 반복마다</strong> 최댓값을 갱신하도록 수정했다.</p>
<h3 id="5-로봇을-움직여주세요--함수-작성-문제-6">5. 로봇을 움직여주세요 — 함수 작성 (문제 6)</h3>
<p>명령 문자열(L, R, U, D)에 따라 로봇을 이동시킨 최종 위치를 반환하는 문제다.</p>
<pre><code class="language-python">from collections import Counter

def solution(commands):
    x, y = 0, 0
    dir = Counter(commands)
    for key, value in dir.items():
        if key == &quot;L&quot;:
            x -= value
        if key == &quot;R&quot;:
            x += value
        if key == &quot;U&quot;:
            y += value
        if key == &quot;D&quot;:
            y -= value
    return [x, y]</code></pre>
<p>Counter로 각 방향의 횟수를 한 번에 세서 처리하는 방식으로 풀었다. 이 문제는 이동 순서와 상관없이 최종 위치만 구하면 되기 때문에 Counter 방식이 유효하다. 만약 이동 경로를 추적해야 했다면 한 글자씩 순회해야 한다.</p>
<hr>
<h3 id="학습-일정-점검">학습 일정 점검</h3>
<p>구름EDU 기출 6세트의 활용 계획을 정리했다:</p>
<table>
<thead>
<tr>
<th>회차</th>
<th>용도</th>
<th>시기</th>
</tr>
</thead>
<tbody><tr>
<td>1~2회차</td>
<td>✅ 학습용 완료</td>
<td>완료</td>
</tr>
<tr>
<td>3~4회차</td>
<td>학습용 (개념 설명 + 풀이)</td>
<td>4월 중</td>
</tr>
<tr>
<td>5회차</td>
<td>90분 타이머 모의고사</td>
<td>시험 2주 전 (5월 초)</td>
</tr>
<tr>
<td>6회차</td>
<td>90분 타이머 모의고사</td>
<td>시험 1주 전 (5월 둘째 주)</td>
</tr>
</tbody></table>
<p>3~4회차 사이에는 프로그래머스, 백준 등 외부 플랫폼으로 추가 연습을 병행할 예정이다.</p>
<hr>
<h2 id="오늘의-결과">오늘의 결과</h2>
<ul>
<li>기출 2회차 5~9번 완료 → <strong>2회차 전체 완료</strong> (빈칸 1 + 디버깅 2 + 함수 작성 2)</li>
<li>힌트 1회 (오르막길 문제: 루프 종료 후 answer 갱신 누락)</li>
<li>다음 학습: 구름EDU 기출 3회차 시작</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[20260402 오늘의 학습: COS Pro 기출 2회차 풀이 (1~4, 10번)]]></title>
            <link>https://velog.io/@lee_yesol421/20260402-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-COS-Pro-%EA%B8%B0%EC%B6%9C-2%ED%9A%8C%EC%B0%A8-%ED%92%80%EC%9D%B4-14-10%EB%B2%88</link>
            <guid>https://velog.io/@lee_yesol421/20260402-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-COS-Pro-%EA%B8%B0%EC%B6%9C-2%ED%9A%8C%EC%B0%A8-%ED%92%80%EC%9D%B4-14-10%EB%B2%88</guid>
            <pubDate>Thu, 02 Apr 2026 14:05:40 GMT</pubDate>
            <description><![CDATA[<h2 id="지난-학습-요약">지난 학습 요약</h2>
<ul>
<li>구름EDU 기출 1회차 6~10번 풀이 완료 (빈칸 1 + 디버깅 3 + 함수 작성 1)</li>
<li><code>&amp;</code> vs <code>and</code> 혼동, 범위 체크 불일치 등 실수 포인트 정리</li>
<li><code>ord()</code>, <code>max(0, x)</code> 패턴 새로 학습</li>
</ul>
<h2 id="오늘-수업-계획">오늘 수업 계획</h2>
<ul>
<li>구름EDU 기출 2회차 시작</li>
<li>새로운 개념(클래스 상속, itertools.combinations)이 등장하면 설명 먼저 → 응용문제 풀기</li>
</ul>
<hr>
<h2 id="학습-내용-정리">학습 내용 정리</h2>
<h3 id="1-클래스-상속과-추상-클래스-문제-1-도서-대여점-운영">1. 클래스 상속과 추상 클래스 (문제 1: 도서 대여점 운영)</h3>
<p>기출 1번 문제에서 <strong>추상 클래스와 상속</strong> 개념이 처음 등장했다. Java의 <code>interface</code>/<code>abstract class</code>와 같은 개념이다.</p>
<h4 id="추상-클래스-만들기">추상 클래스 만들기</h4>
<pre><code class="language-python">from abc import ABC, abstractmethod

class Book(ABC):               # ABC를 상속 = 추상 클래스
    @abstractmethod            # 자식이 반드시 구현해야 하는 메서드
    def get_rental_price(self, day):
        pass</code></pre>
<ul>
<li><code>ABC</code> = Abstract Base Class</li>
<li><code>@abstractmethod</code> = Java의 <code>abstract</code> 키워드와 동일</li>
<li>추상 클래스는 직접 객체 생성 불가 (<code>Book()</code>하면 에러)</li>
</ul>
<p>문제 코드에서는 <code>metaclass=ABCMeta</code> 방식이 사용되었는데, <code>ABC</code>를 상속하는 것과 동일한 의미이다.</p>
<pre><code class="language-python"># 방법 1: metaclass 직접 지정 (기출 코드 방식)
class Book(metaclass=ABCMeta):
    ...

# 방법 2: ABC 상속 (더 간단, 실무에서 주로 사용)
class Book(ABC):
    ...</code></pre>
<h4 id="상속-문법">상속 문법</h4>
<pre><code class="language-python"># Java:   class ComicBook extends Book
# Python: class ComicBook(Book)
class ComicBook(Book):
    def get_rental_price(self, day):   # 추상 메서드 구현
        cost = 500
        day -= 2
        if day &gt; 0:
            cost += day * 200
        return cost</code></pre>
<h4 id="응용문제로-연습-주차장-요금-계산">응용문제로 연습 (주차장 요금 계산)</h4>
<p>같은 구조로 <code>Vehicle</code> 추상 클래스 → <code>SmallCar</code>, <code>LargeCar</code> 상속 문제를 풀었다. 기출 구조를 그대로 응용해서 직접 클래스를 작성하고 <code>solution</code> 함수까지 완성했다.</p>
<p><strong>주의</strong>: <code>type</code>은 Python 내장 함수명이라 변수명으로 쓰면 내장 <code>type()</code>을 덮어쓴다. <code>car_type</code> 같은 이름이 안전하다. (이전에 <code>sum</code>을 변수명으로 쓴 것과 같은 패턴!)</p>
<h3 id="2-시간-→-분-변환-문제-2-지하철-기다리기">2. 시간 → 분 변환 (문제 2: 지하철 기다리기)</h3>
<p>빈칸 채우기 문제. <code>&quot;HH:MM&quot;</code> 문자열을 분 단위 정수로 변환하는 <code>func_a</code>가 핵심이었다.</p>
<pre><code class="language-python">def func_a(times):
    hour = int(times[:2])
    minute = int(times[3:])
    return hour * 60 + minute</code></pre>
<p>도착 시간이 순서대로 정렬되어 있으므로, 현재 시간 이후인 첫 번째 지하철을 찾으면 바로 <code>break</code>하는 패턴이다.</p>
<h3 id="3-이름-없는-함수-역할-파악-문제-3-경품-당첨자를-구해주세요">3. 이름 없는 함수 역할 파악 (문제 3: 경품 당첨자를 구해주세요)</h3>
<p><code>func_a</code>, <code>func_b</code>, <code>func_c</code>처럼 이름만으로 역할을 알 수 없는 함수가 나오는 빈칸 채우기 문제.</p>
<p><strong>역할 파악 팁: 간단한 값을 넣어서 결과를 머릿속으로 돌려보기</strong></p>
<table>
<thead>
<tr>
<th>함수</th>
<th>동작</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><code>func_a(n)</code></td>
<td>10^n (10을 n번 곱함)</td>
<td>func_a(3) → 1000</td>
</tr>
<tr>
<td><code>func_b(n)</code></td>
<td>자릿수 구하기 (<code>n //= 10</code> + 카운트)</td>
<td>func_b(235) → 3</td>
</tr>
<tr>
<td><code>func_c(n)</code></td>
<td>각 자릿수 합 (<code>n % 10</code> + 누적합)</td>
<td>func_c(235) → 10</td>
</tr>
</tbody></table>
<p><strong>자주 나오는 숫자 처리 패턴:</strong></p>
<table>
<thead>
<tr>
<th>패턴</th>
<th>동작</th>
</tr>
</thead>
<tbody><tr>
<td><code>n //= 10</code> + 카운트</td>
<td>자릿수 구하기</td>
</tr>
<tr>
<td><code>n % 10</code> + 누적합</td>
<td>각 자릿수 합</td>
</tr>
<tr>
<td><code>10을 반복 곱하기</code></td>
<td>10의 거듭제곱</td>
</tr>
<tr>
<td><code>n % 10</code> + 역순 조립</td>
<td>숫자 뒤집기</td>
</tr>
</tbody></table>
<p>수업 중 질문: &quot;func_a, func_b, func_c 역할 파악하는데 시간이 오래 걸렸어&quot;
→ 코드를 한 줄씩 읽기 전에 <strong>간단한 값을 넣어서 입출력을 먼저 확인</strong>하면 빠르게 파악할 수 있다. 이런 패턴은 반복 출제되므로 몇 번 더 보면 코드 읽기 전에 바로 알아볼 수 있게 된다.</p>
<p><strong>팁</strong>: <code>length / 2</code>는 Python 3에서 <code>float</code>가 된다 (예: <code>6 / 2 → 3.0</code>). 정수 나눗셈 <code>//</code>를 쓰는 습관을 들이자.</p>
<h3 id="4-itertoolscombinations-문제-4-합이-k배가-되는-수">4. itertools.combinations (문제 4: 합이 K배가 되는 수)</h3>
<p>함수 작성 문제에서 <code>itertools.combinations</code>가 처음 등장했다. 배열에서 n개를 뽑는 모든 조합을 구하는 도구이다.</p>
<pre><code class="language-python">from itertools import combinations

arr = [1, 2, 3, 4, 5]
for combo in combinations(arr, 3):   # 3개씩 뽑기
    print(combo)
# (1, 2, 3), (1, 2, 4), (1, 2, 5), ...</code></pre>
<p>Java로 비유하면 for문 3중첩으로 조합을 구하는 걸 한 줄로 해주는 것이다.</p>
<pre><code class="language-java">// Java: 3중 for문
for (int i = 0; i &lt; n; i++)
    for (int j = i+1; j &lt; n; j++)
        for (int k = j+1; k &lt; n; k++)</code></pre>
<pre><code class="language-python"># Python: 한 줄
for combo in combinations(arr, 3)</code></pre>
<p><strong>실수 포인트</strong>: <code>combinations(arr, K)</code>로 썼다가 교정했다. K=3이라 우연히 맞았지만, <strong>K는 배수 조건(변수)이고 뽑는 개수는 3(상수)</strong>이다. 문제에서 변수와 상수를 정확히 구분하자!</p>
<pre><code class="language-python">from itertools import combinations

def solution(arr, K):
    answer = 0
    for combo in combinations(arr, 3):    # 3개 뽑기 (상수!)
        if sum(combo) % K == 0:           # K의 배수 (변수!)
            answer += 1
    return answer</code></pre>
<h3 id="5-디버깅-else에서-조건부-처리-문제-10-0들을-0으로-만들기">5. 디버깅: else에서 조건부 처리 (문제 10: 0들을 0으로 만들기)</h3>
<p>연속된 0을 하나의 0으로 압축하는 디버깅 문제. 한 줄만 수정해야 한다.</p>
<pre><code class="language-python"># 버그 코드
if s[i] == &#39;0&#39; and s[i + 1] != &#39;0&#39;:
    answer += &#39;0&#39;       # 연속 0의 마지막 → 0 추가 (정상)
else:
    answer += &#39;1&#39;       # 버그! 무조건 &#39;1&#39; 추가</code></pre>
<p>else에 들어오는 경우가 세 가지 섞여있었다:</p>
<ul>
<li><code>s[i] == &#39;1&#39;</code> → <code>&#39;1&#39;</code> 추가해야 함</li>
<li><code>s[i] == &#39;0&#39;</code> (연속 0) → 아무것도 안 해야 함</li>
<li><code>s[i] == &#39;#&#39;</code> (끝 표시) → 아무것도 안 해야 함</li>
</ul>
<p><strong>수정</strong>: 11행을 <code>if s[i] == &#39;1&#39;: answer += &#39;1&#39;</code>로 변경</p>
<pre><code class="language-python">else:
    if s[i] == &#39;1&#39;: answer += &#39;1&#39;   # &#39;1&#39;일 때만 추가, 나머지는 자동 패스</code></pre>
<p>Python은 한 줄에 <code>if 조건: 동작</code>을 쓸 수 있어서 한 줄 수정으로 해결 가능하다.</p>
<p>수업 중 질문: &quot;else: if 대신 그냥 if만 쓰거나, elif를 써도 똑같지 않아?&quot;
→ 안 된다! 11행은 <code>else:</code> 블록 안에 있는 코드라서, <code>elif</code>로 바꾸려면 <code>else:</code>(10행)도 지워야 해서 <strong>두 줄 수정</strong>이 된다. 독립 <code>if</code>도 마찬가지.</p>
<p><strong>디버깅 팁</strong>: else에서 무조건 고정값을 넣는 코드가 보이면 의심하자!</p>
<h3 id="6---공백-실수-주의">6. <code>-=</code> 공백 실수 주의</h3>
<p>응용문제에서 발견한 실수:</p>
<pre><code class="language-python">hours -= 1   # hours = hours - 1  (3 → 2) ✅
hours = -1   # hours에 -1 대입     (3 → -1) ❌</code></pre>
<p><code>+=</code>는 헷갈릴 일이 없지만, <code>-=</code>는 공백 하나로 의미가 완전히 바뀐다. 시험에서는 반드시 <strong>붙여서</strong> 쓰자!</p>
<hr>
<h2 id="오늘의-결과">오늘의 결과</h2>
<ul>
<li>기출 2회차 5문제 + 응용 1문제 풀이, 1차 정답률 83% (5/6)</li>
<li>클래스 상속, itertools.combinations 새로 학습</li>
<li>다음 시간: 기출 2회차 5~9번 마무리 → 함수작성 응용문제</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[20260401 오늘의 학습: COS Pro 기출 1회차 풀이 (6~10번)]]></title>
            <link>https://velog.io/@lee_yesol421/20260401-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-COS-Pro-%EA%B8%B0%EC%B6%9C-1%ED%9A%8C%EC%B0%A8-%ED%92%80%EC%9D%B4-610%EB%B2%88</link>
            <guid>https://velog.io/@lee_yesol421/20260401-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-COS-Pro-%EA%B8%B0%EC%B6%9C-1%ED%9A%8C%EC%B0%A8-%ED%92%80%EC%9D%B4-610%EB%B2%88</guid>
            <pubDate>Wed, 01 Apr 2026 15:33:23 GMT</pubDate>
            <description><![CDATA[<h2 id="지난-학습-요약">지난 학습 요약</h2>
<ul>
<li>구름EDU 기출 1회차 1~5번 풀이 완료 (빈칸 3 + 함수 작성 2)</li>
<li>클래스 상속 문법, 인덱스 vs 값 혼동, 그리드 시뮬레이션 초기값 누락 등 실수 포인트 정리</li>
<li>Phase 2 본격 시작</li>
</ul>
<h2 id="오늘-수업-계획">오늘 수업 계획</h2>
<p>구름EDU COS Pro 1급 기출 1회차 6~10번 문제 풀이 리뷰.
빈칸 채우기 1문제 + 디버깅 2문제 + 함수 작성 2문제 구성.</p>
<hr>
<h2 id="학습-내용-정리">학습 내용 정리</h2>
<h3 id="문제-7--병합-and-정렬-빈칸-채우기">문제 7 — 병합 and 정렬 (빈칸 채우기)</h3>
<p>오름차순으로 정렬된 두 배열을 하나의 정렬된 배열로 합치는 문제. 병합 정렬(merge sort)의 merge 단계와 같은 로직.</p>
<p><strong>빈칸:</strong></p>
<pre><code class="language-python">while arrA_len &gt; arrA_idx and arrB_len &gt; arrB_idx:  # 빈칸 1: 두 배열 모두 남아있을 때
    ...비교하며 작은 값 append...

while arrA_len &gt; arrA_idx:   # 빈칸 2: arrA에 남은 원소 처리
    ...

while arrB_len &gt; arrB_idx:   # 빈칸 3: arrB에 남은 원소 처리
    ...</code></pre>
<p><strong>수업 중 질문: <code>&amp;</code>랑 <code>and</code>는 다른 거야?</strong></p>
<p>→ Python에서 <code>&amp;</code>는 <strong>비트 연산자</strong>, <code>and</code>는 <strong>논리 연산자</strong>다.</p>
<table>
<thead>
<tr>
<th></th>
<th><code>&amp;</code> (비트)</th>
<th><code>and</code> (논리)</th>
</tr>
</thead>
<tbody><tr>
<td>용도</td>
<td>비트 단위 AND</td>
<td>조건 연결</td>
</tr>
<tr>
<td>예시</td>
<td><code>5 &amp; 3</code> → <code>1</code></td>
<td><code>True and False</code> → <code>False</code></td>
</tr>
<tr>
<td>주의</td>
<td>연산자 우선순위가 비교 연산보다 높음</td>
<td>조건문에서는 항상 <code>and</code> 사용</td>
</tr>
</tbody></table>
<p>COS Pro 시험에서 조건 연결할 때는 반드시 <code>and</code>를 쓸 것!</p>
<hr>
<h3 id="문제-8--누가-당선-되나요-디버깅">문제 8 — 누가 당선 되나요 (디버깅)</h3>
<p>투표 결과에서 가장 많은 표를 받은 후보 번호를 반환하는 문제. 공동 1위면 번호를 오름차순으로 모두 반환.</p>
<p><strong>버그 위치: 14번 줄</strong></p>
<pre><code class="language-python"># 수정 전
answer.append(vote_counter[idx])  # 득표수를 넣고 있음

# 수정 후
answer.append(idx)                # 후보 번호를 넣어야 함</code></pre>
<p><strong>핵심:</strong> 문제가 요구하는 반환값이 무엇인지 정확히 파악하기. <code>vote_counter[idx]</code>는 idx번 후보의 <strong>득표수</strong>이고, 문제가 원하는 건 <strong>후보 번호</strong> <code>idx</code> 자체다.</p>
<hr>
<h3 id="문제-9--계단-게임-디버깅">문제 9 — 계단 게임 (디버깅)</h3>
<p>두 학생이 가위바위보를 하면서 계단을 오르내리는 게임. A가 이기면 +3칸, 지면 -1칸, 비기면 제자리. 단, 0칸 아래로는 내려갈 수 없음.</p>
<p><strong><code>func</code> 함수 동작:</strong></p>
<pre><code class="language-python">def func(record):   # 이기는 패를 반환
    0(가위) → 1(바위)
    1(바위) → 2(보)
    2(보)   → 0(가위)</code></pre>
<p><strong>버그 위치: 19번 줄</strong></p>
<pre><code class="language-python"># 수정 전
cnt = cnt - 1              # 0 아래로 내려갈 수 있음

# 수정 후
cnt = max(0, cnt - 1)      # 0 아래로 못 내려가게 보호</code></pre>
<p><strong>기억 포인트:</strong> <code>max(0, 값)</code>은 &quot;0 아래로 내려가지 않게&quot; 할 때 자주 쓰는 패턴. 시험에서 디버깅은 한 줄 교체이므로, if문을 추가하는 것보다 <code>max()</code> 한 줄로 처리하는 게 안전하다.</p>
<hr>
<h3 id="문제-10--주식으로-최대-수익을-내세요-디버깅">문제 10 — 주식으로 최대 수익을 내세요 (디버깅)</h3>
<p>n일 동안의 주가에서 한 번 사고 한 번 팔았을 때 최대 수익을 구하는 문제. 산 날에 바로 못 팔고, 최소 하루 뒤에 팔아야 한다. 수익이 음수일 수도 있다.</p>
<p><strong>버그 위치: 10번 줄</strong></p>
<pre><code class="language-python"># 수정 전
answer = max(answer, tmp - price)    # 매수가 - 판매가 (반대!)

# 수정 후
answer = max(answer, price - tmp)    # 판매가 - 매수가 (정답!)</code></pre>
<p><strong>코드 구조 이해:</strong></p>
<pre><code class="language-python">for price in prices:
    if tmp != INF:                      # ① 먼저 팔아보기 (매수한 적 있을 때만)
        answer = max(answer, price - tmp)
    tmp = min(tmp, price)               # ② 그 다음 사볼지 결정</code></pre>
<p><strong>수업 중 질문: <code>if tmp != INF:</code> 이 조건은 왜 있는 거야?</strong></p>
<p>→ 이 조건은 <strong>&quot;산 날에 바로 못 팔고, 최소 하루 뒤에 팔아야 한다&quot;</strong> 규칙을 지키기 위한 것이다.</p>
<ul>
<li>첫 번째 날: <code>tmp=INF</code> → if 건너뜀 → tmp만 세팅 (산 것)</li>
<li>두 번째 날부터: tmp에 이전 최소가가 있으므로 → 수익 계산 가능 (팔기)</li>
</ul>
<p>&quot;<strong>먼저 팔아보고(answer 갱신) → 그 다음 살지 결정(tmp 갱신)</strong>&quot; 이 순서가 하루 뒤 매도 규칙을 보장한다. 순서를 바꾸면 같은 날 사고파는 셈이 되어 <code>[3, 1]</code> 같은 케이스에서 수익이 0이 나오는 버그가 생긴다.</p>
<hr>
<h3 id="문제-6--체스의-나이트-함수-작성">문제 6 — 체스의 나이트 (함수 작성)</h3>
<p>체스판 위 나이트의 위치가 주어지면, 한 번에 이동할 수 있는 칸의 개수를 반환하는 문제.</p>
<p><strong>작성한 코드:</strong></p>
<pre><code class="language-python">def solution(pos):
    answer = 0
    r = &quot;ABCDEFGH&quot;.find(pos[0])   # A=0, B=1, ... H=7
    c = int(pos[1]) - 1            # 1~8 → 0~7로 통일

    moves = [(-2, -1), (-2, 1), (-1, -2), (-1, 2),
             (1, -2), (1, 2), (2, -1), (2, 1)]

    for move in moves:
        nr, nc = r + move[0], c + move[1]
        if nr &gt;= 0 and nr &lt;= 7 and nc &gt;= 0 and nc &lt;= 7:
            answer += 1

    return answer</code></pre>
<p><strong>풀면서 실수한 포인트:</strong></p>
<table>
<thead>
<tr>
<th>실수</th>
<th>원인</th>
<th>수정</th>
</tr>
</thead>
<tbody><tr>
<td>범위 체크 <code>0~8</code></td>
<td>r(0<del>7)과 c(1</del>8)의 범위가 달랐음</td>
<td>c를 <code>-1</code>해서 둘 다 0~7로 통일</td>
</tr>
</tbody></table>
<p><strong>위치 파싱 두 가지 방법:</strong></p>
<pre><code class="language-python"># 방법 1: find()
r = &quot;ABCDEFGH&quot;.find(pos[0])

# 방법 2: ord()
r = ord(pos[0]) - ord(&#39;A&#39;)</code></pre>
<p><code>ord()</code>는 문자를 아스키 코드(숫자)로 변환하는 함수. <code>chr()</code>는 반대. Java의 <code>(char) - &#39;A&#39;</code> 패턴과 동일하다.</p>
<hr>
<h2 id="오늘의-결과">오늘의 결과</h2>
<ul>
<li>기출 1회차 6~10번 완료 (빈칸 1 + 디버깅 3 + 함수 작성 1), <strong>기출 1회차 전체 10문제 완료</strong></li>
<li>주요 포인트: <code>and</code> vs <code>&amp;</code> 차이, <code>max(0, x)</code> 패턴, 주식 문제의 매수/매도 순서 보장 로직</li>
<li>다음 시간: 구름EDU 기출 2회차 시작</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[20260330 오늘의 학습: COS Pro 기출 1회차 풀이 (1~5번)]]></title>
            <link>https://velog.io/@lee_yesol421/20260330-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-COS-Pro-%EA%B8%B0%EC%B6%9C-1%ED%9A%8C%EC%B0%A8-%ED%92%80%EC%9D%B4-15%EB%B2%88</link>
            <guid>https://velog.io/@lee_yesol421/20260330-%EC%98%A4%EB%8A%98%EC%9D%98-%ED%95%99%EC%8A%B5-COS-Pro-%EA%B8%B0%EC%B6%9C-1%ED%9A%8C%EC%B0%A8-%ED%92%80%EC%9D%B4-15%EB%B2%88</guid>
            <pubDate>Mon, 30 Mar 2026 13:06:41 GMT</pubDate>
            <description><![CDATA[<h2 id="어제-학습-요약">어제 학습 요약</h2>
<ul>
<li>Phase 1 종합 복습 완료 — 딕셔너리+컴프리헨션, set 교집합, 다중 정렬 key 등 마무리</li>
<li>Phase 1 전 범위 종료, Phase 2 전환 준비 완료</li>
<li>총 8회 세션, 67문제 풀이 (1차 정답률 84%)</li>
</ul>
<h2 id="오늘-수업-계획">오늘 수업 계획</h2>
<p>Phase 2 첫 번째 세션. 구름EDU COS Pro 1급 기출 1회차 1~5번 문제를 직접 풀고 리뷰.
빈칸 채우기 3문제 + 함수 작성 2문제 구성.</p>
<hr>
<h2 id="학습-내용-정리">학습 내용 정리</h2>
<h3 id="문제-1--음식전문점-운영-빈칸-채우기">문제 1 — 음식전문점 운영 (빈칸 채우기)</h3>
<p>클래스 상속 구조를 읽고 빈칸을 채우는 문제.</p>
<p><strong>빈칸:</strong></p>
<pre><code class="language-python">class PizzaStore(DeliveryStore):       # 1. 상속
    def set_order_list(self, order_list):  # 2. 추상 메서드 구현
    def get_total_price(self):             # 3. 추상 메서드 구현</code></pre>
<p><strong>Python 클래스 상속 문법 정리:</strong></p>
<table>
<thead>
<tr>
<th></th>
<th>Java</th>
<th>Python</th>
</tr>
</thead>
<tbody><tr>
<td>상속</td>
<td><code>class Child extends Parent</code></td>
<td><code>class Child(Parent):</code></td>
</tr>
<tr>
<td>인터페이스 구현</td>
<td><code>class A implements B</code></td>
<td><code>class A(B):</code> (동일 문법)</td>
</tr>
<tr>
<td>추상 메서드</td>
<td><code>@Override</code></td>
<td>같은 이름으로 그냥 정의 (시그니처 맞춰야 함)</td>
</tr>
<tr>
<td>추상 클래스 선언</td>
<td><code>abstract class</code></td>
<td><code>metaclass=ABCMeta</code> + <code>@abstractmethod</code></td>
</tr>
</tbody></table>
<p><strong>핵심:</strong> Python에서 추상 메서드를 구현할 때는 <code>self</code>를 포함한 시그니처를 부모와 동일하게 맞춰야 한다.</p>
<p>이 문제에서 함수명은 바로 알았지만 상속 문법은 인터넷 검색 후 해결. → 별도 정리 필요 영역으로 기록.</p>
<hr>
<h3 id="문제-2--해밍-거리-구하기-빈칸-채우기">문제 2 — 해밍 거리 구하기 (빈칸 채우기)</h3>
<p>두 이진수 문자열의 해밍 거리(같은 위치에서 다른 문자 수)를 구하는 문제.
길이가 다르면 짧은 쪽 앞에 0을 채워 맞춤.</p>
<p><strong>빈칸:</strong></p>
<pre><code class="language-python">padSize = length - len(string)    # 채워야 할 0의 개수
if binaryA[i] != binaryB[i]:     # 같은 위치에서 다르면 해밍 거리 +1</code></pre>
<p>막힘 없이 해결.</p>
<hr>
<h3 id="문제-3--계산기-by-문자열-빈칸-채우기">문제 3 — 계산기 by 문자열 (빈칸 채우기)</h3>
<p><code>&quot;123+12&quot;</code> 같은 문자열 수식을 받아 계산 결과를 return하는 문제.
<code>func_a</code> (계산), <code>func_b</code> (연산자 위치 탐색), <code>func_c</code> (숫자 분리) 세 함수가 주어지고, <code>solution</code>의 빈칸을 채우는 구조.</p>
<p><strong>빈칸:</strong></p>
<pre><code class="language-python">exp_index = func_b(expression)                          # 연산자 위치(인덱스)
first_num, second_num = func_c(expression, exp_index)   # 두 숫자 분리
result = func_a(first_num, second_num, expression[exp_index])  # 계산</code></pre>
<p><strong>수업 중 질문: 마지막 인자를 <code>exp_index</code>로 넣었더니 <code>None</code>이 출력됐어</strong></p>
<p>→ <code>exp_index</code>는 연산자의 위치를 나타내는 <strong>숫자(int)</strong> 이고, <code>func_a</code>가 필요로 하는 건 <code>&#39;+&#39;</code>, <code>&#39;-&#39;</code>, <code>&#39;*&#39;</code> 같은 <strong>문자(str)</strong> 다.
<code>expression[exp_index]</code>로 인덱스 위치의 값을 꺼내야 한다.</p>
<pre><code class="language-python">exp_index = 3           # 숫자 — 위치
expression[exp_index]   # 문자 — 그 위치의 값 &#39;+&#39;</code></pre>
<p><strong>기억 포인트:</strong> 인덱스를 구했으면 값을 꺼낼 때 <code>[인덱스]</code>를 한 번 더 써줘야 한다.</p>
<hr>
<h3 id="문제-4--타임머신-함수-작성">문제 4 — 타임머신 (함수 작성)</h3>
<p>0이 없는 수 체계(1, 2, ..., 9, 11, 12, ...)에서 주어진 수의 다음 수를 return하는 문제.</p>
<p><strong>작성한 코드:</strong></p>
<pre><code class="language-python">def solution(num):
    tmp_num = str(num + 1)
    answer = int(tmp_num.replace(&quot;0&quot;, &quot;1&quot;))
    return answer</code></pre>
<p><strong>왜 이게 맞는가?</strong></p>
<p><code>num</code>은 0이 없는 수이므로, <code>num + 1</code>을 하면 올림(carry)이 9를 타고 올라가면서 <strong>0은 반드시 뒤에서부터(suffix)만</strong> 생긴다.</p>
<pre><code>예: 1899999 + 1 = 1900000
    └─ 뒤의 9들이 연쇄 올림 → suffix에만 0 생성</code></pre><p>suffix의 0을 전부 1로 바꾸면 → 0 없는 수 중 가장 작은 다음 수가 된다.
<code>replace(&quot;0&quot;, &quot;1&quot;)</code>이 항상 정답인 이유가 바로 이 입력 조건 덕분이다.</p>
<hr>
<h3 id="문제-5--소용돌이-수-함수-작성">문제 5 — 소용돌이 수 (함수 작성)</h3>
<p>n×n 격자에 1~n²을 소용돌이(나선) 순서로 채운 뒤, <strong>주 대각선(좌상→우하)</strong> 값의 합을 return하는 문제.</p>
<p><strong>작성한 코드 (핵심 구조):</strong></p>
<pre><code class="language-python">def solution(n):
    if n == 1: return 1  # 엣지케이스

    grid = [[0]*n for _ in range(n)]
    dr = [0, 1, 0, -1]   # 우 하 좌 상
    dc = [1, 0, -1, 0]
    r, c, d = 0, 0, 0

    answer = 0
    for num in range(1, n*n+1):
        grid[r][c] = num
        if r == c:
            answer += num

        nr, nc = r + dr[d], c + dc[d]
        if nr &lt; 0 or nr &gt;= n or nc &lt; 0 or nc &gt;= n:
            d = (d+1) % 4
            nr, nc = r + dr[d], c + dc[d]
        if grid[nr][nc] != 0:
            d = (d+1) % 4
            nr, nc = r + dr[d], c + dc[d]
        r, c = nr, nc

    return answer</code></pre>
<p><strong>풀면서 실수한 포인트:</strong></p>
<table>
<thead>
<tr>
<th>실수</th>
<th>원인</th>
<th>수정</th>
</tr>
</thead>
<tbody><tr>
<td><code>d = 0</code> 누락</td>
<td>방향 초기값 빠트림</td>
<td>그리드+4방향+시작점+<strong>현재방향</strong> 세트로 기억</td>
</tr>
<tr>
<td><code>range(n*n)</code></td>
<td>0부터 시작해 합계 틀림</td>
<td><code>range(1, n*n+1)</code></td>
</tr>
<tr>
<td><code>(d+1) % 4</code> 헷갈림</td>
<td>방향 순환 공식</td>
<td>0→1→2→3→0 반복, 항상 <code>% 4</code></td>
</tr>
</tbody></table>
<p><strong>대각선 조건:</strong> <code>r == c</code> 이면 주 대각선. 단순하고 정확한 판별식.</p>
<p><strong>그리드 시뮬레이션 세팅 체크리스트:</strong></p>
<ol>
<li>그리드 생성</li>
<li>4방향 정의 (dr, dc)</li>
<li>시작점 (r, c)</li>
<li><strong>현재 방향 (d = 0)</strong>  ← 자주 빠트림!</li>
</ol>
<hr>
<h2 id="오늘의-결과">오늘의 결과</h2>
<ul>
<li>기출 1회차 1~5번 완료 (빈칸 3 + 함수 작성 2), 전부 최종 정답</li>
<li>1차 실수: 문제 3 인덱스vs값 혼동, 문제 5 초기값 누락</li>
<li>다음 시간: 기출 1회차 6~10번 + 클래스 상속/오버라이드 보충 정리</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>