<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>쉽게 쉽게</title>
    <link>https://minsu092274.tistory.com/</link>
    <description>개발자로 경험하면서 겪은 일과 지식을 쉽게 쉽게 써보려고 한다.</description>
    <language>ko</language>
    <pubDate>Mon, 10 Aug 2026 06:22:56 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>곱마2</managingEditor>
    <item>
      <title>[프로그래머스 Lv.3] 이중우선순위큐(Java) - TreeMap과 두 개의 힙</title>
      <link>https://minsu092274.tistory.com/225</link>
      <description>&lt;div class=&quot;summary-box&quot;&gt;
&lt;div class=&quot;summary-label&quot;&gt;  핵심 요약&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;힙 하나로는 안 된다&lt;/b&gt; &amp;mdash; &lt;code&gt;PriorityQueue&lt;/code&gt;는 루트 한쪽 끝만 정렬을 보장한다. 최소 힙에서 최댓값의 위치는 알 수 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;풀이 1: TreeMap&lt;/b&gt; &amp;mdash; 값을 키, 개수를 밸류로 저장한다. &lt;code&gt;firstKey&lt;/code&gt;와 &lt;code&gt;lastKey&lt;/code&gt;로 양쪽 끝을 각각 &lt;code&gt;O(log n)&lt;/code&gt;에 얻는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;풀이 2: 두 개의 힙&lt;/b&gt; &amp;mdash; 최소 힙과 최대 힙에 같은 값을 넣고, 한쪽에서 꺼낸 값을 다른 쪽에서 &lt;code&gt;remove&lt;/code&gt;로 지운다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;차이는 삭제 비용&lt;/b&gt; &amp;mdash; &lt;code&gt;PriorityQueue.remove(Object)&lt;/code&gt;는 선형 탐색이라 &lt;code&gt;O(n)&lt;/code&gt;이다. TreeMap 쪽이 최악의 경우에 안전하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;nav class=&quot;my-toc&quot;&gt;
&lt;div class=&quot;my-toc-header&quot;&gt;
&lt;div class=&quot;my-toc-header-bar&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;my-toc-header-text&quot;&gt;목차&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;my-toc-body&quot;&gt;&lt;!-- h2/h3 태그를 읽어 자동 생성됩니다 --&gt;&lt;/div&gt;
&lt;/nav&gt;
&lt;h2 id=&quot;section1&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;1&lt;/span&gt; 문제&lt;/h2&gt;
&lt;div class=&quot;problem-card&quot;&gt;
&lt;div class=&quot;problem-card-header&quot;&gt;
&lt;div&gt;
&lt;div class=&quot;prob-num&quot;&gt;프로그래머스 #42628&lt;/div&gt;
&lt;div class=&quot;prob-title&quot;&gt;이중우선순위큐&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;prob-meta&quot;&gt;
&lt;div&gt;operations.length &amp;le; 1,000,000&lt;/div&gt;
&lt;div&gt;난이도: &lt;span class=&quot;diff-gold&quot;&gt;Lv.3&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;problem-card-body&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최댓값과 최솟값을 모두 꺼낼 수 있는 큐를 구현하는 문제다. 명령은 &lt;code&gt;&quot;명령어 데이터&quot;&lt;/code&gt; 형식의 문자열로 주어진다.&lt;/p&gt;
&lt;div class=&quot;prob-sub&quot;&gt;명령 종류&lt;/div&gt;
&lt;ol class=&quot;step-list&quot; style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;code&gt;I 숫자&lt;/code&gt; &amp;mdash; 해당 숫자를 큐에 삽입한다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;D 1&lt;/code&gt; &amp;mdash; 큐에서 최댓값을 삭제한다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;D -1&lt;/code&gt; &amp;mdash; 큐에서 최솟값을 삭제한다&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;prob-sub&quot;&gt;놓치면 안 되는 조건&lt;/div&gt;
&lt;ol class=&quot;step-list&quot; style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;최댓값이나 최솟값이 둘 이상이면 &lt;b&gt;하나만&lt;/b&gt; 삭제한다&lt;/li&gt;
&lt;li&gt;빈 큐에 삭제 명령이 오면 그 연산은 무시한다&lt;/li&gt;
&lt;li&gt;모든 연산 후 큐가 비어 있으면 &lt;code&gt;[0, 0]&lt;/code&gt;, 아니면 &lt;code&gt;[최댓값, 최솟값]&lt;/code&gt;을 반환한다&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section2&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;2&lt;/span&gt; 힙 하나로는 왜 안 되는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &lt;code&gt;PriorityQueue&lt;/code&gt; 하나로 시작했다. &lt;code&gt;D -1&lt;/code&gt;은 &lt;code&gt;poll()&lt;/code&gt; 한 번이면 끝나니 절반은 해결된 셈이었다. 그런데 &lt;code&gt;D 1&lt;/code&gt;에서 막혔다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;cdot cdot-r&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-y&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-g&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;Java &amp;mdash; 처음 작성한 뼈대&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body angelscript&quot;&gt;&lt;code&gt;if (num == 1) {
    // 최댓값을 어떻게 꺼내지?
} else {
    pq.poll();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힙은 &lt;b&gt;부모가 자식보다 작다&lt;/b&gt;는 조건만 유지하는 구조다. 형제끼리는 아무 순서도 없고, 서로 다른 가지에 있는 노드 사이에도 순서가 없다. 최소 힙 &lt;code&gt;[1, 3, 2, 9, 5, 8, 4]&lt;/code&gt;를 트리로 그려 보면 이렇다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;깊이&lt;/th&gt;
&lt;th&gt;노드&lt;/th&gt;
&lt;th&gt;보장되는 것&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;0 (루트)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;전체 최솟값&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;3, 2&lt;/td&gt;
&lt;td&gt;각자 부모인 1보다 크다는 것만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;2 (리프)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;9, 5, 8, 4&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-red&quot;&gt;아무 순서도 없음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최댓값 9는 리프 어딘가에 있다는 것만 알 수 있다. 정확한 위치를 알아내려면 결국 배열 전체를 훑어야 하고, &lt;code&gt;D 1&lt;/code&gt;이 나올 때마다 이 작업을 반복하면 최악의 경우 연산량이 &lt;code&gt;operations.length&lt;/code&gt;의 제곱에 비례한다. 제한이 100만이므로 감당할 수 없다.&lt;/p&gt;
&lt;div class=&quot;info-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt;ℹ️&lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;필요한 조건&lt;/div&gt;
&lt;b&gt;양쪽 끝을 모두 로그 시간에 다룰 수 있는 구조&lt;/b&gt;가 필요하다. 여기서 방향이 두 갈래로 갈린다. 하나는 애초에 양쪽 끝을 아는 구조를 쓰는 것(TreeMap), 다른 하나는 한쪽 끝만 아는 구조를 두 개 두는 것(두 개의 힙)이다.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section3&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;3&lt;/span&gt; 풀이 1 &amp;mdash; TreeMap&lt;/h2&gt;
&lt;h3 id=&quot;section3-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;구조 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;TreeMap&lt;/code&gt;은 키를 정렬된 상태로 유지하는 맵이다. &lt;code&gt;firstKey()&lt;/code&gt;는 가장 작은 키를, &lt;code&gt;lastKey()&lt;/code&gt;는 가장 큰 키를 각각 &lt;code&gt;O(log n)&lt;/code&gt;에 반환한다. 하나의 구조가 양쪽 끝을 모두 안다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 맵은 같은 키를 두 번 담을 수 없다. &lt;code&gt;I 5&lt;/code&gt;가 두 번 오면 원소가 두 개여야 하는데 키는 하나뿐이다. 그래서 &lt;b&gt;값을 키로, 그 값이 몇 개 들어 있는지를 밸류로&lt;/b&gt; 저장한다. 삭제할 때는 키를 지우는 것이 아니라 개수를 줄이고, 개수가 0이 되는 순간에만 키를 제거한다.&lt;/p&gt;
&lt;h3 id=&quot;section3-2&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;동작 추적&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;[&quot;I 7&quot;, &quot;I 5&quot;, &quot;I 5&quot;, &quot;D 1&quot;, &quot;D -1&quot;]&lt;/code&gt;로 상태 변화를 따라갔다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령&lt;/th&gt;
&lt;th&gt;하는 일&lt;/th&gt;
&lt;th&gt;맵 상태&lt;/th&gt;
&lt;th&gt;원소 수&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;시작&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;I 7&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;7의 개수 +1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{7:1}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;I 5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;5의 개수 +1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{5:1, 7:1}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;I 5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;5의 개수 +1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{5:2, 7:1}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;D 1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;lastKey=7의 개수 -1 &amp;rarr; 0이므로 키 제거&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{5:2}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;D -1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;firstKey=5의 개수 -1 &amp;rarr; 1이 남아 키 유지&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{5:1}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막에 &lt;code&gt;[lastKey, firstKey]&lt;/code&gt;는 &lt;code&gt;[5, 5]&lt;/code&gt;가 된다. 원소가 하나뿐일 때 최댓값과 최솟값이 같아지는 경우가 따로 처리하지 않아도 맞아떨어진다.&lt;/p&gt;
&lt;h3 id=&quot;section3-3&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;코드&lt;/h3&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;cdot cdot-r&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-y&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-g&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;Java &amp;mdash; 풀이 1 (TreeMap)&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body arduino&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {
    public int[] solution(String[] operations) {
        // 키: 값 자체 / 밸류: 그 값이 큐에 들어 있는 개수
        TreeMap&amp;lt;Integer, Integer&amp;gt; counts = new TreeMap&amp;lt;&amp;gt;();

        for (String operation : operations) {
            String[] parts = operation.split(&quot; &quot;);
            String order = parts[0];
            int num = Integer.parseInt(parts[1]);

            if (order.equals(&quot;I&quot;)) {
                // 처음 등장하는 값이면 기본값 0에서 시작
                counts.put(num, counts.getOrDefault(num, 0) + 1);
            } else {
                // 빈 큐에 대한 삭제 명령은 무시
                if (counts.isEmpty()) {
                    continue;
                }
                int target = (num == 1) ? counts.lastKey() : counts.firstKey();
                int count = counts.get(target);

                if (count &amp;gt; 1) {
                    counts.put(target, count - 1);
                } else {
                    counts.remove(target);
                }
            }
        }

        if (counts.isEmpty()) {
            return new int[]{0, 0};
        }
        return new int[]{counts.lastKey(), counts.firstKey()};
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class=&quot;tip-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt; &lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;getOrDefault를 한쪽에만 쓴 이유&lt;/div&gt;
&lt;code&gt;I&lt;/code&gt; 분기의 &lt;code&gt;num&lt;/code&gt;은 맵에 없을 수 있으므로 기본값 0이 제 역할을 한다. 반면 &lt;code&gt;D&lt;/code&gt; 분기의 &lt;code&gt;target&lt;/code&gt;은 방금 &lt;code&gt;lastKey()&lt;/code&gt;나 &lt;code&gt;firstKey()&lt;/code&gt;로 꺼낸 값이라 &lt;b&gt;반드시 존재한다.&lt;/b&gt; 여기에도 &lt;code&gt;getOrDefault&lt;/code&gt;를 쓰면 읽는 사람이 &quot;키가 없을 수도 있나?&quot; 하고 멈추게 된다. 같은 메서드라도 필요한 자리와 아닌 자리를 구분하면 코드가 스스로 사실을 설명한다.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section4&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;4&lt;/span&gt; 풀이 2 &amp;mdash; 최소 힙 + 최대 힙&lt;/h2&gt;
&lt;h3 id=&quot;section4-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;구조 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;힙 하나가 한쪽 끝만 안다면, 힙을 두 개 두면 양쪽 끝을 다 알 수 있다. 삽입할 때 &lt;b&gt;같은 값을 두 힙에 모두&lt;/b&gt; 넣는다. 그러면 두 힙은 항상 동일한 원소 집합을 갖고, 보는 방향만 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 삭제다. &lt;code&gt;maxHeap.poll()&lt;/code&gt;로 최댓값을 꺼내면 &lt;code&gt;minHeap&lt;/code&gt;에는 그 값이 그대로 남는다. 두 힙의 내용이 어긋나므로 다른 쪽에서도 같은 값을 지워 줘야 한다. 이때 &lt;code&gt;remove(Object)&lt;/code&gt;를 쓴다.&lt;/p&gt;
&lt;h3 id=&quot;section4-2&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;동작 추적&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 입력 &lt;code&gt;[&quot;I 7&quot;, &quot;I 5&quot;, &quot;I 5&quot;, &quot;D 1&quot;, &quot;D -1&quot;]&lt;/code&gt;로 두 힙의 상태를 따라갔다. 힙 내부 배열 순서는 구현에 따라 달라지므로 여기서는 담긴 값의 집합으로 표기했다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;명령&lt;/th&gt;
&lt;th&gt;하는 일&lt;/th&gt;
&lt;th&gt;minHeap&lt;/th&gt;
&lt;th&gt;maxHeap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;I 7&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;양쪽에 7 삽입&lt;/td&gt;
&lt;td&gt;{7}&lt;/td&gt;
&lt;td&gt;{7}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;I 5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;양쪽에 5 삽입&lt;/td&gt;
&lt;td&gt;{5, 7}&lt;/td&gt;
&lt;td&gt;{5, 7}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;I 5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;양쪽에 5 삽입&lt;/td&gt;
&lt;td&gt;{5, 5, 7}&lt;/td&gt;
&lt;td&gt;{5, 5, 7}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;D 1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;maxHeap에서 7을 poll &amp;rarr; minHeap에서 7을 remove&lt;/td&gt;
&lt;td&gt;{5, 5}&lt;/td&gt;
&lt;td&gt;{5, 5}&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;D -1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;minHeap에서 5를 poll &amp;rarr; maxHeap에서 5를 remove&lt;/td&gt;
&lt;td&gt;{5}&lt;/td&gt;
&lt;td&gt;{5}&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 &lt;code&gt;D -1&lt;/code&gt;에서 중복 값 5가 두 개 있었지만 결과는 정확하다. &lt;code&gt;remove(Object)&lt;/code&gt;는 &lt;b&gt;같은 값이 여러 개여도 하나만 제거하기&lt;/b&gt; 때문이다. 문제 조건인 &quot;최댓값이 둘 이상이면 하나만 삭제한다&quot;와 정확히 맞물린다.&lt;/p&gt;
&lt;h3 id=&quot;section4-3&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;코드&lt;/h3&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;cdot cdot-r&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-y&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-g&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;Java &amp;mdash; 풀이 2 (두 개의 힙)&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body arduino&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {
    public int[] solution(String[] operations) {
        PriorityQueue&amp;lt;Integer&amp;gt; minHeap = new PriorityQueue&amp;lt;&amp;gt;();
        PriorityQueue&amp;lt;Integer&amp;gt; maxHeap = new PriorityQueue&amp;lt;&amp;gt;(Collections.reverseOrder());

        for (String op : operations) {
            String[] tokens = op.split(&quot; &quot;);
            String command = tokens[0];
            int value = Integer.parseInt(tokens[1]);

            if (command.equals(&quot;I&quot;)) {
                // 두 힙이 같은 원소 집합을 갖도록 양쪽에 삽입
                minHeap.add(value);
                maxHeap.add(value);
            } else if (command.equals(&quot;D&quot;)) {
                // 두 힙의 크기는 항상 같으므로 한쪽만 검사하면 된다
                if (minHeap.isEmpty()) continue;

                if (value == 1) {
                    int max = maxHeap.poll();
                    minHeap.remove(max);
                } else {
                    int min = minHeap.poll();
                    maxHeap.remove(min);
                }
            }
        }

        if (minHeap.isEmpty()) {
            return new int[]{0, 0};
        }
        return new int[]{maxHeap.peek(), minHeap.peek()};
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class=&quot;warn-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt;⚠️&lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;remove(Object)와 remove(int)의 구분&lt;/div&gt;
&lt;code&gt;minHeap.remove(max)&lt;/code&gt;에서 &lt;code&gt;max&lt;/code&gt;는 &lt;code&gt;int&lt;/code&gt;지만, &lt;code&gt;PriorityQueue&lt;/code&gt;에는 인덱스로 지우는 &lt;code&gt;remove(int)&lt;/code&gt;가 없다. 따라서 &lt;code&gt;Collection.remove(Object)&lt;/code&gt;가 호출되고 &lt;code&gt;max&lt;/code&gt;는 &lt;code&gt;Integer&lt;/code&gt;로 오토박싱된 뒤 &lt;code&gt;equals&lt;/code&gt;로 비교된다. 값 기준 삭제이므로 의도대로 동작한다.&lt;br /&gt;다만 같은 코드를 &lt;code&gt;ArrayList&lt;/code&gt;에 쓰면 &lt;b&gt;인덱스 삭제인 &lt;code&gt;remove(int)&lt;/code&gt;가 선택되어&lt;/b&gt; 전혀 다른 원소가 지워진다. 자료구조를 바꿀 때 이 줄은 반드시 다시 봐야 한다.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section5&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;5&lt;/span&gt; 두 풀이 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 풀이 모두 정답을 낸다. 차이는 &lt;b&gt;삭제 한 번의 비용&lt;/b&gt;에서 갈린다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;풀이 1 (TreeMap)&lt;/th&gt;
&lt;th&gt;풀이 2 (두 개의 힙)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;삽입&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;O(log n)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;O(log n)&lt;/code&gt; &amp;times; 2회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;최댓값 조회&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;lastKey()&lt;/code&gt; &amp;mdash; &lt;code&gt;O(log n)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;peek()&lt;/code&gt; &amp;mdash; &lt;code&gt;O(1)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;삭제&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;O(log n)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-red&quot;&gt;poll은 O(log n), remove는 O(n)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;최악 전체&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;O(n log n)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-red&quot;&gt;O(n&amp;sup2;)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;중복 값 처리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;개수 밸류로 관리&lt;/td&gt;
&lt;td&gt;원소를 그대로 여러 개 보관&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;동기화 필요&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;없음 (구조 하나)&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-amber&quot;&gt;있음 (두 힙을 맞춰야 함)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;메모리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;서로 다른 값의 개수만큼&lt;/td&gt;
&lt;td&gt;전체 원소 &amp;times; 2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3 id=&quot;section5-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;remove가 O(n)인 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번 섹션에서 본 것과 같은 이유다. 힙에서 특정 값이 어디 있는지는 루트가 아닌 이상 알 수 없다. &lt;code&gt;PriorityQueue.remove(Object)&lt;/code&gt;는 내부 배열을 앞에서부터 선형으로 훑어 일치하는 원소를 찾은 뒤, 그 자리를 마지막 원소로 채우고 힙 조건을 복구한다. 찾는 과정이 &lt;code&gt;O(n)&lt;/code&gt;이라 전체가 &lt;code&gt;O(n)&lt;/code&gt;이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;operations.length&lt;/code&gt;가 100만이고 그중 절반이 &lt;code&gt;I&lt;/code&gt;, 절반이 &lt;code&gt;D&lt;/code&gt;라면 힙에 최대 50만 개가 쌓인 상태에서 50만 번의 선형 탐색이 일어난다. 이론상 &lt;code&gt;10&amp;sup1;&amp;sup1;&lt;/code&gt; 수준이다.&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;danger-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt; &lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;통과 여부와 별개로&lt;/div&gt;
두 힙 풀이는 발상이 직관적이라 떠올리기 쉽지만, 최악의 경우를 계산해 보면 제한을 감당하지 못하는 구조다. 제출이 통과하더라도 &quot;왜 통과했는가&quot;가 채점 데이터에 달려 있다면 근거로 삼기 어렵다. &lt;b&gt;제한사항을 먼저 계산하고 자료구조를 고르는 순서&lt;/b&gt;를 지키면 이 판단이 코드를 쓰기 전에 나온다.&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&quot;section5-2&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;두 힙을 살리려면&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 힙 구조를 유지하면서 &lt;code&gt;remove&lt;/code&gt;를 없애는 방법도 있다. &lt;b&gt;지연 삭제(lazy deletion)&lt;/b&gt;다. 삭제 시 다른 쪽 힙은 건드리지 않고 &quot;이 원소는 무효&quot;라는 표시만 남긴 뒤, 나중에 &lt;code&gt;poll&lt;/code&gt;할 때 무효 표시된 원소를 걷어내며 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이때는 값이 아니라 &lt;b&gt;삽입 순서 인덱스&lt;/b&gt;를 함께 담아야 한다. 같은 값이 여러 개일 때 어느 것이 무효인지 구분해야 하기 때문이다. 힙에 넣을 원소가 &lt;code&gt;int&lt;/code&gt;에서 &lt;code&gt;(값, 인덱스)&lt;/code&gt; 쌍으로 바뀌고, 별도의 &lt;code&gt;boolean[]&lt;/code&gt; 배열을 동기화해야 한다. 성능은 &lt;code&gt;O(n log n)&lt;/code&gt;으로 개선되지만 관리할 상태가 셋으로 늘어난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TreeMap은 이 동기화 문제 자체가 생기지 않는다. 구조가 하나라 어긋날 대상이 없다. &lt;b&gt;자료구조가 실수 가능성을 줄여 주는 쪽&lt;/b&gt;을 고른다는 기준으로 보면 풀이 1이 낫다고 판단했다.&lt;/p&gt;
&lt;h2 id=&quot;section6&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;6&lt;/span&gt; 실수했던 부분&lt;/h2&gt;
&lt;h3 id=&quot;section6-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;힙 하나로 최댓값을 다루려 했다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인.&lt;/b&gt; &lt;code&gt;PriorityQueue&lt;/code&gt;를 &quot;정렬된 큐&quot;로 생각했다. 실제로는 루트만 정렬을 보장하는 부분 순서 구조이고, 나머지는 부모-자식 관계만 유지된다. 반대쪽 끝을 상수 시간에 볼 방법이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결.&lt;/b&gt; &lt;code&gt;D 1&lt;/code&gt; 분기를 비워 둔 채로 넘어가지 않고, 그 자리에서 &quot;이 구조가 최댓값의 위치를 아는가&quot;를 먼저 확인했다. 답이 아니라는 것이 확인되자 자료구조 선택으로 문제가 옮겨 갔다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 99.8837%; height: 22px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style11&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 100%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;잘못된 내용&lt;/span&gt;이 있다면&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;지적&lt;/span&gt;부탁드립니다. 방문해주셔서 감사합니다.&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;face&quot; data-emoticon-name=&quot;055&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/055.png&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/055.png&quot; width=&quot;80&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>알고리즘 &amp;amp; 코딩테스트/자료구조(스택 &amp;bull; 큐 &amp;bull; 해시)</category>
      <category>java</category>
      <category>priorityqueue</category>
      <category>TreeMap</category>
      <category>우선순위큐</category>
      <category>이중우선순위큐</category>
      <category>자바</category>
      <category>최대힙</category>
      <category>최소힙</category>
      <category>프로그래머스</category>
      <author>곱마2</author>
      <guid isPermaLink="true">https://minsu092274.tistory.com/225</guid>
      <comments>https://minsu092274.tistory.com/225#entry225comment</comments>
      <pubDate>Thu, 6 Aug 2026 14:48:58 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스 Lv.3] 야근 지수(Java) - 우선순위 큐</title>
      <link>https://minsu092274.tistory.com/224</link>
      <description>&lt;div class=&quot;summary-box&quot;&gt;
&lt;div class=&quot;summary-label&quot;&gt;  핵심 요약&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;야근 지수는 제곱 합&lt;/b&gt; &amp;mdash; 작업량 &lt;code&gt;x&lt;/code&gt;를 1시간 처리하면 지수는 &lt;code&gt;2x - 1&lt;/code&gt;만큼 줄어든다. 이 값은 &lt;code&gt;x&lt;/code&gt;가 클수록 크다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;매 시간 가장 큰 일감을 1 깎는다&lt;/b&gt; &amp;mdash; 최대 힙(&lt;code&gt;PriorityQueue&lt;/code&gt; + &lt;code&gt;reverseOrder&lt;/code&gt;)으로 최댓값을 꺼내 1 줄이고 다시 넣는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;제곱은 &lt;code&gt;(long) x * x&lt;/code&gt;&lt;/b&gt; &amp;mdash; &lt;code&gt;Math.pow&lt;/code&gt;는 오차 보장이 1 ulp라 캐스팅 과정에서 1이 사라질 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;nav class=&quot;my-toc&quot;&gt;
&lt;div class=&quot;my-toc-header&quot;&gt;
&lt;div class=&quot;my-toc-header-bar&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;my-toc-header-text&quot;&gt;목차&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;my-toc-body&quot;&gt;&lt;!-- h2/h3 태그를 읽어 자동 생성됩니다 --&gt;&lt;/div&gt;
&lt;/nav&gt;
&lt;h2 id=&quot;section1&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;1&lt;/span&gt; 문제&lt;/h2&gt;
&lt;div class=&quot;problem-card&quot;&gt;
&lt;div class=&quot;problem-card-header&quot;&gt;
&lt;div&gt;
&lt;div class=&quot;prob-num&quot;&gt;프로그래머스 #12927&lt;/div&gt;
&lt;div class=&quot;prob-title&quot;&gt;야근 지수&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;prob-meta&quot;&gt;
&lt;div&gt;n &amp;le; 1,000,000 &amp;middot; works.length &amp;le; 20,000&lt;/div&gt;
&lt;div&gt;난이도: &lt;span class=&quot;diff-gold&quot;&gt;Lv.3&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;problem-card-body&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;퇴근까지 남은 시간이 &lt;code&gt;n&lt;/code&gt;시간이고, 각 일감의 작업량이 배열 &lt;code&gt;works&lt;/code&gt;에 담겨 있다. 1시간에 아무 일감이나 하나를 골라 작업량을 1 줄일 수 있다. 퇴근 시점에 남아 있는 각 작업량을 제곱해 모두 더한 값이 야근 지수이며, 이 값의 최솟값을 구하는 문제다.&lt;/p&gt;
&lt;div class=&quot;prob-sub&quot;&gt;확인해야 할 것&lt;/div&gt;
&lt;ol class=&quot;step-list&quot; style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;매 시간 어떤 일감을 고르는 것이 이득인지 판단할 기준&lt;/li&gt;
&lt;li&gt;모든 작업량이 0이 되었을 때의 처리&lt;/li&gt;
&lt;li&gt;제곱 합의 최댓값이 int 범위를 넘는지 여부&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section2&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;2&lt;/span&gt; 어떤 일감을 골라야 하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1시간을 쓸 때마다 일감 하나를 골라야 한다. 선택의 기준을 세우려면 &lt;b&gt;어떤 일감을 처리했을 때 야근 지수가 가장 많이 줄어드는지&lt;/b&gt;를 알아야 한다. 그래서 감소량을 식으로 구했다.&lt;/p&gt;
&lt;h3 id=&quot;section2-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;변수 정의&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;x&lt;/code&gt; : 지금 그 일감에 남아 있는 작업량&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x&amp;sup2;&lt;/code&gt; : 그 일감이 야근 지수에서 차지하는 몫&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x - 1&lt;/code&gt; : 1시간 처리한 뒤 남는 작업량, 그때의 몫은 &lt;code&gt;(x - 1)&amp;sup2;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구하려는 값은 &lt;b&gt;처리 전 몫에서 처리 후 몫을 뺀 값&lt;/b&gt;, 즉 &lt;code&gt;x&amp;sup2; - (x-1)&amp;sup2;&lt;/code&gt;이다. 이 식을 한국어로 옮기면 &quot;이 일감 하나에 1시간을 썼을 때 전체 지수에서 깎여 나가는 양&quot;이 된다.&lt;/p&gt;
&lt;h3 id=&quot;section2-2&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;단계별 전개&lt;/h3&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;하는 일&lt;/th&gt;
&lt;th&gt;결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(x-1)&amp;sup2;&lt;/code&gt;을 펼친다&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x&amp;sup2; - 2x + 1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;원래 식에 대입한다&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x&amp;sup2; - (x&amp;sup2; - 2x + 1)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;괄호를 풀어 부호를 뒤집는다&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x&amp;sup2; - x&amp;sup2; + 2x - 1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x&amp;sup2;&lt;/code&gt;끼리 상쇄한다&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2x - 1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3 id=&quot;section2-3&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;왜 2가 곱해지고, 왜 1에는 곱해지지 않는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;x&amp;sup2;&lt;/code&gt;을 한 변이 &lt;code&gt;x&lt;/code&gt;인 정사각형으로 놓고 보면 각 항의 출처가 드러난다. 한 변을 1 줄일 때 떨어져 나가는 칸은 두 종류다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;떨어져 나가는 부분&lt;/th&gt;
&lt;th&gt;칸 수&lt;/th&gt;
&lt;th&gt;대응하는 항&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;가로 한 줄&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x&lt;/code&gt;&lt;/td&gt;
&lt;td rowspan=&quot;2&quot;&gt;&lt;code&gt;+2x&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;세로 한 줄&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;x&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;두 줄이 겹치는 모서리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;1 (중복 계산)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가로와 세로 두 줄이 각각 &lt;code&gt;x&lt;/code&gt;칸이므로 &lt;code&gt;x&lt;/code&gt;에 2가 곱해진다. 반면 두 줄이 만나는 모서리는 &lt;code&gt;x&lt;/code&gt;가 아무리 커져도 항상 딱 1칸이다. 두 번 세어졌으니 한 번 되돌려 주는 것이고, 이 칸 수는 &lt;code&gt;x&lt;/code&gt;와 무관하므로 상수 &lt;code&gt;-1&lt;/code&gt;로 남는다. &lt;code&gt;2x&lt;/code&gt;는 변의 길이에 비례해 늘어나는 항이고 &lt;code&gt;-1&lt;/code&gt;은 그렇지 않은 항이라는 점이 두 항의 차이다.&lt;/p&gt;
&lt;div class=&quot;tip-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt; &lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;선택 기준&lt;/div&gt;
감소량 &lt;code&gt;2x - 1&lt;/code&gt;은 &lt;code&gt;x&lt;/code&gt;가 커질수록 커진다. 따라서 매 시간 &lt;b&gt;지금 남아 있는 값 중 가장 큰 일감&lt;/b&gt;을 1 깎는 것이 항상 최선이다. 이 판단은 매 시간 독립적으로 성립하므로 그리디하게 적용할 수 있다.&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&quot;section2-4&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;동작 추적&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;works = [4, 3, 3]&lt;/code&gt;, &lt;code&gt;n = 4&lt;/code&gt;로 매 시간의 상태를 따라갔다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;시간&lt;/th&gt;
&lt;th&gt;꺼낸 값 x&lt;/th&gt;
&lt;th&gt;감소량 2x-1&lt;/th&gt;
&lt;th&gt;배열 상태&lt;/th&gt;
&lt;th&gt;지수&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;시작&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;td&gt;[4, 3, 3]&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;[3, 3, 3]&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;[2, 3, 3]&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;[2, 2, 3]&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;[2, 2, 2]&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section3&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;3&lt;/span&gt; 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;가장 큰 값을 꺼내고, 줄인 뒤 다시 넣는다&quot;는 동작이 &lt;code&gt;n&lt;/code&gt;번 반복된다. 매번 정렬하면 &lt;code&gt;n&lt;/code&gt;번의 정렬이 필요하지만, 최대 힙을 쓰면 꺼내기와 넣기가 각각 &lt;code&gt;O(log m)&lt;/code&gt;으로 끝난다. &lt;code&gt;PriorityQueue&lt;/code&gt;는 기본이 최소 힙이므로 &lt;code&gt;Collections.reverseOrder()&lt;/code&gt;를 생성자에 넘겨 최대 힙으로 만들었다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;cdot cdot-r&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-y&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-g&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;Java &amp;mdash; Solution.java&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body arduino&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {
    public long solution(int n, int[] works) {
        // 최대 힙: 매 시간 가장 큰 작업량을 꺼내기 위함
        PriorityQueue&amp;lt;Integer&amp;gt; pq = new PriorityQueue&amp;lt;&amp;gt;(Collections.reverseOrder());
        for (int work : works) {
            pq.offer(work);
        }

        for (int hour = 0; hour &amp;lt; n; hour++) {
            int max = pq.poll();
            // 최댓값이 0이면 나머지도 전부 0이므로 지수도 0
            if (max == 0) {
                return 0;
            }
            pq.offer(max - 1);
        }

        long answer = 0;
        for (int remain : pq) {
            answer += (long) remain * remain;
        }
        return answer;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 id=&quot;section3-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;두 가지 판단 근거&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;조기 반환 조건.&lt;/b&gt; 최대 힙에서 꺼낸 값이 0이라는 것은 남은 전부가 0 이하라는 뜻이고, 작업량은 음수가 될 수 없으므로 전부 정확히 0이다. 이때 야근 지수는 0이며 시간이 더 남아 있어도 변하지 않으므로 즉시 반환한다. 별도로 총합을 미리 구해 &lt;code&gt;sum &amp;le; n&lt;/code&gt;을 검사할 필요가 없어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;마지막 합산의 순회 순서.&lt;/b&gt; &lt;code&gt;for (int remain : pq)&lt;/code&gt;는 힙의 내부 배열을 순서대로 훑기 때문에 값이 정렬되어 나오지 않는다. 다만 여기서는 전부 더하기만 하므로 순서가 결과에 영향을 주지 않는다. 정렬된 순서가 필요한 상황이었다면 이 순회는 쓸 수 없다.&lt;/p&gt;
&lt;h2 id=&quot;section4&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;4&lt;/span&gt; 실수했던 부분&lt;/h2&gt;
&lt;h3 id=&quot;section4-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;남은 총량을 균등 분배하려 했다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 전체 작업량 합에서 &lt;code&gt;n&lt;/code&gt;을 빼고, 그 나머지를 일감 개수로 나눠 고르게 분배하려 했다. 제곱 합은 값이 고를수록 작아지기 때문에 방향 자체는 맞다고 판단했다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;cdot cdot-r&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-y&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-g&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;Java &amp;mdash; 처음 작성한 접근&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body maxima&quot;&gt;&lt;code&gt;sum -= n;
long mod = sum / works.length;
long remainder = sum % works.length;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인.&lt;/b&gt; 이 계산은 작업량을 자유롭게 재배치할 수 있다고 가정한다. 그러나 문제에서 허용하는 연산은 &lt;b&gt;줄이는 것뿐&lt;/b&gt;이며 늘릴 수는 없다. &lt;code&gt;works = [1, 1, 10]&lt;/code&gt;, &lt;code&gt;n = 4&lt;/code&gt;로 확인했다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;결과 배열&lt;/th&gt;
&lt;th&gt;야근 지수&lt;/th&gt;
&lt;th&gt;도달 가능 여부&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;균등 분배 계산&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;[3, 3, 2]&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-red&quot;&gt;불가능&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;10만 4번 깎기&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;[1, 1, 6]&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;가능 (정답)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;10을 3번, 1을 1번&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;[0, 1, 7]&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-blue&quot;&gt;가능&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;1을 2번, 10을 2번&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;[0, 0, 8]&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-blue&quot;&gt;가능&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;균등 분배가 내놓은 &lt;code&gt;[3, 3, 2]&lt;/code&gt;는 원래 1이던 일감을 3으로 올려야 나오는 상태다. 실제 최솟값 38과 22는 차이가 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결.&lt;/b&gt; 전체를 한 번에 계산하려는 시도를 버리고, 매 시간의 선택 기준을 세우는 쪽으로 방향을 바꿨다. 균등 분배는 모든 일감이 목표 수준보다 위에 있을 때만 성립하는 특수한 경우였다. 작업량 편차가 작으면 우연히 정답과 일치하기 때문에 일부 테스트 케이스만으로는 잘못을 알아채기 어렵다.&lt;/p&gt;
&lt;h3 id=&quot;section4-2&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;제곱에 Math.pow를 썼다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합산 부분을 처음에는 &lt;code&gt;answer += (long) Math.pow(remain, 2);&lt;/code&gt;로 작성했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인.&lt;/b&gt; &lt;code&gt;Math.pow&lt;/code&gt;는 &lt;code&gt;double&lt;/code&gt;을 받아 &lt;code&gt;double&lt;/code&gt;을 반환하며, 자바 명세는 결과가 &lt;b&gt;정확한 값이 아니라 1 ulp 이내&lt;/b&gt;라고만 보장한다. 정수 제곱이라고 예외가 아니다. &lt;code&gt;remain&lt;/code&gt;이 최댓값 50,000일 때를 따라갔다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;수학적 정답&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;2,500,000,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;이 구간 double의 ulp&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;약 4.8 &amp;times; 10&lt;sup&gt;-7&lt;/sup&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;명세가 허용하는 반환 범위&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;2,499,999,999.9999995 ~ 2,500,000,000.0000005&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;(long) 캐스팅 결과&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;2,499,999,999 또는 2,500,000,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래쪽으로 오차가 생기면 캐스팅이 소수부를 버리면서 1이 통째로 사라진다. 예외도 나지 않고 조용히 틀린 값이 누적된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결.&lt;/b&gt; &lt;code&gt;(long) remain * remain&lt;/code&gt;으로 바꿨다. 정수 곱셈에는 오차 개념 자체가 없다. 캐스팅 위치가 중요하다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;표현&lt;/th&gt;
&lt;th&gt;계산 순서&lt;/th&gt;
&lt;th&gt;결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(long) remain * remain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;long &amp;times; int &amp;rarr; long으로 승격 후 곱셈&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;안전&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(long) (remain * remain)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;int &amp;times; int 먼저 &amp;rarr; 오버플로 후 캐스팅&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-red&quot;&gt;음수&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;warn-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt;⚠️&lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;캐스팅은 곱셈보다 먼저 적용된다&lt;/div&gt;
&lt;code&gt;(long)&lt;/code&gt;은 단항 연산자라 곱셈보다 우선순위가 높다. 따라서 &lt;code&gt;(long) remain * remain&lt;/code&gt;에서 캐스팅은 앞의 &lt;code&gt;remain&lt;/code&gt; 하나에만 붙고, 그 뒤 곱셈에서 나머지 피연산자가 long으로 끌려 올라간다. 반대로 괄호로 곱셈 전체를 감싸면 int끼리 곱해 넘친 뒤에 캐스팅되므로 이미 늦다. 50,000&lt;sup&gt;2&lt;/sup&gt; = 2.5 &amp;times; 10&lt;sup&gt;9&lt;/sup&gt;은 int 최댓값 2,147,483,647을 넘는다.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section5&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;5&lt;/span&gt; 규모 판단&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제약은 &lt;code&gt;n &amp;le; 1,000,000&lt;/code&gt;, &lt;code&gt;works.length &amp;le; 20,000&lt;/code&gt;, &lt;code&gt;works[i] &amp;le; 50,000&lt;/code&gt;이다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;계산&lt;/th&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;힙 높이&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;log&lt;sub&gt;2&lt;/sub&gt;(20,000)&lt;/td&gt;
&lt;td&gt;약 14.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;루프 1회 비용&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;poll + offer&lt;/td&gt;
&lt;td&gt;약 29회 비교&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;전체 연산량&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;10&lt;sup&gt;6&lt;/sup&gt; &amp;times; 29&lt;/td&gt;
&lt;td&gt;약 3 &amp;times; 10&lt;sup&gt;7&lt;/sup&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;제곱 합 최댓값&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;20,000 &amp;times; 50,000&lt;sup&gt;2&lt;/sup&gt;&lt;/td&gt;
&lt;td&gt;5 &amp;times; 10&lt;sup&gt;13&lt;/sup&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연산량은 자바에서 통과 가능한 범위다. 제곱 합 최댓값이 int를 한참 넘으므로 반환형은 &lt;code&gt;long&lt;/code&gt;이어야 한다.&lt;/p&gt;
&lt;div class=&quot;info-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt;ℹ️&lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;오토박싱 비용&lt;/div&gt;
&lt;code&gt;PriorityQueue&amp;lt;Integer&amp;gt;&lt;/code&gt;를 쓰므로 &lt;code&gt;offer&lt;/code&gt;할 때마다 박싱이 일어난다. &lt;code&gt;Integer&lt;/code&gt; 캐시는 -128~127 범위만 재사용하므로 그보다 큰 값은 매번 새 객체가 만들어진다. 최대 10&lt;sup&gt;6&lt;/sup&gt;개의 객체가 생성되고 버려지지만 수명이 짧아 신세대 GC 선에서 정리되므로 통과에 지장은 없다.&lt;/div&gt;
&lt;/div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 99.8837%; height: 22px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style11&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 100%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;잘못된 내용&lt;/span&gt;이 있다면&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;지적&lt;/span&gt;부탁드립니다. 방문해주셔서 감사합니다.&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;face&quot; data-emoticon-name=&quot;055&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/055.png&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/055.png&quot; width=&quot;80&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>알고리즘 &amp;amp; 코딩테스트/자료구조(스택 &amp;bull; 큐 &amp;bull; 해시)</category>
      <category>java</category>
      <category>priorityqueue</category>
      <category>야근지수</category>
      <category>우선순위큐</category>
      <category>자바</category>
      <category>코딩테스트</category>
      <category>프로그래머스</category>
      <author>곱마2</author>
      <guid isPermaLink="true">https://minsu092274.tistory.com/224</guid>
      <comments>https://minsu092274.tistory.com/224#entry224comment</comments>
      <pubDate>Thu, 6 Aug 2026 12:28:13 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스 Lv.3] 정수 삼각형(Java) - DP 두 방향 비교</title>
      <link>https://minsu092274.tistory.com/223</link>
      <description>&lt;div&gt;
&lt;style&gt;@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&amp;family=JetBrains+Mono:wght@400;600&amp;display=swap');/* ── 공통 ── */.blog-post{font-family:'Noto Sans KR',sans-serif;color:#334155;line-height:1.8;font-size:15px;max-width:780px;margin:0 auto;padding:2rem 0;}.blog-post p{margin-bottom:1rem;}.blog-post strong{color:#0f172a;}.blog-post ul,.blog-post ol{padding-left:1.5rem;margin-bottom:1rem;}.blog-post li{margin:5px 0;font-size:14px;}.blog-post code{font-family:'JetBrains Mono',monospace;font-size:12.5px;background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:1px 6px;color:#c7254e;}.blog-post hr{border:none;border-top:1px solid #e2e8f0;margin:2.5rem 0;}/* ── 01 포스트 헤더 ── */.post-header{margin-bottom:2.5rem;padding-bottom:2rem;border-bottom:1px solid #e2e8f0;}.post-tags{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:1rem;}.tag{font-size:12px;padding:3px 12px;border-radius:20px;font-weight:500;}.tag-blue  {background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe;}.tag-green {background:#ecfdf5;color:#059669;border:1px solid #a7f3d0;}.tag-amber {background:#fffbeb;color:#b45309;border:1px solid #fde68a;}.tag-red   {background:#fef2f2;color:#b91c1c;border:1px solid #fecaca;}.tag-gray  {background:#f1f5f9;color:#475569;border:1px solid #e2e8f0;}.post-title{font-size:28px;font-weight:700;color:#0f172a;line-height:1.35;margin:0 0 0.75rem;letter-spacing:-0.3px;}.post-meta{font-size:13px;color:#94a3b8;display:flex;gap:16px;flex-wrap:wrap;}.post-meta .dot::before{content:'·';margin-right:6px;opacity:0.4;}/* ── 02 핵심 요약 박스 ── */.summary-box{background:#eff6ff;border:1px solid #bfdbfe;border-radius:10px;padding:1.25rem 1.5rem;margin-bottom:2rem;}.summary-box .summary-label{font-size:12px;font-weight:700;color:#2563eb;letter-spacing:0.06em;margin-bottom:0.75rem;}.summary-box ul{padding-left:1.25rem;margin:0;}.summary-box li{font-size:14px;color:#334155;margin:5px 0;line-height:1.75;}.summary-box li strong{color:#0f172a;}/* ── 03 목차 (my-toc) ── */.my-toc{border-radius:12px;overflow:hidden;margin-bottom:2.5rem;border:1px solid #162d4a;}.my-toc-header{background:#1e3a5f;padding:16px 22px;display:flex;align-items:center;gap:10px;}.my-toc-header-bar{width:3px;height:22px;background:#93c5fd;border-radius:2px;flex-shrink:0;}.my-toc-header-text{font-size:17px;font-weight:700;color:#f1f5f9;}.my-toc-body{background:#f8fafc;padding:12px 16px 14px;}.my-toc-list{display:flex;flex-direction:column;gap:2px;}.my-toc-list&gt;div&gt;a{display:flex;align-items:center;gap:12px;padding:11px 14px;border-radius:8px;font-size:15px;font-weight:500;color:#1e3a5f;text-decoration:none;transition:background 0.15s,color 0.15s;}.my-toc-list&gt;div&gt;a:hover{background:#dbeafe;color:#1d4ed8;}.my-toc-list&gt;div&gt;a:hover .my-toc-num{background:#1e3a5f;color:#fff;}.my-toc-num{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;min-width:26px;background:#e2e8f0;color:#334155;border-radius:6px;font-size:13px;font-weight:700;flex-shrink:0;transition:background 0.15s,color 0.15s;}.my-toc-num.active{background:#1e3a5f;color:#fff;}.my-toc-list&gt;div&gt;a.my-toc-active{background:#dbeafe;color:#1e3a5f;}.my-toc-list&gt;div&gt;a.my-toc-active .my-toc-num{background:#1e3a5f;color:#fff;}.my-toc-sub{display:flex;flex-direction:column;gap:1px;padding:2px 0 4px 52px;}.my-toc-sub div a{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:6px;font-size:14px;color:#475569;text-decoration:none;transition:background 0.15s,color 0.15s;}.my-toc-sub div a::before{content:'';display:inline-block;width:5px;height:5px;min-width:5px;background:#94a3b8;border-radius:50%;flex-shrink:0;transition:background 0.15s;}.my-toc-sub div a:hover{background:#f1f5f9;color:#1e3a5f;}.my-toc-sub div a:hover::before{background:#1e3a5f;}.my-toc-sub div a.my-toc-active{color:#1e3a5f;}.my-toc-sub div a.my-toc-active::before{background:#1e3a5f;}/* ── 04 섹션 제목 (my-section) ── */.my-section{margin-bottom:3rem;}.my-section-title{font-size:20px;font-weight:700;color:#0f172a;margin-bottom:1rem;padding-bottom:0.5rem;border-bottom:2px solid #2563eb;display:flex;align-items:center;gap:10px;}.my-section-num{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;background:#2563eb;color:#fff;border-radius:50%;font-size:13px;font-weight:700;flex-shrink:0;}.my-sub-title{font-size:16px;font-weight:600;color:#0f172a;margin:1.5rem 0 0.75rem;padding-left:12px;border-left:3px solid #2563eb;}/* ── 05 코드 블록 ── */.code-wrap{margin:1.25rem 0;border-radius:10px;overflow:hidden;border:1px solid #1e293b;box-shadow:0 4px 16px rgba(15,23,42,0.12);}.code-header{background:#1e293b;padding:8px 16px;display:flex;align-items:center;justify-content:space-between;}.code-dots{display:flex;gap:6px;}.cdot{width:12px;height:12px;border-radius:50%;}.cdot-r{background:#ff5f57;}.cdot-y{background:#febc2e;}.cdot-g{background:#28c840;}.code-lang{font-size:11px;color:#64748b;font-family:'JetBrains Mono',monospace;letter-spacing:0.05em;}pre.code-body{background:#0d1117;padding:1.25rem 1.5rem;margin:0;overflow-x:auto;font-family:'JetBrains Mono',monospace;font-size:13px;line-height:1.75;color:#cbd5e1;}.c-blue{color:#93c5fd;}.c-green{color:#4ade80;}.c-lime{color:#86efac;}.c-teal{color:#a5f3fc;}.c-purple{color:#f0abfc;}.c-cyan{color:#6ee7b7;}.c-yellow{color:#fde68a;}.c-gray{color:#475569;font-style:italic;}.c-base{color:#cbd5e1;}/* ── 06 강조 박스 ── */.tip-box,.warn-box,.info-box,.danger-box{border-radius:10px;padding:1rem 1.25rem;margin:1.25rem 0;display:flex;gap:12px;align-items:flex-start;}.tip-box   {background:#ecfdf5;border:1px solid #a7f3d0;}.warn-box  {background:#fffbeb;border:1px solid #fde68a;}.info-box  {background:#eff6ff;border:1px solid #bfdbfe;}.danger-box{background:#fef2f2;border:1px solid #fecaca;}.box-icon{font-size:18px;line-height:1;flex-shrink:0;margin-top:1px;}.box-body{font-size:14px;line-height:1.75;}.box-title{font-weight:700;margin-bottom:3px;font-size:13px;}.tip-box    .box-title{color:#047857;}.tip-box    .box-body{color:#065f46;}.warn-box   .box-title{color:#b45309;}.warn-box   .box-body{color:#92400e;}.info-box   .box-title{color:#2563eb;}.info-box   .box-body{color:#1e40af;}.danger-box .box-title{color:#b91c1c;}.danger-box .box-body{color:#991b1b;}/* ── 07 비교 표 ── */.table-wrap{overflow-x:auto;margin:1.25rem 0;}.blog-table{width:100%;border-collapse:collapse;font-size:14px;}.blog-table thead tr{background:#0f172a;color:#fff;}.blog-table thead th{padding:11px 16px;text-align:left;font-weight:500;font-size:13px;}.blog-table tbody tr{border-bottom:1px solid #e2e8f0;}.blog-table tbody tr:last-child{border-bottom:none;}.blog-table tbody td{padding:11px 16px;color:#334155;vertical-align:middle;}.blog-table tbody tr:nth-child(even){background:#f8fafc;}.badge-green{display:inline-block;background:#ecfdf5;color:#047857;border:1px solid #a7f3d0;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}.badge-red  {display:inline-block;background:#fef2f2;color:#b91c1c;border:1px solid #fecaca;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}.badge-blue {display:inline-block;background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}.badge-amber{display:inline-block;background:#fffbeb;color:#b45309;border:1px solid #fde68a;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}/* ── 08 문제 카드 ── */.problem-card{border:1px solid #e2e8f0;border-radius:10px;overflow:hidden;margin:1.25rem 0;}.problem-card-header{background:#0f172a;color:#fff;padding:12px 18px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;}.prob-num{font-size:12px;color:#64748b;font-family:'JetBrains Mono',monospace;}.prob-title{font-size:15px;font-weight:600;margin-top:2px;}.prob-meta{font-size:12px;color:#64748b;text-align:right;line-height:1.6;}.diff-gold{color:#fbbf24;}.diff-silver{color:#94a3b8;}.diff-bronze{color:#d97706;}.diff-green{color:#4ade80;}.problem-card-body{padding:1.25rem 1.5rem;}.problem-card-body p{font-size:14px;color:#334155;margin-bottom:0.75rem;line-height:1.75;}.prob-sub{font-size:16px;font-weight:600;color:#0f172a;margin:1rem 0 0.6rem;padding-left:12px;border-left:3px solid #2563eb;}.step-list{padding-left:0;list-style:none;counter-reset:steps;}.step-list li{counter-increment:steps;padding:8px 0 8px 2.5rem;border-bottom:1px dashed #e2e8f0;font-size:14px;color:#334155;position:relative;line-height:1.7;}.step-list li:last-child{border-bottom:none;}.step-list li::before{content:counter(steps);position:absolute;left:0;top:9px;display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;background:#2563eb;color:#fff;border-radius:50%;font-size:11px;font-weight:700;}/* ── 포스트 푸터 ── */.post-footer{margin-top:3rem;padding-top:1.5rem;border-top:1px solid #e2e8f0;font-size:13px;color:#94a3b8;text-align:center;line-height:2;}&lt;/style&gt;
&lt;/div&gt;
&lt;div class=&quot;summary-box&quot;&gt;
&lt;div class=&quot;summary-label&quot;&gt;  핵심 요약&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;위&amp;rarr;아래&lt;/b&gt; &amp;mdash; &quot;꼭대기에서 여기까지&quot;의 최대 합. 부모를 참조하므로 &lt;b&gt;양 끝 경계 분기 2개&lt;/b&gt;가 필요하고, 마지막에 바닥 행을 훑어야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;아래&amp;rarr;위&lt;/b&gt; &amp;mdash; &quot;여기서 바닥까지&quot;의 최대 합. 자식을 참조하는데 아랫 행이 항상 한 칸 넓으므로 &lt;b&gt;경계 분기가 아예 없다.&lt;/b&gt; &lt;code&gt;dp[0][0]&lt;/code&gt;이 곧 정답이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;nav class=&quot;my-toc&quot;&gt;
&lt;div class=&quot;my-toc-header&quot;&gt;
&lt;div class=&quot;my-toc-header-bar&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;my-toc-header-text&quot;&gt;목차&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;my-toc-body&quot;&gt;&lt;!-- h2/h3 태그를 읽어 자동 생성됩니다 --&gt;&lt;/div&gt;
&lt;/nav&gt;
&lt;div class=&quot;problem-card&quot;&gt;
&lt;div class=&quot;problem-card-header&quot;&gt;
&lt;div&gt;
&lt;div class=&quot;prob-num&quot;&gt;프로그래머스 #43105&lt;/div&gt;
&lt;div class=&quot;prob-title&quot;&gt;정수 삼각형&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;prob-meta&quot;&gt;
&lt;div&gt;높이 &amp;le; 500 &amp;middot; 원소 0 ~ 9,999&lt;/div&gt;
&lt;div&gt;난이도: &lt;span class=&quot;diff-gold&quot;&gt;[Level 3]&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;problem-card-body&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자가 적힌 삼각형이 주어진다. 꼭대기에서 출발해 아래로 내려가되, 한 칸 이동할 때는 바로 아래 또는 바로 아래 오른쪽 칸으로만 갈 수 있다. 바닥까지 내려가면서 거쳐간 숫자의 합이 최대가 되도록 할 때, 그 합을 구하면 된다.&lt;/p&gt;
&lt;div class=&quot;prob-sub&quot;&gt;풀이 과정&lt;/div&gt;
&lt;ol class=&quot;step-list&quot; style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;경로 수와 칸 수를 비교해 DP로 접근할지 판단한다.&lt;/li&gt;
&lt;li&gt;DP 배열 한 칸에 담을 값의 의미를 한 문장으로 확정한다.&lt;/li&gt;
&lt;li&gt;그 문장에 맞춰 점화식을 세우고 경계를 확인한다.&lt;/li&gt;
&lt;li&gt;정답이 어느 칸에 남는지 확인하고 반환한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section1&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;1&lt;/span&gt; 규모를 먼저 확인했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삼각형 높이는 최대 500이다. 완전탐색으로 모든 경로를 세어보면 어떻게 되는지부터 계산했다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;접근&lt;/th&gt;
&lt;th&gt;연산 횟수&lt;/th&gt;
&lt;th&gt;판정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;모든 경로 완전탐색&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;칸마다 2갈래 &amp;rarr; 2⁴⁹⁹&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-red&quot;&gt;불가능&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;칸마다 한 번씩만 계산&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;1+2+&amp;hellip;+500 = 125,250&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;여유&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로 수는 폭발하는데 칸 수는 12만 개뿐이다. 이 격차가 곧 &lt;b&gt;서로 다른 경로들이 같은 칸을 수없이 중복해서 지나간다&lt;/b&gt;는 뜻이므로, 칸마다 결과를 한 번만 계산해 재사용하면 된다고 판단했다.&lt;/p&gt;
&lt;h2 id=&quot;section2&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;2&lt;/span&gt; DP 배열에 무엇을 담을 것인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DP에서 가장 먼저 결정할 것은 &quot;배열 한 칸에 무엇을 담는가&quot;다. 이것을 한 문장으로 쓰지 못하면 점화식도 세울 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이 문제는 그 문장을 &lt;b&gt;두 가지로 쓸 수 있다.&lt;/b&gt; 어느 쪽을 고르느냐에 따라 코드가 완전히 달라진다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방향&lt;/th&gt;
&lt;th&gt;&lt;code&gt;dp[r][c]&lt;/code&gt;의 의미&lt;/th&gt;
&lt;th&gt;참조 대상&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;위 &amp;rarr; 아래&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;꼭대기에서 출발해 (r,c)까지 도달하는 최대 합&lt;/td&gt;
&lt;td&gt;부모 (윗 행)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;아래 &amp;rarr; 위&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;(r,c)에서 출발해 바닥까지 내려가는 최대 합&lt;/td&gt;
&lt;td&gt;자식 (아랫 행)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;warn-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt;⚠️&lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;방향을 먼저 못박아야 하는 이유&lt;/div&gt;
같은 &lt;code&gt;dp[2][1]&lt;/code&gt;이라도 위&amp;rarr;아래에서는 16, 아래&amp;rarr;위에서는 13이 들어간다. 둘 다 맞는 값이지만 &lt;b&gt;뜻이 다르다.&lt;/b&gt; 방향을 정하지 않고 점화식부터 쓰면 두 의미가 섞인다.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section3&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;3&lt;/span&gt; 방법 1: 위에서 아래로&lt;/h2&gt;
&lt;h3 id=&quot;section3-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;구하려는 값과 변수 정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;dp[r][c]&lt;/code&gt;는 &lt;b&gt;꼭대기에서 출발해 (r행, c열) 칸까지 도달하는 경로들 중, 거쳐간 숫자 합의 최댓값&lt;/b&gt;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;r&lt;/code&gt; &amp;mdash; 행 번호. 꼭대기가 0행이고 아래로 갈수록 증가한다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;c&lt;/code&gt; &amp;mdash; 그 행 안에서의 열 번호. r행은 칸이 &lt;code&gt;r+1&lt;/code&gt;개이므로 c는 0부터 r까지다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;triangle[r][c]&lt;/code&gt; &amp;mdash; 그 칸에 원래 적혀 있는 숫자&lt;/li&gt;
&lt;li&gt;기준점 &amp;mdash; &lt;code&gt;dp[0][0] = triangle[0][0]&lt;/code&gt;. 꼭대기는 출발점이므로 자기 숫자 그대로다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;section3-2&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;식을 한국어 문장으로 옮기면&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;식은 다음과 같다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;cdot cdot-r&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-y&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-g&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;점화식 &amp;mdash; 위에서 아래로&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body markdown&quot;&gt;&lt;code&gt;dp[r][c] = triangle[r][c] + max(dp[r-1][c-1], dp[r-1][c])&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문장으로 번역하면 이렇다.&lt;/p&gt;
&lt;div class=&quot;info-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt;ℹ️&lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;식의 의미&lt;/div&gt;
&quot;(r,c)까지의 최대 합은, &lt;b&gt;나에게 올 수 있는 두 부모 칸 중 더 큰 쪽의 값&lt;/b&gt;에 &lt;b&gt;내 칸 숫자&lt;/b&gt;를 더한 것이다.&quot;&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&quot;section3-3&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;왜 부모가 [r-1][c-1]과 [r-1][c]인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제에 적힌 &quot;바로 아래 또는 바로 아래 오른쪽&quot;은 &lt;b&gt;자식 방향&lt;/b&gt; 설명이다. 지금 필요한 것은 반대인 부모 방향이므로 뒤집어야 한다. (r-1, c)에서 출발한다고 놓고 자식을 따져보았다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;(r-1, c)에서의 이동&lt;/th&gt;
&lt;th&gt;도착 칸&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;바로 아래&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;(r, c)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;바로 아래 오른쪽&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;(r, c+1)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 표를 거꾸로 읽으면 &quot;(r, c)에 도착하려면 어디서 왔어야 하는가&quot;가 나온다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;(r, c)에 도착한 경로&lt;/th&gt;
&lt;th&gt;출발 칸&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;바로 아래로 내려온 경우&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;(r-1, c)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;바로 아래 오른쪽으로 내려온 경우&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;(r-1, c-1)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 줄에서 열 번호가 &lt;b&gt;줄어드는&lt;/b&gt; 이유는, 오른쪽으로 한 칸 이동해서 c에 도착했다면 출발점은 c보다 하나 작은 c-1이었어야 하기 때문이다. 이 지점이 헷갈리기 쉬워 손으로 한 번 그려서 확인했다.&lt;/p&gt;
&lt;h3 id=&quot;section3-4&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;경계 &amp;mdash; 부모가 하나뿐인 칸&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 행의 양 끝은 부모가 하나밖에 없다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;위치&lt;/th&gt;
&lt;th&gt;조건&lt;/th&gt;
&lt;th&gt;없는 부모&lt;/th&gt;
&lt;th&gt;이유&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;왼쪽 끝&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;c == 0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dp[r-1][-1]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;열 번호가 음수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;오른쪽 끝&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;c == r&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dp[r-1][r]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;r-1행은 칸이 r개(0~r-1)뿐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;중간&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;그 외&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;둘 다 존재&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 삼각형의 두 빗변은 각각 &lt;b&gt;한 가지 경로로만 도달 가능&lt;/b&gt;하다. 왼쪽 빗변은 계속 왼쪽 아래로만, 오른쪽 빗변은 계속 오른쪽 아래로만 내려온 경로다.&lt;/p&gt;
&lt;h3 id=&quot;section3-5&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;코드&lt;/h3&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;cdot cdot-r&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-y&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-g&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;Java &amp;mdash; 위에서 아래로&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body angelscript&quot;&gt;&lt;code&gt;class Solution {
    public int solution(int[][] triangle) {
        int n = triangle.length;

        // 행마다 길이가 다른 삼각형 배열
        int[][] dp = new int[n][];
        for (int r = 0; r &amp;lt; n; r++) {
            dp[r] = new int[r + 1];
        }

        dp[0][0] = triangle[0][0];

        for (int r = 1; r &amp;lt; n; r++) {
            for (int c = 0; c &amp;lt;= r; c++) {
                if (c == 0) {
                    // 왼쪽 끝 &amp;mdash; 부모가 하나
                    dp[r][c] = triangle[r][c] + dp[r-1][c];
                } else if (c == r) {
                    // 오른쪽 끝 &amp;mdash; 부모가 하나
                    dp[r][c] = triangle[r][c] + dp[r-1][c-1];
                } else {
                    // 중간 &amp;mdash; 부모 둘 중 큰 쪽
                    dp[r][c] = triangle[r][c]
                                 + Math.max(dp[r-1][c-1], dp[r-1][c]);
                }
            }
        }

        // 바닥의 어느 칸으로 내려가도 되므로 마지막 행 전체를 훑는다
        int answer = 0;
        for (int c = 0; c &amp;lt; n; c++) {
            answer = Math.max(answer, dp[n-1][c]);
        }

        return answer;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class=&quot;warn-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt;⚠️&lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;정답은 특정 칸 하나가 아니다&lt;/div&gt;
바닥의 어느 칸으로 내려가도 되므로 &lt;b&gt;마지막 행 전체의 최댓값&lt;/b&gt;이 정답이다. &lt;code&gt;dp[n-1][0]&lt;/code&gt;이나 &lt;code&gt;dp[n-1][n-1]&lt;/code&gt; 같은 특정 칸이 아니다.&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&quot;section3-6&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;추적&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시 삼각형으로 확인했다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;cdot cdot-r&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-y&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-g&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;입력 &amp;mdash; 예시 삼각형&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body angelscript&quot;&gt;&lt;code&gt;      7
    3   8
  8   1   0
2   7   4   4
4 5   2   6   5&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;0행&lt;/b&gt; &amp;mdash; 기준점이므로 &lt;code&gt;dp[0][0] = 7&lt;/code&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1행&lt;/b&gt; &amp;mdash; c=0이 왼쪽 끝, c=1은 r과 같으므로 오른쪽 끝이다. 둘 다 경계다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;c&lt;/th&gt;
&lt;th&gt;계산&lt;/th&gt;
&lt;th&gt;dp&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;0&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;3 + dp[0][0]=7&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;8 + dp[0][0]=7&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2행&lt;/b&gt; &amp;mdash; c=1이 처음으로 부모가 둘인 칸이다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;c&lt;/th&gt;
&lt;th&gt;계산&lt;/th&gt;
&lt;th&gt;dp&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;0&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;8 + dp[1][0]=10&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;1 + max(10, 15) = 1+15&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;0 + dp[1][1]=15&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3행&lt;/b&gt;&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;c&lt;/th&gt;
&lt;th&gt;계산&lt;/th&gt;
&lt;th&gt;dp&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;0&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;2 + 18&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;7 + max(18, 16) = 7+18&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;4 + max(16, 15) = 4+16&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;4 + 15&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4행&lt;/b&gt;&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;c&lt;/th&gt;
&lt;th&gt;계산&lt;/th&gt;
&lt;th&gt;dp&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;0&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;4 + 20&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;5 + max(20, 25) = 5+25&lt;/td&gt;
&lt;td&gt;&lt;b&gt;30&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;2 + max(25, 20) = 2+25&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;6 + max(20, 19) = 6+20&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;5 + 19&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 행의 최댓값 &lt;b&gt;30&lt;/b&gt;. 예시 답과 일치한다.&lt;/p&gt;
&lt;h2 id=&quot;section4&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;4&lt;/span&gt; 방법 2: 아래에서 위로&lt;/h2&gt;
&lt;h3 id=&quot;section4-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;의미를 뒤집으면 경계가 사라진다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 &lt;code&gt;dp[r][c]&lt;/code&gt;를 &lt;b&gt;(r,c)에서 출발해 바닥까지 내려가는 경로들 중 최대 합&lt;/b&gt;으로 정의했다. 그러면 참조 대상이 부모가 아니라 자식이 된다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;cdot cdot-r&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-y&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-g&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;점화식 &amp;mdash; 아래에서 위로&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body markdown&quot;&gt;&lt;code&gt;dp[i][j] = triangle[i][j] + max(dp[i+1][j], dp[i+1][j+1])&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문장으로 번역하면 &quot;(i,j)에서 바닥까지의 최대 합은, &lt;b&gt;내려갈 수 있는 두 자식 칸 중 더 큰 쪽의 값&lt;/b&gt;에 &lt;b&gt;내 칸 숫자&lt;/b&gt;를 더한 것이다.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 결정적인 차이가 나온다. 두 방향의 경계 상황을 나란히 놓고 비교했다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;위&amp;rarr;아래에서 c 위치&lt;/th&gt;
&lt;th&gt;부모 [r-1][c-1]&lt;/th&gt;
&lt;th&gt;부모 [r-1][c]&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;c = 0&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-red&quot;&gt;인덱스 -1&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;존재&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;c = r&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;존재&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-red&quot;&gt;범위 초과&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;중간&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;존재&lt;/td&gt;
&lt;td&gt;존재&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;아래&amp;rarr;위에서 j 위치&lt;/th&gt;
&lt;th&gt;자식 [i+1][j]&lt;/th&gt;
&lt;th&gt;자식 [i+1][j+1]&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;j = 0&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;존재&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;존재&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;j = i&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;존재&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;존재&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;중간&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;존재&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;존재&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전부 존재한다. 이유는 삼각형의 형태 자체에 있다. i행은 &lt;code&gt;i+1&lt;/code&gt;칸이고 i+1행은 &lt;code&gt;i+2&lt;/code&gt;칸이다. j가 최대 i까지 갈 때 &lt;code&gt;j+1&lt;/code&gt;은 최대 &lt;code&gt;i+1&lt;/code&gt;인데, 아래 행의 마지막 인덱스가 정확히 &lt;code&gt;i+1&lt;/code&gt;이다.&lt;/p&gt;
&lt;div class=&quot;tip-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt; &lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;아랫 행이 항상 한 칸 더 넓다&lt;/div&gt;
그래서 &lt;b&gt;자식은 절대 배열 밖으로 나갈 수 없다.&lt;/b&gt; 경계 분기 두 개가 통째로 사라지는 이유다.&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&quot;section4-2&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;코드&lt;/h3&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;cdot cdot-r&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-y&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-g&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;Java &amp;mdash; 아래에서 위로&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body angelscript&quot;&gt;&lt;code&gt;class Solution {
    public int solution(int[][] triangle) {
        int height = triangle.length;

        int[][] dp = new int[height][];
        for (int i = 0; i &amp;lt; height; i++) {
            dp[i] = new int[i + 1];
        }

        // 바닥은 그 자체가 &quot;여기서 바닥까지&quot;의 값이다
        for (int j = 0; j &amp;lt; height; j++) {
            dp[height-1][j] = triangle[height-1][j];
        }

        // 밑에서 두 번째 줄부터 위로 올라간다
        for (int i = height - 2; i &amp;gt;= 0; i--) {
            for (int j = 0; j &amp;lt; dp[i].length; j++) {
                dp[i][j] = triangle[i][j]
                           + Math.max(dp[i+1][j], dp[i+1][j+1]);
            }
        }

        // 꼭대기에 남은 값이 곧 정답이다
        return dp[0][0];
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복 조건을 &lt;code&gt;j &amp;lt; dp[i].length&lt;/code&gt;로 쓴 이유가 있다. 삼각형 모양으로 배열을 잡아두었기 때문에 &lt;b&gt;배열 자신이 범위 정보를 들고 있다.&lt;/b&gt; &lt;code&gt;j &amp;lt;= i&lt;/code&gt;와 결과는 같지만, 앞의 것은 &quot;배열이 가진 만큼 돈다&quot;이고 뒤의 것은 &quot;내가 계산한 범위만큼 돈다&quot;라는 차이가 있다.&lt;/p&gt;
&lt;h3 id=&quot;section4-3&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;추적&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 예시로 아래에서 올라가 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;i = 3&lt;/b&gt; (자식은 4행 &lt;code&gt;[4,5,2,6,5]&lt;/code&gt;)&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;j&lt;/th&gt;
&lt;th&gt;원래값&lt;/th&gt;
&lt;th&gt;max(자식)&lt;/th&gt;
&lt;th&gt;결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;0&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;max(4, 5) = 5&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;max(5, 2) = 5&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;max(2, 6) = 6&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;max(6, 5) = 6&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;i = 2&lt;/b&gt; (자식은 &lt;code&gt;[7,12,10,10]&lt;/code&gt;)&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;j&lt;/th&gt;
&lt;th&gt;원래값&lt;/th&gt;
&lt;th&gt;max(자식)&lt;/th&gt;
&lt;th&gt;결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;0&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;max(7, 12) = 12&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;max(12, 10) = 12&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;max(10, 10) = 10&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;i = 1&lt;/b&gt; (자식은 &lt;code&gt;[20,13,10]&lt;/code&gt;)&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;j&lt;/th&gt;
&lt;th&gt;원래값&lt;/th&gt;
&lt;th&gt;max(자식)&lt;/th&gt;
&lt;th&gt;결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;0&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;max(20, 13) = 20&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;max(13, 10) = 13&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;i = 0&lt;/b&gt; (자식은 &lt;code&gt;[23,21]&lt;/code&gt;)&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;j&lt;/th&gt;
&lt;th&gt;원래값&lt;/th&gt;
&lt;th&gt;max(자식)&lt;/th&gt;
&lt;th&gt;결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;0&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;max(23, 21) = 23&lt;/td&gt;
&lt;td&gt;&lt;b&gt;30&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;dp[0][0] = 30&lt;/code&gt;으로 일치한다. 여기서 주목할 점은 &lt;b&gt;중간값들이 위&amp;rarr;아래 버전과 전혀 다르다&lt;/b&gt;는 것이다. i=2에서 나온 20, 13, 10은 &quot;꼭대기에서 여기까지&quot;가 아니라 &quot;여기서 바닥까지&quot;의 최댓값이다. 방향이 바뀌면서 dp의 의미 자체가 뒤집혔다.&lt;/p&gt;
&lt;h3 id=&quot;section4-4&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;원본 배열을 직접 수정하는 변형&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방향에서는 &lt;code&gt;dp&lt;/code&gt; 배열을 따로 만들지 않고 &lt;code&gt;triangle&lt;/code&gt;에 바로 누적하는 코드를 자주 본다. 메모리를 아끼는 기법이지만 대가가 있어 정리해 두었다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;triangle 직접 수정&lt;/th&gt;
&lt;th&gt;별도 dp 배열&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;추가 메모리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;없음&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;O(n&amp;sup2;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;원본 데이터&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-red&quot;&gt;파괴됨&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;보존됨&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;디버깅&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;원본과 대조 불가&lt;/td&gt;
&lt;td&gt;나란히 비교 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;재호출&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-red&quot;&gt;두 번째부터 오답&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;항상 동일 결과&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재호출 항목이 실질적인 위험이다. 같은 배열 참조로 &lt;code&gt;solution&lt;/code&gt;을 두 번 호출하면 첫 호출이 이미 값을 덮어썼기 때문에 두 번째는 엉뚱한 답이 나온다. 채점 환경에서는 매번 새 배열이 주어지므로 통과하지만, 일반적인 코드에서는 &quot;인자로 받은 것은 건드리지 않는다&quot;를 기본값으로 두는 편이 낫다고 판단했다.&lt;/p&gt;
&lt;h2 id=&quot;section5&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;5&lt;/span&gt; 두 방향 비교&lt;/h2&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;위 &amp;rarr; 아래&lt;/th&gt;
&lt;th&gt;아래 &amp;rarr; 위&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;dp 의미&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;꼭대기 &amp;rarr; 여기까지 최대 합&lt;/td&gt;
&lt;td&gt;여기 &amp;rarr; 바닥까지 최대 합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;참조 대상&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;부모 (윗 행)&lt;/td&gt;
&lt;td&gt;자식 (아랫 행)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;경계 분기&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-amber&quot;&gt;2개 필요&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;없음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;마지막 단계&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;바닥 행 훑어 최댓값&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;dp[0][0]이 곧 정답&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;사고 방향&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;직관과 일치&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;거꾸로 생각해야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;시간 복잡도&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;O(n&amp;sup2;)&lt;/td&gt;
&lt;td&gt;O(n&amp;sup2;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 관점에서는 아래&amp;rarr;위가 확실히 우세하다. 경계 분기 2개와 마지막 최댓값 루프가 통째로 사라진다. 다만 &quot;출발점에서 쌓아간다&quot;는 직관과 반대 방향이므로, 처음 익힐 때는 위&amp;rarr;아래로 확실히 잡아둔 뒤 넘어가는 편이 낫다고 판단했다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 99.8837%; height: 22px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style11&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 100%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;잘못된 내용&lt;/span&gt;이 있다면&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;지적&lt;/span&gt;부탁드립니다. 방문해주셔서 감사합니다.&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;face&quot; data-emoticon-name=&quot;055&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/055.png&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/055.png&quot; width=&quot;80&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>알고리즘 &amp;amp; 코딩테스트/동적 계획법 (DP)</category>
      <category>DP</category>
      <category>다이나믹프로그래밍</category>
      <category>동적계획법</category>
      <category>상향식</category>
      <category>정수삼각형</category>
      <category>프로그래머스</category>
      <category>하향식</category>
      <author>곱마2</author>
      <guid isPermaLink="true">https://minsu092274.tistory.com/223</guid>
      <comments>https://minsu092274.tistory.com/223#entry223comment</comments>
      <pubDate>Tue, 4 Aug 2026 15:30:31 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스 Lv.3] 네트워크(Java) - BFS</title>
      <link>https://minsu092274.tistory.com/222</link>
      <description>&lt;div&gt;
&lt;style&gt;@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&amp;family=JetBrains+Mono:wght@400;600&amp;display=swap');/* ── 공통 ── */.blog-post{font-family:'Noto Sans KR',sans-serif;color:#334155;line-height:1.8;font-size:15px;max-width:780px;margin:0 auto;padding:2rem 0;}.blog-post p{margin-bottom:1rem;}.blog-post strong{color:#0f172a;}.blog-post ul,.blog-post ol{padding-left:1.5rem;margin-bottom:1rem;}.blog-post li{margin:5px 0;font-size:14px;}.blog-post code{font-family:'JetBrains Mono',monospace;font-size:12.5px;background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:1px 6px;color:#c7254e;}.blog-post hr{border:none;border-top:1px solid #e2e8f0;margin:2.5rem 0;}/* ── 01 포스트 헤더 ── */.post-header{margin-bottom:2.5rem;padding-bottom:2rem;border-bottom:1px solid #e2e8f0;}.post-tags{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:1rem;}.tag{font-size:12px;padding:3px 12px;border-radius:20px;font-weight:500;}.tag-blue  {background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe;}.tag-green {background:#ecfdf5;color:#059669;border:1px solid #a7f3d0;}.tag-amber {background:#fffbeb;color:#b45309;border:1px solid #fde68a;}.tag-red   {background:#fef2f2;color:#b91c1c;border:1px solid #fecaca;}.tag-gray  {background:#f1f5f9;color:#475569;border:1px solid #e2e8f0;}.post-title{font-size:28px;font-weight:700;color:#0f172a;line-height:1.35;margin:0 0 0.75rem;letter-spacing:-0.3px;}.post-meta{font-size:13px;color:#94a3b8;display:flex;gap:16px;flex-wrap:wrap;}.post-meta .dot::before{content:'·';margin-right:6px;opacity:0.4;}/* ── 02 핵심 요약 박스 ── */.summary-box{background:#eff6ff;border:1px solid #bfdbfe;border-radius:10px;padding:1.25rem 1.5rem;margin-bottom:2rem;}.summary-box .summary-label{font-size:12px;font-weight:700;color:#2563eb;letter-spacing:0.06em;margin-bottom:0.75rem;}.summary-box ul{padding-left:1.25rem;margin:0;}.summary-box li{font-size:14px;color:#334155;margin:5px 0;line-height:1.75;}.summary-box li strong{color:#0f172a;}/* ── 03 목차 (my-toc) ── */.my-toc{border-radius:12px;overflow:hidden;margin-bottom:2.5rem;border:1px solid #162d4a;}.my-toc-header{background:#1e3a5f;padding:16px 22px;display:flex;align-items:center;gap:10px;}.my-toc-header-bar{width:3px;height:22px;background:#93c5fd;border-radius:2px;flex-shrink:0;}.my-toc-header-text{font-size:17px;font-weight:700;color:#f1f5f9;}.my-toc-body{background:#f8fafc;padding:12px 16px 14px;}.my-toc-list{display:flex;flex-direction:column;gap:2px;}.my-toc-list&gt;div&gt;a{display:flex;align-items:center;gap:12px;padding:11px 14px;border-radius:8px;font-size:15px;font-weight:500;color:#1e3a5f;text-decoration:none;transition:background 0.15s,color 0.15s;}.my-toc-list&gt;div&gt;a:hover{background:#dbeafe;color:#1d4ed8;}.my-toc-list&gt;div&gt;a:hover .my-toc-num{background:#1e3a5f;color:#fff;}.my-toc-num{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;min-width:26px;background:#e2e8f0;color:#334155;border-radius:6px;font-size:13px;font-weight:700;flex-shrink:0;transition:background 0.15s,color 0.15s;}.my-toc-num.active{background:#1e3a5f;color:#fff;}.my-toc-list&gt;div&gt;a.my-toc-active{background:#dbeafe;color:#1e3a5f;}.my-toc-list&gt;div&gt;a.my-toc-active .my-toc-num{background:#1e3a5f;color:#fff;}.my-toc-sub{display:flex;flex-direction:column;gap:1px;padding:2px 0 4px 52px;}.my-toc-sub div a{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:6px;font-size:14px;color:#475569;text-decoration:none;transition:background 0.15s,color 0.15s;}.my-toc-sub div a::before{content:'';display:inline-block;width:5px;height:5px;min-width:5px;background:#94a3b8;border-radius:50%;flex-shrink:0;transition:background 0.15s;}.my-toc-sub div a:hover{background:#f1f5f9;color:#1e3a5f;}.my-toc-sub div a:hover::before{background:#1e3a5f;}.my-toc-sub div a.my-toc-active{color:#1e3a5f;}.my-toc-sub div a.my-toc-active::before{background:#1e3a5f;}/* ── 04 섹션 제목 (my-section) ── */.my-section{margin-bottom:3rem;}.my-section-title{font-size:20px;font-weight:700;color:#0f172a;margin-bottom:1rem;padding-bottom:0.5rem;border-bottom:2px solid #2563eb;display:flex;align-items:center;gap:10px;}.my-section-num{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;background:#2563eb;color:#fff;border-radius:50%;font-size:13px;font-weight:700;flex-shrink:0;}.my-sub-title{font-size:16px;font-weight:600;color:#0f172a;margin:1.5rem 0 0.75rem;padding-left:12px;border-left:3px solid #2563eb;}/* ── 05 코드 블록 ── */.code-wrap{margin:1.25rem 0;border-radius:10px;overflow:hidden;border:1px solid #1e293b;box-shadow:0 4px 16px rgba(15,23,42,0.12);}.code-header{background:#1e293b;padding:8px 16px;display:flex;align-items:center;justify-content:space-between;}.code-dots{display:flex;gap:6px;}.cdot{width:12px;height:12px;border-radius:50%;}.cdot-r{background:#ff5f57;}.cdot-y{background:#febc2e;}.cdot-g{background:#28c840;}.code-lang{font-size:11px;color:#64748b;font-family:'JetBrains Mono',monospace;letter-spacing:0.05em;}pre.code-body{background:#0d1117;padding:1.25rem 1.5rem;margin:0;overflow-x:auto;font-family:'JetBrains Mono',monospace;font-size:13px;line-height:1.75;color:#cbd5e1;}.c-blue{color:#93c5fd;}.c-green{color:#4ade80;}.c-lime{color:#86efac;}.c-teal{color:#a5f3fc;}.c-purple{color:#f0abfc;}.c-cyan{color:#6ee7b7;}.c-yellow{color:#fde68a;}.c-gray{color:#475569;font-style:italic;}.c-base{color:#cbd5e1;}/* ── 06 강조 박스 ── */.tip-box,.warn-box,.info-box,.danger-box{border-radius:10px;padding:1rem 1.25rem;margin:1.25rem 0;display:flex;gap:12px;align-items:flex-start;}.tip-box   {background:#ecfdf5;border:1px solid #a7f3d0;}.warn-box  {background:#fffbeb;border:1px solid #fde68a;}.info-box  {background:#eff6ff;border:1px solid #bfdbfe;}.danger-box{background:#fef2f2;border:1px solid #fecaca;}.box-icon{font-size:18px;line-height:1;flex-shrink:0;margin-top:1px;}.box-body{font-size:14px;line-height:1.75;}.box-title{font-weight:700;margin-bottom:3px;font-size:13px;}.tip-box    .box-title{color:#047857;}.tip-box    .box-body{color:#065f46;}.warn-box   .box-title{color:#b45309;}.warn-box   .box-body{color:#92400e;}.info-box   .box-title{color:#2563eb;}.info-box   .box-body{color:#1e40af;}.danger-box .box-title{color:#b91c1c;}.danger-box .box-body{color:#991b1b;}/* ── 07 비교 표 ── */.table-wrap{overflow-x:auto;margin:1.25rem 0;}.blog-table{width:100%;border-collapse:collapse;font-size:14px;}.blog-table thead tr{background:#0f172a;color:#fff;}.blog-table thead th{padding:11px 16px;text-align:left;font-weight:500;font-size:13px;}.blog-table tbody tr{border-bottom:1px solid #e2e8f0;}.blog-table tbody tr:last-child{border-bottom:none;}.blog-table tbody td{padding:11px 16px;color:#334155;vertical-align:middle;}.blog-table tbody tr:nth-child(even){background:#f8fafc;}.badge-green{display:inline-block;background:#ecfdf5;color:#047857;border:1px solid #a7f3d0;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}.badge-red  {display:inline-block;background:#fef2f2;color:#b91c1c;border:1px solid #fecaca;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}.badge-blue {display:inline-block;background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}.badge-amber{display:inline-block;background:#fffbeb;color:#b45309;border:1px solid #fde68a;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}/* ── 08 문제 카드 ── */.problem-card{border:1px solid #e2e8f0;border-radius:10px;overflow:hidden;margin:1.25rem 0;}.problem-card-header{background:#0f172a;color:#fff;padding:12px 18px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;}.prob-num{font-size:12px;color:#64748b;font-family:'JetBrains Mono',monospace;}.prob-title{font-size:15px;font-weight:600;margin-top:2px;}.prob-meta{font-size:12px;color:#64748b;text-align:right;line-height:1.6;}.diff-gold{color:#fbbf24;}.diff-silver{color:#94a3b8;}.diff-bronze{color:#d97706;}.diff-green{color:#4ade80;}.problem-card-body{padding:1.25rem 1.5rem;}.problem-card-body p{font-size:14px;color:#334155;margin-bottom:0.75rem;line-height:1.75;}.prob-sub{font-size:16px;font-weight:600;color:#0f172a;margin:1rem 0 0.6rem;padding-left:12px;border-left:3px solid #2563eb;}.step-list{padding-left:0;list-style:none;counter-reset:steps;}.step-list li{counter-increment:steps;padding:8px 0 8px 2.5rem;border-bottom:1px dashed #e2e8f0;font-size:14px;color:#334155;position:relative;line-height:1.7;}.step-list li:last-child{border-bottom:none;}.step-list li::before{content:counter(steps);position:absolute;left:0;top:9px;display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;background:#2563eb;color:#fff;border-radius:50%;font-size:11px;font-weight:700;}/* ── 포스트 푸터 ── */.post-footer{margin-top:3rem;padding-top:1.5rem;border-top:1px solid #e2e8f0;font-size:13px;color:#94a3b8;text-align:center;line-height:2;}&lt;/style&gt;
&lt;/div&gt;
&lt;div class=&quot;summary-box&quot;&gt;
&lt;div class=&quot;summary-label&quot;&gt;  핵심 요약&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;탐색 시작 횟수 = 정답&lt;/b&gt; &amp;mdash; BFS는 시작 노드가 속한 덩어리 전체를 방문하고 멈춘다. 따라서 BFS를 몇 번 시작했는지가 곧 네트워크 개수다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;방문 배열은 1차원&lt;/b&gt; &amp;mdash; 관리해야 할 상태는 &quot;이 컴퓨터를 이미 세었는가&quot;이므로 주어는 컴퓨터다. 간선이 아니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인접 행렬을 그대로 사용&lt;/b&gt; &amp;mdash; &lt;code&gt;computers&lt;/code&gt;가 이미 인접 행렬이므로 별도 자료구조로 옮기지 않았다. n &amp;le; 200이라 O(n&amp;sup2;)로 충분하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;nav class=&quot;my-toc&quot;&gt;
&lt;div class=&quot;my-toc-header&quot;&gt;
&lt;div class=&quot;my-toc-header-bar&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;my-toc-header-text&quot;&gt;목차&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;my-toc-body&quot;&gt;&lt;!-- h2/h3 태그를 읽어 자동 생성됩니다 --&gt;&lt;/div&gt;
&lt;/nav&gt;
&lt;div class=&quot;problem-card&quot;&gt;
&lt;div class=&quot;problem-card-header&quot;&gt;
&lt;div&gt;
&lt;div class=&quot;prob-num&quot;&gt;프로그래머스 #43162&lt;/div&gt;
&lt;div class=&quot;prob-title&quot;&gt;네트워크&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;prob-meta&quot;&gt;
&lt;div&gt;n &amp;le; 200 &amp;middot; 깊이/너비 우선 탐색&lt;/div&gt;
&lt;div&gt;난이도: &lt;span class=&quot;diff-gold&quot;&gt;[Level 3]&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;problem-card-body&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴퓨터 n대가 있고, 어떤 두 컴퓨터가 직접 또는 간접적으로 연결되어 있으면 같은 네트워크로 본다. 연결 정보는 n&amp;times;n 인접 행렬 &lt;code&gt;computers&lt;/code&gt;로 주어진다. &lt;code&gt;computers[i][j]&lt;/code&gt;가 1이면 i번과 j번이 연결되어 있다는 뜻이며, 자기 자신을 나타내는 &lt;code&gt;computers[i][i]&lt;/code&gt;는 항상 1이다. 네트워크의 개수를 반환하면 된다.&lt;/p&gt;
&lt;div class=&quot;prob-sub&quot;&gt;풀이 과정&lt;/div&gt;
&lt;ol class=&quot;step-list&quot; style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;문제를 &quot;연결 요소 개수 세기&quot;로 번역한다.&lt;/li&gt;
&lt;li&gt;0번부터 순서대로 훑으면서 아직 방문하지 않은 컴퓨터를 찾는다.&lt;/li&gt;
&lt;li&gt;찾을 때마다 개수를 1 올리고, 그 지점에서 BFS로 덩어리 전체를 방문 처리한다.&lt;/li&gt;
&lt;li&gt;순회가 끝나면 누적된 개수가 정답이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section1&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;1&lt;/span&gt; 문제를 한 문장으로 번역했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 처음 읽었을 때 가장 먼저 한 일은 &quot;네트워크&quot;라는 단어를 그래프 용어로 바꾸는 것이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴퓨터는 노드이고 연결은 간선이다. 그러면 문제는 이렇게 다시 쓸 수 있다.&lt;/p&gt;
&lt;div class=&quot;info-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt;ℹ️&lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;번역된 문제&lt;/div&gt;
노드 n개와 간선 정보가 주어졌을 때, &lt;b&gt;연결 요소(Connected Component)가 몇 개인가.&lt;/b&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 번역이 되는 순간 문제는 사실상 끝난다고 판단했다. 연결 요소 개수 세기는 정형화된 문제라 풀이의 틀이 이미 정해져 있기 때문이다.&amp;nbsp;&lt;/p&gt;
&lt;h2 id=&quot;section2&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;2&lt;/span&gt; 코드보다 규모 판단을 먼저 했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제약 조건은 &lt;code&gt;n &amp;le; 200&lt;/code&gt;이다. 이 숫자로 어떤 복잡도까지 허용되는지 먼저 계산했다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;복잡도&lt;/th&gt;
&lt;th&gt;n = 200일 때 연산 횟수&lt;/th&gt;
&lt;th&gt;판정&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;O(n&amp;sup2;)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;40,000&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;여유&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;O(n&amp;sup3;)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;8,000,000&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;통과 가능&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;O(2ⁿ)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;계산 불가&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-red&quot;&gt;탈락&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;O(n&amp;sup3;)까지 허용된다는 것은 성능 최적화를 신경 쓸 필요가 전혀 없다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 남는 판단 기준은 하나뿐이다. &lt;b&gt;어느 쪽이 덜 틀리는가.&lt;/b&gt;&lt;/p&gt;
&lt;div class=&quot;tip-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt; &lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;규모 판단을 먼저 하면 얻는 것&lt;/div&gt;
&quot;미리 정리해두면 더 빠르지 않을까&quot; 같은 최적화 유혹에 흔들리지 않게 된다. 성능이 이미 여유롭다는 결론을 코드 작성 전에 확정해두면, 이후의 모든 선택을 가독성 기준으로만 할 수 있다.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section3&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;3&lt;/span&gt; 입력을 있는 그대로 쓰기로 했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;computers&lt;/code&gt;는 이미 인접 행렬이다. 그리고 탐색 중에 던져야 할 질문은 딱 하나뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;mdash; &quot;a와 b는 연결되어 있는가?&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인접 행렬에서 이 질문의 답은 &lt;code&gt;computers[a][b] == 1&lt;/code&gt;, 배열 접근 한 번이다. 이미 최적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;b&gt;이 자료구조를 다른 형태로 바꿔야 할 이유가 있는가&lt;/b&gt;를 스스로에게 물었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인접 리스트로 변환하는 것은 간선이 희소해서 매번 n칸을 훑는 비용이 아까울 때 하는 일이다. 하지만 2번에서 O(n&amp;sup2;)이 여유롭다는 결론이 이미 나 있었으므로, 변환하지 않기로 했다. 손대지 않은 원본이 가장 안전하다고 판단했다.&lt;/p&gt;
&lt;h2 id=&quot;section4&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;4&lt;/span&gt; 무엇을 방문 처리할 것인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제에서 실제로 설계 판단이 필요한 지점은 여기 하나뿐이었다. 답이 &quot;그룹의 개수&quot;이므로, 탐색 중에 관리해야 할 상태는 다음 문장으로 정리된다.&lt;/p&gt;
&lt;div class=&quot;info-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt;ℹ️&lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;관리할 상태&lt;/div&gt;
&quot;이 &lt;b&gt;컴퓨터&lt;/b&gt;는 이미 어떤 그룹에 속한 것으로 세었는가?&quot;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문장의 주어가 &lt;b&gt;컴퓨터&lt;/b&gt;다. 간선이 아니다. 따라서 방문 배열은 &lt;code&gt;boolean[n]&lt;/code&gt; 1차원이면 충분하다.&lt;/p&gt;
&lt;h3 id=&quot;section4-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;답의 단위와 상태의 단위를 맞춰본다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 판단을 굳히기 위해 사용한 확인 절차가 있다. 답의 단위와 상태의 단위가 일치하는지 대조해보는 것이다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;만약 문제가 물었다면&lt;/th&gt;
&lt;th&gt;답의 단위&lt;/th&gt;
&lt;th&gt;필요한 상태&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;네트워크 개수&lt;/b&gt; (이 문제)&lt;/td&gt;
&lt;td&gt;컴퓨터의 집합&lt;/td&gt;
&lt;td&gt;&lt;code&gt;boolean[n]&lt;/code&gt; &amp;mdash; 노드 단위&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;끊어진 연결선의 개수&lt;/td&gt;
&lt;td&gt;간선&lt;/td&gt;
&lt;td&gt;&lt;code&gt;boolean[n][n]&lt;/code&gt; &amp;mdash; 간선 단위&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답이 컴퓨터의 집합을 세는 것이므로 상태도 컴퓨터 단위여야 한다. 간선 단위 상태가 필요한 경우는 두 번째 행처럼 답 자체가 간선을 세는 문제일 때다.&lt;/p&gt;
&lt;h2 id=&quot;section5&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;5&lt;/span&gt; 탐색과 정답을 연결하는 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BFS의 성질을 한 문장으로 쓰면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;mdash; 한 노드에서 시작하면, &lt;b&gt;그 노드가 속한 덩어리 전체&lt;/b&gt;를 방문하고 멈춘다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종이 위에 떨어뜨린 잉크 한 방울과 같다. 이어진 영역까지만 번지고 끊긴 곳은 넘어가지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 전체 구조는 이렇게 잡힌다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;0번부터 순서대로 훑는다&lt;/li&gt;
&lt;li&gt;아직 잉크가 닿지 않은 컴퓨터를 만나면 &amp;rarr; 새 덩어리를 발견한 것 &amp;rarr; 개수 +1&lt;/li&gt;
&lt;li&gt;거기서 BFS를 돌려 그 덩어리를 전부 물들인다&lt;/li&gt;
&lt;li&gt;계속 훑는다&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;tip-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt; &lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;BFS 시작 횟수가 곧 정답이다&lt;/div&gt;
개수를 세는 일은 바깥 루프의 몫이고, BFS는 &quot;물들이기&quot;만 담당한다. 역할이 나뉘어 있으므로 BFS 메서드는 값을 반환할 필요가 없다. &lt;code&gt;void&lt;/code&gt;로 두었다.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section6&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;6&lt;/span&gt; 전체 코드&lt;/h2&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;cdot cdot-r&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-y&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-g&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;Java &amp;mdash; Solution.java&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body pgsql&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {
    public int solution(int n, int[][] computers) {
        boolean[] visited = new boolean[n];
        int answer = 0;

        for (int start = 0; start &amp;lt; n; start++) {
            // 이미 어떤 네트워크에 속한 것으로 세었다면 건너뛴다
            if (visited[start]) continue;

            // 새 네트워크를 발견한 시점
            answer++;
            bfs(start, n, computers, visited);
        }

        return answer;
    }

    private void bfs(int start, int n, int[][] computers, boolean[] visited) {
        Deque&amp;lt;Integer&amp;gt; queue = new ArrayDeque&amp;lt;&amp;gt;();
        queue.add(start);
        visited[start] = true;

        while (!queue.isEmpty()) {
            int current = queue.poll();

            for (int next = 0; next &amp;lt; n; next++) {
                if (computers[current][next] == 1 &amp;amp;&amp;amp; !visited[next]) {
                    visited[next] = true;
                    queue.add(next);
                }
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 id=&quot;section6-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;대각선을 따로 걸러내지 않은 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;computers[i][i]&lt;/code&gt;는 항상 1이므로 자기 자신도 조건을 통과한다. 그런데 &lt;code&gt;i != j&lt;/code&gt; 같은 조건을 추가하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유를 &lt;code&gt;start = 0&lt;/code&gt;인 상황으로 추적해보면 다음과 같다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;시점&lt;/th&gt;
&lt;th&gt;visited[0]&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;BFS 진입 직후&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;visited[start] = true&lt;/code&gt;로 이미 표시함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;current=0, next=0&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;computers[0][0] == 1&lt;/code&gt;은 참이지만 &lt;code&gt;!visited[0]&lt;/code&gt;이 거짓 &amp;rarr; 무시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방문 표시를 &lt;b&gt;큐에 넣는 시점에&lt;/b&gt; 찍어두었기 때문에 대각선이 자동으로 걸러진다. 별도 조건이 필요 없다.&lt;/p&gt;
&lt;div class=&quot;warn-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt;⚠️&lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;방문 표시 위치를 옮기면 안 되는 이유&lt;/div&gt;
방문 표시를 큐에서 꺼낼 때(&lt;code&gt;poll&lt;/code&gt; 직후)로 옮기면 이 보장이 깨진다. 0번이 큐에 두 번 들어가고, 일반 간선에서도 같은 노드가 중복 삽입된다. &lt;b&gt;&quot;큐에 넣을 때 방문 표시&quot;&lt;/b&gt;는 BFS에서 지켜야 할 규칙이다.&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&quot;section6-2&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;동작 추적&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;n = 3&lt;/code&gt;, &lt;code&gt;computers = [[1,1,0],[1,1,0],[0,0,1]]&lt;/code&gt;로 바깥 루프를 따라가보았다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;start&lt;/th&gt;
&lt;th&gt;visited (진입 전)&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;th&gt;answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;0&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[F, F, F]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;미방문 &amp;rarr; 카운트, BFS로 0&amp;middot;1 방문 처리&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[T, T, F]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;이미 방문 &amp;rarr; 건너뜀&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[T, T, F]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;미방문 &amp;rarr; 카운트, BFS로 2 방문 처리&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과 2로 예시 답과 일치한다.&lt;/p&gt;
&lt;h3 id=&quot;section6-3&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;복잡도&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;바깥 루프&lt;/b&gt; &amp;mdash; 노드 n개를 순회하므로 O(n)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BFS 전체&lt;/b&gt; &amp;mdash; 각 노드는 큐에 한 번만 들어가고, 꺼낼 때마다 &lt;code&gt;next&lt;/code&gt;를 n번 훑으므로 O(n&amp;sup2;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합쳐서 &lt;b&gt;O(n&amp;sup2;)&lt;/b&gt;. n &amp;le; 200이므로 40,000회로 여유롭다.&lt;/p&gt;
&lt;h2 id=&quot;section8&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;7&lt;/span&gt; 고르지 않은 선택지&lt;/h2&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;방법&lt;/th&gt;
&lt;th&gt;코드량&lt;/th&gt;
&lt;th&gt;판단&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;BFS&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;짧음&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;선택&lt;/span&gt; 큐 동작이 눈에 보여 추적이 쉽다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;DFS (재귀)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;더 짧음&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-blue&quot;&gt;동등&lt;/span&gt; n=200이라 스택 깊이도 안전하다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;유니온 파인드&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-amber&quot;&gt;과함&lt;/span&gt; 간선을 다 합친 뒤 서로 다른 루트를 세는 방식. 맞지만 이 문제에는 과하다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DFS로 바꾸면 큐 없이 다음과 같이 된다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;cdot cdot-r&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-y&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-g&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;Java &amp;mdash; DFS 버전&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body gradle&quot;&gt;&lt;code&gt;private void dfs(int current, int n, int[][] computers, boolean[] visited) {
    visited[current] = true;

    for (int next = 0; next &amp;lt; n; next++) {
        if (computers[current][next] == 1 &amp;amp;&amp;amp; !visited[next]) {
            dfs(next, n, computers, visited);
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바깥 루프 구조는 완전히 동일하다. &lt;b&gt;&quot;탐색 시작 횟수 = 연결 요소 개수&quot;&lt;/b&gt;라는 틀이 핵심이고, 그 안을 BFS로 채울지 DFS로 채울지는 부차적인 선택이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 99.8837%; height: 22px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style11&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 100%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;잘못된 내용&lt;/span&gt;이 있다면&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;지적&lt;/span&gt;부탁드립니다. 방문해주셔서 감사합니다.&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;face&quot; data-emoticon-name=&quot;055&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/055.png&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/055.png&quot; width=&quot;80&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>알고리즘 &amp;amp; 코딩테스트/BFS &amp;bull; DFS</category>
      <category>BFS</category>
      <category>java</category>
      <category>너비우선탐색</category>
      <category>네트워크</category>
      <category>프로그래머스</category>
      <author>곱마2</author>
      <guid isPermaLink="true">https://minsu092274.tistory.com/222</guid>
      <comments>https://minsu092274.tistory.com/222#entry222comment</comments>
      <pubDate>Tue, 4 Aug 2026 14:14:49 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스 Lv.2] 조이스틱(Java) - 그리디</title>
      <link>https://minsu092274.tistory.com/221</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;summary-box&quot;&gt;
&lt;div class=&quot;summary-label&quot;&gt;
&lt;style&gt;@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&amp;family=JetBrains+Mono:wght@400;600&amp;display=swap');/* ── 공통 ── */.blog-post { font-family: 'Noto Sans KR', sans-serif; color: #334155; line-height: 1.8; font-size: 15px; max-width: 780px; margin: 0 auto; padding: 2rem 0; }.blog-post p { margin-bottom: 1rem; }.blog-post strong { color: #0f172a; }.blog-post ul, .blog-post ol { padding-left: 1.5rem; margin-bottom: 1rem; }.blog-post li { margin: 5px 0; font-size: 14px; }.blog-post code { font-family: 'JetBrains Mono', monospace; font-size: 12.5px; background: #f1f5f9; border: 1px solid #e2e8f0; border-radius: 4px; padding: 1px 6px; color: #c7254e; }.blog-post hr { border: none; border-top: 1px solid #e2e8f0; margin: 2.5rem 0; }/* ── 02 핵심 요약 박스 ── */.summary-box { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 10px; padding: 1.25rem 1.5rem; margin-bottom: 2rem; }.summary-label { font-size: 12px; font-weight: 700; color: #2563eb; letter-spacing: 0.06em; margin-bottom: 0.75rem; }.summary-box ul { padding-left: 1.25rem; margin: 0; }.summary-box li { font-size: 14px; color: #334155; margin: 5px 0; line-height: 1.75; }.summary-box li strong { color: #0f172a; }/* ── 03 목차 ── */.my-toc { border-radius: 12px; overflow: hidden; margin-bottom: 2.5rem; border: 1px solid #162d4a; }.my-toc-header { background: #1e3a5f; padding: 16px 22px; display: flex; align-items: center; gap: 10px; }.my-toc-header-bar { width: 3px; height: 22px; background: #93c5fd; border-radius: 2px; flex-shrink: 0; }.my-toc-header-text { font-size: 17px; font-weight: 700; color: #f1f5f9; }.my-toc-body { background: #f8fafc; padding: 12px 16px 14px; }.my-toc-list { display: flex; flex-direction: column; gap: 2px; }.my-toc-list &gt; div &gt; a { display: flex; align-items: center; gap: 12px; padding: 11px 14px; border-radius: 8px; font-size: 15px; font-weight: 500; color: #1e3a5f; text-decoration: none; transition: background 0.15s, color 0.15s; }.my-toc-list &gt; div &gt; a:hover { background: #dbeafe; color: #1d4ed8; }.my-toc-list &gt; div &gt; a:hover .my-toc-num { background: #1e3a5f; color: #fff; }.my-toc-num { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; min-width: 26px; background: #e2e8f0; color: #334155; border-radius: 6px; font-size: 13px; font-weight: 700; flex-shrink: 0; transition: background 0.15s, color 0.15s; }.my-toc-num.active { background: #1e3a5f; color: #fff; }.my-toc-list &gt; div &gt; a.my-toc-active { background: #dbeafe; color: #1e3a5f; }.my-toc-list &gt; div &gt; a.my-toc-active .my-toc-num { background: #1e3a5f; color: #fff; }.my-toc-sub { display: flex; flex-direction: column; gap: 1px; padding: 2px 0 4px 52px; }.my-toc-sub div a { display: flex; align-items: center; gap: 8px; padding: 7px 10px; border-radius: 6px; font-size: 14px; color: #475569; text-decoration: none; transition: background 0.15s, color 0.15s; }.my-toc-sub div a::before { content: ''; display: inline-block; width: 5px; height: 5px; min-width: 5px; background: #94a3b8; border-radius: 50%; flex-shrink: 0; transition: background 0.15s; }.my-toc-sub div a:hover { background: #f1f5f9; color: #1e3a5f; }.my-toc-sub div a:hover::before { background: #1e3a5f; }.my-toc-sub div a.my-toc-active { color: #1e3a5f; }.my-toc-sub div a.my-toc-active::before { background: #1e3a5f; }/* ── 04 섹션 제목 ── */.my-section-title { font-size: 20px; font-weight: 700; color: #0f172a; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 2px solid #2563eb; display: flex; align-items: center; gap: 10px; }.my-section-num { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; background: #2563eb; color: #fff; border-radius: 50%; font-size: 13px; font-weight: 700; flex-shrink: 0; }.my-sub-title { font-size: 16px; font-weight: 600; color: #0f172a; margin: 1.5rem 0 0.75rem; padding-left: 12px; border-left: 3px solid #2563eb; }.section-status { font-size: 14px; font-weight: 400; margin-left: 8px; }.status-fail { color: #ef4444; }.status-ok   { color: #10b981; }/* ── 05 코드 블록 ── */.code-wrap { margin: 1.25rem 0; border-radius: 10px; overflow: hidden; border: 1px solid #1e293b; box-shadow: 0 4px 16px rgba(15,23,42,0.12); }.code-header { background: #1e293b; padding: 8px 16px; display: flex; align-items: center; justify-content: space-between; }.code-dots { display: flex; gap: 6px; }.cdot { width: 12px; height: 12px; border-radius: 50%; }.cdot-r { background: #ff5f57; }.cdot-y { background: #febc2e; }.cdot-g { background: #28c840; }.code-lang { font-size: 11px; color: #64748b; font-family: 'JetBrains Mono', monospace; letter-spacing: 0.05em; }pre.code-body { background: #0d1117; padding: 1.25rem 1.5rem; margin: 0; overflow-x: auto; font-family: 'JetBrains Mono', monospace; font-size: 13px; line-height: 1.75; color: #cbd5e1; }.c-blue   { color: #93c5fd; }.c-green  { color: #4ade80; }.c-lime   { color: #86efac; }.c-teal   { color: #a5f3fc; }.c-purple { color: #f0abfc; }.c-cyan   { color: #6ee7b7; }.c-yellow { color: #fde68a; }.c-gray   { color: #475569; font-style: italic; }.c-base   { color: #cbd5e1; }/* ── 06 강조 박스 ── */.tip-box, .warn-box, .info-box, .danger-box { border-radius: 10px; padding: 1rem 1.25rem; margin: 1.25rem 0; display: flex; gap: 12px; align-items: flex-start; }.tip-box    { background: #ecfdf5; border: 1px solid #a7f3d0; }.warn-box   { background: #fffbeb; border: 1px solid #fde68a; }.info-box   { background: #eff6ff; border: 1px solid #bfdbfe; }.danger-box { background: #fef2f2; border: 1px solid #fecaca; }.box-icon    { font-size: 18px; line-height: 1; flex-shrink: 0; margin-top: 2px; }.box-body    { font-size: 14px; line-height: 1.75; flex: 1; }.box-title   { font-weight: 700; margin-bottom: 4px; font-size: 13px; }.box-content { margin: 0; }.tip-box    .box-title   { color: #047857; }.tip-box    .box-content { color: #065f46; }.warn-box   .box-title   { color: #b45309; }.warn-box   .box-content { color: #92400e; }.info-box   .box-title   { color: #2563eb; }.info-box   .box-content { color: #1e40af; }.danger-box .box-title   { color: #b91c1c; }.danger-box .box-content { color: #991b1b; }/* ── 07 비교 표 ── */.table-wrap { overflow-x: auto; margin: 1.25rem 0; }.blog-table { width: 100%; border-collapse: collapse; font-size: 14px; }.blog-table thead tr { background: #0f172a; color: #fff; }.blog-table thead th { padding: 11px 16px; text-align: left; font-weight: 500; font-size: 13px; }.blog-table tbody tr { border-bottom: 1px solid #e2e8f0; }.blog-table tbody tr:last-child { border-bottom: none; }.blog-table tbody td { padding: 11px 16px; color: #334155; vertical-align: middle; }.blog-table tbody tr:nth-child(even) { background: #f8fafc; }.badge-green { display: inline-block; background: #ecfdf5; color: #047857; border: 1px solid #a7f3d0; border-radius: 20px; padding: 1px 10px; font-size: 12px; font-weight: 500; }.badge-red   { display: inline-block; background: #fef2f2; color: #b91c1c; border: 1px solid #fecaca; border-radius: 20px; padding: 1px 10px; font-size: 12px; font-weight: 500; }.badge-blue  { display: inline-block; background: #eff6ff; color: #2563eb; border: 1px solid #bfdbfe; border-radius: 20px; padding: 1px 10px; font-size: 12px; font-weight: 500; }.badge-amber { display: inline-block; background: #fffbeb; color: #b45309; border: 1px solid #fde68a; border-radius: 20px; padding: 1px 10px; font-size: 12px; font-weight: 500; }/* ── 08 문제 카드 ── */.problem-card { border: 1px solid #e2e8f0; border-radius: 10px; overflow: hidden; margin: 1.25rem 0; }.problem-card-header { background: #0f172a; color: #fff; padding: 12px 18px; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; }.prob-num   { font-size: 12px; color: #64748b; font-family: 'JetBrains Mono', monospace; }.prob-title { font-size: 15px; font-weight: 600; margin-top: 2px; }.prob-meta  { font-size: 12px; color: #64748b; text-align: right; line-height: 1.6; }.diff-gold   { color: #fbbf24; }.diff-silver { color: #94a3b8; }.diff-bronze { color: #d97706; }.diff-green  { color: #4ade80; }.problem-card-body { padding: 1.25rem 1.5rem; }.problem-card-body p { font-size: 14px; color: #334155; margin-bottom: 0.75rem; line-height: 1.75; }.prob-sub { font-size: 16px; font-weight: 600; color: #0f172a; margin: 1rem 0 0.6rem; padding-left: 12px; border-left: 3px solid #2563eb; }.step-list { padding-left: 0; list-style: none; counter-reset: steps; }.step-list li { counter-increment: steps; padding: 8px 0 8px 2.5rem; border-bottom: 1px dashed #e2e8f0; font-size: 14px; color: #334155; position: relative; line-height: 1.7; }.step-list li:last-child { border-bottom: none; }.step-list li::before { content: counter(steps); position: absolute; left: 0; top: 9px; display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; background: #2563eb; color: #fff; border-radius: 50%; font-size: 11px; font-weight: 700; }/* ── 포스트 푸터 ── */.post-footer { margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid #e2e8f0; font-size: 13px; color: #94a3b8; text-align: center; line-height: 2; }&lt;/style&gt;
&lt;/div&gt;
&lt;div class=&quot;summary-label&quot;&gt;  핵심 요약&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;상하 이동&lt;/b&gt; &amp;mdash; 아래 방향 비용은 &lt;code&gt;'Z' - c&lt;/code&gt;가 아니라 &lt;code&gt;'Z' - c + 1&lt;/code&gt;이다. 커서는 A에서 출발하므로 A &amp;rarr; Z는 1회다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;좌우 이동&lt;/b&gt; &amp;mdash; &lt;code&gt;length - 1&lt;/code&gt;은 최선이 아니다. A는 방문할 필요가 없으므로 되돌아가서 반대편으로 도는 편이 쌀 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;핵심 식&lt;/b&gt; &amp;mdash; 왕복하는 구간에만 2가 곱해진다. 종착지가 되는 구간은 1배로 남는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;nav class=&quot;my-toc&quot;&gt;
&lt;div class=&quot;my-toc-header&quot;&gt;
&lt;div class=&quot;my-toc-header-bar&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;my-toc-header-text&quot;&gt;목차&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;my-toc-body&quot;&gt;&lt;!-- h2/h3 태그를 읽어 자동 생성됩니다 --&gt;&lt;/div&gt;
&lt;/nav&gt;
&lt;h2 id=&quot;section1&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;1&lt;/span&gt; 문제 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조이스틱으로 이름을 만드는 문제다. 처음에는 모든 자리가 &lt;code&gt;A&lt;/code&gt;로 채워져 있고, 목표 문자열이 되도록 조작 횟수를 최소화해야 한다.&lt;/p&gt;
&lt;div class=&quot;problem-card&quot;&gt;
&lt;div class=&quot;problem-card-header&quot;&gt;
&lt;div&gt;
&lt;div class=&quot;prob-num&quot;&gt;프로그래머스 #42860&lt;/div&gt;
&lt;div class=&quot;prob-title&quot;&gt;조이스틱&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;prob-meta&quot;&gt;
&lt;div&gt;이름 길이 &amp;le; 20 &amp;middot; 대문자로만 구성&lt;/div&gt;
&lt;div&gt;난이도: &lt;span class=&quot;diff-silver&quot;&gt;Lv.2&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;problem-card-body&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조작은 네 가지다. 위/아래는 현재 커서 위치의 알파벳을 바꾸고, 좌/우는 커서를 옮긴다. 커서는 맨 왼쪽에서 시작하며, 좌우 이동은 양 끝이 이어져 있다.&lt;/p&gt;
&lt;div class=&quot;prob-sub&quot;&gt;풀이 과정&lt;/div&gt;
&lt;ol class=&quot;step-list&quot; style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;각 문자를 만드는 데 필요한 상하 조작 횟수를 구해 전부 더한다.&lt;/li&gt;
&lt;li&gt;모든 문자를 방문하는 좌우 이동 경로 중 최소 비용을 구한다.&lt;/li&gt;
&lt;li&gt;두 값을 합산한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상하와 좌우는 서로 영향을 주지 않는다. 어느 순서로 이동하든 각 자리에서 눌러야 할 상하 횟수는 동일하기 때문이다. 따라서 두 문제를 분리해 풀 수 있다고 판단했다.&lt;/p&gt;
&lt;h2 id=&quot;section2&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;2&lt;/span&gt; 상하 이동 계산&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커서가 어떤 자리에 도착했을 때 그 자리의 문자는 항상 &lt;code&gt;A&lt;/code&gt;다. 여기서 목표 문자 &lt;code&gt;c&lt;/code&gt;로 가는 방법은 두 가지다.&lt;/p&gt;
&lt;h3 id=&quot;section2-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;위 방향과 아래 방향&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 방향은 A &amp;rarr; B &amp;rarr; C 순으로 올라간다. 목표가 &lt;code&gt;c&lt;/code&gt;라면 &lt;code&gt;c - 'A'&lt;/code&gt;번 누르면 된다. 아래 방향은 A &amp;rarr; Z &amp;rarr; Y 순으로 내려간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여기서 A &amp;rarr; Z가 1회라는 점이 중요하다.&lt;/b&gt;&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;목표 문자&lt;/th&gt;
&lt;th&gt;위 방향 &lt;code&gt;c - 'A'&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;아래 방향 (정답)&lt;/th&gt;
&lt;th&gt;&lt;code&gt;'Z' - c&lt;/code&gt;로 계산하면&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;A&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;N&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Y&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Z&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;1&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-red&quot;&gt;0&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;'Z' - c&lt;/code&gt;는 &lt;b&gt;Z까지의 거리&lt;/b&gt;일 뿐 &lt;b&gt;A에서 Z로 가는 비용&lt;/b&gt;이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 정답보다 1이 작다. 따라서 아래 방향 비용은 &lt;code&gt;'Z' - c + 1&lt;/code&gt;이 되어야 한다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;Java &amp;mdash; 상하 이동 누적&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body arduino&quot;&gt;&lt;code&gt;char c = name.charAt(i);
answer += Math.min(c - 'A', 'Z' - c + 1);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section3&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;3&lt;/span&gt; 좌우 이동은 왜 length - 1이 아닌가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 왼쪽 끝에서 오른쪽 끝까지 한 번 훑으면 되므로 &lt;code&gt;length - 1&lt;/code&gt;이라고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 &lt;b&gt;이미 A인 자리는 방문할 필요가 없다.&lt;/b&gt; 그 자리를 지나가는 것 자체가 낭비다.&lt;/p&gt;
&lt;h3 id=&quot;section3-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;BBBAAAB 추적&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;길이 7이고 인덱스 0, 1, 2, 6만 방문하면 되는 경우다. 인덱스 3, 4, 5는 이미 A라 들를 이유가 없다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;전략&lt;/th&gt;
&lt;th&gt;경로&lt;/th&gt;
&lt;th&gt;이동 횟수&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;쭉 오른쪽&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;0 &amp;rarr; 1 &amp;rarr; 2 &amp;rarr; 3 &amp;rarr; 4 &amp;rarr; 5 &amp;rarr; 6&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;오른쪽 먼저&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;0 &amp;rarr; 2 (2회) &amp;rarr; 되돌아 0 (2회) &amp;rarr; 왼쪽으로 6 (1회)&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;왼쪽 먼저&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;0 &amp;rarr; 6 (1회) &amp;rarr; 되돌아 0 (1회) &amp;rarr; 오른쪽으로 2 (2회)&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;4&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쭉 오른쪽으로 가는 경로는 아무 볼일 없는 A 세 칸을 굳이 밟는다. 되돌아오는 비용을 지불하더라도 A 구간을 건너뛰는 편이 이득일 수 있다는 뜻이다.&lt;/p&gt;
&lt;div class=&quot;info-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt;ℹ️&lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;양 끝이 이어져 있다&lt;/div&gt;
원점(인덱스 0)에서 왼쪽 키를 1번 누르면 마지막 인덱스에 도착한다. 따라서 원점에서 인덱스 &lt;code&gt;j&lt;/code&gt;까지 왼쪽 방향으로 가는 비용은 &lt;code&gt;length - j&lt;/code&gt;다.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section4&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;4&lt;/span&gt; turnRight와 turnLeft 수식 분해&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 식이 구하려는 값은 &lt;b&gt;커서가 좌우 화살표를 누른 총 횟수&lt;/b&gt;다. 상하 조작은 포함하지 않는다.&lt;/p&gt;
&lt;h3 id=&quot;section4-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;변수의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&quot;BBBAAAB&quot;&lt;/code&gt;를 기준으로 고정한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;원점(0)&lt;/b&gt; &amp;mdash; 커서의 시작 위치. 모든 거리는 여기를 기준으로 잰다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;N (length)&lt;/b&gt; &amp;mdash; 전체 길이. 여기서는 7.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;i&lt;/b&gt; &amp;mdash; 원점에서 오른쪽으로 곧장 걸어가 처리할 마지막 지점. 여기서는 2.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;next&lt;/b&gt; &amp;mdash; i 뒤에 이어지는 A를 전부 건너뛴 뒤 다시 처리해야 할 가장 왼쪽 지점. 인덱스 3, 4, 5가 A이므로 6.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;section4-2&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;turnRight &amp;mdash; 오른쪽 갔다가 되돌아와 왼쪽으로 도는 경로&lt;/h3&gt;
&lt;ol class=&quot;step-list&quot; style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;원점에서 i까지 오른쪽으로 간다.&lt;/b&gt; 0 &amp;rarr; 1 &amp;rarr; 2. 왼쪽 구간을 여기서 다 처리한다. 비용 &lt;code&gt;i&lt;/code&gt; = 2&lt;/li&gt;
&lt;li&gt;&lt;b&gt;i에서 원점으로 되돌아온다.&lt;/b&gt; 2 &amp;rarr; 1 &amp;rarr; 0. 이미 처리한 칸을 되짚는 순수 비용이다. 비용 &lt;code&gt;i&lt;/code&gt; = 2&lt;/li&gt;
&lt;li&gt;&lt;b&gt;원점에서 왼쪽 방향으로 next까지 간다.&lt;/b&gt; 왼쪽 키 1번으로 인덱스 6 도착. 비용 &lt;code&gt;N - next&lt;/code&gt; = 1&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합산하면 1단계의 &lt;code&gt;i&lt;/code&gt;, 2단계의 &lt;code&gt;i&lt;/code&gt;, 3단계의 &lt;code&gt;N - next&lt;/code&gt;가 더해져 &lt;code&gt;i * 2 + (N - next)&lt;/code&gt; = 5가 된다.&lt;/p&gt;
&lt;div class=&quot;tip-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt; &lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;왜 i에만 2가 곱해지는가&lt;/div&gt;
오른쪽 구간은 갈 때 한 번, 돌아올 때 한 번 총 두 번 밟는다. 반면 왼쪽 구간은 이 경로의 &lt;b&gt;종착지&lt;/b&gt;라 돌아올 필요가 없다. 마지막 문자를 바꾸고 나면 커서는 그대로 멈춘다.&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&quot;section4-3&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;turnLeft &amp;mdash; 왼쪽으로 먼저 돌았다가 되돌아와 오른쪽으로 가는 경로&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 틀로 다시 따라간다. 순서만 뒤집힌 경로다.&lt;/p&gt;
&lt;ol class=&quot;step-list&quot; style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;원점에서 왼쪽 방향으로 next까지 간다.&lt;/b&gt; 왼쪽 키 1번으로 인덱스 6 도착. 비용 &lt;code&gt;N - next&lt;/code&gt; = 1&lt;/li&gt;
&lt;li&gt;&lt;b&gt;next에서 원점으로 되돌아온다.&lt;/b&gt; 오른쪽 키 1번. 비용 &lt;code&gt;N - next&lt;/code&gt; = 1&lt;/li&gt;
&lt;li&gt;&lt;b&gt;원점에서 i까지 오른쪽으로 간다.&lt;/b&gt; 0 &amp;rarr; 1 &amp;rarr; 2. 여기가 종착지다. 비용 &lt;code&gt;i&lt;/code&gt; = 2&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;합산하면 1단계와 2단계의 &lt;code&gt;N - next&lt;/code&gt;, 3단계의 &lt;code&gt;i&lt;/code&gt;가 더해져 &lt;code&gt;(N - next) * 2 + i&lt;/code&gt; = 4가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 왼쪽 구간이 먼저 갔다가 돌아와야 하는 구간이 되었으므로 2배가 붙었고, &lt;code&gt;i&lt;/code&gt;는 종착지가 되어 1배로 남았다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;오른쪽 구간 (i)&lt;/th&gt;
&lt;th&gt;왼쪽 구간 (N - next)&lt;/th&gt;
&lt;th&gt;BBBAAAB 결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;turnRight&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-amber&quot;&gt;왕복 &amp;rarr; 2배&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;편도 &amp;rarr; 1배&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;turnLeft&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;편도 &amp;rarr; 1배&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-amber&quot;&gt;왕복 &amp;rarr; 2배&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;4&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 식은 완전히 같은 구조이고 &lt;b&gt;어느 구간을 왕복할 것인가&lt;/b&gt;만 다르다. 짧은 쪽을 왕복해야 손해가 적으므로 두 값 중 작은 쪽을 취한다.&lt;/p&gt;
&lt;h2 id=&quot;section5&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;5&lt;/span&gt; 연속된 A가 없을 때&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A가 흩어져 있는 &lt;code&gt;&quot;BBBABA&quot;&lt;/code&gt;로도 확인했다. 길이 6이고 방문해야 할 인덱스는 0, 1, 2, 4다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;i&lt;/th&gt;
&lt;th&gt;next&lt;/th&gt;
&lt;th&gt;건너뛰는 A&lt;/th&gt;
&lt;th&gt;turnRight&lt;/th&gt;
&lt;th&gt;turnLeft&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;0&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;0 + 5 = 5&lt;/td&gt;
&lt;td&gt;10 + 0 = 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;1&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;2 + 4 = 6&lt;/td&gt;
&lt;td&gt;8 + 1 = 9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;인덱스 3&lt;/td&gt;
&lt;td&gt;4 + 2 = 6&lt;/td&gt;
&lt;td&gt;4 + 2 = 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;3&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;6 + 2 = 8&lt;/td&gt;
&lt;td&gt;4 + 3 = 7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;인덱스 5&lt;/td&gt;
&lt;td&gt;8 + 0 = 8&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;0 + 4 = 4&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;10 + 0 = 10&lt;/td&gt;
&lt;td&gt;0 + 5 = 5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;i = 4&lt;/code&gt;일 때 &lt;code&gt;next&lt;/code&gt;가 6이 되어 &lt;code&gt;N - next&lt;/code&gt;가 0이다. 왼쪽으로 갈 곳이 없다는 뜻이고, 실제 동작은 &amp;ldquo;그냥 오른쪽으로 인덱스 4까지 걸어가고 멈춘다&amp;rdquo;가 된다. 인덱스 5의 A까지 갈 이유가 없으므로 &lt;code&gt;length - 1&lt;/code&gt;인 5보다 1칸을 아낀다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;&quot;BBBAAAB&quot;&lt;/code&gt;에서는 &lt;b&gt;가운데&lt;/b&gt; A 구간을 건너뛰는 것이 이득이었고, &lt;code&gt;&quot;BBBABA&quot;&lt;/code&gt;에서는 &lt;b&gt;끝&lt;/b&gt;의 A를 밟지 않고 멈추는 것이 이득이다. 같은 식이 두 상황을 모두 잡아낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;i = 0&lt;/code&gt;의 &lt;code&gt;turnRight&lt;/code&gt;가 정확히 5, 즉 &lt;code&gt;length - 1&lt;/code&gt;과 같다는 점도 확인했다. &lt;code&gt;i = 0&lt;/code&gt;이면 오른쪽 왕복 비용이 0이고 &lt;code&gt;next&lt;/code&gt;가 1이라 &lt;code&gt;N - next&lt;/code&gt;가 &lt;code&gt;length - 1&lt;/code&gt;이 되기 때문이다. 초기값은 사실 이 식의 특수한 경우 중 하나다.&lt;/p&gt;
&lt;div class=&quot;info-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt;ℹ️&lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;while이 0번 도는 경우&lt;/div&gt;
A가 뒤따르지 않는 i에서도 식은 그대로 성립한다. 그때는 &amp;ldquo;0..i는 오른쪽으로, i+1..N-1은 왼쪽으로 처리&amp;rdquo;라는 단순한 분할이 되고, 건너뛰는 칸이 없으니 대체로 &lt;code&gt;length - 1&lt;/code&gt;보다 나빠질 뿐이다. 별도 예외 처리가 필요 없다.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section6&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;6&lt;/span&gt; 전체 코드&lt;/h2&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;Java &amp;mdash; Solution.java&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body arduino&quot;&gt;&lt;code&gt;class Solution {
    public int solution(String name) {
        int answer = 0;
        int length = name.length();
        // 순차적으로 끝까지 오른쪽으로 가는 경우
        int move = length - 1;

        for (int i = 0; i &amp;lt; length; i++) {
            // 1. 상하 이동 계산
            char c = name.charAt(i);
            answer += Math.min(c - 'A', 'Z' - c + 1);

            // 2. 연속된 'A'의 끝 위치(next) 찾기
            int next = i + 1;
            while (next &amp;lt; length &amp;amp;&amp;amp; name.charAt(next) == 'A') {
                next++;
            }

            // 3. 좌우 이동 계산
            int turnRight = (i * 2) + (length - next);
            int turnLeft  = ((length - next) * 2) + i;
            move = Math.min(move, Math.min(turnRight, turnLeft));
        }
        // 상하 이동 횟수 + 최소 좌우 이동 횟수
        return answer + move;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름 길이가 최대 20이므로 이중 루프여도 최악 400회 수준이다. 규모를 먼저 확인했고, 모든 &lt;code&gt;i&lt;/code&gt;를 후보로 놓고 전부 계산해도 부담이 없다고 판단했다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 99.8837%; height: 22px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style11&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 100%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;잘못된 내용&lt;/span&gt;이 있다면&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;지적&lt;/span&gt;부탁드립니다. 방문해주셔서 감사합니다.&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;face&quot; data-emoticon-name=&quot;055&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/055.png&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/055.png&quot; width=&quot;80&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>알고리즘 &amp;amp; 코딩테스트/그리디 (Greedy)</category>
      <category>greedy</category>
      <category>java</category>
      <category>그리디</category>
      <category>알고리즘</category>
      <category>완전탐색</category>
      <category>자바</category>
      <category>조이스틱</category>
      <category>코딩테스트</category>
      <category>프로그래머스</category>
      <author>곱마2</author>
      <guid isPermaLink="true">https://minsu092274.tistory.com/221</guid>
      <comments>https://minsu092274.tistory.com/221#entry221comment</comments>
      <pubDate>Sun, 2 Aug 2026 08:15:39 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스 Lv.2] 숫자 블록(Java) - 약수</title>
      <link>https://minsu092274.tistory.com/220</link>
      <description>&lt;div&gt;
&lt;style&gt;@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&amp;family=JetBrains+Mono:wght@400;600&amp;display=swap');/* ── 공통 ── */.blog-post{font-family:'Noto Sans KR',sans-serif;color:#334155;line-height:1.8;font-size:15px;max-width:780px;margin:0 auto;padding:2rem 0;}.blog-post p{margin-bottom:1rem;}.blog-post strong{color:#0f172a;}.blog-post ul,.blog-post ol{padding-left:1.5rem;margin-bottom:1rem;}.blog-post li{margin:5px 0;font-size:14px;}.blog-post code{font-family:'JetBrains Mono',monospace;font-size:12.5px;background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:1px 6px;color:#c7254e;}.blog-post hr{border:none;border-top:1px solid #e2e8f0;margin:2.5rem 0;}/* ── 01 포스트 헤더 ── */.post-header{margin-bottom:2.5rem;padding-bottom:2rem;border-bottom:1px solid #e2e8f0;}.post-tags{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:1rem;}.tag{font-size:12px;padding:3px 12px;border-radius:20px;font-weight:500;}.tag-blue  {background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe;}.tag-green {background:#ecfdf5;color:#059669;border:1px solid #a7f3d0;}.tag-amber {background:#fffbeb;color:#b45309;border:1px solid #fde68a;}.tag-red   {background:#fef2f2;color:#b91c1c;border:1px solid #fecaca;}.tag-gray  {background:#f1f5f9;color:#475569;border:1px solid #e2e8f0;}.post-title{font-size:28px;font-weight:700;color:#0f172a;line-height:1.35;margin:0 0 0.75rem;letter-spacing:-0.3px;}.post-meta{font-size:13px;color:#94a3b8;display:flex;gap:16px;flex-wrap:wrap;}.post-meta .dot::before{content:'·';margin-right:6px;opacity:0.4;}/* ── 02 핵심 요약 박스 ── */.summary-box{background:#eff6ff;border:1px solid #bfdbfe;border-radius:10px;padding:1.25rem 1.5rem;margin-bottom:2rem;}.summary-box .summary-label{font-size:12px;font-weight:700;color:#2563eb;letter-spacing:0.06em;margin-bottom:0.75rem;}.summary-box ul{padding-left:1.25rem;margin:0;}.summary-box li{font-size:14px;color:#334155;margin:5px 0;line-height:1.75;}.summary-box li strong{color:#0f172a;}/* ── 03 목차 (my-toc) ── */.my-toc{border-radius:12px;overflow:hidden;margin-bottom:2.5rem;border:1px solid #162d4a;}.my-toc-header{background:#1e3a5f;padding:16px 22px;display:flex;align-items:center;gap:10px;}.my-toc-header-bar{width:3px;height:22px;background:#93c5fd;border-radius:2px;flex-shrink:0;}.my-toc-header-text{font-size:17px;font-weight:700;color:#f1f5f9;}.my-toc-body{background:#f8fafc;padding:12px 16px 14px;}.my-toc-list{display:flex;flex-direction:column;gap:2px;}.my-toc-list&gt;div&gt;a{display:flex;align-items:center;gap:12px;padding:11px 14px;border-radius:8px;font-size:15px;font-weight:500;color:#1e3a5f;text-decoration:none;transition:background 0.15s,color 0.15s;}.my-toc-list&gt;div&gt;a:hover{background:#dbeafe;color:#1d4ed8;}.my-toc-list&gt;div&gt;a:hover .my-toc-num{background:#1e3a5f;color:#fff;}.my-toc-num{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;min-width:26px;background:#e2e8f0;color:#334155;border-radius:6px;font-size:13px;font-weight:700;flex-shrink:0;transition:background 0.15s,color 0.15s;}.my-toc-num.active{background:#1e3a5f;color:#fff;}.my-toc-list&gt;div&gt;a.my-toc-active{background:#dbeafe;color:#1e3a5f;}.my-toc-list&gt;div&gt;a.my-toc-active .my-toc-num{background:#1e3a5f;color:#fff;}.my-toc-sub{display:flex;flex-direction:column;gap:1px;padding:2px 0 4px 52px;}.my-toc-sub div a{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:6px;font-size:14px;color:#475569;text-decoration:none;transition:background 0.15s,color 0.15s;}.my-toc-sub div a::before{content:'';display:inline-block;width:5px;height:5px;min-width:5px;background:#94a3b8;border-radius:50%;flex-shrink:0;transition:background 0.15s;}.my-toc-sub div a:hover{background:#f1f5f9;color:#1e3a5f;}.my-toc-sub div a:hover::before{background:#1e3a5f;}.my-toc-sub div a.my-toc-active{color:#1e3a5f;}.my-toc-sub div a.my-toc-active::before{background:#1e3a5f;}/* ── 04 섹션 제목 (my-section) ── */.my-section{margin-bottom:3rem;}.my-section-title{font-size:20px;font-weight:700;color:#0f172a;margin-bottom:1rem;padding-bottom:0.5rem;border-bottom:2px solid #2563eb;display:flex;align-items:center;gap:10px;}.my-section-num{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;background:#2563eb;color:#fff;border-radius:50%;font-size:13px;font-weight:700;flex-shrink:0;}.my-sub-title{font-size:16px;font-weight:600;color:#0f172a;margin:1.5rem 0 0.75rem;padding-left:12px;border-left:3px solid #2563eb;}/* ── 05 코드 블록 ── */.code-wrap{margin:1.25rem 0;border-radius:10px;overflow:hidden;border:1px solid #1e293b;box-shadow:0 4px 16px rgba(15,23,42,0.12);}.code-header{background:#1e293b;padding:8px 16px;display:flex;align-items:center;justify-content:space-between;}.code-dots{display:flex;gap:6px;}.cdot{width:12px;height:12px;border-radius:50%;}.cdot-r{background:#ff5f57;}.cdot-y{background:#febc2e;}.cdot-g{background:#28c840;}.code-lang{font-size:11px;color:#64748b;font-family:'JetBrains Mono',monospace;letter-spacing:0.05em;}pre.code-body{background:#0d1117;padding:1.25rem 1.5rem;margin:0;overflow-x:auto;font-family:'JetBrains Mono',monospace;font-size:13px;line-height:1.75;color:#cbd5e1;}.c-blue{color:#93c5fd;}.c-green{color:#4ade80;}.c-lime{color:#86efac;}.c-teal{color:#a5f3fc;}.c-purple{color:#f0abfc;}.c-cyan{color:#6ee7b7;}.c-yellow{color:#fde68a;}.c-gray{color:#475569;font-style:italic;}.c-base{color:#cbd5e1;}/* ── 06 강조 박스 ── */.tip-box,.warn-box,.info-box,.danger-box{border-radius:10px;padding:1rem 1.25rem;margin:1.25rem 0;display:flex;gap:12px;align-items:flex-start;}.tip-box   {background:#ecfdf5;border:1px solid #a7f3d0;}.warn-box  {background:#fffbeb;border:1px solid #fde68a;}.info-box  {background:#eff6ff;border:1px solid #bfdbfe;}.danger-box{background:#fef2f2;border:1px solid #fecaca;}.box-icon{font-size:18px;line-height:1;flex-shrink:0;margin-top:1px;}.box-body{font-size:14px;line-height:1.75;}.box-title{font-weight:700;margin-bottom:3px;font-size:13px;}.tip-box    .box-title{color:#047857;}.tip-box    .box-body{color:#065f46;}.warn-box   .box-title{color:#b45309;}.warn-box   .box-body{color:#92400e;}.info-box   .box-title{color:#2563eb;}.info-box   .box-body{color:#1e40af;}.danger-box .box-title{color:#b91c1c;}.danger-box .box-body{color:#991b1b;}/* ── 07 비교 표 ── */.table-wrap{overflow-x:auto;margin:1.25rem 0;}.blog-table{width:100%;border-collapse:collapse;font-size:14px;}.blog-table thead tr{background:#0f172a;color:#fff;}.blog-table thead th{padding:11px 16px;text-align:left;font-weight:500;font-size:13px;}.blog-table tbody tr{border-bottom:1px solid #e2e8f0;}.blog-table tbody tr:last-child{border-bottom:none;}.blog-table tbody td{padding:11px 16px;color:#334155;vertical-align:middle;}.blog-table tbody tr:nth-child(even){background:#f8fafc;}.badge-green{display:inline-block;background:#ecfdf5;color:#047857;border:1px solid #a7f3d0;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}.badge-red  {display:inline-block;background:#fef2f2;color:#b91c1c;border:1px solid #fecaca;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}.badge-blue {display:inline-block;background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}.badge-amber{display:inline-block;background:#fffbeb;color:#b45309;border:1px solid #fde68a;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}/* ── 08 문제 카드 ── */.problem-card{border:1px solid #e2e8f0;border-radius:10px;overflow:hidden;margin:1.25rem 0;}.problem-card-header{background:#0f172a;color:#fff;padding:12px 18px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;}.prob-num{font-size:12px;color:#64748b;font-family:'JetBrains Mono',monospace;}.prob-title{font-size:15px;font-weight:600;margin-top:2px;}.prob-meta{font-size:12px;color:#64748b;text-align:right;line-height:1.6;}.diff-gold{color:#fbbf24;}.diff-silver{color:#94a3b8;}.diff-bronze{color:#d97706;}.diff-green{color:#4ade80;}.problem-card-body{padding:1.25rem 1.5rem;}.problem-card-body p{font-size:14px;color:#334155;margin-bottom:0.75rem;line-height:1.75;}.prob-sub{font-size:16px;font-weight:600;color:#0f172a;margin:1rem 0 0.6rem;padding-left:12px;border-left:3px solid #2563eb;}.step-list{padding-left:0;list-style:none;counter-reset:steps;}.step-list li{counter-increment:steps;padding:8px 0 8px 2.5rem;border-bottom:1px dashed #e2e8f0;font-size:14px;color:#334155;position:relative;line-height:1.7;}.step-list li:last-child{border-bottom:none;}.step-list li::before{content:counter(steps);position:absolute;left:0;top:9px;display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;background:#2563eb;color:#fff;border-radius:50%;font-size:11px;font-weight:700;}/* ── 포스트 푸터 ── */.post-footer{margin-top:3rem;padding-top:1.5rem;border-top:1px solid #e2e8f0;font-size:13px;color:#94a3b8;text-align:center;line-height:2;}&lt;/style&gt;
&lt;/div&gt;
&lt;div class=&quot;summary-box&quot;&gt;
&lt;div class=&quot;summary-label&quot;&gt;  핵심 요약&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;문제 재진술&lt;/b&gt; &amp;mdash; 위치 p에 남는 블록은 &lt;b&gt;p의 약수 중 p 자신을 제외한 최댓값&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;제약 반영&lt;/b&gt; &amp;mdash; 블록은 1,000만까지만 존재하므로 최대 약수가 그보다 크면 다음 약수를 찾아야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;탐색 범위&lt;/b&gt; &amp;mdash; 약수는 &lt;code&gt;i&lt;/code&gt;와 &lt;code&gt;p/i&lt;/code&gt;가 짝을 이루므로 &amp;radic;p까지만 훑으면 충분하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;조기 반환&lt;/b&gt; &amp;mdash; i가 커질수록 &lt;code&gt;p/i&lt;/code&gt;는 작아지므로, 상한 이하가 되는 첫 값이 곧 정답이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;nav class=&quot;my-toc&quot;&gt;
&lt;div class=&quot;my-toc-header&quot;&gt;
&lt;div class=&quot;my-toc-header-bar&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;my-toc-header-text&quot;&gt;목차&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;my-toc-body&quot;&gt;&lt;!-- h2/h3 태그를 읽어 자동 생성됩니다 --&gt;&lt;/div&gt;
&lt;/nav&gt;
&lt;div class=&quot;problem-card&quot;&gt;
&lt;div class=&quot;problem-card-header&quot;&gt;
&lt;div&gt;
&lt;div class=&quot;prob-num&quot;&gt;Programmers #12923&lt;/div&gt;
&lt;div class=&quot;prob-title&quot;&gt;숫자 블록&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;prob-meta&quot;&gt;
&lt;div&gt;도로 길이 10억 &amp;middot; 블록 번호 &amp;le; 1,000만 &amp;middot; 구간 &amp;le; 5,000&lt;/div&gt;
&lt;div&gt;난이도: &lt;span class=&quot;diff-silver&quot;&gt;Lv.3&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;problem-card-body&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번호 n인 블록은 n&amp;times;2, n&amp;times;3, n&amp;times;4, ... 위치에 설치된다. 블록은 1번부터 순서대로 깔리며 기존 블록을 덮어쓴다. 구간 &lt;code&gt;[begin, end]&lt;/code&gt;에 최종적으로 깔린 블록 번호 배열을 반환한다.&lt;/p&gt;
&lt;div class=&quot;prob-sub&quot;&gt;풀이 과정&lt;/div&gt;
&lt;ol class=&quot;step-list&quot; style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;위치 p 하나를 고정해 &quot;여기에 놓일 수 있는 블록&quot;을 약수로 환원한다.&lt;/li&gt;
&lt;li&gt;덮어쓰기 순서가 오름차순이므로 최댓값만 남는다.&lt;/li&gt;
&lt;li&gt;&amp;radic;p까지 훑으며 1,000만 이하가 되는 첫 짝을 반환한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section1&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;1&lt;/span&gt; 규칙을 위치 기준으로 뒤집기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지문은 블록을 기준으로 서술되어 있다. 번호 n인 블록이 n&amp;times;2, n&amp;times;3, ... 위치에 깔리고, n을 1부터 늘려 가며 기존 블록을 덮어쓴다. 이 서술을 그대로 따라가면 시뮬레이션을 떠올리게 되는데, 도로 길이가 10억이라 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 관점을 뒤집었다. 블록이 아니라 &lt;b&gt;위치 p 하나를 고정&lt;/b&gt;하고, 여기에 어떤 블록들이 거쳐 갔는지 물었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블록 n이 위치 p에 놓이려면 &lt;code&gt;p = n &amp;times; k&lt;/code&gt;이면서 &lt;code&gt;k &amp;ge; 2&lt;/code&gt;여야 한다. 첫 설치 위치가 n&amp;times;2부터이기 때문이다. 이 조건을 정리하면 이렇게 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n은 p의 약수이면서, p 자신은 아니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;section1-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;p = 12의 추적&lt;/h3&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;진행&lt;/th&gt;
&lt;th&gt;놓이는 블록&lt;/th&gt;
&lt;th&gt;근거&lt;/th&gt;
&lt;th&gt;위치 12의 상태&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;n = 1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;12 = 1 &amp;times; 12&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;n = 2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;12 = 2 &amp;times; 6&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;n = 3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;12 = 3 &amp;times; 4&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;n = 4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;12 = 4 &amp;times; 3&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;n = 6&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;12 = 6 &amp;times; 2&lt;/td&gt;
&lt;td&gt;&lt;b&gt;6&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;n = 12&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-red&quot;&gt;해당 없음&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;12&amp;times;2 = 24부터 시작&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n이 커지는 순서로 덮어쓰므로 &lt;b&gt;마지막에 남는 것은 후보 중 최댓값&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 n = 12는 자기 자리에 놓이지 않으므로 후보에서 빠진다. 즉 구할 값은 p의 최대 약수이고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 &lt;code&gt;p &amp;divide; (p의 가장 작은 소인수)&lt;/code&gt;와 같다. 가장 작은 값으로 나눌수록 몫이 커지기 때문이다.&lt;/p&gt;
&lt;h3 id=&quot;section1-2&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;예시 검증&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;begin = 1, end = 10&lt;/code&gt;일 때 기대 출력은 &lt;code&gt;[0, 1, 1, 2, 1, 3, 1, 4, 3, 5]&lt;/code&gt;다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;p&lt;/th&gt;
&lt;th&gt;자신을 제외한 약수&lt;/th&gt;
&lt;th&gt;최댓값&lt;/th&gt;
&lt;th&gt;결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;0&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1, 2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;1, 2, 3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;1, 2, 4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;1, 3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;1, 2, 5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위치 1이 0인 이유는 &lt;code&gt;n &amp;times; 2 = 1&lt;/code&gt;을 만족하는 블록이 존재하지 않아서다. 소수 위치는 약수가 1뿐이라 항상 1이 남는다.&lt;/p&gt;
&lt;h2 id=&quot;section2&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;2&lt;/span&gt; 규모 판단&lt;/h2&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;th&gt;함의&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;도로 길이&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;10억&lt;/td&gt;
&lt;td&gt;전체 배열 생성 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;구간 길이&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;최대 5,001&lt;/td&gt;
&lt;td&gt;위치별 독립 계산이 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;위치 p&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;최대 10억&lt;/td&gt;
&lt;td&gt;&amp;radic;p &amp;asymp; 31,623까지 탐색 허용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;블록 번호&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;최대 1,000만&lt;/td&gt;
&lt;td&gt;답에 상한이 걸린다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째와 세 번째 줄을 곱하면 약 1.6억이다. 상한으로만 보면 아슬아슬하지만, 실제로는 대부분의 위치가 몇 번 만에 끝난다는 점을 뒤에서 확인했다.&lt;/p&gt;
&lt;h2 id=&quot;section3&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;3&lt;/span&gt; 약수 탐색 설계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약수는 &lt;code&gt;i&lt;/code&gt;와 &lt;code&gt;p/i&lt;/code&gt;가 짝을 이루므로 &amp;radic;p까지만 훑으면 전부 확인된다. 여기에 관찰 하나를 더했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;i가 커질수록 짝인 &lt;code&gt;p/i&lt;/code&gt;는 작아진다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 원하는 것은 상한(1,000만) 이하에서 가장 큰 값이다. 따라서 i를 2부터 올리며 처음으로 &lt;code&gt;p/i &amp;le; 1,000만&lt;/code&gt;이 되는 순간이 곧 정답이고, 더 볼 필요 없이 반환할 수 있다.&lt;/p&gt;
&lt;div class=&quot;info-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt;ℹ️&lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;조기 반환이 성립하는 이유&lt;/div&gt;
i가 증가하면 p/i는 단조 감소한다. 즉 조건을 만족하는 첫 값이 후보 중 최댓값이고, 이후에 나오는 값들은 모두 그보다 작다.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section4&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;4&lt;/span&gt; 코드&lt;/h2&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;Java &amp;mdash; Solution.java&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body arduino&quot;&gt;&lt;code&gt;class Solution {
    public int[] solution(long begin, long end) {
        int b = (int) begin;
        int e = (int) end;
        int[] answer = new int[e - b + 1];

        for (int i = b; i &amp;lt;= e; i++) {
            answer[i - b] = getMaxBlock(i);
        }

        return answer;
    }

    private int getMaxBlock(int n) {
        if (n == 1) return 0;

        int maxBlock = 1; // 기본적으로 자기 자신 외 가장 작은 약수는 1

        for (int i = 2; (long) i * i &amp;lt;= n; i++) {
            if (n % i == 0) {
                // n / i가 10,000,000 이하인 경우, 이 값이 나누어떨어지는 가장 큰 약수
                if (n / i &amp;lt;= 10_000_000) {
                    return n / i;
                }

                // n / i가 10,000,000을 초과하면 i(더 작은 약수)를 후보로 저장해두고 계속 탐색
                maxBlock = i;
            }
        }

        return maxBlock;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 id=&quot;section4-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;복잡도&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위치는 최대 5,001개이고 각 위치는 최대 &amp;radic;p &amp;asymp; 31,623번 돈다. 상한을 곱하면 약 1.6억이지만, 실제 반복 횟수는 훨씬 적다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;위치 유형&lt;/th&gt;
&lt;th&gt;반복 횟수&lt;/th&gt;
&lt;th&gt;5,000개 중 대략&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;짝수&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;1회 (i=2에서 즉시 반환)&lt;/td&gt;
&lt;td&gt;2,500개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;3의 배수&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;2회&lt;/td&gt;
&lt;td&gt;800개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;소수&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;약 31,623회&lt;/td&gt;
&lt;td&gt;240개&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끝까지 도는 것은 소수뿐이고, 10억 부근에서 5,000개 중 소수의 개수는 대략 &lt;code&gt;5000 / ln(10⁹) &amp;asymp; 240&lt;/code&gt;이다. 전체 연산은 1천만 회 미만이라 여유롭게 통과한다.&lt;/p&gt;
&lt;h2 id=&quot;section5&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;5&lt;/span&gt; 실수했던 부분&lt;/h2&gt;
&lt;h3 id=&quot;section5-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;지문의 중간 상태 배열을 최종 답으로 읽었다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인.&lt;/b&gt; 문제 설명에 &lt;code&gt;[0, 1, 1, 2, 1, 3, 1, 2, 3, 2]&lt;/code&gt;라는 배열이 등장한다. 처음에는 이것이 &lt;code&gt;begin=1, end=10&lt;/code&gt;의 정답이라고 판단했다. 그런데 입출력 예의 결과는 &lt;code&gt;[0, 1, 1, 2, 1, 3, 1, 4, 3, 5]&lt;/code&gt;로 다르다. 두 배열을 같은 것으로 놓고 규칙을 역산하려 하니 앞뒤가 맞지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결.&lt;/b&gt; 지문을 다시 읽어 보니 앞의 배열은 &lt;b&gt;블록 3번까지만 깔았을 때의 중간 상태&lt;/b&gt;였다. 이후 블록 4와 5가 8번, 10번 자리를 덮어써서 최종 배열이 된다. 규칙 설명용 예시와 정답을 구분하지 않은 것이 원인이었다.&lt;/p&gt;
&lt;div class=&quot;warn-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt;⚠️&lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;두 배열의 차이&lt;/div&gt;
8번 위치: 중간 상태 2 &amp;rarr; 최종 4 (블록 4가 덮어씀). 10번 위치: 중간 상태 2 &amp;rarr; 최종 5 (블록 5가 덮어씀).&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&quot;section5-2&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;블록 번호 상한을 반영하지 않을 뻔했다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인.&lt;/b&gt; &quot;최대 약수를 구한다&quot;까지 정리한 뒤 바로 코드를 쓰려 했다. 그런데 블록은 &lt;b&gt;1,000만까지만&lt;/b&gt; 존재하고 도로는 10억이다. 이 차이를 놓치면 존재하지 않는 블록 번호를 답으로 내게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;code&gt;p = 30,000,000&lt;/code&gt;의 최대 약수는 15,000,000인데, 그런 번호의 블록은 설치된 적이 없다. 실제로는 그다음 약수인 10,000,000이 답이다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;i&lt;/th&gt;
&lt;th&gt;n / i&lt;/th&gt;
&lt;th&gt;판정&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;15,000,000&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-red&quot;&gt;상한 초과&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;maxBlock = 2, 계속&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;10,000,000&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;통과&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;반환&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결.&lt;/b&gt; 조건을 &quot;p보다 작은 최대 약수&quot;가 아니라 &lt;b&gt;&quot;p보다 작고 1,000만 이하인 최대 약수&quot;&lt;/b&gt;로 다시 적었다. 구간이 1,000만 이하에 있으면 이 제약이 무해하기 때문에, 작은 값으로만 테스트하면 끝까지 드러나지 않는 종류의 조건이었다.&lt;/p&gt;
&lt;h3 id=&quot;section5-3&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;루프 종료 시 1을 반환하려 했다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;원인.&lt;/b&gt; 조기 반환에 걸리지 않고 루프가 끝나는 경우는 소수뿐이라고 판단해, 마지막 줄을 &lt;code&gt;return 1;&lt;/code&gt;로 쓰려 했다. 그러나 반례가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;p = 999,999,998 = 2 &amp;times; 499,999,999&lt;/code&gt;를 보자. i = 2에서 나누어떨어지지만 짝인 499,999,999가 상한을 넘는다. 이후 &amp;radic;p까지 다른 약수를 찾지 못하고 루프가 끝난다. 이때 정답은 1이 아니라 &lt;b&gt;2&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결.&lt;/b&gt; &lt;code&gt;maxBlock = i&lt;/code&gt;로 후보를 저장해 두고 루프 종료 시 그 값을 반환하도록 했다. &lt;b&gt;큰 소수와 짝을 이루는 작은 약수&lt;/b&gt;가 답이 되는 경우를 담당하는 줄이다.&lt;/p&gt;
&lt;div class=&quot;danger-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt; &lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;이 줄이 없으면 놓치는 케이스&lt;/div&gt;
p가 &quot;작은 수 &amp;times; 매우 큰 소수&quot; 꼴일 때. 짝이 상한을 넘어 조기 반환에 걸리지 않고, 다른 약수도 없어 루프가 끝난다. maxBlock 없이는 1을 반환하게 된다.&lt;/div&gt;
&lt;/div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 99.8837%; height: 22px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style11&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 100%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;잘못된 내용&lt;/span&gt;이 있다면&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;지적&lt;/span&gt;부탁드립니다. 방문해주셔서 감사합니다.&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;face&quot; data-emoticon-name=&quot;055&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/055.png&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/055.png&quot; width=&quot;80&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>알고리즘 &amp;amp; 코딩테스트/수학(약수 &amp;bull; 소수 &amp;bull; 비트연산)</category>
      <category>java</category>
      <category>숫자블록</category>
      <category>알고리즘</category>
      <category>약수</category>
      <category>자바</category>
      <category>최대약수</category>
      <category>코딩테스트</category>
      <category>프로그래머스</category>
      <category>프로그래머스Lv2</category>
      <author>곱마2</author>
      <guid isPermaLink="true">https://minsu092274.tistory.com/220</guid>
      <comments>https://minsu092274.tistory.com/220#entry220comment</comments>
      <pubDate>Fri, 31 Jul 2026 18:04:40 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스 Lv.2] 요격 시스템(Java) - 그리디</title>
      <link>https://minsu092274.tistory.com/219</link>
      <description>&lt;div&gt;
&lt;style&gt;@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&amp;family=JetBrains+Mono:wght@400;600&amp;display=swap');/* ── 공통 ── */.blog-post{font-family:'Noto Sans KR',sans-serif;color:#334155;line-height:1.8;font-size:15px;max-width:780px;margin:0 auto;padding:2rem 0;}.blog-post p{margin-bottom:1rem;}.blog-post strong{color:#0f172a;}.blog-post ul,.blog-post ol{padding-left:1.5rem;margin-bottom:1rem;}.blog-post li{margin:5px 0;font-size:14px;}.blog-post code{font-family:'JetBrains Mono',monospace;font-size:12.5px;background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:1px 6px;color:#c7254e;}.blog-post hr{border:none;border-top:1px solid #e2e8f0;margin:2.5rem 0;}/* ── 01 포스트 헤더 ── */.post-header{margin-bottom:2.5rem;padding-bottom:2rem;border-bottom:1px solid #e2e8f0;}.post-tags{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:1rem;}.tag{font-size:12px;padding:3px 12px;border-radius:20px;font-weight:500;}.tag-blue  {background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe;}.tag-green {background:#ecfdf5;color:#059669;border:1px solid #a7f3d0;}.tag-amber {background:#fffbeb;color:#b45309;border:1px solid #fde68a;}.tag-red   {background:#fef2f2;color:#b91c1c;border:1px solid #fecaca;}.tag-gray  {background:#f1f5f9;color:#475569;border:1px solid #e2e8f0;}.post-title{font-size:28px;font-weight:700;color:#0f172a;line-height:1.35;margin:0 0 0.75rem;letter-spacing:-0.3px;}.post-meta{font-size:13px;color:#94a3b8;display:flex;gap:16px;flex-wrap:wrap;}.post-meta .dot::before{content:'·';margin-right:6px;opacity:0.4;}/* ── 02 핵심 요약 박스 ── */.summary-box{background:#eff6ff;border:1px solid #bfdbfe;border-radius:10px;padding:1.25rem 1.5rem;margin-bottom:2rem;}.summary-box .summary-label{font-size:12px;font-weight:700;color:#2563eb;letter-spacing:0.06em;margin-bottom:0.75rem;}.summary-box ul{padding-left:1.25rem;margin:0;}.summary-box li{font-size:14px;color:#334155;margin:5px 0;line-height:1.75;}.summary-box li strong{color:#0f172a;}/* ── 03 목차 (my-toc) ── */.my-toc{border-radius:12px;overflow:hidden;margin-bottom:2.5rem;border:1px solid #162d4a;}.my-toc-header{background:#1e3a5f;padding:16px 22px;display:flex;align-items:center;gap:10px;}.my-toc-header-bar{width:3px;height:22px;background:#93c5fd;border-radius:2px;flex-shrink:0;}.my-toc-header-text{font-size:17px;font-weight:700;color:#f1f5f9;}.my-toc-body{background:#f8fafc;padding:12px 16px 14px;}.my-toc-list{display:flex;flex-direction:column;gap:2px;}.my-toc-list&gt;div&gt;a{display:flex;align-items:center;gap:12px;padding:11px 14px;border-radius:8px;font-size:15px;font-weight:500;color:#1e3a5f;text-decoration:none;transition:background 0.15s,color 0.15s;}.my-toc-list&gt;div&gt;a:hover{background:#dbeafe;color:#1d4ed8;}.my-toc-list&gt;div&gt;a:hover .my-toc-num{background:#1e3a5f;color:#fff;}.my-toc-num{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;min-width:26px;background:#e2e8f0;color:#334155;border-radius:6px;font-size:13px;font-weight:700;flex-shrink:0;transition:background 0.15s,color 0.15s;}.my-toc-num.active{background:#1e3a5f;color:#fff;}.my-toc-list&gt;div&gt;a.my-toc-active{background:#dbeafe;color:#1e3a5f;}.my-toc-list&gt;div&gt;a.my-toc-active .my-toc-num{background:#1e3a5f;color:#fff;}.my-toc-sub{display:flex;flex-direction:column;gap:1px;padding:2px 0 4px 52px;}.my-toc-sub div a{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:6px;font-size:14px;color:#475569;text-decoration:none;transition:background 0.15s,color 0.15s;}.my-toc-sub div a::before{content:'';display:inline-block;width:5px;height:5px;min-width:5px;background:#94a3b8;border-radius:50%;flex-shrink:0;transition:background 0.15s;}.my-toc-sub div a:hover{background:#f1f5f9;color:#1e3a5f;}.my-toc-sub div a:hover::before{background:#1e3a5f;}.my-toc-sub div a.my-toc-active{color:#1e3a5f;}.my-toc-sub div a.my-toc-active::before{background:#1e3a5f;}/* ── 04 섹션 제목 (my-section) ── */.my-section{margin-bottom:3rem;}.my-section-title{font-size:20px;font-weight:700;color:#0f172a;margin-bottom:1rem;padding-bottom:0.5rem;border-bottom:2px solid #2563eb;display:flex;align-items:center;gap:10px;}.my-section-num{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;background:#2563eb;color:#fff;border-radius:50%;font-size:13px;font-weight:700;flex-shrink:0;}.my-sub-title{font-size:16px;font-weight:600;color:#0f172a;margin:1.5rem 0 0.75rem;padding-left:12px;border-left:3px solid #2563eb;}/* ── 05 코드 블록 ── */.code-wrap{margin:1.25rem 0;border-radius:10px;overflow:hidden;border:1px solid #1e293b;box-shadow:0 4px 16px rgba(15,23,42,0.12);}.code-header{background:#1e293b;padding:8px 16px;display:flex;align-items:center;justify-content:space-between;}.code-dots{display:flex;gap:6px;}.cdot{width:12px;height:12px;border-radius:50%;}.cdot-r{background:#ff5f57;}.cdot-y{background:#febc2e;}.cdot-g{background:#28c840;}.code-lang{font-size:11px;color:#64748b;font-family:'JetBrains Mono',monospace;letter-spacing:0.05em;}pre.code-body{background:#0d1117;padding:1.25rem 1.5rem;margin:0;overflow-x:auto;font-family:'JetBrains Mono',monospace;font-size:13px;line-height:1.75;color:#cbd5e1;}.c-blue{color:#93c5fd;}.c-green{color:#4ade80;}.c-lime{color:#86efac;}.c-teal{color:#a5f3fc;}.c-purple{color:#f0abfc;}.c-cyan{color:#6ee7b7;}.c-yellow{color:#fde68a;}.c-gray{color:#475569;font-style:italic;}.c-base{color:#cbd5e1;}/* ── 06 강조 박스 ── */.tip-box,.warn-box,.info-box,.danger-box{border-radius:10px;padding:1rem 1.25rem;margin:1.25rem 0;display:flex;gap:12px;align-items:flex-start;}.tip-box   {background:#ecfdf5;border:1px solid #a7f3d0;}.warn-box  {background:#fffbeb;border:1px solid #fde68a;}.info-box  {background:#eff6ff;border:1px solid #bfdbfe;}.danger-box{background:#fef2f2;border:1px solid #fecaca;}.box-icon{font-size:18px;line-height:1;flex-shrink:0;margin-top:1px;}.box-body{font-size:14px;line-height:1.75;}.box-title{font-weight:700;margin-bottom:3px;font-size:13px;}.tip-box    .box-title{color:#047857;}.tip-box    .box-body{color:#065f46;}.warn-box   .box-title{color:#b45309;}.warn-box   .box-body{color:#92400e;}.info-box   .box-title{color:#2563eb;}.info-box   .box-body{color:#1e40af;}.danger-box .box-title{color:#b91c1c;}.danger-box .box-body{color:#991b1b;}/* ── 07 비교 표 ── */.table-wrap{overflow-x:auto;margin:1.25rem 0;}.blog-table{width:100%;border-collapse:collapse;font-size:14px;}.blog-table thead tr{background:#0f172a;color:#fff;}.blog-table thead th{padding:11px 16px;text-align:left;font-weight:500;font-size:13px;}.blog-table tbody tr{border-bottom:1px solid #e2e8f0;}.blog-table tbody tr:last-child{border-bottom:none;}.blog-table tbody td{padding:11px 16px;color:#334155;vertical-align:middle;}.blog-table tbody tr:nth-child(even){background:#f8fafc;}.badge-green{display:inline-block;background:#ecfdf5;color:#047857;border:1px solid #a7f3d0;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}.badge-red  {display:inline-block;background:#fef2f2;color:#b91c1c;border:1px solid #fecaca;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}.badge-blue {display:inline-block;background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}.badge-amber{display:inline-block;background:#fffbeb;color:#b45309;border:1px solid #fde68a;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}/* ── 08 문제 카드 ── */.problem-card{border:1px solid #e2e8f0;border-radius:10px;overflow:hidden;margin:1.25rem 0;}.problem-card-header{background:#0f172a;color:#fff;padding:12px 18px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;}.prob-num{font-size:12px;color:#64748b;font-family:'JetBrains Mono',monospace;}.prob-title{font-size:15px;font-weight:600;margin-top:2px;}.prob-meta{font-size:12px;color:#64748b;text-align:right;line-height:1.6;}.diff-gold{color:#fbbf24;}.diff-silver{color:#94a3b8;}.diff-bronze{color:#d97706;}.diff-green{color:#4ade80;}.problem-card-body{padding:1.25rem 1.5rem;}.problem-card-body p{font-size:14px;color:#334155;margin-bottom:0.75rem;line-height:1.75;}.prob-sub{font-size:16px;font-weight:600;color:#0f172a;margin:1rem 0 0.6rem;padding-left:12px;border-left:3px solid #2563eb;}.step-list{padding-left:0;list-style:none;counter-reset:steps;}.step-list li{counter-increment:steps;padding:8px 0 8px 2.5rem;border-bottom:1px dashed #e2e8f0;font-size:14px;color:#334155;position:relative;line-height:1.7;}.step-list li:last-child{border-bottom:none;}.step-list li::before{content:counter(steps);position:absolute;left:0;top:9px;display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;background:#2563eb;color:#fff;border-radius:50%;font-size:11px;font-weight:700;}/* ── 포스트 푸터 ── */.post-footer{margin-top:3rem;padding-top:1.5rem;border-top:1px solid #e2e8f0;font-size:13px;color:#94a3b8;text-align:center;line-height:2;}&lt;/style&gt;
&lt;/div&gt;
&lt;div class=&quot;summary-box&quot;&gt;
&lt;div class=&quot;summary-label&quot;&gt;  핵심 요약&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;문제 재진술&lt;/b&gt; &amp;mdash; 미사일과 요격이라는 표현을 지우면 &quot;구간들을 모두 찌르는 최소 개수의 점&quot;을 구하는 문제다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;정렬 기준&lt;/b&gt; &amp;mdash; 끝값(e) 오름차순으로 정렬하면 최소 끝값 탐색이 해결된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;경계 조건&lt;/b&gt; &amp;mdash; 개구간이므로 &lt;code&gt;s == shot&lt;/code&gt;도 커버되지 않은 것으로 취급해야 한다. 비교 연산자는 &lt;code&gt;&amp;gt;=&lt;/code&gt;다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상태 설계&lt;/b&gt; &amp;mdash; 필요한 정보는 스칼라 두 개뿐이므로 덱이나 리스트가 필요하지 않다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;정렬 습관&lt;/b&gt; &amp;mdash; 뺄셈 비교 대신 &lt;code&gt;Integer.compare&lt;/code&gt;를 쓰면 입력 범위와 무관하게 안전하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;nav class=&quot;my-toc&quot;&gt;
&lt;div class=&quot;my-toc-header&quot;&gt;
&lt;div class=&quot;my-toc-header-bar&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;my-toc-header-text&quot;&gt;목차&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;my-toc-body&quot;&gt;&lt;!-- h2/h3 태그를 읽어 자동 생성됩니다 --&gt;&lt;/div&gt;
&lt;/nav&gt;
&lt;div class=&quot;problem-card&quot;&gt;
&lt;div class=&quot;problem-card-header&quot;&gt;
&lt;div&gt;
&lt;div class=&quot;prob-num&quot;&gt;Programmers #181188&lt;/div&gt;
&lt;div class=&quot;prob-title&quot;&gt;요격 시스템&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;prob-meta&quot;&gt;
&lt;div&gt;표적 개수 &amp;le; 500,000 &amp;middot; 좌표 &amp;le; 100,000,000&lt;/div&gt;
&lt;div&gt;난이도: &lt;span class=&quot;diff-green&quot;&gt;Lv.2&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;problem-card-body&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표적은 개구간 &lt;code&gt;(s, e)&lt;/code&gt;로 주어진다. 미사일은 한 점 x에서 발사되며 &lt;code&gt;s &amp;lt; x &amp;lt; e&lt;/code&gt;를 만족하는 표적을 요격한다. 모든 표적을 요격하기 위해 필요한 미사일의 최소 개수를 구한다.&lt;/p&gt;
&lt;div class=&quot;prob-sub&quot;&gt;풀이 과정&lt;/div&gt;
&lt;ol class=&quot;step-list&quot; style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;표적을 끝값 e 기준 오름차순으로 정렬한다.&lt;/li&gt;
&lt;li&gt;마지막으로 발사한 위치를 &lt;code&gt;shot&lt;/code&gt; 변수 하나로 유지한다.&lt;/li&gt;
&lt;li&gt;현재 표적이 &lt;code&gt;shot&lt;/code&gt;으로 커버되지 않으면 발사 횟수를 늘리고 &lt;code&gt;shot&lt;/code&gt;을 갱신한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section2&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;1&lt;/span&gt; 규모 판단과 유형 후보 소거&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 작성하기 전에 제약 조건을 먼저 확인했다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;th&gt;함의&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;표적 개수 n&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;최대 500,000&lt;/td&gt;
&lt;td&gt;O(n&amp;sup2;) 불가, 목표는 O(n log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;좌표 범위&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;최대 100,000,000&lt;/td&gt;
&lt;td&gt;좌표축 전체를 순회하는 접근은 배제&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 줄이 중요했다. 좌표 범위가 작았다면 &quot;좌표별로 몇 개 구간이 덮고 있는지&quot;를 세는 누적 합 계열을 떠올릴 수 있었다. 하지만 범위가 1억이므로 좌표를 직접 다루지 않고 &lt;b&gt;구간 자체를 다뤄야 한다&lt;/b&gt;고 판단했다. 그리고 목표 복잡도가 O(n log n)이라면 정렬 한 번은 이미 예산 안에 들어 있다는 뜻이었다.&lt;/p&gt;
&lt;h3 id=&quot;section2-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;후보를 늘어놓고 지우기&lt;/h3&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;후보&lt;/th&gt;
&lt;th&gt;판단&lt;/th&gt;
&lt;th&gt;근거&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;완전탐색&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-red&quot;&gt;배제&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;점의 후보 위치가 사실상 무한하다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;DP&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-amber&quot;&gt;보류&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;정의는 가능하나 개수만 묻는 문제에 과하다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;그리디&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;채택&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;1차원 순서 구조 + 최솟값 요구&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;누적 합 / 스위핑&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-red&quot;&gt;배제&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;좌표 범위가 크고, 덮인 횟수가 아니라 점 개수를 묻는다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DP를 완전히 지우지 않고 보류로 남겨 둔 이유는, 그리디 가설이 반례로 무너질 경우 후퇴할 곳이 필요했기 때문이다. 다만 먼저 그리디를 시도했다.&lt;/p&gt;
&lt;h2 id=&quot;section3&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;2&lt;/span&gt; 그리디 기준 세우기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리디에서 정할 것은 하나뿐이다. &lt;b&gt;어떤 기준으로 선택하면 손해가 없는가.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발상은 이러했다. 점을 하나 찍을 때 가능한 한 오른쪽에 찍으면 뒤에 오는 구간까지 덮을 기회가 커진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 오른쪽으로 밀 수 있는 한계선은 어디인가.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 반드시 처리해야 하는 구간들 중 &lt;b&gt;가장 빨리 끝나는 것의 끝값&lt;/b&gt;이다. 그 선을 넘으면 해당 구간을 놓치게 된다.&lt;/p&gt;
&lt;div class=&quot;info-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt;ℹ️&lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;그리디 가설&lt;/div&gt;
끝값이 가장 작은 구간의 끝 바로 왼쪽에 점을 찍는다. 그 점으로 덮이는 구간을 모두 지운다. 남은 것에 대해 반복한다.&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&quot;section3-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;증명보다 먼저 반례&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가설을 믿기 전에 작은 케이스로 흔들어 보았다. 구조가 특이한 세 가지를 골랐다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;케이스&lt;/th&gt;
&lt;th&gt;입력&lt;/th&gt;
&lt;th&gt;가설의 답&lt;/th&gt;
&lt;th&gt;정답&lt;/th&gt;
&lt;th&gt;통과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;포함 관계&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;[1,10] [2,3]&lt;/td&gt;
&lt;td&gt;[2,3] 먼저 처리 &amp;rarr; 1개&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;일치&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;완전 분리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;[1,2] [5,6]&lt;/td&gt;
&lt;td&gt;2개&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;일치&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;끝점 = 시작점&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;[1,4] [4,5]&lt;/td&gt;
&lt;td&gt;4의 왼쪽 &amp;rarr; [4,5] 미커버 &amp;rarr; 2개&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;일치&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;개구간이라는 조건이 여기서만 드러났고, 나중에 비교 연산자를 &lt;code&gt;&amp;gt;&lt;/code&gt;로 쓸지 &lt;code&gt;&amp;gt;=&lt;/code&gt;로 쓸지가 이 한 줄에서 결정되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;반례 사냥이 곧 경계 조건 발견&lt;/b&gt;이었다.&lt;/p&gt;
&lt;h2 id=&quot;section4&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;3&lt;/span&gt; 상태 설계 &amp;mdash; 덱이 필요 없었던 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &lt;code&gt;ArrayDeque&lt;/code&gt;에 &lt;code&gt;int[]{s, e}&lt;/code&gt;를 담아 두고 그룹 단위로 처리하려 했다. 하지만 무엇을 나중에 다시 꺼내 볼 것인지 자문하니 답이 나왔다. 꺼내 볼 것이 없었다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;th&gt;어디에 두는가&lt;/th&gt;
&lt;th&gt;근거&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;구간 하나의 s, e&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;루프 변수&lt;/td&gt;
&lt;td&gt;판정 직후 버려도 무방하다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;마지막 발사 위치&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;바깥 변수&lt;/td&gt;
&lt;td&gt;순회 전체에 걸쳐 유지되어야 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;발사 횟수&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;바깥 변수&lt;/td&gt;
&lt;td&gt;같은 이유&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BFS에서 &quot;노드별 상태는 큐에, 컴포넌트 전체 속성은 바깥 변수에&quot;를 구분했던 것과 같은 원칙이다. 다만 이 문제에서 그룹 전체 속성은 스칼라 두 개로 끝나므로 자료구조가 아예 필요하지 않았다.&lt;/p&gt;
&lt;div class=&quot;tip-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt; &lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;자료구조 필요 여부 판정&lt;/div&gt;
&quot;여기에 담은 값을 나중에 다시 꺼내 볼 일이 있는가?&quot; 이 질문에 아니라고 답할 수 있으면 변수 하나로 충분하다.&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&quot;section4-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;s 정렬과 e 정렬의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 떠올린 것은 s 기준 정렬이었다. 그쪽도 정답에 이르지만 관리할 상태가 하나 더 늘어난다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;s 정렬&lt;/th&gt;
&lt;th&gt;e 정렬&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;바깥 변수&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;minEnd&lt;/code&gt;, &lt;code&gt;answer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;shot&lt;/code&gt;, &lt;code&gt;answer&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;루프 내 분기&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;2갈래 (새 그룹 / 갱신)&lt;/td&gt;
&lt;td&gt;1갈래 (미커버면 발사)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;최소 끝값 갱신&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Math.min&lt;/code&gt; 필요&lt;/td&gt;
&lt;td&gt;정렬이 보장하므로 불필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;루프 종료 후 처리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;마지막 그룹 +1 필요&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;e 정렬에서 갱신이 사라지는 이유가 핵심이다. e 오름차순이면 그룹에 새로 들어오는 구간의 끝값은 항상 현재 기준점보다 크거나 같다. 즉 최소 끝값은 그룹의 첫 구간이 확정하고 이후 바뀌지 않는다. s 정렬에서 손으로 갱신했던 일을 &lt;b&gt;정렬이 대신 해 주는 셈&lt;/b&gt;이다.&lt;/p&gt;
&lt;h2 id=&quot;section6&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;4&lt;/span&gt; 코드&lt;/h2&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;Java &amp;mdash; Solution.java&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body angelscript&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {
    public int solution(int[][] targets) {
        // 끝값이 작은 구간부터 처리하면, 그 구간의 끝 직전이 최적 발사 위치가 된다
        Arrays.sort(targets, (a, b) -&amp;gt; Integer.compare(a[1], b[1]));

        int answer = 0;
        int shot = -1;   // 마지막 발사 위치. -1은 아직 발사하지 않았음을 의미

        for (int[] target : targets) {
            int s = target[0];
            int e = target[1];

            // shot은 실제로 'e 바로 왼쪽'을 뜻하므로 s == shot도 미커버로 취급
            if (s &amp;gt;= shot) {
                answer++;
                shot = e;
            }
        }
        return answer;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시간 복잡도는 정렬이 O(n log n)이고, 추가 공간은 상수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좌표를 더하거나 곱하지 않고 비교와 대입만 하므로 &lt;code&gt;int&lt;/code&gt;로 충분하다.&lt;/p&gt;
&lt;h3 id=&quot;section6-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;추적&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입력 &lt;code&gt;[[4,5],[4,8],[10,14],[11,13],[5,12],[3,7],[1,4]]&lt;/code&gt;을 e 오름차순으로 정렬하면 &lt;code&gt;[1,4] [4,5] [3,7] [4,8] [5,12] [11,13] [10,14]&lt;/code&gt; 순서가 된다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구간&lt;/th&gt;
&lt;th&gt;s&lt;/th&gt;
&lt;th&gt;shot(직전)&lt;/th&gt;
&lt;th&gt;s &amp;gt;= shot&lt;/th&gt;
&lt;th&gt;동작&lt;/th&gt;
&lt;th&gt;shot(이후)&lt;/th&gt;
&lt;th&gt;answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;[1,4]&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;-1&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;참&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;발사&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[4,5]&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;참&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;발사&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[3,7]&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-red&quot;&gt;거짓&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;커버됨&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[4,8]&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-red&quot;&gt;거짓&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;커버됨&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[5,12]&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;참&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;발사&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[11,13]&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-red&quot;&gt;거짓&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;커버됨&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;[10,14]&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-red&quot;&gt;거짓&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;커버됨&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째와 다섯 번째 행이 개구간 조건이 작동하는 지점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;s == shot&lt;/code&gt;인 상황에서 발사를 선택했고, 이 부분을 &lt;code&gt;&amp;gt;&lt;/code&gt;로 썼다면 두 케이스 모두 틀렸을 것이다.&lt;/p&gt;
&lt;h2 id=&quot;section7&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;5&lt;/span&gt; 실수했던 부분&lt;/h2&gt;
&lt;h3 id=&quot;section7-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;뺄셈 비교로 정렬 기준을 작성했다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 제출한 코드는 &lt;code&gt;(a, b) -&amp;gt; a[1] - b[1]&lt;/code&gt;이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;comparator가 참조하는 것은 반환값의 부호뿐이므로 뺄셈으로도 동작한다고 판단했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제에서는 실제로 통과했다. e가 항상 0 이상이고 최대 1억이므로 차이가 &lt;code&gt;int&lt;/code&gt; 상한을 넘지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 안전성의 근거가 코드 안에 없고 &lt;b&gt;문제 지문에 있다&lt;/b&gt;는 점이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부호가 섞인 값을 다루는 문제에 같은 습관을 가져가면 조용히 틀린다. &lt;code&gt;a[1] = 2000000000&lt;/code&gt;, &lt;code&gt;b[1] = -2000000000&lt;/code&gt;인 경우를 따라가 보면 이렇게 된다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;값&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;수학적 결과&lt;/td&gt;
&lt;td&gt;4,000,000,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;int 표현 범위&lt;/td&gt;
&lt;td&gt;최대 2,147,483,647 &amp;rarr; 초과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실제 저장값&lt;/td&gt;
&lt;td&gt;&lt;b&gt;-294,967,296&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;comparator 판정&lt;/td&gt;
&lt;td&gt;음수 &amp;rarr; &quot;a가 앞&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;참값&lt;/td&gt;
&lt;td&gt;a가 더 크므로 뒤에 와야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;danger-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt; &lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;TimSort 계약 위반&lt;/div&gt;
오버플로로 부호가 뒤집히면 &quot;a가 b보다 앞이면서 동시에 뒤&quot;인 모순이 생겨 &lt;code&gt;IllegalArgumentException: Comparison method violates its general contract!&lt;/code&gt;가 던져질 수 있다. 병합 과정에서 모순을 실제로 마주쳐야 발생하므로, 작은 테스트는 통과하고 큰 입력에서만 실패하는 형태로 나타난다.&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해결.&lt;/b&gt; &lt;code&gt;Integer.compare&lt;/code&gt;로 교체했다. JDK 구현은 산술 연산 없이 비교 연산만 하므로 오버플로가 발생할 계산 자체가 없다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;Java &amp;mdash; Integer.compare 구현과 사용&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body reasonml&quot;&gt;&lt;code&gt;// JDK 구현 &amp;mdash; 뺄셈이 없다
public static int compare(int x, int y) {
    return (x &amp;lt; y) ? -1 : ((x == y) ? 0 : 1);
}

// 안전성이 문제 제약에 의존 &amp;mdash; 이 문제에서는 동작하지만 습관으로는 부적절
Arrays.sort(targets, (a, b) -&amp;gt; a[1] - b[1]);

// 입력 범위와 무관하게 항상 옳음
Arrays.sort(targets, (a, b) -&amp;gt; Integer.compare(a[1], b[1]));

// 기준이 여러 개일 때
Arrays.sort(targets, Comparator
        .&amp;lt;int[]&amp;gt;comparingInt(t -&amp;gt; t[1])
        .thenComparingInt(t -&amp;gt; t[0]));&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class=&quot;warn-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt;⚠️&lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;타입별 위험도&lt;/div&gt;
&lt;code&gt;long&lt;/code&gt;을 &lt;code&gt;(int)(a - b)&lt;/code&gt;로 캐스팅하면 오버플로에 더해 상위 비트가 소실된다. &lt;code&gt;double&lt;/code&gt;은 더 치명적이다. 1.5와 1.0의 차이 0.5가 &lt;code&gt;int&lt;/code&gt; 캐스팅에서 0이 되어 &quot;두 값이 같다&quot;로 판정된다. 각각 &lt;code&gt;Long.compare&lt;/code&gt;, &lt;code&gt;Double.compare&lt;/code&gt;를 쓴다.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;info-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt;ℹ️&lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;박싱 오해&lt;/div&gt;
&lt;code&gt;Integer&lt;/code&gt;라는 클래스 이름 때문에 박싱을 걱정했으나, &lt;code&gt;compare&lt;/code&gt;의 파라미터 타입은 &lt;code&gt;int, int&lt;/code&gt; 원시형이다. 박싱이 일어나지 않고 JIT가 인라이닝하므로 뺄셈과 실질적인 성능 차이가 없다.&lt;/div&gt;
&lt;/div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 99.8837%; height: 22px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style11&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 100%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;잘못된 내용&lt;/span&gt;이 있다면&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;지적&lt;/span&gt;부탁드립니다. 방문해주셔서 감사합니다.&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;face&quot; data-emoticon-name=&quot;055&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/055.png&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/055.png&quot; width=&quot;80&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>알고리즘 &amp;amp; 코딩테스트/그리디 (Greedy)</category>
      <category>java</category>
      <category>그리디</category>
      <category>알고리즘</category>
      <category>요격시스템</category>
      <category>자바</category>
      <category>코딩테스트</category>
      <category>프로그래머스</category>
      <category>프로그래머스Lv2</category>
      <author>곱마2</author>
      <guid isPermaLink="true">https://minsu092274.tistory.com/219</guid>
      <comments>https://minsu092274.tistory.com/219#entry219comment</comments>
      <pubDate>Thu, 30 Jul 2026 15:04:07 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] 이벤트 버블링과 캡처링, 그리고 이벤트 위임</title>
      <link>https://minsu092274.tistory.com/218</link>
      <description>&lt;div&gt;
&lt;style&gt;@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&amp;family=JetBrains+Mono:wght@400;500;600&amp;display=swap');/* ── 공통 ── */.blog-post{font-family:'Noto Sans KR',sans-serif;color:#334155;line-height:1.8;font-size:15px;max-width:780px;margin:0 auto;padding:2rem 0;}.blog-post p{margin-bottom:1rem;}.blog-post strong{color:#0f172a;}.blog-post ul,.blog-post ol{padding-left:1.5rem;margin-bottom:1rem;}.blog-post li{margin:5px 0;font-size:14px;}.blog-post code{font-family:'JetBrains Mono',monospace;font-size:12.5px;background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:1px 6px;color:#c7254e;}.blog-post hr{border:none;border-top:1px solid #e2e8f0;margin:2.5rem 0;}/* ── 01 포스트 헤더 ── */.post-header{margin-bottom:2.5rem;padding-bottom:2rem;border-bottom:1px solid #e2e8f0;}.post-tags{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:1rem;}.tag{font-size:12px;padding:3px 12px;border-radius:20px;font-weight:500;}.tag-blue{background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe;}.tag-green{background:#ecfdf5;color:#059669;border:1px solid #a7f3d0;}.tag-amber{background:#fffbeb;color:#b45309;border:1px solid #fde68a;}.tag-red{background:#fef2f2;color:#b91c1c;border:1px solid #fecaca;}.tag-gray{background:#f1f5f9;color:#475569;border:1px solid #e2e8f0;}.post-title{font-size:28px;font-weight:700;color:#0f172a;line-height:1.35;margin:0 0 0.75rem;letter-spacing:-0.3px;}.post-meta{font-size:13px;color:#94a3b8;display:flex;gap:16px;flex-wrap:wrap;}.post-meta .dot::before{content:'·';margin-right:6px;opacity:0.4;}/* ── 02 핵심 요약 박스 ── */.summary-box{background:#eff6ff;border:1px solid #bfdbfe;border-radius:10px;padding:1.25rem 1.5rem;margin-bottom:2rem;}.summary-box .summary-label{font-size:12px;font-weight:700;color:#2563eb;letter-spacing:0.06em;margin-bottom:0.75rem;}.summary-box ul{padding-left:1.25rem;margin:0;}.summary-box li{font-size:14px;color:#334155;margin:5px 0;line-height:1.75;}.summary-box li strong{color:#0f172a;}/* ── 03 목차 (my-toc) ── */.my-toc{border-radius:12px;overflow:hidden;margin-bottom:2.5rem;border:1px solid #162d4a;}.my-toc-header{background:#1e3a5f;padding:16px 22px;display:flex;align-items:center;gap:10px;}.my-toc-header-bar{width:3px;height:22px;background:#93c5fd;border-radius:2px;flex-shrink:0;}.my-toc-header-text{font-size:17px;font-weight:700;color:#f1f5f9;}.my-toc-body{background:#f8fafc;padding:12px 16px 14px;}.my-toc-list{display:flex;flex-direction:column;gap:2px;}.my-toc-list&gt;div&gt;a{display:flex;align-items:center;gap:12px;padding:11px 14px;border-radius:8px;font-size:15px;font-weight:500;color:#1e3a5f;text-decoration:none;transition:background 0.15s,color 0.15s;}.my-toc-list&gt;div&gt;a:hover{background:#dbeafe;color:#1d4ed8;}.my-toc-list&gt;div&gt;a:hover .my-toc-num{background:#1e3a5f;color:#fff;}.my-toc-num{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;min-width:26px;background:#e2e8f0;color:#334155;border-radius:6px;font-size:13px;font-weight:700;flex-shrink:0;transition:background 0.15s,color 0.15s;}.my-toc-num.active{background:#1e3a5f;color:#fff;}.my-toc-list&gt;div&gt;a.my-toc-active{background:#dbeafe;color:#1e3a5f;}.my-toc-list&gt;div&gt;a.my-toc-active .my-toc-num{background:#1e3a5f;color:#fff;}.my-toc-sub{display:flex;flex-direction:column;gap:1px;padding:2px 0 4px 52px;}.my-toc-sub div a{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:6px;font-size:14px;color:#475569;text-decoration:none;transition:background 0.15s,color 0.15s;}.my-toc-sub div a::before{content:'';display:inline-block;width:5px;height:5px;min-width:5px;background:#94a3b8;border-radius:50%;flex-shrink:0;transition:background 0.15s;}.my-toc-sub div a:hover{background:#f1f5f9;color:#1e3a5f;}.my-toc-sub div a:hover::before{background:#1e3a5f;}.my-toc-sub div a.my-toc-active{color:#1e3a5f;}.my-toc-sub div a.my-toc-active::before{background:#1e3a5f;}/* ── 04 섹션 제목 (my-section) ── */.my-section{margin-bottom:3rem;}.my-section-title{font-size:20px;font-weight:700;color:#0f172a;margin-bottom:1rem;padding-bottom:0.5rem;border-bottom:2px solid #2563eb;display:flex;align-items:center;gap:10px;}.my-section-num{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;background:#2563eb;color:#fff;border-radius:50%;font-size:13px;font-weight:700;flex-shrink:0;}.my-sub-title{font-size:16px;font-weight:600;color:#0f172a;margin:1.5rem 0 0.75rem;padding-left:12px;border-left:3px solid #2563eb;}/* ── 05 코드 블록 ── */.code-wrap{margin:1.25rem 0;border-radius:10px;overflow:hidden;border:1px solid #1e293b !important;box-shadow:0 4px 20px rgba(15,23,42,0.18);}.code-header{background:#1e293b !important;padding:9px 18px;display:flex;align-items:center;justify-content:space-between;}.code-dots{display:flex;gap:6px;align-items:center;}.cdot{width:12px;height:12px;border-radius:50%;}.cdot-r{background:#ff5f57;}.cdot-y{background:#febc2e;}.cdot-g{background:#28c840;}.code-lang{font-size:12px;color:#94a3b8 !important;font-family:'JetBrains Mono',monospace;letter-spacing:0.05em;font-weight:500;}pre.code-body{background:#0d1117 !important;padding:1.5rem 1.75rem !important;margin:0 !important;overflow-x:auto;font-family:'JetBrains Mono',monospace !important;font-size:14px !important;font-weight:500 !important;line-height:1.9 !important;color:#e2e8f0 !important;border:none !important;border-radius:0 !important;}.code-body *{font-family:'JetBrains Mono',monospace !important;font-size:14px !important;font-weight:500 !important;line-height:1.9 !important;background:transparent !important;border:none !important;padding:0 !important;}.c-blue  {color:#7dd3fc !important;}.c-green {color:#86efac !important;}.c-lime  {color:#bef264 !important;}.c-teal  {color:#5eead4 !important;}.c-purple{color:#e879f9 !important;}.c-cyan  {color:#67e8f9 !important;}.c-yellow{color:#fef08a !important;}.c-gray  {color:#94a3b8 !important;font-style:italic;}.c-base  {color:#e2e8f0 !important;}/* ── 06 강조 박스 ── */.tip-box,.warn-box,.info-box,.danger-box{border-radius:10px;padding:1rem 1.25rem;margin:1.25rem 0;display:flex;gap:12px;align-items:flex-start;}.tip-box{background:#ecfdf5;border:1px solid #a7f3d0;}.warn-box{background:#fffbeb;border:1px solid #fde68a;}.info-box{background:#eff6ff;border:1px solid #bfdbfe;}.danger-box{background:#fef2f2;border:1px solid #fecaca;}.box-icon{font-size:18px;line-height:1;flex-shrink:0;margin-top:1px;}.box-body{font-size:14px;line-height:1.75;}.box-title{font-weight:700;margin-bottom:3px;font-size:13px;}.tip-box .box-title{color:#047857;}.tip-box .box-body{color:#065f46;}.warn-box .box-title{color:#b45309;}.warn-box .box-body{color:#92400e;}.info-box .box-title{color:#2563eb;}.info-box .box-body{color:#1e40af;}.danger-box .box-title{color:#b91c1c;}.danger-box .box-body{color:#991b1b;}/* ── 07 비교 표 ── */.table-wrap{overflow-x:auto;margin:1.25rem 0;}.blog-table{width:100%;border-collapse:collapse;font-size:14px;}.blog-table thead tr{background:#0f172a;color:#fff;}.blog-table thead th{padding:11px 16px;text-align:left;font-weight:500;font-size:13px;}.blog-table tbody tr{border-bottom:1px solid #e2e8f0;}.blog-table tbody tr:last-child{border-bottom:none;}.blog-table tbody td{padding:11px 16px;color:#334155;vertical-align:middle;}.blog-table tbody tr:nth-child(even){background:#f8fafc;}.badge-green{display:inline-block;background:#ecfdf5;color:#047857;border:1px solid #a7f3d0;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}.badge-red{display:inline-block;background:#fef2f2;color:#b91c1c;border:1px solid #fecaca;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}.badge-blue{display:inline-block;background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}.badge-amber{display:inline-block;background:#fffbeb;color:#b45309;border:1px solid #fde68a;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}/* ── 08 문제 카드 ── */.problem-card{border:1px solid #e2e8f0;border-radius:10px;overflow:hidden;margin:1.25rem 0;}.problem-card-header{background:#0f172a;color:#fff;padding:12px 18px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;}.prob-num{font-size:12px;color:#64748b;font-family:'JetBrains Mono',monospace;}.prob-title{font-size:15px;font-weight:600;margin-top:2px;}.prob-meta{font-size:12px;color:#64748b;text-align:right;line-height:1.6;}.diff-gold{color:#fbbf24;}.diff-silver{color:#94a3b8;}.diff-bronze{color:#d97706;}.diff-green{color:#4ade80;}.problem-card-body{padding:1.25rem 1.5rem;}.problem-card-body p{font-size:14px;color:#334155;margin-bottom:0.75rem;line-height:1.75;}.prob-sub{font-size:16px;font-weight:600;color:#0f172a;margin:1rem 0 0.6rem;padding-left:12px;border-left:3px solid #2563eb;}.step-list{padding-left:0;list-style:none;counter-reset:steps;}.step-list li{counter-increment:steps;padding:8px 0 8px 2.5rem;border-bottom:1px dashed #e2e8f0;font-size:14px;color:#334155;position:relative;line-height:1.7;}.step-list li:last-child{border-bottom:none;}.step-list li::before{content:counter(steps);position:absolute;left:0;top:9px;display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;background:#2563eb;color:#fff;border-radius:50%;font-size:11px;font-weight:700;}/* ── 포스트 푸터 ── */.post-footer{margin-top:3rem;padding-top:1.5rem;border-top:1px solid #e2e8f0;font-size:13px;color:#94a3b8;text-align:center;line-height:2;}&lt;/style&gt;
&lt;/div&gt;
&lt;!-- ==================== 02 : 핵심 요약 ==================== --&gt;
&lt;div class=&quot;summary-box&quot;&gt;
&lt;div class=&quot;summary-label&quot;&gt;  핵심 요약&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;클릭은 한 곳에서 끝나지 않는다&lt;/b&gt; &amp;mdash; 이벤트는 &lt;b&gt;캡처링 &amp;rarr; 타깃 &amp;rarr; 버블링&lt;/b&gt; 3단계로 흐르며, 자식에서 발생한 이벤트가 부모까지 전달된다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;addEventListener의 세 번째 인자&lt;/b&gt; &amp;mdash; 생략하면 버블링 단계에서 실행되고, &lt;code&gt;true&lt;/code&gt;를 주면 캡처링 단계에서 먼저 실행된다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;stopPropagation과 preventDefault는 다른 일을 한다&lt;/b&gt; &amp;mdash; 전자는 &lt;b&gt;전파&lt;/b&gt;를 막고, 후자는 &lt;b&gt;브라우저 기본 동작&lt;/b&gt;을 막는다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이벤트 위임&lt;/b&gt; &amp;mdash; 자식마다 리스너를 다는 대신 부모에 하나만 달면, 나중에 추가된 요소까지 자동으로 처리된다&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;!-- ==================== 03 : 목차 ==================== --&gt;&lt;nav class=&quot;my-toc&quot;&gt;
&lt;div class=&quot;my-toc-header&quot;&gt;
&lt;div class=&quot;my-toc-header-bar&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;my-toc-header-text&quot;&gt;목차&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;my-toc-body&quot;&gt;&lt;!-- h2/h3 태그를 읽어 자동 생성됩니다 --&gt;&lt;/div&gt;
&lt;/nav&gt;&lt;!-- ==================== 1 ==================== --&gt;
&lt;h2 id=&quot;section1&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;1&lt;/span&gt; 왜 이벤트 흐름을 알아야 할까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게시판 목록을 만들다 보면 이런 화면을 자주 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;행 전체를 클릭하면 상세 페이지로 이동&lt;/b&gt;하고, 행 안에 있는 &lt;b&gt;삭제 버튼을 누르면 삭제&lt;/b&gt;되는 구조입니다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;HTML &amp;mdash; 흔한 목록 구조&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body javascript&quot;&gt;&lt;code&gt;&amp;lt;tr class=&quot;row&quot;&amp;gt;
    &amp;lt;td&amp;gt;홍길동&amp;lt;/td&amp;gt;
    &amp;lt;td&amp;gt;
        &amp;lt;button class=&quot;btn-delete&quot;&amp;gt;삭제&amp;lt;/button&amp;gt;
    &amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 막상 &lt;b&gt;삭제 버튼을 누르면 삭제가 되면서 상세 페이지까지 같이 열립니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버튼에만 클릭 이벤트를 걸었는데도 행(&lt;code&gt;tr&lt;/code&gt;)에 걸어둔 이벤트가 함께 실행되는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 간단합니다. &lt;b&gt;클릭이 발생한 요소 하나에서만 이벤트가 처리되는 것이 아니기 때문&lt;/b&gt;입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 클릭이 일어나면 그 사실을 조상 요소들에게도 순서대로 알려주는데, 이 과정을 &lt;b&gt;이벤트 전파(Event Propagation)&lt;/b&gt;라고 합니다.&lt;/p&gt;
&lt;div class=&quot;info-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt;ℹ️&lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;이벤트 전파(Event Propagation)란?&lt;/div&gt;
어떤 요소에서 이벤트가 발생했을 때, 그 이벤트가 &lt;b&gt;DOM 트리를 따라 위아래로 이동하면서&lt;/b&gt; 경로에 있는 모든 요소에 전달되는 현상을 말합니다. 오늘 다룰 &lt;b&gt;캡처링&amp;middot;버블링&amp;middot;이벤트 위임&lt;/b&gt;은 전부 이 전파를 이해하고 다루는 방법입니다.&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ==================== 2 ==================== --&gt;
&lt;h2 id=&quot;section2&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;2&lt;/span&gt; 이벤트 전파의 3단계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트는 발생 즉시 해당 요소에서 처리되는 것이 아니라, &lt;b&gt;최상위(window)에서 출발해 내려갔다가 다시 올라오는&lt;/b&gt; 왕복 경로를 거칩니다. 이 경로는 3단계로 나뉩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;848&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQ2MOS/dJMcahehYRv/4dmSEimkyfstWW0S6viPr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQ2MOS/dJMcahehYRv/4dmSEimkyfstWW0S6viPr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQ2MOS/dJMcahehYRv/4dmSEimkyfstWW0S6viPr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQ2MOS%2FdJMcahehYRv%2F4dmSEimkyfstWW0S6viPr1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1226&quot; height=&quot;848&quot; data-origin-width=&quot;1226&quot; data-origin-height=&quot;848&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;단계&lt;/th&gt;
&lt;th&gt;영문&lt;/th&gt;
&lt;th&gt;방향&lt;/th&gt;
&lt;th&gt;설명&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;1. 캡처링&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Capturing&lt;/td&gt;
&lt;td&gt;window &amp;rarr; 대상&lt;/td&gt;
&lt;td&gt;최상위에서 클릭된 요소까지 &lt;b&gt;내려가는&lt;/b&gt; 단계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;2. 타깃&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Target&lt;/td&gt;
&lt;td&gt;대상&lt;/td&gt;
&lt;td&gt;실제 클릭된 요소에 도달한 순간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;3. 버블링&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Bubbling&lt;/td&gt;
&lt;td&gt;대상 &amp;rarr; window&lt;/td&gt;
&lt;td&gt;클릭된 요소에서 최상위까지 &lt;b&gt;올라가는&lt;/b&gt; 단계&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버블링(bubbling)이라는 이름은 &lt;b&gt;물속에서 기포가 위로 떠오르는 모습&lt;/b&gt;에서 왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자식 &amp;rarr; 부모 &amp;rarr; 조상 순으로 이벤트가 떠오른다고 이해하면 됩니다.&lt;/p&gt;
&lt;h3 id=&quot;section2-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;직접 확인해보기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3중으로 감싼 구조를 만들고 각 요소에 클릭 이벤트를 걸어봅니다. 실행 순서를 눈으로 확인하는 것이 가장 빠른 이해 방법입니다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;HTML &amp;mdash; 3중 구조&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body applescript&quot;&gt;&lt;code&gt;&amp;lt;div id=&quot;grandparent&quot;&amp;gt;
    &amp;lt;div id=&quot;parent&quot;&amp;gt;
        &amp;lt;button id=&quot;child&quot;&amp;gt;클릭&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;JavaScript &amp;mdash; 버블링 확인&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body qml&quot;&gt;&lt;code&gt;const grandparent = document.getElementById('grandparent');
const parent      = document.getElementById('parent');
const child       = document.getElementById('child');

grandparent.addEventListener('click', function(){
    console.log('3. 할아버지 div');
});

parent.addEventListener('click', function(){
    console.log('2. 부모 div');
});

child.addEventListener('click', function(){
    console.log('1. 버튼');
});

/* 버튼을 클릭했을 때 콘솔 출력
   1. 버튼
   2. 부모 div
   3. 할아버지 div            &amp;larr; 안쪽에서 바깥쪽으로 올라간다 */&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버튼 하나만 눌렀는데 &lt;b&gt;세 개의 함수가 모두 실행&lt;/b&gt;되었습니다. 1번 섹션에서 겪은 문제의 원인이 바로 이것입니다. &lt;code&gt;addEventListener&lt;/code&gt;는 기본적으로 &lt;b&gt;버블링 단계&lt;/b&gt;에서 동작하기 때문에, 안쪽 요소부터 바깥쪽 순서로 실행됩니다.&lt;/p&gt;
&lt;!-- ==================== 3 ==================== --&gt;
&lt;h2 id=&quot;section3&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;3&lt;/span&gt; addEventListener의 세 번째 인자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 캡처링 단계는 언제 쓸까요? &lt;code&gt;addEventListener&lt;/code&gt;의 &lt;b&gt;세 번째 인자&lt;/b&gt;로 어느 단계에서 실행할지 선택할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;JavaScript &amp;mdash; 기본 형태&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body reasonml&quot;&gt;&lt;code&gt;요소.addEventListener(이벤트명, 실행할함수, useCapture);

// useCapture 생략 또는 false &amp;rarr; 버블링 단계에서 실행 (기본값)
parent.addEventListener('click', fn);
parent.addEventListener('click', fn, false);

// true &amp;rarr; 캡처링 단계에서 실행
parent.addEventListener('click', fn, true);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 예제에서 &lt;b&gt;할아버지 div만&lt;/b&gt; &lt;code&gt;true&lt;/code&gt;로 바꾸면 출력 순서가 달라집니다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;JavaScript &amp;mdash; 캡처링 적용&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body javascript&quot;&gt;&lt;code&gt;grandparent.addEventListener('click', function(){
    console.log('할아버지 div');
}, true);  // &amp;larr; 캡처링

/* 출력 결과
   할아버지 div     &amp;larr; 내려오는 길에 먼저 실행
   버튼
   부모 div         &amp;larr; 올라가는 길에 실행 */&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;할아버지 div가 &lt;b&gt;가장 먼저&lt;/b&gt; 실행되었습니다. 캡처링은 위에서 아래로 내려오는 단계이므로, 클릭된 버튼보다 먼저 신호를 받는 것입니다.&lt;/p&gt;
&lt;h3 id=&quot;section3-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;options 객체로 쓰는 방법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최신 문법에서는 &lt;code&gt;true/false&lt;/code&gt; 대신 &lt;b&gt;객체&lt;/b&gt;를 넘길 수 있습니다. 옵션을 여러 개 조합할 수 있어 실무에서는 이쪽을 더 많이 씁니다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;JavaScript &amp;mdash; options 객체&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body actionscript&quot;&gt;&lt;code&gt;el.addEventListener('click', fn, {
    capture: true,   // 캡처링 단계에서 실행
    once:    true    // 딱 한 번만 실행되고 자동 해제
});

/* passive 옵션은 스크롤을 유발하는 이벤트에서만 의미가 있다
   (touchstart, touchmove, wheel 등 &amp;mdash; click에 붙이면 효과 없음)  */
el.addEventListener('touchmove', fn, {
    passive: true    // preventDefault를 쓰지 않겠다는 약속 &amp;rarr; 스크롤 성능 향상
});&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class=&quot;tip-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt; &lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;캡처링은 언제 쓰나요?&lt;/div&gt;
대부분의 코드는 버블링만으로 충분합니다. 다만 &lt;b&gt;자식보다 무조건 먼저 가로채야 하는 경우&lt;/b&gt;&amp;mdash;로딩 중일 때 화면 전체의 클릭을 막거나, 전역 로깅을 걸 때&amp;mdash;캡처링이 유용합니다. &lt;br /&gt;&lt;br /&gt;또 하나, &lt;b&gt;버블링되지 않는 이벤트를 상위에서 감지&lt;/b&gt;할 때도 캡처링이 답이 됩니다. 이 경우는 6번 섹션에서 다룹니다.&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ==================== 4 ==================== --&gt;
&lt;h2 id=&quot;section4&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;4&lt;/span&gt; stopPropagation과 preventDefault&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 1번에서 만난 문제를 해결해봅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;삭제 버튼을 눌렀을 때 행 클릭 이벤트가 실행되지 않게&lt;/b&gt; 하려면 이벤트가 위로 올라가는 것을 막아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 쓰는 것이 &lt;code&gt;stopPropagation()&lt;/code&gt;입니다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;JavaScript &amp;mdash; 전파 차단&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body reasonml&quot;&gt;&lt;code&gt;deleteBtn.addEventListener('click', function(e){
    e.stopPropagation();   // 여기서 전파를 끊는다
    deleteUser();
});

// 이제 삭제 버튼을 눌러도 tr의 클릭 이벤트는 실행되지 않는다&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class=&quot;info-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt;ℹ️&lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;단, 부모가 버블링 단계일 때만 막힙니다&lt;/div&gt;
부모(&lt;code&gt;tr&lt;/code&gt;)가 &lt;code&gt;{ capture: true }&lt;/code&gt;로 리스너를 걸어뒀다면, 그 리스너는 &lt;b&gt;버튼보다 이미 먼저 실행된 뒤&lt;/b&gt;입니다. 내려가는 길에 끝난 일이라 버튼에서 전파를 끊어도 되돌릴 수 없습니다.&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름이 비슷해서 자주 혼동되는 &lt;code&gt;preventDefault()&lt;/code&gt;는 &lt;b&gt;완전히 다른 일&lt;/b&gt;을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이쪽은 전파와 무관하게 &lt;b&gt;브라우저가 원래 하려던 동작&lt;/b&gt;을 취소합니다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;메서드&lt;/th&gt;
&lt;th&gt;막는 것&lt;/th&gt;
&lt;th&gt;대표 사용 예&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;stopPropagation()&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;부모로의 이벤트 &lt;b&gt;전파&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;버튼 클릭 시 부모 행 이벤트 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;preventDefault()&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;브라우저 &lt;b&gt;기본 동작&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;a&lt;/code&gt; 태그 이동 취소, form 새로고침 방지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;stopImmediatePropagation()&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;전파 + &lt;b&gt;같은 요소의 다른 리스너&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;같은 요소에 리스너가 여러 개일 때&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;JavaScript &amp;mdash; preventDefault 예시&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body reasonml&quot;&gt;&lt;code&gt;// a 태그를 눌러도 페이지가 이동하지 않는다
link.addEventListener('click', function(e){
    e.preventDefault();
    openModal();
});

// 폼 제출 시 페이지 새로고침을 막고 ajax로 처리
form.addEventListener('submit', function(e){
    e.preventDefault();
    sendAjax();
});&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class=&quot;warn-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt;⚠️&lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;stopPropagation() 남용 주의&lt;/div&gt;
전파를 끊으면 &lt;b&gt;그 위에 있는 모든 코드가 이벤트를 못 받습니다.&lt;/b&gt; 예를 들어 &quot;화면 아무 곳이나 클릭하면 팝업 닫기&quot; 기능을 &lt;code&gt;document&lt;/code&gt;에 걸어뒀다면, 중간에서 전파를 끊은 순간 그 팝업은 영영 닫히지 않습니다. 원인을 찾기도 어려운 종류의 버그라서, 꼭 필요한 자리에서만 쓰는 것이 좋습니다. &lt;br /&gt;&lt;br /&gt;대안으로는 &lt;b&gt;전파를 막는 대신 부모 쪽에서 걸러내는 방법&lt;/b&gt;이 있습니다. 이것이 다음에 볼 이벤트 위임과 자연스럽게 이어집니다.&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ==================== 5 ==================== --&gt;
&lt;h2 id=&quot;section5&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;5&lt;/span&gt; 이벤트 위임(Event Delegation)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버블링은 골칫거리로만 보이지만, 오히려 &lt;b&gt;적극적으로 활용하는 기법&lt;/b&gt;이 있습니다. 바로 이벤트 위임입니다.&lt;/p&gt;
&lt;h3 id=&quot;section5-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;먼저, 무엇이 문제인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목록 100건에 삭제 버튼이 하나씩 있다고 해봅시다. 보통은 이렇게 작성합니다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;JavaScript &amp;mdash; 문제가 되는 방식&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body reasonml&quot;&gt;&lt;code&gt;document.querySelectorAll('.btn-delete').forEach(function(btn){
    btn.addEventListener('click', onDelete);
});
// 리스너가 100개 등록된다&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식에는 두 가지 문제가 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;등록 비용&lt;/b&gt; &amp;mdash; 리스너 등록을 요소 수만큼 반복해야 하고, 콜백을 인라인 함수로 작성하면 함수 객체도 그만큼 만들어집니다. (위 예제처럼 &lt;code&gt;onDelete&lt;/code&gt; 하나를 재사용하면 함수 자체는 1개입니다.)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;동적 요소 누락&lt;/b&gt; &amp;mdash; 훨씬 결정적인 문제입니다. ajax로 목록을 다시 그리거나 행을 추가하면, &lt;b&gt;새로 생긴 버튼에는 리스너가 없습니다.&lt;/b&gt; 코드는 그대로인데 갑자기 클릭이 안 되는 현상이 여기서 나옵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;section5-2&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;부모 하나에만 걸기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 위임은 &lt;b&gt;자식들에게 일일이 리스너를 다는 대신, 부모 하나에만 걸고 버블링으로 올라온 이벤트를 부모가 대신 처리&lt;/b&gt;하는 방식입니다. &quot;누가 클릭됐는지&quot;는 이벤트 객체가 알려주므로 구분이 가능합니다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;JavaScript &amp;mdash; 이벤트 위임&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body qml&quot;&gt;&lt;code&gt;const list = document.querySelector('#userList');

list.addEventListener('click', function(e){

    // 클릭 지점에서 가장 가까운 .btn-delete를 찾는다
    const btn = e.target.closest('.btn-delete');

    // 삭제 버튼이 아니거나, 컨테이너 밖의 요소라면 종료
    if(!btn || !list.contains(btn)) return;

    const row = btn.closest('tr');
    deleteUser(row.dataset.id);
});

/* 리스너는 단 1개.
   나중에 ajax로 행을 100개 더 추가해도 코드 수정이 필요 없다. */&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;closest()&lt;/code&gt;는 조건에 맞는 요소를 찾을 때까지 &lt;b&gt;컨테이너를 넘어서도 계속 위로 올라갑니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;list.contains(btn)&lt;/code&gt;으로 &quot;정말 이 목록 안의 버튼인지&quot; 한 번 더 확인해두면 안전합니다.&lt;/p&gt;
&lt;h3 id=&quot;section5-3&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;target과 currentTarget의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위임을 쓸 때 반드시 구분해야 하는 두 값입니다. 이걸 헷갈리면 위임 코드가 엉뚱하게 동작합니다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;속성&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;위 예제에서의 값&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;e.target&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;실제로 클릭된 요소&lt;/td&gt;
&lt;td&gt;삭제 버튼 (또는 그 안의 아이콘)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;e.currentTarget&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;리스너를 등록한 요소&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#userList&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div class=&quot;danger-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt; &lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;closest()를 쓰는 이유&lt;/div&gt;
버튼 안에 아이콘이 들어 있는 경우&amp;mdash;예를 들어 &lt;code&gt;&amp;lt;button class=&quot;btn-delete&quot;&amp;gt;&amp;lt;i class=&quot;icon&quot;&amp;gt;&amp;lt;/i&amp;gt;삭제&amp;lt;/button&amp;gt;&lt;/code&gt;&amp;mdash;&lt;b&gt;아이콘을 클릭하면 &lt;code&gt;e.target&lt;/code&gt;은 버튼이 아니라 &lt;code&gt;i&lt;/code&gt; 태그&lt;/b&gt;가 됩니다. &lt;br /&gt;&lt;br /&gt;그래서 &lt;code&gt;e.target.classList.contains('btn-delete')&lt;/code&gt;처럼 검사하면 &lt;b&gt;어떤 날은 되고 어떤 날은 안 되는&lt;/b&gt; 버그가 생깁니다. &lt;code&gt;closest()&lt;/code&gt;는 자기 자신부터 부모 방향으로 올라가며 조건에 맞는 요소를 찾아주므로 이 문제를 깔끔하게 해결합니다.&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&quot;section5-4&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;jQuery에서의 이벤트 위임&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jQuery를 쓰고 있다면 이미 위임 문법이 준비되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;on()&lt;/code&gt;의 &lt;b&gt;두 번째 인자에 선택자를 넣으면&lt;/b&gt; 위임 방식으로 동작합니다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;jQuery &amp;mdash; 직접 바인딩 vs 위임&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body javascript&quot;&gt;&lt;code&gt;// ❌ 직접 바인딩 &amp;mdash; 지금 존재하는 요소에만 적용된다
$('.btn-delete').on('click', function(){
    deleteUser();
});

// ✅ 위임 &amp;mdash; 나중에 추가된 요소도 자동으로 동작한다
$('#userList').on('click', '.btn-delete', function(){
    var id = $(this).closest('tr').data('id');
    deleteUser(id);
});&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ajax로 목록을 다시 그린 뒤 클릭이 안 먹는 문제를 겪어본 적이 있다면, 원인이 대부분 첫 번째 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에 쓰던 &lt;code&gt;live()&lt;/code&gt;가 사라지고 &lt;code&gt;on()&lt;/code&gt;의 위임 문법으로 통합되었습니다.&lt;/p&gt;
&lt;div class=&quot;warn-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt;⚠️&lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;IE11을 지원해야 한다면&lt;/div&gt;
앞의 순수 JavaScript 예제에 쓰인 &lt;code&gt;closest()&lt;/code&gt;와 &lt;code&gt;NodeList.forEach()&lt;/code&gt;는 &lt;b&gt;IE11에서 동작하지 않습니다.&lt;/b&gt; polyfill을 추가하거나, 위와 같이 jQuery의 &lt;code&gt;on()&lt;/code&gt; 위임 문법을 사용하세요.&lt;/div&gt;
&lt;/div&gt;
&lt;!-- ==================== 6 ==================== --&gt;
&lt;h2 id=&quot;section6&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;6&lt;/span&gt; 실전에서 주의할 점&lt;/h2&gt;
&lt;h3 id=&quot;section6-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;버블링되지 않는 이벤트가 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 이벤트가 위로 올라가는 것은 아닙니다. 위임이 안 먹는다면 이 목록을 먼저 확인해보세요.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;버블링 안 됨&lt;/th&gt;
&lt;th&gt;대응 방법&lt;/th&gt;
&lt;th&gt;비고&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;focus&lt;/code&gt; / &lt;code&gt;blur&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;focusin&lt;/code&gt; / &lt;code&gt;focusout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-amber&quot;&gt;발생 시점 다름&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mouseenter&lt;/code&gt; / &lt;code&gt;mouseleave&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mouseover&lt;/code&gt; / &lt;code&gt;mouseout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-amber&quot;&gt;자식 진입 시에도 발생&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scroll&lt;/code&gt; (요소)&lt;/td&gt;
&lt;td&gt;캡처 단계로 감지&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-amber&quot;&gt;버블링 위임만 불가&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;focusin&lt;/code&gt;은 버블링이 된다는 점 외에 &lt;b&gt;발생 시점도 다릅니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;focusin&lt;/code&gt;은 포커스가 들어가기 &lt;b&gt;직전&lt;/b&gt;에, &lt;code&gt;focus&lt;/code&gt;는 들어간 &lt;b&gt;직후&lt;/b&gt;에 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평소엔 티가 나지 않지만 포커스 이동 순서를 직접 제어할 때 차이가 드러납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요소의 &lt;code&gt;scroll&lt;/code&gt;은 버블링되지 않지만, &lt;b&gt;캡처 단계는 여전히 그 요소를 지나갑니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번에서 배운 세 번째 인자를 &lt;code&gt;true&lt;/code&gt;로 주면 상위에서도 잡을 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;JavaScript &amp;mdash; scroll을 상위에서 감지&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body javascript&quot;&gt;&lt;code&gt;// 요소의 scroll은 버블링되지 않아 일반 위임으로는 잡히지 않는다
// 캡처 단계는 내려가는 길이므로 상위에서도 감지할 수 있다
document.addEventListener('scroll', function(e){
    console.log(e.target);   // 실제로 스크롤된 요소
}, true);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 id=&quot;section6-2&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;위임은 가까운 부모에 걸자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;document&lt;/code&gt;에 위임을 걸면 &lt;b&gt;페이지의 모든 클릭이 그 함수를 거쳐갑니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작은 하지만 불필요한 검사가 늘어나고, 다른 위임 코드와 순서가 얽히기 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;해당 요소들을 감싸는 가장 가까운 컨테이너&lt;/b&gt;에 거는 것이 좋습니다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;JavaScript &amp;mdash; 위임 대상 선택&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body reasonml&quot;&gt;&lt;code&gt;// △ 동작은 하지만 범위가 너무 넓다
document.addEventListener('click', handler);

// ○ 목록을 감싸는 컨테이너에 건다
document.querySelector('#userList').addEventListener('click', handler);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class=&quot;tip-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt; &lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;위임할 컨테이너 자체가 교체된다면&lt;/div&gt;
목록 전체를 &lt;code&gt;innerHTML&lt;/code&gt;로 통째로 갈아끼우는 구조라면, &lt;b&gt;컨테이너까지 새로 만들어지면서 리스너도 함께 사라집니다.&lt;/b&gt; 이럴 때는 &lt;b&gt;교체되지 않는 바깥쪽 요소&lt;/b&gt;에 위임을 걸어야 합니다.&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&quot;section6-3&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;정리 &amp;mdash; 언제 무엇을 쓸까&lt;/h3&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;상황&lt;/th&gt;
&lt;th&gt;해결 방법&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;자식 클릭 시 부모 이벤트가 같이 실행됨&lt;/td&gt;
&lt;td&gt;&lt;code&gt;e.stopPropagation()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a 태그 이동 / form 새로고침을 막고 싶음&lt;/td&gt;
&lt;td&gt;&lt;code&gt;e.preventDefault()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ajax로 추가한 요소에 클릭이 안 먹음&lt;/td&gt;
&lt;td&gt;&lt;b&gt;이벤트 위임&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;목록 항목마다 같은 리스너를 반복 등록 중&lt;/td&gt;
&lt;td&gt;&lt;b&gt;이벤트 위임&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;자식보다 무조건 먼저 이벤트를 가로채야 함&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ capture: true }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;버블링되지 않는 이벤트를 상위에서 감지&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ capture: true }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면, 이벤트가 &lt;b&gt;왜 부모까지 올라가는지&lt;/b&gt;를 이해하는 순간 두 가지가 함께 풀립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나는 &quot;누르지도 않은 이벤트가 실행되는&quot; 버그의 원인이고, 다른 하나는 그 성질을 역이용하는 이벤트 위임입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 같은 원리를 앞뒤로 뒤집어 본 것뿐입니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 99.8837%; height: 22px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style11&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 100%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;잘못된 내용&lt;/span&gt;이 있다면&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;지적&lt;/span&gt;부탁드립니다. 방문해주셔서 감사합니다.&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;face&quot; data-emoticon-name=&quot;055&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/055.png&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/055.png&quot; width=&quot;80&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Javascript</category>
      <category>addEventListener</category>
      <category>Event Bubbling</category>
      <category>Event Capturing</category>
      <category>javascript</category>
      <category>preventDefault</category>
      <category>stopPropagation</category>
      <category>이벤트버블링</category>
      <category>이벤트위임</category>
      <category>이벤트캡처링</category>
      <category>자바스크립트</category>
      <author>곱마2</author>
      <guid isPermaLink="true">https://minsu092274.tistory.com/218</guid>
      <comments>https://minsu092274.tistory.com/218#entry218comment</comments>
      <pubDate>Wed, 29 Jul 2026 21:47:54 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스 Lv.2] N-Queen(Java) - 완전탐색과 가지치기</title>
      <link>https://minsu092274.tistory.com/217</link>
      <description>&lt;div&gt;
&lt;style&gt;@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&amp;family=JetBrains+Mono:wght@400;600&amp;display=swap');/* ── 공통 ── */.blog-post{font-family:'Noto Sans KR',sans-serif;color:#334155;line-height:1.8;font-size:15px;max-width:780px;margin:0 auto;padding:2rem 0;}.blog-post p{margin-bottom:1rem;}.blog-post strong{color:#0f172a;}.blog-post ul,.blog-post ol{padding-left:1.5rem;margin-bottom:1rem;}.blog-post li{margin:5px 0;font-size:14px;}.blog-post code{font-family:'JetBrains Mono',monospace;font-size:12.5px;background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:1px 6px;color:#c7254e;}.blog-post hr{border:none;border-top:1px solid #e2e8f0;margin:2.5rem 0;}/* ── 01 포스트 헤더 ── */.post-header{margin-bottom:2.5rem;padding-bottom:2rem;border-bottom:1px solid #e2e8f0;}.post-tags{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:1rem;}.tag{font-size:12px;padding:3px 12px;border-radius:20px;font-weight:500;}.tag-blue  {background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe;}.tag-green {background:#ecfdf5;color:#059669;border:1px solid #a7f3d0;}.tag-amber {background:#fffbeb;color:#b45309;border:1px solid #fde68a;}.tag-red   {background:#fef2f2;color:#b91c1c;border:1px solid #fecaca;}.tag-gray  {background:#f1f5f9;color:#475569;border:1px solid #e2e8f0;}.post-title{font-size:28px;font-weight:700;color:#0f172a;line-height:1.35;margin:0 0 0.75rem;letter-spacing:-0.3px;}.post-meta{font-size:13px;color:#94a3b8;display:flex;gap:16px;flex-wrap:wrap;}.post-meta .dot::before{content:'·';margin-right:6px;opacity:0.4;}/* ── 02 핵심 요약 박스 ── */.summary-box{background:#eff6ff;border:1px solid #bfdbfe;border-radius:10px;padding:1.25rem 1.5rem;margin-bottom:2rem;}.summary-box .summary-label{font-size:12px;font-weight:700;color:#2563eb;letter-spacing:0.06em;margin-bottom:0.75rem;}.summary-box ul{padding-left:1.25rem;margin:0;}.summary-box li{font-size:14px;color:#334155;margin:5px 0;line-height:1.75;}.summary-box li strong{color:#0f172a;}/* ── 03 목차 (my-toc) ── */.my-toc{border-radius:12px;overflow:hidden;margin-bottom:2.5rem;border:1px solid #162d4a;}.my-toc-header{background:#1e3a5f;padding:16px 22px;display:flex;align-items:center;gap:10px;}.my-toc-header-bar{width:3px;height:22px;background:#93c5fd;border-radius:2px;flex-shrink:0;}.my-toc-header-text{font-size:17px;font-weight:700;color:#f1f5f9;}.my-toc-body{background:#f8fafc;padding:12px 16px 14px;}.my-toc-list{display:flex;flex-direction:column;gap:2px;}.my-toc-list&gt;div&gt;a{display:flex;align-items:center;gap:12px;padding:11px 14px;border-radius:8px;font-size:15px;font-weight:500;color:#1e3a5f;text-decoration:none;transition:background 0.15s,color 0.15s;}.my-toc-list&gt;div&gt;a:hover{background:#dbeafe;color:#1d4ed8;}.my-toc-list&gt;div&gt;a:hover .my-toc-num{background:#1e3a5f;color:#fff;}.my-toc-num{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;min-width:26px;background:#e2e8f0;color:#334155;border-radius:6px;font-size:13px;font-weight:700;flex-shrink:0;transition:background 0.15s,color 0.15s;}.my-toc-num.active{background:#1e3a5f;color:#fff;}.my-toc-list&gt;div&gt;a.my-toc-active{background:#dbeafe;color:#1e3a5f;}.my-toc-list&gt;div&gt;a.my-toc-active .my-toc-num{background:#1e3a5f;color:#fff;}.my-toc-sub{display:flex;flex-direction:column;gap:1px;padding:2px 0 4px 52px;}.my-toc-sub div a{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:6px;font-size:14px;color:#475569;text-decoration:none;transition:background 0.15s,color 0.15s;}.my-toc-sub div a::before{content:'';display:inline-block;width:5px;height:5px;min-width:5px;background:#94a3b8;border-radius:50%;flex-shrink:0;transition:background 0.15s;}.my-toc-sub div a:hover{background:#f1f5f9;color:#1e3a5f;}.my-toc-sub div a:hover::before{background:#1e3a5f;}.my-toc-sub div a.my-toc-active{color:#1e3a5f;}.my-toc-sub div a.my-toc-active::before{background:#1e3a5f;}/* ── 04 섹션 제목 (my-section) ── */.my-section{margin-bottom:3rem;}.my-section-title{font-size:20px;font-weight:700;color:#0f172a;margin-bottom:1rem;padding-bottom:0.5rem;border-bottom:2px solid #2563eb;display:flex;align-items:center;gap:10px;}.my-section-num{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;background:#2563eb;color:#fff;border-radius:50%;font-size:13px;font-weight:700;flex-shrink:0;}.my-sub-title{font-size:16px;font-weight:600;color:#0f172a;margin:1.5rem 0 0.75rem;padding-left:12px;border-left:3px solid #2563eb;}/* ── 05 코드 블록 ── */.code-wrap{margin:1.25rem 0;border-radius:10px;overflow:hidden;border:1px solid #1e293b;box-shadow:0 4px 16px rgba(15,23,42,0.12);}.code-header{background:#1e293b;padding:8px 16px;display:flex;align-items:center;justify-content:space-between;}.code-dots{display:flex;gap:6px;}.cdot{width:12px;height:12px;border-radius:50%;}.cdot-r{background:#ff5f57;}.cdot-y{background:#febc2e;}.cdot-g{background:#28c840;}.code-lang{font-size:11px;color:#64748b;font-family:'JetBrains Mono',monospace;letter-spacing:0.05em;}pre.code-body{background:#0d1117;padding:1.25rem 1.5rem;margin:0;overflow-x:auto;font-family:'JetBrains Mono',monospace;font-size:13px;line-height:1.75;color:#cbd5e1;}.c-blue{color:#93c5fd;}.c-green{color:#4ade80;}.c-lime{color:#86efac;}.c-teal{color:#a5f3fc;}.c-purple{color:#f0abfc;}.c-cyan{color:#6ee7b7;}.c-yellow{color:#fde68a;}.c-gray{color:#475569;font-style:italic;}.c-base{color:#cbd5e1;}/* ── 06 강조 박스 ── */.tip-box,.warn-box,.info-box,.danger-box{border-radius:10px;padding:1rem 1.25rem;margin:1.25rem 0;display:flex;gap:12px;align-items:flex-start;}.tip-box   {background:#ecfdf5;border:1px solid #a7f3d0;}.warn-box  {background:#fffbeb;border:1px solid #fde68a;}.info-box  {background:#eff6ff;border:1px solid #bfdbfe;}.danger-box{background:#fef2f2;border:1px solid #fecaca;}.box-icon{font-size:18px;line-height:1;flex-shrink:0;margin-top:1px;}.box-body{font-size:14px;line-height:1.75;}.box-title{font-weight:700;margin-bottom:3px;font-size:13px;}.tip-box    .box-title{color:#047857;}.tip-box    .box-body{color:#065f46;}.warn-box   .box-title{color:#b45309;}.warn-box   .box-body{color:#92400e;}.info-box   .box-title{color:#2563eb;}.info-box   .box-body{color:#1e40af;}.danger-box .box-title{color:#b91c1c;}.danger-box .box-body{color:#991b1b;}/* ── 07 비교 표 ── */.table-wrap{overflow-x:auto;margin:1.25rem 0;}.blog-table{width:100%;border-collapse:collapse;font-size:14px;}.blog-table thead tr{background:#0f172a;color:#fff;}.blog-table thead th{padding:11px 16px;text-align:left;font-weight:500;font-size:13px;}.blog-table tbody tr{border-bottom:1px solid #e2e8f0;}.blog-table tbody tr:last-child{border-bottom:none;}.blog-table tbody td{padding:11px 16px;color:#334155;vertical-align:middle;}.blog-table tbody tr:nth-child(even){background:#f8fafc;}.badge-green{display:inline-block;background:#ecfdf5;color:#047857;border:1px solid #a7f3d0;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}.badge-red  {display:inline-block;background:#fef2f2;color:#b91c1c;border:1px solid #fecaca;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}.badge-blue {display:inline-block;background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}.badge-amber{display:inline-block;background:#fffbeb;color:#b45309;border:1px solid #fde68a;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}/* ── 08 문제 카드 ── */.problem-card{border:1px solid #e2e8f0;border-radius:10px;overflow:hidden;margin:1.25rem 0;}.problem-card-header{background:#0f172a;color:#fff;padding:12px 18px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;}.prob-num{font-size:12px;color:#64748b;font-family:'JetBrains Mono',monospace;}.prob-title{font-size:15px;font-weight:600;margin-top:2px;}.prob-meta{font-size:12px;color:#64748b;text-align:right;line-height:1.6;}.diff-gold{color:#fbbf24;}.diff-silver{color:#94a3b8;}.diff-bronze{color:#d97706;}.diff-green{color:#4ade80;}.problem-card-body{padding:1.25rem 1.5rem;}.problem-card-body p{font-size:14px;color:#334155;margin-bottom:0.75rem;line-height:1.75;}.prob-sub{font-size:16px;font-weight:600;color:#0f172a;margin:1rem 0 0.6rem;padding-left:12px;border-left:3px solid #2563eb;}.step-list{padding-left:0;list-style:none;counter-reset:steps;}.step-list li{counter-increment:steps;padding:8px 0 8px 2.5rem;border-bottom:1px dashed #e2e8f0;font-size:14px;color:#334155;position:relative;line-height:1.7;}.step-list li:last-child{border-bottom:none;}.step-list li::before{content:counter(steps);position:absolute;left:0;top:9px;display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;background:#2563eb;color:#fff;border-radius:50%;font-size:11px;font-weight:700;}/* ── 포스트 푸터 ── */.post-footer{margin-top:3rem;padding-top:1.5rem;border-top:1px solid #e2e8f0;font-size:13px;color:#94a3b8;text-align:center;line-height:2;}&lt;/style&gt;
&lt;/div&gt;
&lt;div class=&quot;summary-box&quot;&gt;
&lt;div class=&quot;summary-label&quot;&gt;  핵심 요약&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;2차원 배열이 필요 없다&lt;/b&gt; &amp;mdash; 행마다 퀸이 하나뿐이므로 &lt;code&gt;queenCol[row] = col&lt;/code&gt; 형태의 1차원 배열이면 충분하다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;행 단위로 재귀를 내려간다&lt;/b&gt; &amp;mdash; 같은 행 충돌은 구조적으로 불가능해지고, 확인할 조건이 열과 대각선 둘로 줄어든다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대각선은 차이로 판정한다&lt;/b&gt; &amp;mdash; 행 차이와 열 차이가 같으면 대각선이며, 절댓값 하나로 두 방향을 함께 처리한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;되돌리기 코드가 필요 없다&lt;/b&gt; &amp;mdash; 다음 후보가 같은 자리를 덮어쓰고, 검사는 이미 확정된 행만 읽는다&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;nav class=&quot;my-toc&quot;&gt;
&lt;div class=&quot;my-toc-header&quot;&gt;
&lt;div class=&quot;my-toc-header-bar&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;my-toc-header-text&quot;&gt;목차&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;my-toc-body&quot;&gt;&lt;!-- h2/h3 태그를 읽어 자동 생성됩니다 --&gt;&lt;/div&gt;
&lt;/nav&gt;
&lt;h2 id=&quot;section1&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;1&lt;/span&gt; 문제 정리&lt;/h2&gt;
&lt;div class=&quot;problem-card&quot;&gt;
&lt;div class=&quot;problem-card-header&quot;&gt;
&lt;div&gt;
&lt;div class=&quot;prob-num&quot;&gt;Programmers #12952&lt;/div&gt;
&lt;div class=&quot;prob-title&quot;&gt;N-Queen&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;prob-meta&quot;&gt;
&lt;div&gt;4 &amp;le; n &amp;le; 12&lt;/div&gt;
&lt;div&gt;난이도: &lt;span class=&quot;diff-silver&quot;&gt;Lv.2&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;problem-card-body&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n &amp;times; n 체스판에 퀸 n개를 서로 공격할 수 없도록 배치하는 경우의 수를 구하는 문제다. 퀸은 가로, 세로, 대각선 방향으로 제한 없이 이동하므로, 두 퀸이 같은 행이나 같은 열에 있거나 대각선상에 놓이면 안 된다.&lt;/p&gt;
&lt;div class=&quot;prob-sub&quot;&gt;배치 조건&lt;/div&gt;
&lt;ol class=&quot;step-list&quot; style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;같은 행에 두 퀸이 있으면 안 된다&lt;/li&gt;
&lt;li&gt;같은 열에 두 퀸이 있으면 안 된다&lt;/li&gt;
&lt;li&gt;같은 대각선상에 두 퀸이 있으면 안 된다&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section2&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;2&lt;/span&gt; 완전탐색의 범위를 좁히기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n이 최대 12이므로 완전탐색으로 접근한다고 판단했다. 다만 &lt;b&gt;무엇을 전부 시도할 것인가&lt;/b&gt;에 따라 규모가 크게 달라진다.&lt;/p&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;접근&lt;/th&gt;
&lt;th&gt;후보 개수 (n = 12)&lt;/th&gt;
&lt;th&gt;가능 여부&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;모든 칸에서 12개 고르기&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;sub&gt;144&lt;/sub&gt;C&lt;sub&gt;12&lt;/sub&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-red&quot;&gt;불가능&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;행마다 열 하나씩 고르기&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;12&lt;sup&gt;12&lt;/sup&gt; &amp;asymp; 8.9조&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-amber&quot;&gt;여전히 많음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;+ 열 중복 제거&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;12! &amp;asymp; 4.8억&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-amber&quot;&gt;가지치기 필요&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;+ 대각선 가지치기&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;실제 탐색은 훨씬 적음&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;통과&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 결정적인 것은 &lt;b&gt;각 행에 퀸이 정확히 하나씩 놓인다&lt;/b&gt;는 사실이다. 퀸이 n개이고 행도 n개이며 같은 행에 둘을 놓을 수 없으니, 모든 행이 정확히 하나씩 차지하는 배치만 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 사실을 탐색 구조에 반영하면 &amp;ldquo;몇 번 행에 놓을까&amp;rdquo;를 고민할 필요가 사라진다. &lt;code&gt;dfs(row)&lt;/code&gt;가 그 행의 열만 정하고 다음 행으로 내려가면 된다.&lt;/p&gt;
&lt;h2 id=&quot;section3&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;3&lt;/span&gt; 2차원 배열을 쓰지 않은 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 체스판을 그대로 옮겨 &lt;code&gt;boolean[n][n]&lt;/code&gt; 형태의 2차원 배열을 만들 생각을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 행마다 퀸이 하나뿐이므로 &lt;b&gt;1차원 배열 하나로 상태를 전부 표현할 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;cdot cdot-r&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-y&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-g&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;example &amp;mdash; 1차원 배열로 표현한 배치&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body arduino&quot;&gt;&lt;code&gt;// queenCol[row] = 그 행에 놓인 퀸의 열 번호

int[] queenCol = {1, 3, 0, 2};

//      col 0  1  2  3
// 행 0      .  Q  .  .      queenCol[0] = 1
// 행 1      .  .  .  Q      queenCol[1] = 3
// 행 2      Q  .  .  .      queenCol[2] = 0
// 행 3      .  .  Q  .      queenCol[3] = 2&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스가 행이고 값이 열이다. 2차원 배열을 쓰면 충돌을 확인할 때마다 격자 전체를 훑어야 하지만, 1차원 배열이면 &lt;b&gt;이미 놓인 행들만 순회&lt;/b&gt;하면 된다.&lt;/p&gt;
&lt;h2 id=&quot;section4&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;4&lt;/span&gt; 충돌 판정&lt;/h2&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;조건&lt;/th&gt;
&lt;th&gt;판정 방법&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;같은 행&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;확인 불필요&lt;/span&gt; &amp;mdash; 행마다 하나씩 놓으므로 구조상 불가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;같은 열&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;queenCol[i] == col&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;대각선&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Math.abs(queenCol[i] - col) == row - i&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3 id=&quot;section4-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;대각선 조건이 성립하는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 점이 대각선상에 있다는 것은 &lt;b&gt;행 차이와 열 차이가 같다&lt;/b&gt;는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기울기가 1 또는 &amp;minus;1인 직선 위에 있다는 것과 같은 말이다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;cdot cdot-r&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-y&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-g&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;example &amp;mdash; row = 3에 놓을 때 (i = 1에 퀸이 있음)&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body arduino&quot;&gt;&lt;code&gt;//      col 0  1  2  3
// 행 1      .  Q  .  .      queenCol[1] = 1
// 행 2      .  .  .  .
// 행 3      .  .  ?  ?

// col = 3 &amp;rarr; 열 차이 |1 - 3| = 2, 행 차이 3 - 1 = 2  &amp;rarr; 대각선 충돌
// col = 2 &amp;rarr; 열 차이 |1 - 2| = 1, 행 차이 3 - 1 = 2  &amp;rarr; 안전&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;열 차이에만 절댓값을 씌우는 이유는 두 대각선 방향을 한 번에 처리하기 위해서다. 반면 행 차이 &lt;code&gt;row - i&lt;/code&gt;는 &lt;code&gt;i &amp;lt; row&lt;/code&gt;이므로 항상 양수라 절댓값이 필요 없다.&lt;/p&gt;
&lt;h2 id=&quot;section5&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;5&lt;/span&gt; 전체 코드&lt;/h2&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;cdot cdot-r&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-y&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-g&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;Java &amp;mdash; Solution.java&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body arduino&quot;&gt;&lt;code&gt;class Solution {
    private int[] queenCol;   // queenCol[row] = 그 행에 놓인 퀸의 열
    private int n;
    private int answer;

    public int solution(int n) {
        this.queenCol = new int[n];
        this.n = n;
        this.answer = 0;

        setQueen(0);

        return answer;
    }

    // row행에 퀸을 놓는다
    private void setQueen(int row) {
        // 모든 행에 놓았으면 배치 하나가 완성된 것
        if (row == n) {
            answer++;
            return;
        }

        for (int col = 0; col &amp;lt; n; col++) {
            if (isSafe(row, col)) {
                queenCol[row] = col;
                setQueen(row + 1);
            }
        }
    }

    // (row, col)에 퀸을 놓을 수 있는지 확인
    private boolean isSafe(int row, int col) {
        for (int i = 0; i &amp;lt; row; i++) {
            // 같은 열
            if (queenCol[i] == col) {
                return false;
            }
            // 대각선 &amp;mdash; 행 차이와 열 차이가 같으면 대각선
            if (Math.abs(queenCol[i] - col) == row - i) {
                return false;
            }
        }
        return true;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section7&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;6&lt;/span&gt; 되돌리기 코드가 없는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;백트래킹이라고 하면 보통 &lt;b&gt;선택 &amp;rarr; 탐색 &amp;rarr; 되돌리기&lt;/b&gt; 세 단계를 떠올린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 이 코드에는 &lt;code&gt;queenCol[row] = col&lt;/code&gt; 뒤에 원복하는 줄이 없다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;cdot cdot-r&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-y&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-g&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;비교 &amp;mdash; 되돌리기가 필요한 경우와 아닌 경우&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body arduino&quot;&gt;&lt;code&gt;// 되돌리기가 필요한 형태 (공유 컬렉션에 누적)
currentKey.add(i);
dfs(...);
currentKey.remove(i);   // 빠뜨리면 상태가 오염된다

// 되돌리기가 불필요한 형태 (인덱스에 덮어쓰기)
queenCol[row] = col;
setQueen(row + 1);
// 다음 col이 같은 자리를 덮어쓴다&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 가지 이유가 겹친다. 첫째로 다음 &lt;code&gt;col&lt;/code&gt; 후보가 &lt;b&gt;같은 인덱스를 덮어쓰므로&lt;/b&gt; 잔재가 남지 않는다. 둘째로 &lt;code&gt;isSafe&lt;/code&gt;가 &lt;code&gt;i &amp;lt; row&lt;/code&gt; 범위만 읽으므로, 아직 확정되지 않은 행의 값은 &lt;b&gt;애초에 참조되지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;div class=&quot;tip-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt; &lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;상태를 어떻게 담는가가 되돌리기 여부를 결정한다&lt;/div&gt;
컬렉션에 원소를 &lt;b&gt;쌓아 올리는&lt;/b&gt; 구조라면 되돌려야 하고, 고정 크기 배열의 &lt;b&gt;정해진 자리에 덮어쓰는&lt;/b&gt; 구조라면 되돌릴 필요가 없다. 자료구조 선택이 실수 가능성 자체를 줄여준다.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section8&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;7&lt;/span&gt; 정리&lt;/h2&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;자료구조&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int[n]&lt;/code&gt; 하나 &amp;mdash; 인덱스는 행, 값은 열&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;탐색 단위&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;행 &amp;mdash; &lt;code&gt;setQueen(row)&lt;/code&gt;가 그 행의 열만 결정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;확인할 조건&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;열 일치, 대각선 &amp;mdash; 같은 행은 구조상 불가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;가지치기&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;isSafe&lt;/code&gt;가 false면 그 아래 가지를 전부 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;공간 복잡도&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;O(n) &amp;mdash; 2차원 배열 대비 크게 절약&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제에서 얻은 것은 &lt;b&gt;문제의 제약을 자료구조에 미리 반영하면 확인할 조건이 줄어든다&lt;/b&gt;는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;행마다 퀸이 하나&amp;rdquo;라는 사실을 배열 구조에 담아두면 같은 행 충돌은 검사할 필요조차 없어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체스판이니 2차원 배열이라는 생각이 자연스럽지만, 실제로 저장해야 할 정보가 무엇인지 따져보면 1차원으로 충분한 경우가 많다고 판단했다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 99.8837%; height: 22px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style11&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 100%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;잘못된 내용&lt;/span&gt;이 있다면&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;지적&lt;/span&gt;부탁드립니다. 방문해주셔서 감사합니다.&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;face&quot; data-emoticon-name=&quot;055&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/055.png&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/055.png&quot; width=&quot;80&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>알고리즘 &amp;amp; 코딩테스트/완전 탐색 및 백트래킹</category>
      <category>DFS</category>
      <category>java</category>
      <category>nqueen</category>
      <category>N퀸</category>
      <category>백트래킹</category>
      <category>알고리즘</category>
      <category>자바</category>
      <category>코딩테스트</category>
      <category>프로그래머스</category>
      <category>프로그래머스Lv2</category>
      <author>곱마2</author>
      <guid isPermaLink="true">https://minsu092274.tistory.com/217</guid>
      <comments>https://minsu092274.tistory.com/217#entry217comment</comments>
      <pubDate>Wed, 29 Jul 2026 13:40:23 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스 Lv.2] 후보키(Java) - DFS와 비트마스크 두 가지 풀이</title>
      <link>https://minsu092274.tistory.com/216</link>
      <description>&lt;div&gt;
&lt;style&gt;@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;700&amp;family=JetBrains+Mono:wght@400;600&amp;display=swap');/* ── 공통 ── */.blog-post{font-family:'Noto Sans KR',sans-serif;color:#334155;line-height:1.8;font-size:15px;max-width:780px;margin:0 auto;padding:2rem 0;}.blog-post p{margin-bottom:1rem;}.blog-post strong{color:#0f172a;}.blog-post ul,.blog-post ol{padding-left:1.5rem;margin-bottom:1rem;}.blog-post li{margin:5px 0;font-size:14px;}.blog-post code{font-family:'JetBrains Mono',monospace;font-size:12.5px;background:#f1f5f9;border:1px solid #e2e8f0;border-radius:4px;padding:1px 6px;color:#c7254e;}.blog-post hr{border:none;border-top:1px solid #e2e8f0;margin:2.5rem 0;}/* ── 01 포스트 헤더 ── */.post-header{margin-bottom:2.5rem;padding-bottom:2rem;border-bottom:1px solid #e2e8f0;}.post-tags{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:1rem;}.tag{font-size:12px;padding:3px 12px;border-radius:20px;font-weight:500;}.tag-blue  {background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe;}.tag-green {background:#ecfdf5;color:#059669;border:1px solid #a7f3d0;}.tag-amber {background:#fffbeb;color:#b45309;border:1px solid #fde68a;}.tag-red   {background:#fef2f2;color:#b91c1c;border:1px solid #fecaca;}.tag-gray  {background:#f1f5f9;color:#475569;border:1px solid #e2e8f0;}.post-title{font-size:28px;font-weight:700;color:#0f172a;line-height:1.35;margin:0 0 0.75rem;letter-spacing:-0.3px;}.post-meta{font-size:13px;color:#94a3b8;display:flex;gap:16px;flex-wrap:wrap;}.post-meta .dot::before{content:'·';margin-right:6px;opacity:0.4;}/* ── 02 핵심 요약 박스 ── */.summary-box{background:#eff6ff;border:1px solid #bfdbfe;border-radius:10px;padding:1.25rem 1.5rem;margin-bottom:2rem;}.summary-box .summary-label{font-size:12px;font-weight:700;color:#2563eb;letter-spacing:0.06em;margin-bottom:0.75rem;}.summary-box ul{padding-left:1.25rem;margin:0;}.summary-box li{font-size:14px;color:#334155;margin:5px 0;line-height:1.75;}.summary-box li strong{color:#0f172a;}/* ── 03 목차 (my-toc) ── */.my-toc{border-radius:12px;overflow:hidden;margin-bottom:2.5rem;border:1px solid #162d4a;}.my-toc-header{background:#1e3a5f;padding:16px 22px;display:flex;align-items:center;gap:10px;}.my-toc-header-bar{width:3px;height:22px;background:#93c5fd;border-radius:2px;flex-shrink:0;}.my-toc-header-text{font-size:17px;font-weight:700;color:#f1f5f9;}.my-toc-body{background:#f8fafc;padding:12px 16px 14px;}.my-toc-list{display:flex;flex-direction:column;gap:2px;}.my-toc-list&gt;div&gt;a{display:flex;align-items:center;gap:12px;padding:11px 14px;border-radius:8px;font-size:15px;font-weight:500;color:#1e3a5f;text-decoration:none;transition:background 0.15s,color 0.15s;}.my-toc-list&gt;div&gt;a:hover{background:#dbeafe;color:#1d4ed8;}.my-toc-list&gt;div&gt;a:hover .my-toc-num{background:#1e3a5f;color:#fff;}.my-toc-num{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;min-width:26px;background:#e2e8f0;color:#334155;border-radius:6px;font-size:13px;font-weight:700;flex-shrink:0;transition:background 0.15s,color 0.15s;}.my-toc-num.active{background:#1e3a5f;color:#fff;}.my-toc-list&gt;div&gt;a.my-toc-active{background:#dbeafe;color:#1e3a5f;}.my-toc-list&gt;div&gt;a.my-toc-active .my-toc-num{background:#1e3a5f;color:#fff;}.my-toc-sub{display:flex;flex-direction:column;gap:1px;padding:2px 0 4px 52px;}.my-toc-sub div a{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:6px;font-size:14px;color:#475569;text-decoration:none;transition:background 0.15s,color 0.15s;}.my-toc-sub div a::before{content:'';display:inline-block;width:5px;height:5px;min-width:5px;background:#94a3b8;border-radius:50%;flex-shrink:0;transition:background 0.15s;}.my-toc-sub div a:hover{background:#f1f5f9;color:#1e3a5f;}.my-toc-sub div a:hover::before{background:#1e3a5f;}.my-toc-sub div a.my-toc-active{color:#1e3a5f;}.my-toc-sub div a.my-toc-active::before{background:#1e3a5f;}/* ── 04 섹션 제목 (my-section) ── */.my-section{margin-bottom:3rem;}.my-section-title{font-size:20px;font-weight:700;color:#0f172a;margin-bottom:1rem;padding-bottom:0.5rem;border-bottom:2px solid #2563eb;display:flex;align-items:center;gap:10px;}.my-section-num{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;background:#2563eb;color:#fff;border-radius:50%;font-size:13px;font-weight:700;flex-shrink:0;}.my-sub-title{font-size:16px;font-weight:600;color:#0f172a;margin:1.5rem 0 0.75rem;padding-left:12px;border-left:3px solid #2563eb;}/* ── 05 코드 블록 ── */.code-wrap{margin:1.25rem 0;border-radius:10px;overflow:hidden;border:1px solid #1e293b;box-shadow:0 4px 16px rgba(15,23,42,0.12);}.code-header{background:#1e293b;padding:8px 16px;display:flex;align-items:center;justify-content:space-between;}.code-dots{display:flex;gap:6px;}.cdot{width:12px;height:12px;border-radius:50%;}.cdot-r{background:#ff5f57;}.cdot-y{background:#febc2e;}.cdot-g{background:#28c840;}.code-lang{font-size:11px;color:#64748b;font-family:'JetBrains Mono',monospace;letter-spacing:0.05em;}pre.code-body{background:#0d1117;padding:1.25rem 1.5rem;margin:0;overflow-x:auto;font-family:'JetBrains Mono',monospace;font-size:13px;line-height:1.75;color:#cbd5e1;}.c-blue{color:#93c5fd;}.c-green{color:#4ade80;}.c-lime{color:#86efac;}.c-teal{color:#a5f3fc;}.c-purple{color:#f0abfc;}.c-cyan{color:#6ee7b7;}.c-yellow{color:#fde68a;}.c-gray{color:#475569;font-style:italic;}.c-base{color:#cbd5e1;}/* ── 06 강조 박스 ── */.tip-box,.warn-box,.info-box,.danger-box{border-radius:10px;padding:1rem 1.25rem;margin:1.25rem 0;display:flex;gap:12px;align-items:flex-start;}.tip-box   {background:#ecfdf5;border:1px solid #a7f3d0;}.warn-box  {background:#fffbeb;border:1px solid #fde68a;}.info-box  {background:#eff6ff;border:1px solid #bfdbfe;}.danger-box{background:#fef2f2;border:1px solid #fecaca;}.box-icon{font-size:18px;line-height:1;flex-shrink:0;margin-top:1px;}.box-body{font-size:14px;line-height:1.75;}.box-title{font-weight:700;margin-bottom:3px;font-size:13px;}.tip-box    .box-title{color:#047857;}.tip-box    .box-body{color:#065f46;}.warn-box   .box-title{color:#b45309;}.warn-box   .box-body{color:#92400e;}.info-box   .box-title{color:#2563eb;}.info-box   .box-body{color:#1e40af;}.danger-box .box-title{color:#b91c1c;}.danger-box .box-body{color:#991b1b;}/* ── 07 비교 표 ── */.table-wrap{overflow-x:auto;margin:1.25rem 0;}.blog-table{width:100%;border-collapse:collapse;font-size:14px;}.blog-table thead tr{background:#0f172a;color:#fff;}.blog-table thead th{padding:11px 16px;text-align:left;font-weight:500;font-size:13px;}.blog-table tbody tr{border-bottom:1px solid #e2e8f0;}.blog-table tbody tr:last-child{border-bottom:none;}.blog-table tbody td{padding:11px 16px;color:#334155;vertical-align:middle;}.blog-table tbody tr:nth-child(even){background:#f8fafc;}.badge-green{display:inline-block;background:#ecfdf5;color:#047857;border:1px solid #a7f3d0;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}.badge-red  {display:inline-block;background:#fef2f2;color:#b91c1c;border:1px solid #fecaca;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}.badge-blue {display:inline-block;background:#eff6ff;color:#2563eb;border:1px solid #bfdbfe;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}.badge-amber{display:inline-block;background:#fffbeb;color:#b45309;border:1px solid #fde68a;border-radius:20px;padding:1px 10px;font-size:12px;font-weight:500;}/* ── 08 문제 카드 ── */.problem-card{border:1px solid #e2e8f0;border-radius:10px;overflow:hidden;margin:1.25rem 0;}.problem-card-header{background:#0f172a;color:#fff;padding:12px 18px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;}.prob-num{font-size:12px;color:#64748b;font-family:'JetBrains Mono',monospace;}.prob-title{font-size:15px;font-weight:600;margin-top:2px;}.prob-meta{font-size:12px;color:#64748b;text-align:right;line-height:1.6;}.diff-gold{color:#fbbf24;}.diff-silver{color:#94a3b8;}.diff-bronze{color:#d97706;}.diff-green{color:#4ade80;}.problem-card-body{padding:1.25rem 1.5rem;}.problem-card-body p{font-size:14px;color:#334155;margin-bottom:0.75rem;line-height:1.75;}.prob-sub{font-size:16px;font-weight:600;color:#0f172a;margin:1rem 0 0.6rem;padding-left:12px;border-left:3px solid #2563eb;}.step-list{padding-left:0;list-style:none;counter-reset:steps;}.step-list li{counter-increment:steps;padding:8px 0 8px 2.5rem;border-bottom:1px dashed #e2e8f0;font-size:14px;color:#334155;position:relative;line-height:1.7;}.step-list li:last-child{border-bottom:none;}.step-list li::before{content:counter(steps);position:absolute;left:0;top:9px;display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;background:#2563eb;color:#fff;border-radius:50%;font-size:11px;font-weight:700;}/* ── 포스트 푸터 ── */.post-footer{margin-top:3rem;padding-top:1.5rem;border-top:1px solid #e2e8f0;font-size:13px;color:#94a3b8;text-align:center;line-height:2;}&lt;/style&gt;
&lt;/div&gt;
&lt;div class=&quot;summary-box&quot;&gt;
&lt;div class=&quot;summary-label&quot;&gt;  핵심 요약&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;후보키 = 유일성 + 최소성&lt;/b&gt; &amp;mdash; 두 조건을 각각 독립된 메서드로 분리하면 코드가 정의를 그대로 옮긴 형태가 된다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;크기가 작은 조합부터 검사해야 한다&lt;/b&gt; &amp;mdash; 최소성 판단은 이미 확정된 키가 있어야 성립하므로 순서가 곧 전제 조건이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DFS와 비트마스크 두 가지로 풀었다&lt;/b&gt; &amp;mdash; 컬럼이 8개뿐이라 어느 쪽이든 통과하며, 서로 다른 지점에서 명쾌하다&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;nav class=&quot;my-toc&quot;&gt;
&lt;div class=&quot;my-toc-header&quot;&gt;
&lt;div class=&quot;my-toc-header-bar&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;span class=&quot;my-toc-header-text&quot;&gt;목차&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;my-toc-body&quot;&gt;&lt;!-- h2/h3 태그를 읽어 자동 생성됩니다 --&gt;&lt;/div&gt;
&lt;/nav&gt;
&lt;h2 id=&quot;section1&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;1&lt;/span&gt; 문제 정리&lt;/h2&gt;
&lt;div class=&quot;problem-card&quot;&gt;
&lt;div class=&quot;problem-card-header&quot;&gt;
&lt;div&gt;
&lt;div class=&quot;prob-num&quot;&gt;Programmers #42890&lt;/div&gt;
&lt;div class=&quot;prob-title&quot;&gt;2019 카카오 개발자 겨울 인턴십 / 후보키&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;prob-meta&quot;&gt;
&lt;div&gt;1 &amp;le; 행 &amp;le; 20 &amp;middot; 1 &amp;le; 열 &amp;le; 8&lt;/div&gt;
&lt;div&gt;난이도: &lt;span class=&quot;diff-silver&quot;&gt;Lv.2&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;problem-card-body&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;릴레이션(표)이 문자열 2차원 배열로 주어진다. 이때 후보키가 될 수 있는 &lt;b&gt;속성(컬럼) 조합의 개수&lt;/b&gt;를 구하는 문제다. 후보키는 아래 두 조건을 모두 만족해야 한다.&lt;/p&gt;
&lt;div class=&quot;prob-sub&quot;&gt;후보키의 두 조건&lt;/div&gt;
&lt;ol class=&quot;step-list&quot; style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;유일성&lt;/b&gt; &amp;mdash; 그 컬럼들의 값으로 모든 행을 서로 구분할 수 있어야 한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최소성&lt;/b&gt; &amp;mdash; 컬럼 중 하나라도 빼면 유일성이 깨져야 한다. 즉 이미 후보키인 조합을 부분집합으로 포함하면 안 된다&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&quot;section1-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;예제 데이터&lt;/h3&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;cdot cdot-r&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-y&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-g&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;example &amp;mdash; relation&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body arduino&quot;&gt;&lt;code&gt;//      col 0    col 1     col 2       col 3
// 행0   100      ryan      music       2
// 행1   200      apeach    math        2
// 행2   300      tube      computer    3
// 행3   400      con       computer    4
// 행4   500      muzi      music       3
// 행5   600      apeach    music       2

// {0}   &amp;rarr; 코드번호가 전부 다르므로 유일성 만족. 후보키
// {1,2} &amp;rarr; 이름+전공 조합이 전부 다르므로 후보키
// {0,2} &amp;rarr; 유일하지만 {0}을 포함하므로 최소성 위반
// 답: 2&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section2&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;2&lt;/span&gt; 접근 방향 잡기&lt;/h2&gt;
&lt;h3 id=&quot;section2-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;무엇의 조합인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 &amp;ldquo;각 튜플별로 가능한 모든 조합을 탐색하면 되는가&amp;rdquo;라고 생각했는데, 조합의 대상을 잘못 잡은 것이었다. 후보키는 &lt;b&gt;어떤 컬럼들을 묶을 것인가&lt;/b&gt;를 고르는 문제이고, 행은 그 조합이 조건을 만족하는지 &lt;b&gt;검증하는 데 쓰이는 데이터&lt;/b&gt;다.&lt;/p&gt;
&lt;h3 id=&quot;section2-2&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;완전탐색이 가능한 규모인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제약을 보면 열은 최대 8개다. 부분집합은 2&lt;sup&gt;8&lt;/sup&gt; &amp;minus; 1 = 255개에 불과하다. 행도 최대 20개이므로 조합마다 유일성을 확인해도 연산량이 수만 회 수준이다. &lt;b&gt;완전탐색으로 밀어붙여도 된다&lt;/b&gt;는 판단이 여기서 선다.&lt;/p&gt;
&lt;div class=&quot;info-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt;ℹ️&lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;코드를 짜기 전에 규모부터&lt;/div&gt;
이 판단이 서야 이후 설계가 정해진다. 컬럼이 8개라는 사실이 &amp;ldquo;재귀로 전부 만들어도 된다&amp;rdquo;와 &amp;ldquo;정수 하나로 조합을 표현할 수 있다&amp;rdquo;는 두 선택지를 모두 열어준다.&lt;/div&gt;
&lt;/div&gt;
&lt;h3 id=&quot;section2-3&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;순서가 곧 전제 조건이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최소성은 &amp;ldquo;이미 확정된 후보키를 포함하지 않는가&amp;rdquo;로 판단한다. 그러려면 &lt;b&gt;작은 조합이 먼저 확정되어 있어야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;{0}&lt;/code&gt;을 먼저 후보키로 등록해야 &lt;code&gt;{0,2}&lt;/code&gt;를 걸러낼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 순서를 보장하는 방법은 두 가지다. 조합을 만드는 단계에서 크기를 1부터 올리거나, 만든 뒤 크기 순으로 정렬하거나. 아래 두 풀이는 각각 다른 쪽을 택했다.&lt;/p&gt;
&lt;h2 id=&quot;section3&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;3&lt;/span&gt; 풀이 1 &amp;mdash; DFS와 백트래킹&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바깥 for문에서 조합의 크기를 1부터 &lt;code&gt;colCount&lt;/code&gt;까지 늘리고, 각 크기마다 DFS로 그 크기의 조합만 만든다. 크기 순 처리가 루프 구조로 드러나므로 별도 정렬이 필요 없다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;cdot cdot-r&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-y&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-g&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;Java &amp;mdash; DFS 풀이&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body arduino&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {
    private int rowCount, colCount;
    private String[][] relation;
    private List&amp;lt;Set&amp;lt;Integer&amp;gt;&amp;gt; candidateKeys;   // 확정된 후보키

    public int solution(String[][] relation) {
        this.relation = relation;
        this.rowCount = relation.length;
        this.colCount = relation[0].length;
        this.candidateKeys = new ArrayList&amp;lt;&amp;gt;();

        // 컬럼 개수를 1개부터 늘려가며 탐색한다
        // 작은 조합이 먼저 확정되므로 최소성 가지치기가 성립한다
        for (int size = 1; size &amp;lt;= colCount; size++) {
            dfs(0, 0, size, new HashSet&amp;lt;&amp;gt;());
        }

        return candidateKeys.size();
    }

    /**
     * @param start       다음에 고를 수 있는 최소 컬럼 인덱스
     * @param depth       현재까지 고른 컬럼 개수
     * @param targetSize  이번 회차에 만들 조합의 크기
     * @param currentKey  현재까지 고른 컬럼 집합
     */
    private void dfs(int start, int depth, int targetSize, Set&amp;lt;Integer&amp;gt; currentKey) {
        // 최소성 검사 &amp;mdash; 이미 확정된 후보키를 포함하면 이 가지는 버린다
        for (Set&amp;lt;Integer&amp;gt; key : candidateKeys) {
            if (currentKey.containsAll(key)) {
                return;
            }
        }

        // 목표 크기에 도달했으면 유일성만 확인하면 된다
        if (depth == targetSize) {
            if (isUnique(currentKey)) {
                candidateKeys.add(new HashSet&amp;lt;&amp;gt;(currentKey));   // 복사본
            }
            return;
        }

        // 오름차순으로만 컬럼을 골라 중복 조합을 방지한다
        for (int i = start; i &amp;lt; colCount; i++) {
            currentKey.add(i);
            dfs(i + 1, depth + 1, targetSize, currentKey);
            currentKey.remove(i);   // 백트래킹
        }
    }

    /** 이 컬럼 조합으로 모든 행이 서로 구분되는가 */
    private boolean isUnique(Set&amp;lt;Integer&amp;gt; keySet) {
        Set&amp;lt;String&amp;gt; rowSet = new HashSet&amp;lt;&amp;gt;();

        for (int r = 0; r &amp;lt; rowCount; r++) {
            StringBuilder sb = new StringBuilder();
            for (int col : keySet) {
                // 구분자를 넣어야 경계 정보가 보존된다
                sb.append(relation[r][col]).append(&quot;/&quot;);
            }
            rowSet.add(sb.toString());
        }

        // 중복 없이 모든 행이 서로 다른 문자열이면 유일성 만족
        return rowSet.size() == rowCount;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 id=&quot;section3-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;start 인덱스로 중복 조합 차단&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;for (int i = start; ...)&lt;/code&gt;로 시작하고 다음 재귀에 &lt;code&gt;i + 1&lt;/code&gt;을 넘기므로 항상 오름차순 조합만 만들어진다. &lt;code&gt;{0,1}&lt;/code&gt;과 &lt;code&gt;{1,0}&lt;/code&gt;이 따로 생기지 않는다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;cdot cdot-r&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-y&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-g&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;trace &amp;mdash; targetSize = 2, colCount = 4&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body arduino&quot;&gt;&lt;code&gt;dfs(start=0, depth=0)   currentKey = {}
 ├ i=0 &amp;rarr; add(0)         currentKey = {0}
 │   dfs(start=1, depth=1)
 │    ├ i=1 &amp;rarr; add(1)    currentKey = {0,1}  &amp;rarr; depth==2, 검사
 │    │       remove(1) currentKey = {0}
 │    ├ i=2 &amp;rarr; add(2)    currentKey = {0,2}  &amp;rarr; 검사
 │    │       remove(2) currentKey = {0}
 │    └ i=3 &amp;rarr; add(3)    currentKey = {0,3}  &amp;rarr; 검사
 │            remove(3) currentKey = {0}
 │   remove(0)            currentKey = {}
 └ i=1 &amp;rarr; ...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;remove&lt;/code&gt;가 실행될 때마다 &lt;code&gt;currentKey&lt;/code&gt;가 직전 상태로 정확히 돌아간다. 이 되돌리기가 백트래킹의 핵심이다.&lt;/p&gt;
&lt;h3 id=&quot;section3-2&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;가지치기 위치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최소성 검사를 &lt;b&gt;재귀 진입 시점&lt;/b&gt;에 두었다. 종료 조건보다 앞에 있으므로 조합을 끝까지 만들기 전에 잘라낸다. &lt;code&gt;{0}&lt;/code&gt;이 확정된 뒤 &lt;code&gt;{0,1}&lt;/code&gt; 경로로 들어가는 순간 바로 되돌아 나오고, 그 아래 &lt;code&gt;{0,1,2}&lt;/code&gt;, &lt;code&gt;{0,1,3}&lt;/code&gt; 같은 가지가 통째로 사라진다.&lt;/p&gt;
&lt;h2 id=&quot;section5&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;4&lt;/span&gt; 풀이 2 &amp;mdash; 비트마스크&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컬럼이 8개이므로 조합을 &lt;b&gt;정수 하나&lt;/b&gt;로 표현할 수 있다. 0부터 255까지의 수에서 켜진 비트가 곧 고른 컬럼이다. 재귀도 백트래킹도 필요 없어진다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;cdot cdot-r&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-y&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-g&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;Java &amp;mdash; 비트마스크 풀이&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body arduino&quot;&gt;&lt;code&gt;import java.util.*;

class Solution {
    public int solution(String[][] relation) {
        int rowCount = relation.length;
        int colCount = relation[0].length;

        // 1단계: 모든 조합을 만들어 크기 순으로 정렬한다
        List&amp;lt;Integer&amp;gt; masks = new ArrayList&amp;lt;&amp;gt;();
        for (int mask = 1; mask &amp;lt; (1 &amp;lt;&amp;lt; colCount); mask++) {
            masks.add(mask);
        }
        masks.sort(Comparator.comparingInt(Integer::bitCount));

        // 2단계: 유일성 + 최소성 검사
        List&amp;lt;Integer&amp;gt; keys = new ArrayList&amp;lt;&amp;gt;();

        for (int mask : masks) {
            // 최소성 &amp;mdash; 이미 확정된 키를 통째로 포함하면 탈락
            boolean minimal = true;
            for (int key : keys) {
                if ((mask &amp;amp; key) == key) {
                    minimal = false;
                    break;
                }
            }
            if (!minimal) continue;

            // 유일성 &amp;mdash; 선택된 컬럼 값을 이어붙여 중복 확인
            Set&amp;lt;String&amp;gt; seen = new HashSet&amp;lt;&amp;gt;();
            for (String[] row : relation) {
                StringBuilder sb = new StringBuilder();
                for (int col = 0; col &amp;lt; colCount; col++) {
                    if ((mask &amp;amp; (1 &amp;lt;&amp;lt; col)) != 0) {
                        sb.append(row[col]).append(&quot;/&quot;);
                    }
                }
                seen.add(sb.toString());
            }

            if (seen.size() == rowCount) {
                keys.add(mask);
            }
        }

        return keys.size();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h3 id=&quot;section5-1&quot; class=&quot;my-sub-title&quot; data-ke-size=&quot;size23&quot;&gt;알아야 할 연산은 세 가지뿐&lt;/h3&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;연산&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;대응되는 표현&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1 &amp;lt;&amp;lt; colCount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2의 colCount제곱&lt;/td&gt;
&lt;td&gt;가능한 조합의 총 개수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(mask &amp;amp; (1 &amp;lt;&amp;lt; col)) != 0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;col번 비트가 켜져 있는가&lt;/td&gt;
&lt;td&gt;&lt;code&gt;set.contains(col)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;(mask &amp;amp; key) == key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;key의 모든 비트가 mask에도 있는가&lt;/td&gt;
&lt;td&gt;&lt;code&gt;set.containsAll(key)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째가 핵심이다. &lt;code&gt;key&lt;/code&gt;에서 켜진 비트가 &lt;code&gt;mask&lt;/code&gt;에도 전부 켜져 있으면 &lt;code&gt;and&lt;/code&gt; 결과가 &lt;code&gt;key&lt;/code&gt; 그대로 남는다. 부분집합 판정을 정수 연산 하나로 끝내는 관용구다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Integer.bitCount(mask)&lt;/code&gt;는 켜진 비트의 개수, 즉 조합의 크기를 돌려준다. 이것으로 정렬하면 크기 순 처리가 보장된다.&lt;/p&gt;
&lt;div class=&quot;danger-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt; &lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;마스크 값 순서는 크기 순이 아니다&lt;/div&gt;
&lt;code&gt;3&lt;/code&gt;(&lt;code&gt;0b011&lt;/code&gt;, 크기 2)이 &lt;code&gt;4&lt;/code&gt;(&lt;code&gt;0b100&lt;/code&gt;, 크기 1)보다 작다. 정렬을 빼고 1부터 순서대로 훑으면 크기 2짜리가 크기 1짜리보다 먼저 검사되어 최소성 판단이 무너진다.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section6&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;6&lt;/span&gt; 구분자가 필수인 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 풀이 모두 값을 이어붙일 때 &lt;code&gt;append(&quot;/&quot;)&lt;/code&gt;를 넣었다. 이어붙이기는 &lt;b&gt;경계 정보를 잃는 연산&lt;/b&gt;이기 때문이다.&lt;/p&gt;
&lt;div class=&quot;code-wrap&quot;&gt;
&lt;div class=&quot;code-header&quot;&gt;
&lt;div class=&quot;code-dots&quot;&gt;&lt;span class=&quot;cdot cdot-r&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-y&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span class=&quot;cdot cdot-g&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&quot;code-lang&quot;&gt;example &amp;mdash; 구분자 유무의 차이&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&quot;code-body arduino&quot;&gt;&lt;code&gt;// 구분자 없음 &amp;mdash; 서로 다른 행이 같은 문자열이 된다
행A: &quot;ab&quot; + &quot;c&quot;   &amp;rarr; &quot;abc&quot;
행B: &quot;a&quot;  + &quot;bc&quot;  &amp;rarr; &quot;abc&quot;   &amp;larr; 중복으로 판정됨

// 구분자 있음 &amp;mdash; 경계가 복원된다
행A: &quot;ab/c/&quot;
행B: &quot;a/bc/&quot;              &amp;larr; 정상적으로 구분됨&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제의 값은 1~8자의 알파벳 소문자와 숫자이므로 위와 같은 데이터가 실제로 존재할 수 있다. 숫자만 있는 컬럼이라면 &lt;code&gt;12 + 3&lt;/code&gt;과 &lt;code&gt;1 + 23&lt;/code&gt;이 둘 다 &lt;code&gt;&quot;123&quot;&lt;/code&gt;이 되는 식으로도 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 값 뒤에도 구분자가 붙어 &lt;code&gt;&quot;a/b/&quot;&lt;/code&gt;처럼 끝나지만 문제되지 않는다. 모든 행이 같은 규칙으로 만들어지므로 비교에 영향이 없고, &amp;ldquo;마지막만 제외&amp;rdquo; 같은 분기를 넣지 않아도 되어 오히려 단순하다.&lt;/p&gt;
&lt;h2 id=&quot;section7&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;7&lt;/span&gt; 두 풀이 비교&lt;/h2&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구분&lt;/th&gt;
&lt;th&gt;DFS + 백트래킹&lt;/th&gt;
&lt;th&gt;비트마스크&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;메서드 개수&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;3개&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;1개&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;재귀&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;필요&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;불필요&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;크기 순 보장&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;바깥 for문 (구조로 표현)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bitCount&lt;/code&gt; 정렬&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;최소성 판정&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;containsAll&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(mask &amp;amp; key) == key&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;가지치기&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;완성 전 차단 가능&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;조합 단위로만 건너뜀&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;실수 지점&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-red&quot;&gt;remove, 복사본&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;없음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;사전지식&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;span class=&quot;badge-green&quot;&gt;불필요&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;비트 연산 이해 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;ldquo;사전지식 없이 읽히는가&amp;rdquo;를 기준으로 하면 DFS가 낫다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;containsAll&lt;/code&gt;은 그대로 읽히지만 &lt;code&gt;(mask &amp;amp; key) == key&lt;/code&gt;는 한 번 해석해야 한다.&lt;/p&gt;
&lt;div class=&quot;info-box&quot;&gt;&lt;span class=&quot;box-icon&quot;&gt;ℹ️&lt;/span&gt;
&lt;div class=&quot;box-body&quot;&gt;
&lt;div class=&quot;box-title&quot;&gt;비트마스크는 관용구다&lt;/div&gt;
부분집합을 다루는 문제에서 반복적으로 등장한다. 외판원 순회나 집합 DP 계열로 넘어가면 사실상 필수가 되므로, 컬럼이 8개로 작은 이 문제에서 두 방식을 나란히 두고 익혀두면 이후에 도움이 된다.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&quot;section8&quot; class=&quot;my-section-title&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span class=&quot;my-section-num&quot;&gt;8&lt;/span&gt; 정리&lt;/h2&gt;
&lt;div class=&quot;table-wrap&quot;&gt;
&lt;table class=&quot;blog-table&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;내용&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;조합 개수&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;2&lt;sup&gt;8&lt;/sup&gt; &amp;minus; 1 = 255개 (열 8개 기준)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;유일성 검사&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;조합당 20행 &amp;times; 8컬럼&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;최소성 검사&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;조합당 최대 확정 키 개수만큼 비교&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;전체 연산량&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;수만 회 수준 &amp;mdash; &lt;span class=&quot;badge-green&quot;&gt;완전탐색 가능&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제에서 어려웠던 부분은 알고리즘 자체가 아니라 &lt;b&gt;조합의 대상을 무엇으로 잡을 것인가&lt;/b&gt;였다. 튜플을 조합하려 하면 방향이 어긋나고, 컬럼을 조합 대상으로 잡는 순간 나머지는 완전탐색으로 정리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 최소성이라는 조건이 &lt;b&gt;탐색 순서에 제약을 건다&lt;/b&gt;는 점도 확인했다. 검사 로직만 옳게 짜면 되는 것이 아니라, 작은 조합이 먼저 확정되어야 그 검사가 의미를 갖는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건과 순서가 얽혀 있는 문제였다고 판단했다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 99.8837%; height: 22px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style11&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 100%; height: 22px; text-align: center;&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;잘못된 내용&lt;/span&gt;이 있다면&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&gt;지적&lt;/span&gt;부탁드립니다. 방문해주셔서 감사합니다.&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;face&quot; data-emoticon-name=&quot;055&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/055.png&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/face/large/055.png&quot; width=&quot;80&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>알고리즘 &amp;amp; 코딩테스트/BFS &amp;bull; DFS</category>
      <category>DFS</category>
      <category>java</category>
      <category>백트래킹</category>
      <category>비트마스크</category>
      <category>알고리즘</category>
      <category>자바</category>
      <category>코딩테스트</category>
      <category>프로그래머스</category>
      <category>프로그래머스Lv2</category>
      <category>후보키</category>
      <author>곱마2</author>
      <guid isPermaLink="true">https://minsu092274.tistory.com/216</guid>
      <comments>https://minsu092274.tistory.com/216#entry216comment</comments>
      <pubDate>Tue, 28 Jul 2026 22:07:07 +0900</pubDate>
    </item>
  </channel>
</rss>