<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>김신건의 로그 - 위키</title><description>위키 페이지 최신 업데이트 모음</description><link>https://shinkeonkim.com/</link><language>ko-KR</language><item><title>[DB] Transaction Isolation Levels: 완벽 가이드</title><link>https://shinkeonkim.com/wiki/database-internals/transaction-isolation-levels/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/database-internals/transaction-isolation-levels/</guid><pubDate>Thu, 02 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Transaction Isolation Level (트랜잭션 격리 수준)&lt;/strong&gt; = &lt;em&gt;동시에 실행되는 여러 트랜잭션이 서로의 변경을 어디까지 볼 수 있는지&lt;/em&gt; 규정하는 수준. ACID 의 &lt;em&gt;I&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;격리를 완전히 없애면 최대 처리량, 완전히 직렬화하면 최상의 정합성. DB 는 이 사이 &lt;em&gt;4단계 (ANSI SQL 표준)&lt;/em&gt; 를 제공한다. &lt;strong&gt;더 강한 격리 = 더 많은 이상 현상 방지 = 더 낮은 동시성.&lt;/strong&gt; 실무는 이 트레이드오프를 이해하고 상황에 맞게 고르는 문제다.&lt;/p&gt;
&lt;h2 id=&quot;이-문서의-흐름&quot;&gt;이 문서의 흐름&lt;/h2&gt;
&lt;p&gt;각 격리 수준을 낮은 것부터 하나씩 짚는다. 각 수준에서:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;이 수준은 무엇을 보장하는가&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;하지만 여기서도 어떤 문제가 발생하는가&lt;/strong&gt; (애니메이션으로 시각화)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;그 문제의 해결법과 대안&lt;/strong&gt; (다음 수준으로 올릴지, 다른 기법으로 우회할지)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;격리-수준-매트릭스-전체-지도&quot;&gt;격리 수준 매트릭스 (전체 지도)&lt;/h2&gt;
&lt;p&gt;먼저 어떤 이상 현상이 어느 수준에서 방지되는지 한눈에.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;이상 현상&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;READ UNCOMMITTED&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;READ COMMITTED&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;REPEATABLE READ&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;SERIALIZABLE&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Dirty Read&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;발생&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;✓ 방지&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;✓ 방지&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;✓ 방지&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Non-Repeatable Read&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;발생&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;발생&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;✓ 방지&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;✓ 방지&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Phantom Read&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;발생&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;발생&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;△ 엔진별&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;✓ 방지&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Lost Update&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;발생&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;발생&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;△ SI는 감지&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;✓ 방지&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Write Skew&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;발생&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;발생&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;발생&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;✓ 방지&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;엔진별 차이&lt;/strong&gt;가 매우 크다. 특히 REPEATABLE READ 는 &lt;em&gt;같은 이름이라도 완전히 다른 것&lt;/em&gt;. 아래에서 자세히.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;level-1-read-uncommitted&quot;&gt;Level 1: READ UNCOMMITTED&lt;/h2&gt;
&lt;h3 id=&quot;이-수준의-정의&quot;&gt;이 수준의 정의&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;커밋되지 않은 값까지 다른 트랜잭션에 노출된다.&lt;/strong&gt; 어떤 격리도 없다시피 하는 상태.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ANSI 4단계 중 가장 낮음&lt;/li&gt;
&lt;li&gt;락을 거의 사용 안 함 → 최대 동시성&lt;/li&gt;
&lt;li&gt;그러나 정합성은 최하&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;발생하는-문제-dirty-read&quot;&gt;발생하는 문제: Dirty Read&lt;/h3&gt;
&lt;p&gt;트랜잭션 A 가 UPDATE 만 하고 아직 COMMIT 안 한 시점에 트랜잭션 B 가 그 값을 읽어 간다. A 가 ROLLBACK 하면 B 는 &lt;em&gt;실존하지 않는 값&lt;/em&gt; 을 본 것.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;시나리오&lt;/strong&gt; (위 애니메이션):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 초기: balance = 500&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- T1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; TRANSACTION&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ISOLATION&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; LEVEL&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; READ&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; UNCOMMITTED&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BEGIN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;UPDATE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; accounts &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; balance &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1000&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 아직 COMMIT 안 함&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- T2 (별도 세션)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BEGIN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; balance &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; accounts &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 결과: 1000  ← 미커밋 값 (dirty)!&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- T1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;ROLLBACK&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- balance 는 다시 500 이 됨. T2 가 봤던 1000 은 유령.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;왜 문제인가&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;T2 는 1000 을 보고 다음 판단 (송금 승인, 통계 계산 등) 을 내림&lt;/li&gt;
&lt;li&gt;실제 커밋된 적 없는 값에 기반한 결정 → 데이터 일관성 파괴&lt;/li&gt;
&lt;li&gt;ROLLBACK 이 잦다면 유령 데이터가 시스템 곳곳에 남음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;해결법--대안&quot;&gt;해결법 / 대안&lt;/h3&gt;






























&lt;table&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;strong&gt;격리 올림&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;READ COMMITTED 이상 사용&lt;/td&gt;&lt;td&gt;대부분의 실무 (권장)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;MVCC 엔진 채택&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;PostgreSQL, MySQL, Oracle 등&lt;/td&gt;&lt;td&gt;&lt;em&gt;구조적으로&lt;/em&gt; dirty read 가 불가능&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;read-only 명시&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;SET TRANSACTION READ ONLY;&lt;/code&gt;&lt;/td&gt;&lt;td&gt;대략적 통계, 리포트&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;replica 분리&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Analytics 는 replica DB 로&lt;/td&gt;&lt;td&gt;최신성 덜 중요한 워크로드&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;strong&gt;PostgreSQL&lt;/strong&gt; 은 &lt;code&gt;READ UNCOMMITTED&lt;/code&gt; 를 지정해도 &lt;em&gt;내부적으로 READ COMMITTED 로 승격&lt;/em&gt;. MVCC 특성상 dirty read 가 원천 불가능. 즉 PostgreSQL 에서 이 수준을 “쓰는” 것은 명목상만 의미 있음.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;READ UNCOMMITTED 를 실제 쓰는 경우가 있나?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SQL Server 의 &lt;code&gt;WITH (NOLOCK)&lt;/code&gt; hint 로 유사 효과. 대시보드 스냅샷 등에 종종 쓰이지만 &lt;em&gt;공식적으로 지양&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Analytics/OLAP 워크로드는 대개 replica 로 분리하는 편이 낫다.&lt;/li&gt;
&lt;li&gt;결국 &lt;em&gt;거의 안 씀&lt;/em&gt;. 실무에서는 이 수준을 이해하되 실제 사용은 피한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;level-2-read-committed&quot;&gt;Level 2: READ COMMITTED&lt;/h2&gt;
&lt;h3 id=&quot;이-수준의-정의-1&quot;&gt;이 수준의 정의&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;커밋된 값만 읽는다.&lt;/strong&gt; Dirty Read 는 완전히 방지.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PostgreSQL, Oracle, SQL Server 의 &lt;em&gt;기본값&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;MVCC 엔진에서는 &lt;em&gt;문장 (statement) 마다&lt;/em&gt; 새 스냅샷을 잡음&lt;/li&gt;
&lt;li&gt;락 사용 최소 → 높은 동시성&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- PostgreSQL 기본&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BEGIN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; balance &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; accounts &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 이 시점의 committed 값만 봄&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;COMMIT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;발생하는-문제-non-repeatable-read&quot;&gt;발생하는 문제: Non-Repeatable Read&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;같은 트랜잭션 안에서 같은 SELECT 를 두 번 실행했는데 결과가 다르다.&lt;/strong&gt; 두 SELECT 사이에 다른 tx 가 UPDATE + COMMIT 하면 발생.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;시나리오&lt;/strong&gt; (위 애니메이션):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 초기: balance = 500&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- T1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; TRANSACTION&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ISOLATION&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; LEVEL&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; READ&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; COMMITTED&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BEGIN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; balance &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; accounts &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 500&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- T2 (별도 세션): UPDATE + COMMIT 완료&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- T1 (계속)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; balance &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; accounts &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 1000  ← 같은 tx인데 다름!&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;COMMIT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;왜 문제인가&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;트랜잭션 안에서 계산에 쓴 값이 중간에 바뀜&lt;/li&gt;
&lt;li&gt;예: &lt;code&gt;총액 = 첫 SELECT&lt;/code&gt;, &lt;code&gt;수수료 = 두번째 SELECT * 0.1&lt;/code&gt; 조합에서 총액과 수수료 비율이 불일치&lt;/li&gt;
&lt;li&gt;리포트 생성 중 값이 흔들리면 통계가 왜곡됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;해결법--대안-1&quot;&gt;해결법 / 대안&lt;/h3&gt;






























&lt;table&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;strong&gt;격리 올림&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;REPEATABLE READ 이상&lt;/td&gt;&lt;td&gt;같은 값 보장, 성능 약간 손해&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;명시적 락&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; (row lock)&lt;/td&gt;&lt;td&gt;정확, 대기 시간 발생&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;원자 연산&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;UPDATE ... SET x = x - 100&lt;/code&gt;&lt;/td&gt;&lt;td&gt;애초에 read-modify-write 안 함&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;애플리케이션 캐싱&lt;/strong&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;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 옵션 1: REPEATABLE READ 로 올림&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; TRANSACTION&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ISOLATION&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; LEVEL&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; REPEATABLE&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; READ&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 옵션 2: FOR UPDATE 로 row 잠금&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; balance &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; accounts &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; FOR&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; UPDATE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 이후 tx 끝까지 다른 tx의 UPDATE는 대기&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 옵션 3: 원자 UPDATE 로 변경&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;UPDATE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; accounts &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; balance &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; balance &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 100&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- read-modify-write 자체를 피함&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
&lt;strong&gt;일반 CRUD 웹 서비스는 READ COMMITTED 로 충분.&lt;/strong&gt; Non-Repeatable Read 가 실제 문제가 되는 상황은 리포트, 잔액 계산 등 &lt;em&gt;같은 값을 여러 번 참조하는 로직&lt;/em&gt;. 그런 부분만 골라 REPEATABLE READ 나 FOR UPDATE 를 붙이는 편이 낫다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;level-3-repeatable-read&quot;&gt;Level 3: REPEATABLE READ&lt;/h2&gt;
&lt;h3 id=&quot;이-수준의-정의-2&quot;&gt;이 수준의 정의&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;같은 트랜잭션 안에서 같은 row 는 항상 같은 값.&lt;/strong&gt; ANSI 정의로는 Non-Repeatable Read 만 방지하고 Phantom Read 는 허용. 하지만 &lt;em&gt;실제 엔진마다 다르다&lt;/em&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MySQL InnoDB 의 &lt;em&gt;기본값&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;PostgreSQL 은 명시적으로 지정 시 사용&lt;/li&gt;
&lt;li&gt;MVCC 엔진에서는 &lt;em&gt;트랜잭션 시작 시점의 스냅샷&lt;/em&gt;을 tx 끝까지 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;엔진별 실제 구현&lt;/strong&gt;:&lt;/p&gt;













































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;엔진&lt;/th&gt;&lt;th&gt;구현 방식&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;Non-Rep&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;Phantom&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;Lost Update&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;Write Skew&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;MySQL InnoDB&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;2PL + next-key lock (범위 잠금)&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;✓ 방지&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;✗&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;✗&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;PostgreSQL&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Snapshot Isolation (MVCC)&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;✓ 사실상 방지&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;△ 감지&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;✗&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Oracle&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;미지원 (READ COMMITTED / SERIALIZABLE 만)&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;-&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;SQL Server&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;2PL&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;✓&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;✗&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;✗&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;✗&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;strong&gt;같은 REPEATABLE READ 라도 엔진마다 완전히 다르다.&lt;/strong&gt; MySQL RR 은 사실상 SERIALIZABLE 에 가까운 락 사용, PostgreSQL RR 은 Snapshot Isolation (락 없이 스냅샷). 이름 하나로 여러 개념을 뭉뚱그린 대표적 사례.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;발생하는-문제-phantom-read-ansi-정의상&quot;&gt;발생하는 문제: Phantom Read (ANSI 정의상)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;같은 WHERE 조건을 두 번 실행했는데 row 개수가 다르다.&lt;/strong&gt; UPDATE 가 아니라 &lt;em&gt;INSERT/DELETE&lt;/em&gt; 가 원인.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;시나리오&lt;/strong&gt; (위 애니메이션):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 초기: orders 에 amount &gt; 100 인 row 3개&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- T1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; TRANSACTION&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ISOLATION&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; LEVEL&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; REPEATABLE&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; READ&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BEGIN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; COUNT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; orders &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; amount &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 100&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 3&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- T2 (별도 세션)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BEGIN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;INSERT INTO&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; orders (id, amount) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;VALUES&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;999&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;200&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;COMMIT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- T1 (계속)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; COUNT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; orders &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; amount &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 100&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- ANSI 정의상: 4 (phantom!)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- MySQL InnoDB: 3 (next-key lock으로 방지)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- PostgreSQL: 3 (snapshot으로 방지)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;COMMIT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;왜 문제인가&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WHERE 범위에 해당하는 row 개수를 두 번 재는 로직에서 결과가 달라짐&lt;/li&gt;
&lt;li&gt;예: 재고 검색 → 예약 처리 → 재확인 시 새 항목이 튀어나옴&lt;/li&gt;
&lt;li&gt;리포트 통계, 배치 처리에서 count/sum 이 흔들림&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;해결법--대안-2&quot;&gt;해결법 / 대안&lt;/h3&gt;






























&lt;table&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;strong&gt;MySQL InnoDB&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;REPEATABLE READ (기본)&lt;/td&gt;&lt;td&gt;&lt;em&gt;이미 방지됨&lt;/em&gt; (next-key lock). 별도 조치 불필요&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;PostgreSQL&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;REPEATABLE READ (SI)&lt;/td&gt;&lt;td&gt;&lt;em&gt;이미 방지됨&lt;/em&gt; (snapshot). 스냅샷 시점 이후 insert 는 안 보임&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Oracle / SQL Server (2PL)&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;REPEATABLE READ&lt;/td&gt;&lt;td&gt;SERIALIZABLE 로 올리거나 &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; 로 범위 잠금&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;모든 엔진&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;안전 확실 필요&lt;/td&gt;&lt;td&gt;SERIALIZABLE 또는 predicate lock&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;MySQL 의 next-key lock 이란?&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;[gap lock][row lock][gap lock][row lock][gap lock]...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;WHERE 조건에 매칭되는 인덱스 범위 전체에 lock. 새 row 가 그 범위에 INSERT 되지 못하게 막아 phantom 방지. 강력하지만 &lt;em&gt;예상치 못한 deadlock&lt;/em&gt; 이 잦음.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PostgreSQL 의 SI 는?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;트랜잭션 시작 시점의 스냅샷만 계속 봄. T2 가 나중에 INSERT + COMMIT 해도 T1 의 스냅샷에는 없으므로 count 가 그대로.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;strong&gt;PostgreSQL REPEATABLE READ 는 Snapshot Isolation 이지 진짜 Serializable 이 아니다.&lt;/strong&gt; 아래 Write Skew 절에서 다룰 함정이 있다. 정합성이 극단적으로 중요하면 SERIALIZABLE 로 올려야 함.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;level-4-serializable&quot;&gt;Level 4: SERIALIZABLE&lt;/h2&gt;
&lt;h3 id=&quot;이-수준의-정의-3&quot;&gt;이 수준의 정의&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;개념적으로 트랜잭션이 하나씩 직렬 실행된 것과 같은 결과.&lt;/strong&gt; 모든 이상 현상을 방지.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;가장 강한 격리 수준&lt;/li&gt;
&lt;li&gt;성능 대가 큼 (엔진마다 다름)&lt;/li&gt;
&lt;li&gt;진짜 필요한 경우에만 (금융, 재고, 티켓팅 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;엔진별 구현 방식&lt;/strong&gt;:&lt;/p&gt;






























&lt;table&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;strong&gt;PostgreSQL 9.1+&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;SSI (Serializable Snapshot Isolation)&lt;/td&gt;&lt;td&gt;&lt;em&gt;성능 손해 적음&lt;/em&gt;, 충돌 시 abort → retry 필요&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;MySQL InnoDB&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Strict 2PL (모든 SELECT 이 shared lock 획득)&lt;/td&gt;&lt;td&gt;대기 많음, deadlock 위험&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;SQL Server&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;2PL (SERIALIZABLE 힌트)&lt;/td&gt;&lt;td&gt;MySQL 유사&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;CockroachDB&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;SSI 변형&lt;/td&gt;&lt;td&gt;기본이 SERIALIZABLE&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 id=&quot;serializable-을-쓸-때-알아야-할-것&quot;&gt;SERIALIZABLE 을 쓸 때 알아야 할 것&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;abort → retry 패턴 필수&lt;/strong&gt; (특히 PostgreSQL SSI):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; time&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; psycopg2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; attempt &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; range&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;5&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    try&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        with&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; conn.cursor() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; cur:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            cur.execute(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;BEGIN ISOLATION LEVEL SERIALIZABLE&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;            # ... 비즈니스 로직 ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            cur.execute(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;COMMIT&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        break&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    except&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; psycopg2.errors.SerializationFailure:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        conn.rollback()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        time.sleep(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0.01&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; **&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; attempt))  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 지수 backoff&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; attempt &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 4&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            raise&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;왜 abort 가 필요한가&lt;/strong&gt;: SSI 는 각 tx 가 자유롭게 진행하다가 커밋 직전에 &lt;em&gt;충돌 감지&lt;/em&gt;. 감지되면 한쪽을 강제 abort (SQLSTATE 40001). 애플리케이션은 이를 잡아 재시도해야 함.&lt;/p&gt;
&lt;h3 id=&quot;성능-트레이드오프&quot;&gt;성능 트레이드오프&lt;/h3&gt;
&lt;p&gt;일반 워크로드에서 READ COMMITTED 대비:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PostgreSQL SSI&lt;/strong&gt;: 약 10-20% 느림&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2PL 기반 (MySQL, SQL Server)&lt;/strong&gt;: 최대 수 배 느림 (경합 심한 경우)&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
“무조건 SERIALIZABLE” 은 나쁜 선택. 대부분 상황은 &lt;em&gt;READ COMMITTED + 필요한 로직에 SELECT FOR UPDATE 또는 원자 UPDATE&lt;/em&gt; 조합이 최적. SERIALIZABLE 은 &lt;em&gt;진짜로 write skew 위험이 있는 로직&lt;/em&gt; 에만.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;ansi-표준을-벗어난-이상-현상들&quot;&gt;ANSI 표준을 벗어난 이상 현상들&lt;/h2&gt;
&lt;p&gt;ANSI SQL-92 표준의 4단계 매트릭스는 &lt;em&gt;3가지 이상 현상만&lt;/em&gt; 정의했다. Berenson et al. (1995) 논문이 이 결함을 지적하며 2가지를 추가했다.&lt;/p&gt;
&lt;h3 id=&quot;lost-update-읽고--계산--쓰기-경쟁&quot;&gt;Lost Update (읽고 → 계산 → 쓰기 경쟁)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;같은 데이터를 두 tx 가 read-modify-write 하는 사이, 한쪽의 UPDATE 가 사라진다.&lt;/strong&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;시나리오&lt;/strong&gt; (위 애니메이션):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 초기: count = 10&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- T1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BEGIN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; count &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; counters &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 10&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 로컬 계산: 10 + 1 = 11&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;UPDATE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; counters &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; count &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 11&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;COMMIT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- T2 (T1과 병렬)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BEGIN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; count &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; counters &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 10 (T1 커밋 전에 SELECT)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 로컬 계산: 10 + 1 = 11&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;UPDATE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; counters &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; count &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 11&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- T1이 쓴 값 위에 덮어씀&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;COMMIT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 결과: count = 11 (기대치는 12)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;어느 수준에서 발생하는가&lt;/strong&gt;: READ COMMITTED, REPEATABLE READ 모두 가능. PostgreSQL 의 SI (REPEATABLE READ) 는 first-writer-wins 로 감지해서 두번째 UPDATE 에서 에러를 냄. SERIALIZABLE 은 항상 방지.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결법&lt;/strong&gt; (권장 순서):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- ① 원자 UPDATE (가장 단순, 항상 우선)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;UPDATE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; counters &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; count &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; count &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 단일 UPDATE 문은 atomic. race condition 없음.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- ② 낙관적 lock (version 컬럼)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;UPDATE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; counters &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; count &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; count &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;version&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; version&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; AND&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; version&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; &amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;읽었을 때의 값&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 0 row affected 면 race 발생 → 애플리케이션에서 재시도&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- ③ 비관적 lock (SELECT FOR UPDATE)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BEGIN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; count &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; counters &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; FOR&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; UPDATE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- row 잠금&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;UPDATE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; counters &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; count &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; count &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;COMMIT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- ④ SERIALIZABLE 로 격리 올림&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BEGIN&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ISOLATION&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; LEVEL&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; SERIALIZABLE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 충돌 시 abort 재시도&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- ⑤ 별도 원자 저장소 (예: Redis INCR)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;



































&lt;table&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;strong&gt;원자 UPDATE&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;단순 counter/누계, 사용 가능하면 항상&lt;/td&gt;&lt;td&gt;복잡 로직 못 표현&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;낙관적 lock&lt;/strong&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;strong&gt;비관적 lock&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;충돌 잦음&lt;/td&gt;&lt;td&gt;대기 시간 ↑, deadlock 위험&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;SERIALIZABLE&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;정확성 우선&lt;/td&gt;&lt;td&gt;abort 재시도 필요&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Redis 등 별도 저장소&lt;/strong&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;h3 id=&quot;write-skew-snapshot-isolation-의-치명적-함정&quot;&gt;Write Skew (Snapshot Isolation 의 치명적 함정)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;각자의 스냅샷 안에서는 조건이 참이지만, 두 tx 의 결과를 합치면 불변량이 깨진다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Lost Update 와 다른 점: Lost Update 는 &lt;em&gt;같은 row&lt;/em&gt; 를 두 tx 가 UPDATE 해서 하나가 사라짐. Write Skew 는 &lt;em&gt;다른 row&lt;/em&gt; 를 각자 UPDATE 해서 first-writer-wins 감지가 안 됨.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;시나리오: 병원 온콜 스케줄&lt;/strong&gt; (위 애니메이션)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;규칙: 최소 1명은 항상 on-call&lt;/li&gt;
&lt;li&gt;현재 Alice, Bob 두 명이 on-call&lt;/li&gt;
&lt;li&gt;둘 다 아파서 동시에 off-call 요청&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 초기: on-call 2명 (alice, bob)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- T1 (Alice의 세션)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BEGIN&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ISOLATION&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; LEVEL&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; REPEATABLE&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; READ&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; COUNT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; doctors &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; on_call &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; true;  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 2, 조건 OK&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;UPDATE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; doctors &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; on_call &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; false &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;alice&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;COMMIT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 성공&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- T2 (Bob의 세션, T1과 동시)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BEGIN&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ISOLATION&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; LEVEL&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; REPEATABLE&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; READ&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; COUNT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; doctors &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; on_call &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; true;  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 2 (자기 스냅샷), 조건 OK&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;UPDATE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; doctors &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; on_call &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; false &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;bob&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;COMMIT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 성공 (T1이 alice 를 건드렸을 뿐 bob 은 안 건드렸으므로)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; COUNT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; doctors &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; on_call &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; true;  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 0 !! 규칙 위반&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;왜 SI 가 못 막나&lt;/strong&gt;: 두 tx 가 &lt;em&gt;다른 row&lt;/em&gt; 를 UPDATE 했으므로 write-write 충돌이 없음. SI 의 first-writer-wins 는 같은 row 에 대해서만 감지. 각자의 스냅샷 안에서는 count(on_call) &gt;= 1 조건이 참이었지만, 실제로는 결합되어 위반됨.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;해결법&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- ① SERIALIZABLE (PostgreSQL SSI는 이를 자동 감지)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BEGIN&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ISOLATION&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; LEVEL&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; SERIALIZABLE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; COUNT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; doctors &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; on_call &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; true;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;UPDATE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; doctors &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; on_call &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; false &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;alice&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;COMMIT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 동시 T2 시도 시 ERROR: could not serialize access due to read/write dependencies&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- → 애플리케이션은 재시도&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- ② SELECT ... FOR UPDATE 로 predicate 잠금&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BEGIN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; COUNT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; doctors &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; on_call &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; true &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FOR&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; UPDATE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 이제 다른 tx는 on_call=true 인 row 를 UPDATE 할 수 없음&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;UPDATE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; doctors &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; on_call &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; false &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;alice&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;COMMIT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- ③ materializing the conflict (강제 write-write 충돌 만들기)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;CREATE&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; TABLE&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; on_call_lock&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (locked &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BOOLEAN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;INSERT INTO&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; on_call_lock &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;VALUES&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (true);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BEGIN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;UPDATE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; on_call_lock &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; locked &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; true;  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 모든 tx가 이 row 를 UPDATE → 충돌 감지 가능&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; COUNT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;*&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; doctors &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; on_call &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; true;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;UPDATE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; doctors &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; on_call &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; false &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;WHERE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; id &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;alice&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;COMMIT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
Write Skew 는 &lt;em&gt;ANSI 표준에 없다&lt;/em&gt;. Berenson et al. 이 SI 의 함정을 지적하며 도입한 개념. PostgreSQL SSI (9.1+) 가 이를 자동 감지하는 유일한 격리 수준. MySQL SERIALIZABLE 은 strict 2PL 로 결과적으로 방지하지만 방법이 다름.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;postgresql-ssi-serializable-snapshot-isolation&quot;&gt;PostgreSQL SSI (Serializable Snapshot Isolation)&lt;/h2&gt;
&lt;p&gt;PostgreSQL 9.1 부터 도입된 SSI 는 &lt;em&gt;Snapshot Isolation 위에 predicate lock 추적을 얹어&lt;/em&gt; 진정한 serializability 를 달성한다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    T1[T1 시작 스냅샷] --&gt; Read1[T1 SELECT / UPDATE]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    T2[T2 시작 스냅샷] --&gt; Read2[T2 SELECT / UPDATE]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Read1 --&gt; Track[predicate + row 접근 기록]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Read2 --&gt; Track&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Track --&gt; Commit{둘 commit 시도}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Commit --&gt; Cycle{의존성 순환 감지?}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Cycle --&gt;|예| Abort[한쪽 abort&amp;#x3C;br/&gt;SQLSTATE 40001]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Cycle --&gt;|아니오| OK[둘 다 commit]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Abort --&gt; Retry[애플리케이션 재시도]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;핵심 아이디어&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;각 tx 는 자기 스냅샷 위에서 자유롭게 실행 (락 최소)&lt;/li&gt;
&lt;li&gt;백그라운드에서 &lt;em&gt;predicate lock&lt;/em&gt; 추적 (T1 이 이 범위를 읽었다, T2 는 이 row 를 썼다)&lt;/li&gt;
&lt;li&gt;커밋 시점에 &lt;em&gt;rw-dependency&lt;/em&gt; 순환 있으면 한쪽 abort&lt;/li&gt;
&lt;li&gt;애플리케이션은 40001 잡아서 재시도&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;장점&lt;/strong&gt;: throughput 손해 적음 (2PL 대비 큰 차이), 정확성 완벽.
&lt;strong&gt;단점&lt;/strong&gt;: 재시도 코드 필수. false positive 가능하지만 정확성에는 영향 없음.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;mvcc-기반-스냅샷-격리의-원리&quot;&gt;MVCC 기반 스냅샷 격리의 원리&lt;/h2&gt;
&lt;p&gt;각 트랜잭션은 시작 시점의 &lt;em&gt;스냅샷&lt;/em&gt; 을 잡고, 그 시점 이후의 변경은 자기에게 보이지 않는다.&lt;/p&gt;
&lt;p&gt;PostgreSQL 은 각 row (tuple) 에 hidden column &lt;code&gt;xmin&lt;/code&gt;, &lt;code&gt;xmax&lt;/code&gt; 를 저장:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;row 1: (id=1, name=&apos;Alice&apos;, xmin=100, xmax=0)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        → T200 이 UPDATE&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;row 1: (id=1, name=&apos;Alice&apos;, xmin=100, xmax=200)  ← 옛 버전&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;row 1: (id=1, name=&apos;Bob&apos;,   xmin=200, xmax=0)    ← 새 버전&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;트랜잭션 T150 은:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;xmin ≤ 150 이고 (xmax = 0 or xmax &gt; 150) 인 tuple 만 봄&lt;/li&gt;
&lt;li&gt;즉, “내 스냅샷 시점에 살아 있던” 버전만 봄&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;자세한 구현은 [[mvcc]] 참조.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;실무-가이드&quot;&gt;실무 가이드&lt;/h2&gt;
&lt;h3 id=&quot;언제-어느-수준을&quot;&gt;언제 어느 수준을?&lt;/h3&gt;



































&lt;table&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;일반 CRUD, 짧은 tx&lt;/td&gt;&lt;td&gt;&lt;strong&gt;READ COMMITTED&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;성능 좋음, dirty read 만 방지&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;배치 리포트 (일관 스냅샷)&lt;/td&gt;&lt;td&gt;&lt;strong&gt;REPEATABLE READ&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;같은 값 여러 번 사용&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;잔액 계산, 재고 확인&lt;/td&gt;&lt;td&gt;&lt;strong&gt;REPEATABLE READ + FOR UPDATE&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;phantom 안전&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;금융 거래, 티켓 발권&lt;/td&gt;&lt;td&gt;&lt;strong&gt;SERIALIZABLE&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;write skew 방지&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Analytics (최신성 덜 중요)&lt;/td&gt;&lt;td&gt;&lt;strong&gt;READ COMMITTED READ ONLY&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;최대 동시성&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 id=&quot;격리-수준-명시-방법&quot;&gt;격리 수준 명시 방법&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 세션 전체&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; SESSION&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; TRANSACTION&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ISOLATION&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; LEVEL&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; SERIALIZABLE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 트랜잭션 하나만&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BEGIN&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; TRANSACTION&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ISOLATION&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; LEVEL&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; REPEATABLE&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; READ&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;COMMIT&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- PostgreSQL: 시작과 동시에&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;BEGIN&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; ISOLATION&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; LEVEL&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; SERIALIZABLE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- 프로그램 레벨 (JDBC 예시)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;connection&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;setTransactionIsolation&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;Connection&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;TRANSACTION_SERIALIZABLE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;프레임워크-예시&quot;&gt;프레임워크 예시&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Django&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; django.db &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; transaction&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; django.db &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; connection&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;@transaction.atomic&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; transfer&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(from_id, to_id, amount):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    with&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; connection.cursor() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;as&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; cursor:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        cursor.execute(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;SET TRANSACTION ISOLATION LEVEL SERIALIZABLE&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # ... 로직 ...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;java&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;// Spring&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;@&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;Transactional&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;isolation&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Isolation.SERIALIZABLE)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;public&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; void&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; transfer&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(Long from, Long to, BigDecimal amount) { ... }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자세한 프레임워크별 사용은 [[django-transactions]], [[spring-transaction]] 참조.&lt;/p&gt;
&lt;h2 id=&quot;실제-db-엔진-매핑&quot;&gt;실제 DB 엔진 매핑&lt;/h2&gt;



























































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;엔진&lt;/th&gt;&lt;th&gt;기본 격리&lt;/th&gt;&lt;th&gt;SERIALIZABLE 구현&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;strong&gt;PostgreSQL&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;READ COMMITTED&lt;/td&gt;&lt;td&gt;SSI (Serializable Snapshot Isolation)&lt;/td&gt;&lt;td&gt;RR = Snapshot Isolation&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;MySQL InnoDB&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;REPEATABLE READ&lt;/td&gt;&lt;td&gt;Strict 2PL&lt;/td&gt;&lt;td&gt;RR 에서 next-key lock 으로 phantom 방지&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Oracle&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;READ COMMITTED&lt;/td&gt;&lt;td&gt;SI 유사 (완전 SS 아님)&lt;/td&gt;&lt;td&gt;RR 미지원&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;SQL Server&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;READ COMMITTED&lt;/td&gt;&lt;td&gt;2PL&lt;/td&gt;&lt;td&gt;RCSI 옵션 (MVCC), Snapshot 옵션 별개&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;CockroachDB&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;SERIALIZABLE&lt;/td&gt;&lt;td&gt;SSI 변형&lt;/td&gt;&lt;td&gt;기본이 SS&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;MongoDB&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;(트랜잭션은 SS)&lt;/td&gt;&lt;td&gt;SI&lt;/td&gt;&lt;td&gt;단일 문서는 원자적&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;DynamoDB&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;(item 원자성)&lt;/td&gt;&lt;td&gt;TransactWriteItems (SSI 유사)&lt;/td&gt;&lt;td&gt;트랜잭션 API 사용 시&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Cassandra&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;(트랜잭션 미지원)&lt;/td&gt;&lt;td&gt;LWT (Lightweight Transactions)&lt;/td&gt;&lt;td&gt;Paxos 기반 우회&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;“REPEATABLE READ 라니 안전하겠지”&lt;/strong&gt; = MySQL 과 PostgreSQL 의 RR 은 완전히 다른 것. Write Skew 는 어느 쪽에서도 발생.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SERIALIZABLE 만 박고 abort 처리 없음&lt;/strong&gt; = 부하 시 40001 폭증하여 요청 실패. &lt;em&gt;재시도 코드 필수&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;READ COMMITTED 로 잔액 계산&lt;/strong&gt; = tx 안에서도 잔액이 바뀔 수 있음. 은행 앱에서 문제.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MySQL RR + gap lock deadlock&lt;/strong&gt; = 의도치 않은 범위 잠금으로 데드락 폭증. &lt;code&gt;SHOW ENGINE INNODB STATUS\G&lt;/code&gt; 로 진단.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;낙관적 lock 없이 read-modify-write&lt;/strong&gt; = 트래픽 증가 시 lost update 폭증.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;SELECT FOR UPDATE&lt;/code&gt; 를 잔뜩 걸음&lt;/strong&gt; = 대기 시간 폭증 + deadlock. 필요한 최소 범위만.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;긴 트랜잭션 (long-running tx)&lt;/strong&gt; = VACUUM 방해, snapshot 유지 비용, lock 대기 폭증. 5분 넘으면 검토.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;격리 수준을 애플리케이션 코드에서 매번 바꿈&lt;/strong&gt; = 세션 풀에서 오염 (다음 요청이 이전 설정 상속). 명시적 리셋 필요.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;anti-pattern-lock-으로-모든-것-해결-시도&quot;&gt;Anti-Pattern: Lock 으로 모든 것 해결 시도&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;sql&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- ✗ 나쁜 예: 모든 SELECT 에 FOR UPDATE&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; users &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FOR&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; UPDATE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; orders &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FOR&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; UPDATE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;SELECT&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; *&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; FROM&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; products &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;FOR&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; UPDATE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;-- → deadlock 폭증, 대기 시간 ↑&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;개선&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;필요한 곳만 SELECT … FOR UPDATE (충돌 실제 발생하는 row 만)&lt;/li&gt;
&lt;li&gt;순서 일관성 (항상 users → orders → products 순으로 lock)&lt;/li&gt;
&lt;li&gt;SSI 로 전환 (retry 로 처리)&lt;/li&gt;
&lt;li&gt;원자 UPDATE 가능하면 그것으로&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[mvcc]], MVCC 구현 상세, xmin/xmax, snapshot&lt;/li&gt;
&lt;li&gt;[[postgresql]], PostgreSQL 프로세스 모델 + MVCC&lt;/li&gt;
&lt;li&gt;[[mysql-innodb]], InnoDB 락 시스템 (record, gap, next-key)&lt;/li&gt;
&lt;li&gt;[[sql-dcl-tcl]], TCL 명령 (BEGIN, COMMIT, SAVEPOINT)&lt;/li&gt;
&lt;li&gt;[[wal-write-ahead-log]], 크래시 복구와 durability&lt;/li&gt;
&lt;li&gt;[[spring-transaction]], Spring &lt;code&gt;@Transactional&lt;/code&gt; 사용법&lt;/li&gt;
&lt;li&gt;[[django-transactions]], Django 트랜잭션 관리&lt;/li&gt;
&lt;li&gt;[[distributed-systems-distributed-transaction]], 분산 트랜잭션 (2PC, Saga)&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>transaction</category><category>isolation</category><category>database</category><category>acid</category><category>mvcc</category><category>postgresql</category><category>mysql</category><author>koa (김신건)</author></item><item><title>[DRF] Filtering: django-filter, SearchFilter, OrderingFilter</title><link>https://shinkeonkim.com/wiki/django/drf-filtering/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/django/drf-filtering/</guid><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Filtering&lt;/strong&gt; = 쿼리 파라미터로 &lt;em&gt;결과 제한&lt;/em&gt;. 3가지 backend + custom.&lt;/p&gt;
&lt;h2 id=&quot;3가지-내장-backend&quot;&gt;3가지 내장 Backend&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q{Filter Backend}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; DF[&quot;DjangoFilterBackend&amp;#x3C;br/&gt;(django-filter, 완전 필터)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; SF[&quot;SearchFilter&amp;#x3C;br/&gt;(전문 검색 like)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; OF[&quot;OrderingFilter&amp;#x3C;br/&gt;(정렬)&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;설치--설정&quot;&gt;설치 + 설정&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;pip&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; django-filter&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;INSTALLED_APPS&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;django_filters&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;REST_FRAMEWORK&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &apos;DEFAULT_FILTER_BACKENDS&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &apos;django_filters.rest_framework.DjangoFilterBackend&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &apos;rest_framework.filters.SearchFilter&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &apos;rest_framework.filters.OrderingFilter&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;1-djangofilterbackend&quot;&gt;1. DjangoFilterBackend&lt;/h2&gt;
&lt;h3 id=&quot;간단한-필드-매칭&quot;&gt;간단한 필드 매칭&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ArticleList&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;generics&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ListAPIView&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    queryset &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Article.objects.all()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    serializer_class &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ArticleSerializer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    filterset_fields &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;author&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;category&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;is_published&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;GET&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; /articles/?author=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x26;category&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;tech&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x26;is_published&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;true&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;상세-filterset&quot;&gt;상세 FilterSet&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; django_filters&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ArticleFilter&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;django_filters&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;FilterSet&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    title &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; django_filters.CharFilter(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;lookup_expr&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;icontains&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    min_views &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; django_filters.NumberFilter(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;field_name&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;views&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;lookup_expr&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;gte&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    max_views &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; django_filters.NumberFilter(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;field_name&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;views&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;lookup_expr&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;lte&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    published_after &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; django_filters.DateFilter(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;field_name&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;published_at&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;lookup_expr&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;gte&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    tag &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; django_filters.CharFilter(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;method&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;filter_tag&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    ordering &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; django_filters.OrderingFilter(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        fields&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            (&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;title&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;title&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            (&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;views&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;views&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            (&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;published_at&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;published_at&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Meta&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        model &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Article&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        fields &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;author&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;category&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; filter_tag&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, queryset, name, value):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; queryset.filter(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;tags__name__iexact&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;value)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ArticleList&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;generics&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ListAPIView&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    queryset &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Article.objects.all()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    serializer_class &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ArticleSerializer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    filterset_class &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ArticleFilter&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;GET&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; /articles/?title=django&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x26;min_views&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;100&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x26;published_after&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;2026-01-01&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x26;tag&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;python&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;lookup-expressions&quot;&gt;Lookup expressions&lt;/h2&gt;

















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Lookup&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;exact&lt;/code&gt; (기본)&lt;/td&gt;&lt;td&gt;&lt;code&gt;field=x&lt;/code&gt; → WHERE field = x&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;iexact&lt;/code&gt;&lt;/td&gt;&lt;td&gt;대소문자 무시&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;contains&lt;/code&gt; / &lt;code&gt;icontains&lt;/code&gt;&lt;/td&gt;&lt;td&gt;LIKE&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;startswith&lt;/code&gt; / &lt;code&gt;endswith&lt;/code&gt;&lt;/td&gt;&lt;td&gt;prefix/suffix&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;gt&lt;/code&gt;, &lt;code&gt;gte&lt;/code&gt;, &lt;code&gt;lt&lt;/code&gt;, &lt;code&gt;lte&lt;/code&gt;&lt;/td&gt;&lt;td&gt;크기 비교&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;in&lt;/code&gt;&lt;/td&gt;&lt;td&gt;field__in&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;range&lt;/code&gt;&lt;/td&gt;&lt;td&gt;범위&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;date&lt;/code&gt;, &lt;code&gt;year&lt;/code&gt;, &lt;code&gt;month&lt;/code&gt;, &lt;code&gt;week&lt;/code&gt;&lt;/td&gt;&lt;td&gt;날짜 부분&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;isnull&lt;/code&gt;&lt;/td&gt;&lt;td&gt;NULL 여부&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;regex&lt;/code&gt; / &lt;code&gt;iregex&lt;/code&gt;&lt;/td&gt;&lt;td&gt;정규식&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;2-searchfilter&quot;&gt;2. SearchFilter&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ArticleList&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;generics&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ListAPIView&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    queryset &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Article.objects.all()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    serializer_class &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ArticleSerializer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    filter_backends &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [filters.SearchFilter]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    search_fields &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;title&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;body&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;author__username&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;tags__name&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;GET&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; /articles/?search=django+rest&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;prefix-조정&quot;&gt;Prefix 조정&lt;/h3&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Prefix&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;icontains&lt;/code&gt; (기본)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;^&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;istartswith&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;=&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;iexact&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;@&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;full text search&lt;/code&gt; (Postgres)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;$&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;iregex&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;search_fields &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;=username&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;^title&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;@body&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;3-orderingfilter&quot;&gt;3. OrderingFilter&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ArticleList&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;generics&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ListAPIView&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    queryset &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Article.objects.all()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    serializer_class &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ArticleSerializer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    filter_backends &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [filters.OrderingFilter]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    ordering_fields &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;created_at&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;views&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;title&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    ordering &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;-created_at&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]       &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 기본 정렬&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;GET&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; /articles/?ordering=-views&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;GET&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; /articles/?ordering=title,-views&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # 다중&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;다중-backend-조합&quot;&gt;다중 Backend 조합&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ArticleList&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;generics&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ListAPIView&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    queryset &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Article.objects.all()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    serializer_class &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ArticleSerializer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    filter_backends &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        DjangoFilterBackend,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        filters.SearchFilter,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        filters.OrderingFilter,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    filterset_class &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ArticleFilter&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    search_fields &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;title&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;body&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    ordering_fields &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;created_at&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;views&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;GET&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; /articles/?category=tech&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x26;search&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;django&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;&amp;#x26;ordering&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;-views&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;custom-filter-backend&quot;&gt;Custom Filter Backend&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; rest_framework &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; filters&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; IsOwnerFilterBackend&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;filters&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;BaseFilterBackend&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; filter_queryset&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, request, queryset, view):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; queryset.filter(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;owner&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;request.user)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ArticleList&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;generics&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ListAPIView&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    filter_backends &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [IsOwnerFilterBackend, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;postgresql-full-text-search&quot;&gt;PostgreSQL Full-Text Search&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; django.contrib.postgres.search &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; SearchVector, SearchQuery, SearchRank&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ArticleList&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;generics&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ListAPIView&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    serializer_class &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ArticleSerializer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; get_queryset&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        q &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.request.query_params.get(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;q&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; q:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            vector &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; SearchVector(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;title&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;weight&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;A&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; SearchVector(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;body&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;weight&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;B&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            query &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; SearchQuery(q)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Article.objects.annotate(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;                rank&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;SearchRank(vector, query),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            ).filter(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;rank__gte&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0.1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).order_by(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;-rank&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Article.objects.all()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자세한 건 [[postgresql]] 의 full-text.&lt;/p&gt;
&lt;h2 id=&quot;openapi-문서화&quot;&gt;OpenAPI 문서화&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;django-filter&lt;/code&gt; 는 OpenAPI schema 자동:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;filterset_class &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ArticleFilter&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# → OpenAPI 에 파라미터 자동 추가&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자세한 건 [[drf-schemas-openapi]].&lt;/p&gt;
&lt;h2 id=&quot;다른-프레임워크&quot;&gt;다른 프레임워크&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Framework&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;strong&gt;DRF&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;django-filter, SearchFilter&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Rails&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;ransack, filterrific&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Spring Data&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Specification, Querydsl&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;FastAPI&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;수동 Query params&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;NestJS&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;nestjs-typeorm-crud&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;filter_fields (옛 이름)&lt;/strong&gt; = deprecated. &lt;code&gt;filterset_fields&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;큰 filterset_class + 많은 필드&lt;/strong&gt; = OpenAPI schema 폭발.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;search_fields&lt;/code&gt; 에 큰 컬럼&lt;/strong&gt; = 매 요청 full-table scan. GIN 인덱스.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ordering&lt;/code&gt; 사용자 입력 신뢰&lt;/strong&gt; = 임의 필드 정렬 → 성능. &lt;code&gt;ordering_fields&lt;/code&gt; 화이트리스트.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[drf-views]]&lt;/li&gt;
&lt;li&gt;[[drf-pagination-throttling]]&lt;/li&gt;
&lt;li&gt;[[postgresql]] (full-text)&lt;/li&gt;
&lt;li&gt;[[gin-index-deep]]&lt;/li&gt;
&lt;li&gt;[[django-queryset]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>drf</category><category>filter</category><category>search</category><category>ordering</category><category>django-filter</category><author>koa (김신건)</author></item><item><title>[DRF] Schemas + OpenAPI: 자동 문서화</title><link>https://shinkeonkim.com/wiki/django/drf-schemas-openapi/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/django/drf-schemas-openapi/</guid><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;OpenAPI&lt;/strong&gt; (전 Swagger) = REST API를 &lt;em&gt;기계 가독 스펙&lt;/em&gt;으로 기술하는 표준. DRF는 이 스펙을 &lt;em&gt;ViewSet / Serializer 로부터 자동 추출&lt;/em&gt;할 수 있어, Postman 없이 브라우저에서 API를 탐색 + 실행 가능.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;이 페이지는 &lt;a href=&quot;https://www.django-rest-framework.org/topics/documenting-your-api/&quot;&gt;DRF Documenting your API&lt;/a&gt; 를 기반으로, 실무에서 자주 쓰는 &lt;strong&gt;drf-spectacular&lt;/strong&gt; 를 중심으로 정리한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;왜-api-문서화가-필요한가&quot;&gt;왜 API 문서화가 필요한가&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    NoDoc[&quot;문서 없음&quot;] --&gt; A1[&quot;프론트, Postman 매번 시도&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    NoDoc --&gt; A2[&quot;백엔드, Slack으로 스펙 공유&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    NoDoc --&gt; A3[&quot;변경 시 fault 전파, 늦게 발견&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Doc[&quot;OpenAPI 문서&quot;] --&gt; B1[&quot;프론트, 자동 완성 + type 생성&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Doc --&gt; B2[&quot;QA, 자동 테스트 케이스 생성&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Doc --&gt; B3[&quot;변경 즉시 감지 (CI)&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;





























&lt;table&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;strong&gt;탐색&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;브라우저에서 endpoint 목록 + 파라미터 확인&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;테스트&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;Try it out 으로 즉시 호출&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;타입 생성&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;TypeScript, Kotlin 등 client SDK 자동 생성&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;계약 (Contract)&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;프론트/백 합의된 스펙, breaking change 방지&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;온보딩&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;신규 개발자 학습 시간 단축&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;도구-선택&quot;&gt;도구 선택&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q{DRF 문서화 도구}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; Spec[&quot;drf-spectacular ★ 사실상 표준&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; Yasg[&quot;drf-yasg (legacy, 유지보수 저조)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; Native[&quot;DRF 내장 (제한적)&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;






























&lt;table&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;strong&gt;drf-spectacular&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;em&gt;활발&lt;/em&gt;&lt;/td&gt;&lt;td&gt;OpenAPI 3.1, type hint 인식, Sane defaults&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;drf-yasg&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;em&gt;deprecated 권장&lt;/em&gt;&lt;/td&gt;&lt;td&gt;OpenAPI 2.0, 여전히 인기 있지만 3.1 미지원&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;DRF 내장 SchemaGenerator&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;em&gt;기본 포함&lt;/em&gt;&lt;/td&gt;&lt;td&gt;OpenAPI 3.0 하지만 커스텀 어려움&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;coreapi&lt;/strong&gt; (2020 이전)&lt;/td&gt;&lt;td&gt;&lt;em&gt;사용 금지&lt;/em&gt;&lt;/td&gt;&lt;td&gt;DRF 3.10에서 제거됨&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
2026년 현재 &lt;strong&gt;&lt;code&gt;drf-spectacular&lt;/code&gt; 를 기본으로 선택&lt;/strong&gt;하는 것이 안전. &lt;code&gt;drf-yasg&lt;/code&gt; 는 유지보수 프로젝트만.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;drf-spectacular-설치-5분-완성&quot;&gt;drf-spectacular 설치 (5분 완성)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;pip&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; drf-spectacular&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# settings.py&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;INSTALLED_APPS&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    ...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &apos;rest_framework&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &apos;drf_spectacular&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;REST_FRAMEWORK&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &apos;DEFAULT_SCHEMA_CLASS&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;drf_spectacular.openapi.AutoSchema&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;SPECTACULAR_SETTINGS&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &apos;TITLE&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;My API&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &apos;DESCRIPTION&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;내 서비스 REST API 문서&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &apos;VERSION&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;1.0.0&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &apos;SERVE_INCLUDE_SCHEMA&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;False&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# /api/schema/ 자체는 문서에서 숨김&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &apos;COMPONENT_SPLIT_REQUEST&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# request/response schema 분리&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# urls.py&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; drf_spectacular.views &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    SpectacularAPIView,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    SpectacularSwaggerView,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    SpectacularRedocView,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;urlpatterns &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    ...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # OpenAPI 3.1 스펙 (JSON/YAML)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    path(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;api/schema/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, SpectacularAPIView.as_view(), &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;schema&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # 두 종류 UI (원하는 것 하나만 쓰거나 둘 다)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    path(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;api/docs/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, SpectacularSwaggerView.as_view(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;url_name&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;schema&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;swagger-ui&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    path(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;api/redoc/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, SpectacularRedocView.as_view(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;url_name&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;schema&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;), &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;redoc&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;브라우저에서 &lt;code&gt;http://localhost:8000/api/docs/&lt;/code&gt; 접속 → Swagger UI 자동 렌더링.&lt;/p&gt;
&lt;h2 id=&quot;swagger-ui-vs-redoc&quot;&gt;Swagger UI vs Redoc&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q{UI 선택}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; S[&quot;Swagger UI&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; R[&quot;Redoc&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    S --&gt; S1[&quot;Try it out 지원 (실제 호출)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    S --&gt; S2[&quot;개발자 도구 스타일&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    R --&gt; R1[&quot;3열 레이아웃, 읽기 편함&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    R --&gt; R2[&quot;문서/스펙 공유용에 적합&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;항목&lt;/th&gt;&lt;th&gt;Swagger UI&lt;/th&gt;&lt;th&gt;Redoc&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;/td&gt;&lt;td&gt;3열, 문서 스타일&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Try it out&lt;/td&gt;&lt;td&gt;&lt;em&gt;예&lt;/em&gt;&lt;/td&gt;&lt;td&gt;아니오&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;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;커스텀 CSS&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;/td&gt;&lt;td&gt;&lt;em&gt;개발자 테스트&lt;/em&gt;&lt;/td&gt;&lt;td&gt;&lt;em&gt;외부 공개 문서&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;자동-추출-되는-것&quot;&gt;자동 추출 되는 것&lt;/h2&gt;
&lt;p&gt;DRF &lt;code&gt;ViewSet&lt;/code&gt; + &lt;code&gt;Serializer&lt;/code&gt; 만 잘 짜면 아래가 자동 문서화:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; UserViewSet&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;viewsets&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ModelViewSet&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &quot;&quot;&quot;유저 리소스 관리.&quot;&quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      # ← 이 docstring이 태그 description&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    queryset &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; User.objects.all()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    serializer_class &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; UserSerializer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    permission_classes &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [IsAuthenticated]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    filterset_fields &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;is_staff&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;is_active&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    search_fields &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;username&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;email&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    @action&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;detail&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;methods&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;post&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; deactivate&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, request, pk&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &quot;&quot;&quot;유저 비활성화.&quot;&quot;&quot;&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;      # ← action의 description&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        ...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자동 추출되는 요소:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Endpoint 경로&lt;/strong&gt; ← Router URL&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP method&lt;/strong&gt; ← list/create/retrieve/…&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Request body schema&lt;/strong&gt; ← Serializer fields&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Response schema&lt;/strong&gt; ← Serializer fields&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Query params&lt;/strong&gt; ← &lt;code&gt;filterset_fields&lt;/code&gt;, &lt;code&gt;search_fields&lt;/code&gt;, &lt;code&gt;ordering_fields&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Path params&lt;/strong&gt; ← &lt;code&gt;&amp;#x3C;int:pk&gt;&lt;/code&gt; etc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authentication&lt;/strong&gt; ← &lt;code&gt;authentication_classes&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Description&lt;/strong&gt; ← docstring&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;extend_schema-로-커스터마이즈&quot;&gt;&lt;code&gt;@extend_schema&lt;/code&gt; 로 커스터마이즈&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; drf_spectacular.utils &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; extend_schema, OpenApiParameter, OpenApiExample&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; UserViewSet&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;viewsets&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ModelViewSet&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    @extend_schema&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        summary&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;유저 검색&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        description&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;이름 또는 이메일로 유저를 검색합니다. 최소 2자 이상 필요.&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        parameters&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            OpenApiParameter(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;                name&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;q&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;                type&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;                location&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;OpenApiParameter.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;QUERY&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;                description&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;검색어&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;                required&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            ),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        responses&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;            200&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: UserSerializer(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;many&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;            400&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: OpenApiExample(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;잘못된 요청&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;value&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;detail&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;검색어 부족&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        tags&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;users&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    @action&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;detail&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;False&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;methods&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;get&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; search&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, request):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        ...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;주요 인자:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;summary&lt;/code&gt; = endpoint 한 줄 요약&lt;/li&gt;
&lt;li&gt;&lt;code&gt;description&lt;/code&gt; = 긴 설명 (Markdown 지원)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;parameters&lt;/code&gt; = query/header/path param 명시&lt;/li&gt;
&lt;li&gt;&lt;code&gt;request&lt;/code&gt; = request body serializer&lt;/li&gt;
&lt;li&gt;&lt;code&gt;responses&lt;/code&gt; = status code별 응답 serializer&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tags&lt;/code&gt; = 그룹핑&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deprecated&lt;/code&gt; = 곧 제거 예정 표시&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exclude&lt;/code&gt; = 문서에서 숨김&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;enum-필드-문서화&quot;&gt;Enum 필드 문서화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; drf_spectacular.utils &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; extend_schema_field&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; drf_spectacular.types &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; OpenApiTypes&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; UserSerializer&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;serializers&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ModelSerializer&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    role &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; serializers.ChoiceField(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        choices&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;admin&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;user&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;guest&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        help_text&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;유저 역할&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    )&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DRF ChoiceField → OpenAPI &lt;code&gt;enum&lt;/code&gt; 으로 자동 변환.&lt;/p&gt;
&lt;h2 id=&quot;nested-serializer-문서화&quot;&gt;Nested Serializer 문서화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ProfileSerializer&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;serializers&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ModelSerializer&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Meta&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        model &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Profile&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        fields &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;bio&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;avatar_url&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; UserSerializer&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;serializers&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ModelSerializer&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    profile &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ProfileSerializer(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;read_only&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)      &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# ← nested schema 자동 추론&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Meta&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        model &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; User&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        fields &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;id&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;username&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;profile&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;serializermethodfield-타입-힌트&quot;&gt;SerializerMethodField 타입 힌트&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; UserSerializer&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;serializers&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ModelSerializer&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    is_online &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; serializers.SerializerMethodField()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    @extend_schema_field&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(OpenApiTypes.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;BOOL&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)     &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# ← 타입 힌트 없으면 unknown&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; get_is_online&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, obj):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; obj.last_seen &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;and&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; (now() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; obj.last_seen).seconds &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 300&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;code&gt;SerializerMethodField&lt;/code&gt; 는 반환 타입이 문서에 나오지 않는다. &lt;code&gt;@extend_schema_field&lt;/code&gt; 로 명시.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;인증-문서화&quot;&gt;인증 문서화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;SPECTACULAR_SETTINGS&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    ...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &apos;SECURITY&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;jwtAuth&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: []}],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &apos;APPEND_COMPONENTS&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &apos;securitySchemes&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            &apos;jwtAuth&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;                &apos;type&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;http&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;                &apos;scheme&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;bearer&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;                &apos;bearerFormat&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;JWT&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Swagger UI 우측 상단 “Authorize” 버튼으로 JWT 토큰 입력 → 이후 요청에 자동 헤더 추가.&lt;/p&gt;
&lt;p&gt;자세한 인증 방식은 [[drf-authentication]].&lt;/p&gt;
&lt;h2 id=&quot;여러-개-응답-union&quot;&gt;여러 개 응답 (union)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;@extend_schema&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    responses&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        200&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: UserSerializer,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        202&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: OpenApiExample(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;pending&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;value&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;status&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;pending&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        400&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: OpenApiExample(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;invalid&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;value&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;detail&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;error&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        429&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: OpenApiExample(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;name&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;throttled&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;value&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;detail&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Rate limit&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; create&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, request):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    ...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;요청-예시&quot;&gt;요청 예시&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; drf_spectacular.utils &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; OpenApiExample, extend_schema&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;@extend_schema&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    examples&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;[&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        OpenApiExample(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            &apos;Simple case&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;            summary&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;간단한 유저 생성&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;            description&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;기본 필드만 사용&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;            value&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;username&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;koa&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;email&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;koa@example.com&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;            request_only&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        ),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        OpenApiExample(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            &apos;With profile&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;            value&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;username&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;koa&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;email&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;koa@example.com&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;profile&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;bio&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;개발자&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;            request_only&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        ),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; create&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, request):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    ...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Swagger UI에서 예시 드롭다운으로 선택 가능.&lt;/p&gt;
&lt;h2 id=&quot;serializer-재사용-같은-이름-충돌-방지&quot;&gt;Serializer 재사용 (같은 이름 충돌 방지)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; UserCreateSerializer&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;serializers&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ModelSerializer&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Meta&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        model &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; User&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        fields &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;username&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;email&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;password&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; UserReadSerializer&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;serializers&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ModelSerializer&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Meta&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        model &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; User&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        fields &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;id&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;username&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;email&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; UserViewSet&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ModelViewSet&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    queryset &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; User.objects.all()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; get_serializer_class&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.action &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;create&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; UserCreateSerializer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; UserReadSerializer&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;drf-spectacular 는 자동으로 두 스키마를 &lt;code&gt;UserCreate&lt;/code&gt; / &lt;code&gt;UserRead&lt;/code&gt; 로 분리 문서화.&lt;/p&gt;
&lt;h2 id=&quot;schema-파일-export&quot;&gt;Schema 파일 export&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# YAML로 저장 (버전 관리)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;python&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; manage.py&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; spectacular&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --file&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; schema.yml&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 검증&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;python&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; manage.py&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; spectacular&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --validate&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; --file&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; schema.yml&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CI에 넣어두면 &lt;em&gt;스펙 breaking change 를 조기 감지&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;client-sdk-자동-생성&quot;&gt;Client SDK 자동 생성&lt;/h2&gt;
&lt;p&gt;OpenAPI 스펙만 있으면 다양한 언어 client 를 코드 생성:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# TypeScript&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;npx&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; openapi-typescript&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; schema.yml&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -o&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; api-types.ts&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Kotlin&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;openapi-generator-cli&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; generate&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -i&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; schema.yml&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -g&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; kotlin&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -o&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; ./kotlin-client&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;프론트에서:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;typescript&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;type&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; UserResponse&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; paths&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/api/users/{id}/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;][&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;get&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;][&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;responses&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;][&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;200&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;][&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;content&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;][&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;application/json&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;];&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;백엔드 스펙과 프론트 타입이 자동 동기화&lt;/em&gt;. Breaking change → 컴파일 에러.&lt;/p&gt;
&lt;h2 id=&quot;다른-프레임워크-문서화-대응&quot;&gt;다른 프레임워크 문서화 대응&lt;/h2&gt;





































&lt;table&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;strong&gt;DRF + drf-spectacular&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;ViewSet + Serializer 자동&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;FastAPI&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;em&gt;내장&lt;/em&gt; (Pydantic type hint 기반)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Django Ninja&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;em&gt;내장&lt;/em&gt; (Pydantic type hint 기반)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;NestJS&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;@nestjs/swagger 데코레이터 명시&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Spring Boot&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;springdoc-openapi 자동&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Express&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;swagger-jsdoc 주석 명시&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Rails&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;rswag, apipie-rails&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;FastAPI/Ninja 는 &lt;em&gt;type hint 만으로&lt;/em&gt; 자동. DRF는 &lt;em&gt;Serializer 정의&lt;/em&gt;가 문서 기반.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;실무-팁&quot;&gt;실무 팁&lt;/h2&gt;
&lt;h3 id=&quot;1-태그로-그룹핑&quot;&gt;1. 태그로 그룹핑&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; UserViewSet&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;ModelViewSet&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;    @extend_schema&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;tags&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;User Management&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; list&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, request):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        ...&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Swagger UI에서 카테고리별 아코디언.&lt;/p&gt;
&lt;h3 id=&quot;2-cors-문제&quot;&gt;2. CORS 문제&lt;/h3&gt;
&lt;p&gt;Swagger UI 에서 Try it out 시도 → CORS 에러 자주 발생.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;INSTALLED_APPS&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;corsheaders&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;MIDDLEWARE&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &apos;corsheaders.middleware.CorsMiddleware&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    ...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;CORS_ALLOWED_ORIGINS&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;http://localhost:8000&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3-문서-접근-권한-제한&quot;&gt;3. 문서 접근 권한 제한&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 프로덕션은 문서 접근 제한&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; DEBUG&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    urlpatterns &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        path(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;api/schema/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, permission_required(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;is_staff&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)(SpectacularAPIView.as_view())),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    ]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;4-버전별-문서-분리&quot;&gt;4. 버전별 문서 분리&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# v1 / v2 각각 별도 스키마&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;SPECTACULAR_SETTINGS&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &apos;SERVERS&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        {&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;url&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/api/v1&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;description&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;v1&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        {&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;url&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/api/v2&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;description&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;v2 (신규 권장)&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    ],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자세한 API 버전 관리는 [[drf-versioning-content-negotiation]].&lt;/p&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Serializer 없는 endpoint&lt;/strong&gt; = 문서에 body 안 나옴. &lt;code&gt;@extend_schema(request=...)&lt;/code&gt; 명시.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SerializerMethodField 타입 안 명시&lt;/strong&gt; = OpenAPI &lt;code&gt;type: unknown&lt;/code&gt;. &lt;code&gt;@extend_schema_field&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;@action&lt;/code&gt; 에 methods 만 명시&lt;/strong&gt; = summary/description 없어 문서 초라. docstring 사용.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;문서 URL 프로덕션 개방&lt;/strong&gt; = 내부 endpoint 노출. 접근 제한 필수.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;drf-yasg&lt;/code&gt; 를 새 프로젝트에 도입&lt;/strong&gt; = OpenAPI 2.0 만 지원. &lt;code&gt;drf-spectacular&lt;/code&gt; 사용.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[django-rest-framework]] (DRF 개요)&lt;/li&gt;
&lt;li&gt;[[drf-tutorial-quickstart]]&lt;/li&gt;
&lt;li&gt;[[drf-views]]&lt;/li&gt;
&lt;li&gt;[[django-drf-serializers]]&lt;/li&gt;
&lt;li&gt;[[drf-authentication]]&lt;/li&gt;
&lt;li&gt;[[drf-testing]] (테스트, 다음 단계)&lt;/li&gt;
&lt;li&gt;[[REST API Design]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>drf</category><category>openapi</category><category>swagger</category><category>documentation</category><category>spectacular</category><author>koa (김신건)</author></item><item><title>[DRF] Testing: APIClient, APITestCase, force_authenticate</title><link>https://shinkeonkim.com/wiki/django/drf-testing/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/django/drf-testing/</guid><pubDate>Wed, 01 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;DRF의 테스트 도구는 Django &lt;code&gt;TestCase&lt;/code&gt; 위에 &lt;em&gt;API 특화 helper&lt;/em&gt; 를 얹은 것. &lt;code&gt;APIClient&lt;/code&gt; 로 real endpoint 를 호출하거나 &lt;code&gt;APIRequestFactory&lt;/code&gt; 로 View 단위 테스트가 가능.&lt;/p&gt;
&lt;h2 id=&quot;테스트-도구-계층&quot;&gt;테스트 도구 계층&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q{테스트 종류}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; Unit[&quot;단위 (Serializer, custom logic)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; Slice[&quot;View 단위 (APIRequestFactory)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; Int[&quot;통합 (APIClient, DB 포함)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; E2E[&quot;End-to-End (playwright, cypress)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Unit --&gt; U1[&quot;빠름 (ms)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Slice --&gt; S1[&quot;View 로직만&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Int --&gt; I1[&quot;실제 URL + DB, 권장&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    E2E --&gt; E1[&quot;프론트 포함, 느림&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;



































&lt;table&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;DB&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;TestCase&lt;/code&gt; + Serializer 직접&lt;/td&gt;&lt;td&gt;단위&lt;/td&gt;&lt;td&gt;&lt;em&gt;ms&lt;/em&gt;&lt;/td&gt;&lt;td&gt;아니오 (or 필요 시)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;APIRequestFactory&lt;/code&gt;&lt;/td&gt;&lt;td&gt;View&lt;/td&gt;&lt;td&gt;&lt;em&gt;빠름&lt;/em&gt;&lt;/td&gt;&lt;td&gt;아니오 (mock)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;APIClient&lt;/code&gt; (DRF)&lt;/td&gt;&lt;td&gt;통합&lt;/td&gt;&lt;td&gt;&lt;em&gt;중간&lt;/em&gt;&lt;/td&gt;&lt;td&gt;&lt;em&gt;예&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Django &lt;code&gt;Client&lt;/code&gt;&lt;/td&gt;&lt;td&gt;통합 (form only)&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;blockquote&gt;
&lt;p&gt;[!TIP]
DRF 프로젝트는 &lt;code&gt;APIClient&lt;/code&gt; 를 기본으로 쓴다. 실제 URL 라우팅 + 인증 + 응답 렌더링 모두 검증.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;설치&quot;&gt;설치&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;pip&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; pytest&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; pytest-django&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; factory-boy&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; freezegun&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;ini&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# pytest.ini&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;[pytest]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;DJANGO_SETTINGS_MODULE&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; = myproject.settings.test&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;python_files&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; = tests.py test_*.py&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;1-apiclient-통합-테스트&quot;&gt;1. APIClient (통합 테스트)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; rest_framework.test &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; APITestCase&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; rest_framework &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; status&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; UserAPITest&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;APITestCase&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; setUp&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.user &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; User.objects.create_user(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;koa&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;password&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;pw&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_list_users&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.client.force_authenticate(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.user)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.client.get(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/api/users/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.assertEqual(response.status_code, status.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;HTTP_200_OK&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.assertEqual(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;len&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(response.data[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;results&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]), &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_create_requires_auth&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.client.post(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/api/users/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;username&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;new&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.assertEqual(response.status_code, status.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;HTTP_401_UNAUTHORIZED&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;self.client&lt;/code&gt; = &lt;code&gt;APIClient&lt;/code&gt; 인스턴스.&lt;/p&gt;
&lt;h2 id=&quot;force_authenticate-권장&quot;&gt;&lt;code&gt;force_authenticate&lt;/code&gt; (권장)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# ★ 가장 자주 쓰는 인증 helper&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.client.force_authenticate(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.user)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# → JWT 발급, 헤더 첨부 등 우회. 유닛 테스트에 이상적.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.client.force_authenticate(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)  &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 로그아웃&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DRF 인증 클래스 무시 하고 &lt;code&gt;request.user&lt;/code&gt; 를 직접 주입. &lt;em&gt;가장 빠름&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;세션-로그인&quot;&gt;세션 로그인&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.client.login(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;username&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;koa&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;password&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;pw&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 또는&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.client.force_login(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.user)   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# Django 스타일&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SessionAuthentication&lt;/code&gt; 검증까지 하고 싶을 때.&lt;/p&gt;
&lt;h2 id=&quot;토큰-인증&quot;&gt;토큰 인증&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# TokenAuthentication&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;token &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Token.objects.create(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.user)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.client.credentials(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;HTTP_AUTHORIZATION&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Token &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;token.key&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# JWTAuthentication&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; rest_framework_simplejwt.tokens &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; RefreshToken&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;refresh &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; RefreshToken.for_user(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.user)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.client.credentials(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;HTTP_AUTHORIZATION&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Bearer &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;refresh.access_token&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;실제 인증 파이프라인&lt;/em&gt; 을 통과. E2E 시나리오 검증에 적합.&lt;/p&gt;
&lt;h2 id=&quot;http-메서드&quot;&gt;HTTP 메서드&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# GET&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.client.get(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/api/users/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;q&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;koa&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;})    &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# ?q=koa 자동&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# POST (JSON 자동)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.client.post(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/api/users/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;username&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;new&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;format&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;json&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# PATCH&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.client.patch(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/api/users/1/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;username&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;updated&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;format&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;json&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# DELETE&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.client.delete(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/api/users/1/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 헤더&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.client.get(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/api/users/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;HTTP_X_CUSTOM&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;value&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;code&gt;format=&apos;json&apos;&lt;/code&gt; 명시 권장. 안 하면 form-urlencoded 로 보내져 nested 객체 실패.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;응답-검증&quot;&gt;응답 검증&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_get_user&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.client.get(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/api/users/1/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.assertEqual(response.status_code, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;200&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.assertEqual(response.data[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;username&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;], &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;koa&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.assertIn(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;email&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, response.data)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.assertIsInstance(response.data[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;groups&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;], &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;list&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # 헤더&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.assertEqual(response[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Content-Type&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;], &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;application/json&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # JSON 직렬화된 문자열&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.assertEqual(response.json(), {&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;id&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;username&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;koa&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;})&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;2-apirequestfactory-view-단위&quot;&gt;2. APIRequestFactory (View 단위)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; rest_framework.test &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; APIRequestFactory&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; UserViewTest&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;TestCase&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; setUp&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.factory &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; APIRequestFactory()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.user &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; User.objects.create_user(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;koa&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;password&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;pw&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_view_directly&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        view &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; UserViewSet.as_view({&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;get&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;list&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        request &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.factory.get(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/api/users/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        force_authenticate(request, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.user)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; view(request)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.assertEqual(response.status_code, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;200&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;URL 라우팅 없이 &lt;em&gt;View 직접 호출&lt;/em&gt;. 빠르지만 pipeline 일부만 검증.&lt;/p&gt;
&lt;h2 id=&quot;3-serializer-단위-테스트&quot;&gt;3. Serializer 단위 테스트&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_valid_serializer&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;():&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    data &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;username&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;koa&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;email&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;koa@example.com&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    serializer &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; UserSerializer(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;data)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    assert&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; serializer.is_valid()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    assert&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; serializer.validated_data[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;username&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;koa&apos;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_invalid_email&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;():&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    data &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;username&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;koa&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;email&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;not-an-email&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    serializer &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; UserSerializer(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;data)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    assert&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; serializer.is_valid()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    assert&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;email&apos;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; serializer.errors&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;4-pytest-django-사용&quot;&gt;4. pytest-django 사용&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; pytest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;@pytest.fixture&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; api_client&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;():&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; rest_framework.test &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; APIClient&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; APIClient()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;@pytest.fixture&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; user&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(db):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; User.objects.create_user(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;koa&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;password&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;pw&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;@pytest.fixture&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; auth_client&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(api_client, user):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    api_client.force_authenticate(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;user)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; api_client&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_list_users&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(auth_client):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; auth_client.get(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/api/users/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    assert&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; response.status_code &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 200&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;fixture 조합 이 재사용성 극대화. &lt;code&gt;pytest.ini&lt;/code&gt; 의 &lt;code&gt;--reuse-db&lt;/code&gt; 로 DB 재생성 없이 반복 실행.&lt;/p&gt;
&lt;h2 id=&quot;5-factory-boy-테스트-데이터&quot;&gt;5. Factory Boy (테스트 데이터)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# factories.py&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; factory&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; django.contrib.auth.models &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; User&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; UserFactory&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;factory&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;django&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;DjangoModelFactory&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Meta&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        model &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; User&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    username &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; factory.Sequence(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;lambda&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; n: &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;user&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;n&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    email &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; factory.LazyAttribute(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;lambda&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; o: &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;o.username&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;@example.com&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    is_active &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; True&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 사용&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_many_users&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;():&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    UserFactory.create_batch(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;50&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    assert&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; User.objects.count() &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 50&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 관계&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ArticleFactory&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;factory&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;django&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;DjangoModelFactory&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; Meta&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        model &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Article&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    title &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; factory.Faker(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;sentence&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    author &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; factory.SubFactory(UserFactory)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;fixtures/*.json&lt;/code&gt; 대신 factory 로 &lt;em&gt;동적 생성&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;응답-status-code-매트릭스&quot;&gt;응답 status code 매트릭스&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;@pytest.mark.parametrize&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;user_type,expected_status&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    (&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;anonymous&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;401&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    (&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;regular&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;403&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    (&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;admin&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;200&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_admin_endpoint&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(api_client, user_type, expected_status):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; user_type &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;regular&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        api_client.force_authenticate(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;UserFactory())&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    elif&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; user_type &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &apos;admin&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        api_client.force_authenticate(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;user&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;UserFactory(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;is_staff&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; api_client.get(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/api/admin/stats/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    assert&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; response.status_code &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; expected_status&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;parametrize&lt;/code&gt; 로 &lt;em&gt;권한 매트릭스 완전 커버&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;권한-테스트-패턴&quot;&gt;권한 테스트 패턴&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; PostPermissionTest&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;APITestCase&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; setUp&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.author &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; UserFactory()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.other &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; UserFactory()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.post &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Post.objects.create(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;author&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.author, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;title&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;...&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_author_can_delete&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.client.force_authenticate(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.author)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.client.delete(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/api/posts/&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.post.pk&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.assertEqual(response.status_code, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;204&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_other_cannot_delete&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.client.force_authenticate(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.other)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.client.delete(&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/api/posts/&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.post.pk&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.assertEqual(response.status_code, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;403&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자세한 권한 개념은 [[django-drf-permissions]].&lt;/p&gt;
&lt;h2 id=&quot;throttle-테스트&quot;&gt;Throttle 테스트&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; django.core.cache &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; cache&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; ThrottleTest&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;APITestCase&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; setUp&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        cache.clear()   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# ★ throttle 카운트 초기화&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_burst_limit&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        with&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.settings(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;REST_FRAMEWORK&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            &apos;DEFAULT_THROTTLE_RATES&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;login&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;2/min&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; _ &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; range&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;                self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.assertEqual(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.client.post(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/api/login/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).status_code, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;200&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;            self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.assertEqual(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.client.post(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/api/login/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;).status_code, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;429&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]
&lt;code&gt;setUp&lt;/code&gt; 에서 &lt;code&gt;cache.clear()&lt;/code&gt; 필수. 안 하면 이전 테스트 카운트 남음.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;시간-조작-freezegun&quot;&gt;시간 조작 (freezegun)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; freezegun &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; freeze_time&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;@freeze_time&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;2026-06-25 12:00:00&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_expired_token&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;():&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    token &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; RefreshToken.for_user(user)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    with&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; freeze_time(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;2026-07-25 12:00:00&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):    &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 30일 후&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; api_client.get(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/api/me/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;HTTP_AUTHORIZATION&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;Bearer &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;token.access_token&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        assert&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; response.status_code &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 401&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;JWT 만료, 세션 timeout 등 &lt;em&gt;시간 종속 테스트&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;mocking-외부-api&quot;&gt;Mocking 외부 API&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; unittest.mock &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; patch&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;@patch&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;myapp.services.send_email&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_signup_sends_email&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(mock_send):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; api_client.post(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/api/signup/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, {&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;email&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;a@b.c&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    assert&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; response.status_code &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 201&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    mock_send.assert_called_once_with(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;a@b.c&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;db-snapshot&quot;&gt;DB Snapshot&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; test_no_extra_queries&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;():&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # N+1 감시&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    with&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.assertNumQueries(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;3&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.client.get(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;/api/articles/&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DRF 응답이 예상보다 많은 쿼리를 하면 실패. Performance regression 방어.&lt;/p&gt;
&lt;p&gt;자세한 N+1 개념은 [[django-orm-advanced]].&lt;/p&gt;
&lt;h2 id=&quot;coverage-측정&quot;&gt;Coverage 측정&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;pip&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; coverage&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;coverage&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; run&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -m&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; pytest&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;coverage&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; report&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;coverage&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; html&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # htmlcov/index.html&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;목표: &lt;em&gt;serializer/view 는 90%+, permission/utility 는 100%.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;다른-프레임워크-테스트-도구&quot;&gt;다른 프레임워크 테스트 도구&lt;/h2&gt;

































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Framework&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;strong&gt;DRF&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;APIClient&lt;/code&gt;, &lt;code&gt;APIRequestFactory&lt;/code&gt;, pytest-django&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;FastAPI&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;TestClient&lt;/code&gt; (Starlette)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Spring&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;MockMvc, &lt;code&gt;@WebMvcTest&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Rails&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;ActionDispatch, RSpec, request specs&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Express&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;supertest&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;NestJS&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;@nestjs/testing + supertest&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;format=&apos;json&apos;&lt;/code&gt; 누락&lt;/strong&gt; = form-urlencoded 로 전송, nested 데이터 실패.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;cache.clear()&lt;/code&gt; 안 함 (Throttle 테스트)&lt;/strong&gt; = flaky test.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;self.client.login()&lt;/code&gt; 을 JWT 라우트에&lt;/strong&gt; = 세션이라 401. &lt;code&gt;force_authenticate&lt;/code&gt; 사용.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;setUp&lt;/code&gt; 에서 heavy DB 초기화&lt;/strong&gt; = 매 테스트마다 실행. fixture 로 격리.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;assertEqual(response.data, dict)&lt;/code&gt;&lt;/strong&gt; = OrderedDict 차이로 실패 가능. &lt;code&gt;json.loads(response.content)&lt;/code&gt; 로 정규화.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[django-testing]] (Django 기본 테스트)&lt;/li&gt;
&lt;li&gt;[[django-rest-framework]]&lt;/li&gt;
&lt;li&gt;[[drf-tutorial-quickstart]]&lt;/li&gt;
&lt;li&gt;[[drf-authentication]]&lt;/li&gt;
&lt;li&gt;[[django-drf-permissions]]&lt;/li&gt;
&lt;li&gt;[[drf-schemas-openapi]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>drf</category><category>testing</category><category>pytest</category><category>api-testing</category><author>koa (김신건)</author></item><item><title>[Voice AI] Agent 패턴: Supervisor, Handoff, ReAct, HITL</title><link>https://shinkeonkim.com/wiki/ai/agent-patterns/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/ai/agent-patterns/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;복잡한 음성 에이전트 = &lt;em&gt;여러 sub-agent + 조정 패턴&lt;/em&gt;. 단일 LLM 으로 처리하기 어려운 흐름.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
각 sub-agent 내부는 &lt;em&gt;VAD → STT → LLM → TTS 순차 파이프라인&lt;/em&gt; 위에서 동작. 본 페이지의 패턴은 &lt;em&gt;상위 조정 (orchestration)&lt;/em&gt; 레이어.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;4가지-핵심-패턴&quot;&gt;4가지 핵심 패턴&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q[Agent Patterns]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; S[Supervisor]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; H[Handoff]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; R[ReAct]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; HITL[Human-in-the-Loop]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;1-supervisor&quot;&gt;1. Supervisor&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    User --&gt; Sup[Supervisor Agent]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Sup --&gt; Choose{어느 sub-agent?}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Choose --&gt;|결제 문의| Pay[Payment Agent]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Choose --&gt;|환불| Ref[Refund Agent]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Choose --&gt;|기술 지원| Tech[Tech Agent]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Pay --&gt; Sup&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Ref --&gt; Sup&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Tech --&gt; Sup&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Sup --&gt; User&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; SupervisorAgent&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &quot;&quot;&quot;질의 → 적절 sub-agent 라우팅&quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    sub_agents &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &quot;payment&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: PaymentAgent(),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &quot;refund&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: RefundAgent(),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &quot;tech_support&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: TechAgent(),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; handle&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, user_input: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) -&gt; &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        # LLM 으로 의도 분류&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        intent &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;._classify_intent(user_input)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        agent &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.sub_agents[intent]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; agent.handle(user_input)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; _classify_intent&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, text: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) -&gt; &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        result &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; llm.structured_output(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;            schema&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;IntentSchema,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;            messages&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;[{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: text}],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; result.intent&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;작은 분류 모델 (gpt-4o-mini, Haiku) 가 &lt;em&gt;intent classifier&lt;/em&gt;, &lt;em&gt;세부 agent 가 더 큰 모델&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;2-handoff&quot;&gt;2. Handoff&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    User-&gt;&gt;Booking: 예약 문의&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Booking-&gt;&gt;Booking: 예약 처리&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over Booking: 결제 단계&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Booking-&gt;&gt;Payment: HANDOFF (context 전달)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Payment-&gt;&gt;User: &quot;결제 진행하겠습니다&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    User-&gt;&gt;Payment: 카드 정보&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Payment-&gt;&gt;Payment: 결제 처리&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Payment--&gt;&gt;Booking: HANDOFF BACK&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Booking-&gt;&gt;User: &quot;예약 + 결제 완료&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;OpenAI Swarm 의 &lt;em&gt;handoff&lt;/em&gt; 패턴:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; swarm &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Swarm, Agent&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;booking_agent &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Agent(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    name&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Booking&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    instructions&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;식당 예약을 도와줍니다.&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    functions&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;[lookup_restaurant, check_availability, transfer_to_payment],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;payment_agent &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Agent(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    name&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Payment&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    instructions&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;결제를 처리합니다.&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    functions&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;[charge_card, transfer_to_booking],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; transfer_to_payment&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;():&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; payment_agent&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; transfer_to_booking&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;():&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; booking_agent&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;client &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Swarm()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; client.run(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;agent&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;booking_agent, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;messages&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;messages)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Function return 으로 agent 교체&lt;/em&gt;. 깔끔.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;3-react-reason--act&quot;&gt;3. ReAct (Reason + Act)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Thought: 사용자가 &quot;내일 오후 회의 잡아줘&quot; 라고 했다.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Action: search_calendar(date=&quot;내일&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Observation: 14:00-15:00 비어있음&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Thought: 14시에 잡으면 되겠다. 참석자는 누구?&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Action: ask_user(&quot;참석자는 누구신가요?&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Observation: &quot;민수씨와 영희씨요&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Thought: 그들의 가용성 확인 필요&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Action: check_availability([&quot;민수&quot;, &quot;영희&quot;], &quot;내일 14:00&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Observation: 모두 OK&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Thought: 회의 생성&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Action: create_meeting(...)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Observation: 회의 생성됨, ID=m_42&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Thought: 사용자에게 알림&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Action: respond(&quot;내일 오후 2시에 민수씨, 영희씨와 회의 잡았습니다.&quot;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; react_loop&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(user_input, max_steps&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;10&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    messages &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: user_input}]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; step &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; range&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(max_steps):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; llm.chat(messages, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;tools&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;tools)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; response.finish_reason &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;==&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;stop&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; response.content   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 최종 답변&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; response.tool_calls:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; tc &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; response.tool_calls:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;                result &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; execute_tool(tc)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;                messages.append({&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;tool&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;tool_call_id&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: tc.id, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: result})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    raise&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ReActLoopExceededError()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;음성에서는 &lt;em&gt;너무 많은 step&lt;/em&gt; = 지연 폭증. &lt;em&gt;max_steps 3-5&lt;/em&gt; 권장.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;4-human-in-the-loop-hitl&quot;&gt;4. Human-in-the-Loop (HITL)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    User-&gt;&gt;Agent: &quot;1억 송금&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Agent-&gt;&gt;HITL: critical action approval request&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    HITL-&gt;&gt;Human[운영자]: 알림 (&quot;승인 필요&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Human--&gt;&gt;HITL: approve / reject&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    HITL--&gt;&gt;Agent: result&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Agent-&gt;&gt;User: &quot;송금 완료&quot; 또는 &quot;보안 확인 필요&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;@agent.tool&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; transfer_money&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(amount: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, to: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; amount &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 10_000_000&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        approval &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; request_human_approval(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;            action&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;f&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;송금 &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;amount&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:,&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;원 → &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;to&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;            timeout&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;30&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; approval:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;송금이 보류되었습니다. 보안팀 확인 후 처리됩니다.&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; bank_api.transfer(amount, to)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; f&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;amount&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:,&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;원이 송금되었습니다.&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;고위험 작업&lt;/em&gt; (금액 큰 송금, 데이터 삭제, 외부 noti) 에 인간 승인.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;패턴-조합&quot;&gt;패턴 조합&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    User --&gt; Sup[Supervisor]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Sup --&gt;|일반| Single[Single Agent + ReAct]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Sup --&gt;|복잡 → 전문| Hand[Handoff Chain]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Hand --&gt; Risk{고위험?}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Risk --&gt;|예| HITL[HITL Approval]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    HITL --&gt; Exec[실행]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;langgraph-예시-다중-agent--state-machine&quot;&gt;LangGraph 예시 (다중 agent + state machine)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; langgraph.graph &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; StateGraph, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;END&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; State&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;TypedDict&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    user_input: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    intent: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    result: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; classify&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(state):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    state[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;intent&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; classify_intent(state[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;user_input&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; state&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; payment&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(state):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    state[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;result&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; handle_payment(state[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;user_input&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; state&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; refund&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(state):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    state[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;result&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; handle_refund(state[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;user_input&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; state&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;graph &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; StateGraph(State)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;graph.add_node(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;classify&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, classify)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;graph.add_node(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;payment&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, payment)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;graph.add_node(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;refund&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, refund)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;graph.set_entry_point(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;classify&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;graph.add_conditional_edges(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &quot;classify&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    lambda&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; s: s[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;intent&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    {&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;payment&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;payment&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;refund&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;refund&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;other&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;END&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;graph.add_edge(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;payment&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;END&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;graph.add_edge(&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;refund&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;END&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;app &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; graph.compile()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;모든 query 에 ReAct&lt;/strong&gt; = 단순 FAQ 도 3-step. 의도 분류 후 &lt;em&gt;복잡한 것만&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Handoff 후 &lt;em&gt;context 손실&lt;/em&gt;&lt;/strong&gt; = 사용자가 같은 정보 반복. handoff 시 &lt;em&gt;summary&lt;/em&gt; 전달.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;너무 많은 sub-agent&lt;/strong&gt; = supervisor 가 &lt;em&gt;분류 어려움&lt;/em&gt;. 5-7개 이내.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HITL 없이 critical action&lt;/strong&gt; = 사고. 임계 정의 + 자동 승인 vs 인간.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[voice-agent-architecture]]&lt;/li&gt;
&lt;li&gt;[[voice-first-prompt]]&lt;/li&gt;
&lt;li&gt;[[function-calling]] (만일 별도 wiki 있다면)&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>agent</category><category>multi-agent</category><category>supervisor</category><category>handoff</category><category>react</category><category>hitl</category><author>koa (김신건)</author></item><item><title>[Voice AI] LLM Serving: vLLM, PagedAttention, continuous batching</title><link>https://shinkeonkim.com/wiki/ai/llm-serving-vllm/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/ai/llm-serving-vllm/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;LLM Serving&lt;/strong&gt; = 자체 호스팅 LLM 을 &lt;em&gt;낮은 TTFT + 높은 throughput&lt;/em&gt; 으로 제공. 음성 에이전트의 &lt;em&gt;LLM 단계 지연&lt;/em&gt; 결정.&lt;/p&gt;





























&lt;table&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;strong&gt;vLLM&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;em&gt;PagedAttention&lt;/em&gt;, 표준. 한국에서도 압도적&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;TGI&lt;/strong&gt; (Hugging Face)&lt;/td&gt;&lt;td&gt;안정, 옛 표준&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;SGLang&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;structured output 강함, 2024+&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;LMDeploy&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;InternLM, 중국&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;TensorRT-LLM&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;NVIDIA 전용, 최고 성능&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;pagedattention&quot;&gt;PagedAttention&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1[Request 1&amp;#x3C;br/&gt;&apos;안녕&apos;] --&gt; KV1[KV cache]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q2[Request 2&amp;#x3C;br/&gt;&apos;오늘 날씨&apos;] --&gt; KV2[KV cache]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    KV1 --&gt; Page[Memory Pages (16 tokens 단위)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    KV2 --&gt; Page&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Page --&gt; GPU[GPU memory]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;기존 (LLaMA, TGI 옛)&lt;/th&gt;&lt;th&gt;PagedAttention (vLLM)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;요청 시작 시 &lt;em&gt;최대 길이 만큼 KV cache 예약&lt;/em&gt;&lt;/td&gt;&lt;td&gt;&lt;em&gt;Page 단위 동적 할당&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;짧은 응답도 &lt;em&gt;2K-32K 토큰 메모리&lt;/em&gt;&lt;/td&gt;&lt;td&gt;실제 사용 만큼만&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;GPU 메모리 &lt;em&gt;심한 단편화&lt;/em&gt;&lt;/td&gt;&lt;td&gt;&lt;em&gt;&amp;#x3C; 4% 낭비&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Throughput 낮음&lt;/td&gt;&lt;td&gt;&lt;em&gt;2-4x 향상&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;운영체제의 &lt;em&gt;Virtual Memory + paging&lt;/em&gt; 의 &lt;em&gt;LLM inference 적용&lt;/em&gt;. 모든 modern serving 의 기본.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;continuous-batching&quot;&gt;Continuous Batching&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Static[&quot;Static batching:&amp;#x3C;br/&gt;요청 8개 모일 때까지 대기&amp;#x3C;br/&gt;한 batch 끝날 때까지 다음 안 받음&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Cont[&quot;Continuous batching:&amp;#x3C;br/&gt;매 step 마다 batch 재구성&amp;#x3C;br/&gt;새 요청 즉시 합류, 완료된 거 즉시 빠짐&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Static --&gt; Slow[GPU idle 많음, latency 높음]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Cont --&gt; Fast[GPU 100% 활용, latency 낮음]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;step 단위 dynamic batching&lt;/em&gt; = vLLM, TGI 등의 &lt;em&gt;2026 표준&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;ttft-time-to-first-token-vs-tps&quot;&gt;TTFT (Time-to-First-Token) vs TPS&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Req[Request 도착] --&gt;|prefill (전체 prompt 처리)| F1[First token]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    F1 --&gt;|decode (1 토큰씩)| T2[Token 2]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    T2 --&gt;|decode| T3[Token 3]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note[&quot;TTFT = prefill 시간&amp;#x3C;br/&gt;TPS = decode 속도&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

























&lt;table&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;strong&gt;TTFT&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;첫 토큰까지&lt;/td&gt;&lt;td&gt;&lt;em&gt;&amp;#x3C; 200ms&lt;/em&gt; (음성)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;TPS&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;초당 토큰&lt;/td&gt;&lt;td&gt;&gt; 50 tok/s&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Throughput&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;동시 요청 처리&lt;/td&gt;&lt;td&gt;GPU 효율&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;음성 에이전트 = &lt;em&gt;TTFT 가 결정적&lt;/em&gt;. sentence aggregator 가 &lt;em&gt;첫 문장 받으면 TTS 시작&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;sse-스트리밍-server-sent-events&quot;&gt;SSE 스트리밍 (Server-Sent Events)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;http&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;GET&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; /v1/chat/completions&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;Accept&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; text/event-stream&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; {&quot;choices&quot;: [{&quot;delta&quot;: {&quot;content&quot;: &quot;안&quot;}}]}\n\n&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; {&quot;choices&quot;: [{&quot;delta&quot;: {&quot;content&quot;: &quot;녕&quot;}}]}\n\n&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; {&quot;choices&quot;: [{&quot;delta&quot;: {&quot;content&quot;: &quot;하세요&quot;}}]}\n\n&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#85E89D&quot;&gt;data&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;:&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; [DONE]\n\n&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; openai &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; AsyncOpenAI&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;client &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; AsyncOpenAI(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;base_url&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;http://localhost:8000/v1&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;stream &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; client.chat.completions.create(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    model&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;llama-3.1-8b-instruct&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    messages&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;messages,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    stream&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; chunk &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; stream:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    delta &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; chunk.choices[&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;].delta.content&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; delta:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        sentence_aggregator.feed(delta)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자세한 SSE 는 [[SSE]] 의 일반.&lt;/p&gt;
&lt;h2 id=&quot;vllm-시작&quot;&gt;vLLM 시작&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;pip&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; install&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; vllm&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# OpenAI 호환 server&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;python&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -m&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; vllm.entrypoints.openai.api_server&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    --model&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; meta-llama/Llama-3.1-8B-Instruct&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    --max-model-len&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 8192&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    --gpu-memory-utilization&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0.9&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    --enable-chunked-prefill&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    --enable-prefix-caching&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    --port&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 8000&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

































&lt;table&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;code&gt;--max-model-len&lt;/code&gt;&lt;/td&gt;&lt;td&gt;최대 context&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;--gpu-memory-utilization&lt;/code&gt;&lt;/td&gt;&lt;td&gt;GPU 메모리 사용률 (0.9 권장)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;--enable-chunked-prefill&lt;/code&gt;&lt;/td&gt;&lt;td&gt;긴 prompt 를 청크로 분할 (latency 균형)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;--enable-prefix-caching&lt;/code&gt;&lt;/td&gt;&lt;td&gt;&lt;em&gt;같은 prefix 의 KV cache 재사용&lt;/em&gt; (system prompt!)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;--tensor-parallel-size N&lt;/code&gt;&lt;/td&gt;&lt;td&gt;N GPU 분산&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;--pipeline-parallel-size N&lt;/code&gt;&lt;/td&gt;&lt;td&gt;pipeline 분산&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;prefix-caching-system-prompt-재사용&quot;&gt;Prefix Caching (system prompt 재사용)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Req1[&quot;Req 1: SYS+&apos;안녕&apos;&quot;] --&gt; Cache[Prefix Cache (SYS)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Req2[&quot;Req 2: SYS+&apos;오늘&apos;&quot;] --&gt; Cache&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Req3[&quot;Req 3: SYS+&apos;주문&apos;&quot;] --&gt; Cache&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note[&quot;같은 system prompt 의 KV cache 재사용&amp;#x3C;br/&gt;→ TTFT 60-90% 단축&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;모든 요청이 &lt;em&gt;같은 긴 system prompt&lt;/em&gt; 면 &lt;em&gt;prefix caching&lt;/em&gt; 으로 &lt;em&gt;prefill 시간 거의 0&lt;/em&gt;. 음성 에이전트의 &lt;em&gt;고정 페르소나 prompt&lt;/em&gt; 와 완벽 매칭.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;quantization&quot;&gt;Quantization&lt;/h2&gt;



































&lt;table&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;FP16&lt;/td&gt;&lt;td&gt;100%&lt;/td&gt;&lt;td&gt;기준&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;FP8 (H100+)&lt;/td&gt;&lt;td&gt;50%&lt;/td&gt;&lt;td&gt;~99%&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;INT8 (smoothquant)&lt;/td&gt;&lt;td&gt;50%&lt;/td&gt;&lt;td&gt;~98%&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;INT4 (AWQ, GPTQ)&lt;/td&gt;&lt;td&gt;25%&lt;/td&gt;&lt;td&gt;~95%&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;INT4 + KV cache FP8&lt;/td&gt;&lt;td&gt;작음&lt;/td&gt;&lt;td&gt;~93%&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;bash&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# AWQ 4-bit 모델&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;python&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; -m&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; vllm.entrypoints.openai.api_server&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    --model&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; TheBloke/Llama-3.1-8B-Instruct-AWQ&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; \&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;    --quantization&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; awq&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자세한 quantization 은 [[quantization]] 참고.&lt;/p&gt;
&lt;h2 id=&quot;모델-크기-vs-음성-적합성&quot;&gt;모델 크기 vs 음성 적합성&lt;/h2&gt;





















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;모델&lt;/th&gt;&lt;th&gt;한국어&lt;/th&gt;&lt;th&gt;TTFT (8B GPU)&lt;/th&gt;&lt;th&gt;적합&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Llama-3.1 8B Instruct&lt;/td&gt;&lt;td&gt;보통&lt;/td&gt;&lt;td&gt;150ms&lt;/td&gt;&lt;td&gt;일반&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Llama-3.1 70B AWQ&lt;/td&gt;&lt;td&gt;우수&lt;/td&gt;&lt;td&gt;400ms&lt;/td&gt;&lt;td&gt;복잡 task&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Qwen2.5 7B Instruct&lt;/td&gt;&lt;td&gt;우수&lt;/td&gt;&lt;td&gt;130ms&lt;/td&gt;&lt;td&gt;한국어 강함&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;EXAONE 3 7.8B&lt;/td&gt;&lt;td&gt;&lt;em&gt;최우수 (한국어)&lt;/em&gt;&lt;/td&gt;&lt;td&gt;150ms&lt;/td&gt;&lt;td&gt;한국어 voice agent&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Phi-3.5 mini (3.8B)&lt;/td&gt;&lt;td&gt;보통&lt;/td&gt;&lt;td&gt;60ms&lt;/td&gt;&lt;td&gt;edge, latency critical&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;GPT-4o-mini (API)&lt;/td&gt;&lt;td&gt;우수&lt;/td&gt;&lt;td&gt;200ms (API)&lt;/td&gt;&lt;td&gt;일반&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Gemini Flash 2.0 (API)&lt;/td&gt;&lt;td&gt;우수&lt;/td&gt;&lt;td&gt;250ms (API)&lt;/td&gt;&lt;td&gt;일반&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;prefix caching 없이 큰 system prompt&lt;/strong&gt; = 매 요청 prefill 1초+. 활성화 필수.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;streaming 안 함&lt;/strong&gt; = 응답 끝나길 기다림. &lt;em&gt;항상 stream=True&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPU memory util 너무 높음 (0.95+)&lt;/strong&gt; = OOM 위험. 0.85-0.9.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;연속 batching 없는 옛 서빙&lt;/strong&gt; = throughput 1/5. vLLM 으로.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[voice-agent-architecture]]&lt;/li&gt;
&lt;li&gt;[[tts-streaming-ssml]] (LLM 토큰 → SSE → TTS)&lt;/li&gt;
&lt;li&gt;[[quantization]]&lt;/li&gt;
&lt;li&gt;[[SSE]]&lt;/li&gt;
&lt;li&gt;[[latency-percentiles]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>llm-serving</category><category>vllm</category><category>paged-attention</category><category>kv-cache</category><category>ttft</category><author>koa (김신건)</author></item><item><title>[Voice AI] Pipecat vs LiveKit Agents: 프레임워크 비교</title><link>https://shinkeonkim.com/wiki/ai/pipecat-livekit/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/ai/pipecat-livekit/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;2026 시점 &lt;em&gt;voice agent 프레임워크&lt;/em&gt; 4가지 선택지:&lt;/p&gt;






















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Pipecat&lt;/th&gt;&lt;th&gt;LiveKit Agents&lt;/th&gt;&lt;th&gt;Vapi / Retell&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;OSS framework&lt;/td&gt;&lt;td&gt;OSS SFU + framework&lt;/td&gt;&lt;td&gt;Managed&lt;/td&gt;&lt;td&gt;직접&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;운영&lt;/td&gt;&lt;td&gt;self-host&lt;/td&gt;&lt;td&gt;self-host 또는 LiveKit Cloud&lt;/td&gt;&lt;td&gt;호스팅&lt;/td&gt;&lt;td&gt;self&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;/td&gt;&lt;td&gt;&lt;em&gt;낮음&lt;/em&gt;&lt;/td&gt;&lt;td&gt;높음&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;비용&lt;/td&gt;&lt;td&gt;인프라만&lt;/td&gt;&lt;td&gt;인프라만 (LK Cloud 옵션)&lt;/td&gt;&lt;td&gt;비쌈 ($0.13-0.30/min)&lt;/td&gt;&lt;td&gt;인프라&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;유연성&lt;/td&gt;&lt;td&gt;&lt;em&gt;높음&lt;/em&gt;&lt;/td&gt;&lt;td&gt;높음&lt;/td&gt;&lt;td&gt;&lt;em&gt;낮음&lt;/em&gt; (블랙박스)&lt;/td&gt;&lt;td&gt;&lt;em&gt;최고&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;시간&lt;/td&gt;&lt;td&gt;1-2주&lt;/td&gt;&lt;td&gt;1-2주&lt;/td&gt;&lt;td&gt;1-2일&lt;/td&gt;&lt;td&gt;1-3개월&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;pipecat&quot;&gt;Pipecat&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Frame[Frame] --&gt; P1[Input Processor (mic)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P1 --&gt; P2[VAD Processor]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P2 --&gt; P3[STT Processor]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P3 --&gt; P4[LLM Processor]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P4 --&gt; P5[Aggregator]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P5 --&gt; P6[TTS Processor]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    P6 --&gt; P7[Output Processor (speaker)]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Frame 이 processor 파이프라인을 흐름&lt;/em&gt;. Linux pipe 의 기능적 추상. 컴포넌트 &lt;em&gt;한 줄 교체&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; pipecat.pipeline.pipeline &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Pipeline&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; pipecat.services.deepgram &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; DeepgramSTTService&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; pipecat.services.openai &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; OpenAILLMService&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; pipecat.services.cartesia &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; CartesiaTTSService&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; pipecat.transports.services.daily &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; DailyTransport&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; pipecat.vad.silero &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; SileroVADAnalyzer&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;transport &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; DailyTransport(room_url, token, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;Bot&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, DailyParams(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    audio_in_enabled&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    audio_out_enabled&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    vad_analyzer&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;SileroVADAnalyzer(),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;pipeline &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; Pipeline([&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    transport.input(),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    DeepgramSTTService(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;api_key&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;...&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    OpenAILLMService(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;api_key&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;...&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;model&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;gpt-4o-mini&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    CartesiaTTSService(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;api_key&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;...&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;voice_id&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;...&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    transport.output(),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;runner &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; PipelineRunner()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; runner.run(PipelineTask(pipeline))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;pipecat-의-frame-종류&quot;&gt;Pipecat 의 Frame 종류&lt;/h3&gt;









































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Frame&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;AudioRawFrame&lt;/code&gt;&lt;/td&gt;&lt;td&gt;오디오 청크&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;TextFrame&lt;/code&gt;&lt;/td&gt;&lt;td&gt;텍스트&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;TranscriptionFrame&lt;/code&gt;&lt;/td&gt;&lt;td&gt;전사&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;LLMResponseStartFrame&lt;/code&gt; / &lt;code&gt;EndFrame&lt;/code&gt;&lt;/td&gt;&lt;td&gt;LLM 응답 경계&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;TTSStartFrame&lt;/code&gt; / &lt;code&gt;EndFrame&lt;/code&gt;&lt;/td&gt;&lt;td&gt;TTS 시작/종료&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;UserStartedSpeakingFrame&lt;/code&gt;&lt;/td&gt;&lt;td&gt;사용자 발화 시작&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;UserStoppedSpeakingFrame&lt;/code&gt;&lt;/td&gt;&lt;td&gt;종료&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;code&gt;InterruptionFrame&lt;/code&gt;&lt;/td&gt;&lt;td&gt;끼어들기&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Frame 종류 추가 = 새 processor 추가&lt;/em&gt;. 확장이 깔끔.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;livekit-agents&quot;&gt;LiveKit Agents&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    User1[&quot;사용자 1&quot;] --&gt;|WebRTC| SFU[LiveKit SFU]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    User2[&quot;사용자 2&quot;] --&gt;|WebRTC| SFU&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    SFU --&gt;|Room API| Agent[&quot;Voice Agent&amp;#x3C;br/&gt;(headless participant)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Agent --&gt; STT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Agent --&gt; LLM&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Agent --&gt; TTS&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;LiveKit SFU&lt;/em&gt; 가 &lt;em&gt;WebRTC + room&lt;/em&gt; 인프라. &lt;em&gt;Agent 가 room 에 headless participant 로 합류&lt;/em&gt; → 다중 사용자 회의 / 룸 기반 voice agent 자연스러움.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; livekit.agents &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; VoiceAssistant, AutoSubscribe, JobContext, WorkerOptions, cli&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; livekit.agents.llm &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ChatContext&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;from&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; livekit.plugins &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;import&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; openai, silero, deepgram, cartesia&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; entrypoint&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(ctx: JobContext):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ctx.connect(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;auto_subscribe&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;AutoSubscribe.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;AUDIO_ONLY&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    initial_ctx &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; ChatContext().append(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        role&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;system&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        text&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;당신은 친절한 한국어 음성 어시스턴트입니다. 짧게 답하세요.&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    assistant &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; VoiceAssistant(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        vad&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;silero.&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;VAD&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.load(),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        stt&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;deepgram.STT(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;language&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;ko&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        llm&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;openai.LLM(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;model&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;gpt-4o-mini&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        tts&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;cartesia.TTS(&lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;voice&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;...&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;language&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;ko&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        chat_ctx&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;initial_ctx,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        min_endpointing_delay&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0.5&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;        allow_interruptions&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    assistant.start(ctx.room)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; assistant.aclose()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;pipecat-vs-livekit-agents-결정&quot;&gt;Pipecat vs LiveKit Agents 결정&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1{시나리오}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|1:1 콜봇, 전화| Pipecat&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|다중 사용자 방, WebRTC 우선| LK[LiveKit Agents]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|화상 회의 + AI 참여자| LK&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|복잡한 frame 변환| Pipecat&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|이미 LiveKit SFU 운영| LK&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Pipecat&lt;/th&gt;&lt;th&gt;LiveKit Agents&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Transport&lt;/td&gt;&lt;td&gt;Daily, Twilio, Plivo, FastAPI 등 다양&lt;/td&gt;&lt;td&gt;LiveKit SFU 중심&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;추상 단위&lt;/td&gt;&lt;td&gt;Frame + Processor&lt;/td&gt;&lt;td&gt;Function 기반&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;em&gt;강력&lt;/em&gt; (Multi-participant)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Python&lt;/td&gt;&lt;td&gt;&lt;em&gt;우선&lt;/em&gt;&lt;/td&gt;&lt;td&gt;Python + Node + Go&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;학습&lt;/td&gt;&lt;td&gt;약간 가파름&lt;/td&gt;&lt;td&gt;친절한 docs&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;managed-vapi-retell&quot;&gt;Managed (Vapi, Retell)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    You[당신] --&gt;|시스템 프롬프트만| Vapi[Vapi / Retell]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Vapi --&gt;|호스팅 파이프라인| Tel[Twilio 전화]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Vapi --&gt;|기본 STT/LLM/TTS| Pipeline&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;




















&lt;table&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;Vapi&lt;/td&gt;&lt;td&gt;5분 만에 PoC&lt;/td&gt;&lt;td&gt;&lt;em&gt;블랙박스&lt;/em&gt;, $0.13-0.30/min&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Retell&lt;/td&gt;&lt;td&gt;빠른 시작, 음성 좋음&lt;/td&gt;&lt;td&gt;동일, &lt;em&gt;latency 가시성 낮음&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;build-vs-buy-기준-hamming-ai-가이드&quot;&gt;Build vs Buy 기준 (Hamming AI 가이드)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q{기준}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt;|&quot;월 &amp;#x3C; 10K 분&quot;| Buy[Vapi / Retell]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt;|&quot;월 10-50K 분&quot;| Mid[Pipecat / LiveKit (자체)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt;|&quot;월 &gt; 50K 분&quot;| Build[자체 + 깊은 최적화]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt;|&quot;엄격한 컴플라이언스 (HIPAA, FINRA)&quot;| Build&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt;|&quot;500ms 미만 지연 필수&quot;| Build&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt;|&quot;기존 system 깊은 통합&quot;| Build&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Build --&gt; Save[&quot;최대 80% 비용 절감 가능&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;em&gt;PoC = Vapi/Retell&lt;/em&gt;. &lt;em&gt;확장 시 → Pipecat/LiveKit 마이그레이션&lt;/em&gt; 흐름이 일반.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;복원력-설계&quot;&gt;복원력 설계&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q[복원력 요소]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; Fail[Independent failure points: STT, LLM, TTS]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q --&gt; Strat[전략]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Strat --&gt; Retry[Retry + backoff]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Strat --&gt; Fallback[Fallback (다른 vendor)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Strat --&gt; Graceful[Graceful: &quot;잠시 후 다시&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Strat --&gt; Health[&quot;Health check + circuit breaker&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;각 단계 독립 실패 → 우아하게 처리. 자세한 건 [[retry-with-backoff]] / [[circuit-breaker]].&lt;/p&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Managed 만 사용&lt;/strong&gt; = 비용 폭증 + 가시성 0. 1000분+ 면 self-host.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pipecat 의 Frame 무시&lt;/strong&gt; = 직접 콜백으로 짜다가 &lt;em&gt;경합 / 순서 문제&lt;/em&gt;. Frame 모델 활용.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LiveKit 의 transcription frame&lt;/strong&gt; = STT 결과가 &lt;em&gt;partial vs final&lt;/em&gt; 분리 안 보임. 명시 처리.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자체 구축 시 turn detection&lt;/strong&gt; = VAD 만으로 한계. 시맨틱 모델 필요. Pipecat / LK 가 내장한 이유.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[voice-agent-architecture]]&lt;/li&gt;
&lt;li&gt;[[vad-silero]]&lt;/li&gt;
&lt;li&gt;[[turn-detection-barge-in]]&lt;/li&gt;
&lt;li&gt;[[webrtc]]&lt;/li&gt;
&lt;li&gt;[[circuit-breaker]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>pipecat</category><category>livekit</category><category>voice-framework</category><category>agent</category><category>sfu</category><author>koa (김신건)</author></item><item><title>[Voice AI] 음성 에이전트 아키텍처: Cascading / Streaming / S2S + Latency Budget</title><link>https://shinkeonkim.com/wiki/ai/voice-agent-architecture/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/ai/voice-agent-architecture/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;실시간 음성 AI 의 &lt;em&gt;3가지 패턴&lt;/em&gt;. 각자 &lt;em&gt;지연 vs 유연성 vs 비용&lt;/em&gt; 트레이드오프.&lt;/p&gt;
&lt;h2 id=&quot;3-패턴-비교&quot;&gt;3 패턴 비교&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph Cascading[&quot;1. Cascading (순차)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        C1[STT] --&gt; C2[LLM] --&gt; C3[TTS]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        Note1[&quot;2-4초 지연 (직렬)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph Streaming[&quot;2. Streaming (병행)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        S1[STT chunks] -.partial.-&gt; S2[LLM streaming]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        S2 -.tokens.-&gt; S3[TTS streaming]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        Note2[&quot;&amp;#x3C; 1초 지연 (병행)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    subgraph S2S[&quot;3. Speech-to-Speech&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        D1[Audio In] --&gt; D2[Multimodal Model]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        D2 --&gt; D3[Audio Out]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        Note3[&quot;&amp;#x3C; 300ms (직접 처리)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;



























































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Cascading&lt;/th&gt;&lt;th&gt;Streaming&lt;/th&gt;&lt;th&gt;S2S&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;em&gt;2-4초&lt;/em&gt;&lt;/td&gt;&lt;td&gt;&lt;em&gt;&amp;#x3C; 1초&lt;/em&gt;&lt;/td&gt;&lt;td&gt;&lt;em&gt;&amp;#x3C; 300ms&lt;/em&gt;&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;/td&gt;&lt;td&gt;어려움 (제약)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;유연성&lt;/td&gt;&lt;td&gt;&lt;em&gt;최고&lt;/em&gt; (각 단계 교체)&lt;/td&gt;&lt;td&gt;높음&lt;/td&gt;&lt;td&gt;&lt;em&gt;낮음&lt;/em&gt; (벤더 lock)&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;/td&gt;&lt;td&gt;&lt;em&gt;높음&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Function calling&lt;/td&gt;&lt;td&gt;OK&lt;/td&gt;&lt;td&gt;OK&lt;/td&gt;&lt;td&gt;&lt;em&gt;제한&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;톤/감정 보존&lt;/td&gt;&lt;td&gt;X&lt;/td&gt;&lt;td&gt;X&lt;/td&gt;&lt;td&gt;&lt;em&gt;예&lt;/em&gt;&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;/td&gt;&lt;td&gt;일부 (Realtime API 한국어)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;사용&lt;/td&gt;&lt;td&gt;MVP / 일반&lt;/td&gt;&lt;td&gt;&lt;em&gt;프로덕션 표준&lt;/em&gt;&lt;/td&gt;&lt;td&gt;고급 UX&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;1-cascading-순차&quot;&gt;1. Cascading (순차)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    User-&gt;&gt;STT: 발화 (3초)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over STT: 발화 종료 대기 + 전사 (500ms)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    STT-&gt;&gt;LLM: 전체 텍스트&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over LLM: 응답 생성 (1500ms)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    LLM-&gt;&gt;TTS: 전체 응답&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over TTS: 음성 합성 (800ms)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    TTS-&gt;&gt;User: 음성 시작&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over User,TTS: 총: ~3초 지연&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; cascaded_pipeline&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(audio_url):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    transcript &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; stt.transcribe(audio_url)   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 발화 끝나길 기다림&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; llm.chat(transcript)           &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 전체 응답&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    audio &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; tts.synthesize(response)          &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 전체 합성&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; audio&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;MVP 에는 OK&lt;/em&gt;. 프로덕션은 &lt;em&gt;streaming&lt;/em&gt; 으로 진화.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;2-streaming-현재-표준&quot;&gt;2. Streaming (현재 표준)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    User-&gt;&gt;STT: 발화 (3초)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    par&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        STT--&gt;&gt;LLM: partial &quot;안녕&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        STT--&gt;&gt;LLM: partial &quot;안녕하세요&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        STT--&gt;&gt;LLM: FINAL &quot;안녕하세요, 오늘 회의&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        LLM--&gt;&gt;TTS: 토큰 &quot;오늘은&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        LLM--&gt;&gt;TTS: 토큰 &quot;수요일입니다&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        TTS--&gt;&gt;User: 오디오 청크 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        TTS--&gt;&gt;User: 오디오 청크 2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    end&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over User: 음성 종료 후 &amp;#x3C; 1초 응답&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; streaming_pipeline&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(audio_stream):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; transcript_chunk &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; stt.stream(audio_stream):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; transcript_chunk.is_final:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; token &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; llm.stream(transcript_chunk.text):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;                sentence &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; aggregator.feed(token)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;                if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; sentence:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;                    async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; audio_chunk &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; tts.stream(sentence):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;                        yield&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; audio_chunk&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;em&gt;모든 단계 스트리밍 + sentence aggregator&lt;/em&gt;. 2026 시점 &lt;em&gt;프로덕션 음성 AI 의 표준&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;3-speech-to-speech-s2s&quot;&gt;3. Speech-to-Speech (S2S)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    User-&gt;&gt;Realtime: 오디오 stream (WebRTC / WS)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Note over Realtime: GPT-4o / Gemini Live&amp;#x3C;br/&gt;(직접 오디오 처리)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Realtime--&gt;&gt;User: 오디오 stream (&amp;#x3C; 300ms)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;





























&lt;table&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;strong&gt;OpenAI Realtime API (GPT-4o)&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;2024-10&lt;/td&gt;&lt;td&gt;우수&lt;/td&gt;&lt;td&gt;WebRTC / WS, function calling 부분&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Gemini Live (gemini-live-2.5)&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;2024-12&lt;/td&gt;&lt;td&gt;우수&lt;/td&gt;&lt;td&gt;Google AI Studio&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Moshi (Kyutai)&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;OSS&lt;/td&gt;&lt;td&gt;영어&lt;/td&gt;&lt;td&gt;full-duplex, 자체 호스팅&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!CAUTION]
S2S 의 &lt;em&gt;function calling 제약&lt;/em&gt; = enterprise 시스템 연동 어려움. &lt;em&gt;function 이 필수면 cascading + streaming&lt;/em&gt; 채택. [arXiv 참조].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;latency-budget-지연-예산&quot;&gt;Latency Budget (지연 예산)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Mic[&quot;마이크 (사용자 발화 끝)&quot;] --&gt;|VAD endpointing 200-500ms| STT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    STT --&gt;|&quot;STT (100-500ms)&quot;| LLM&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    LLM --&gt;|&quot;LLM TTFT (200-2000ms)&quot;| TTS&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    TTS --&gt;|&quot;TTS TTFA (200-800ms)&quot;| Speaker&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Speaker --&gt;|&amp;#x3C;= 1000ms| User&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;


















































&lt;table&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;Network (Mic → Server)&lt;/td&gt;&lt;td&gt;30-100ms&lt;/td&gt;&lt;td&gt;20ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;VAD endpointing&lt;/td&gt;&lt;td&gt;500ms&lt;/td&gt;&lt;td&gt;200ms (시맨틱)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;STT (final)&lt;/td&gt;&lt;td&gt;200-500ms&lt;/td&gt;&lt;td&gt;100ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;LLM TTFT (Time-to-First-Token)&lt;/td&gt;&lt;td&gt;500-2000ms&lt;/td&gt;&lt;td&gt;200ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Sentence aggregation&lt;/td&gt;&lt;td&gt;50-200ms&lt;/td&gt;&lt;td&gt;0 (streaming)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;TTS TTFB&lt;/td&gt;&lt;td&gt;200-800ms&lt;/td&gt;&lt;td&gt;90ms (Cartesia)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Network (Server → Speaker)&lt;/td&gt;&lt;td&gt;30-100ms&lt;/td&gt;&lt;td&gt;20ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;총&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;2-4초&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;&amp;#x3C; 700ms&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;em&gt;800ms 가 “로봇 같지 않게 느껴지는” 임계&lt;/em&gt;. 사람 평균 응답 300-500ms. &lt;em&gt;모든 단계 동시 최적화&lt;/em&gt; 필요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;단계별-최적화&quot;&gt;단계별 최적화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Opt[지연 줄이기]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Opt --&gt; S1[STT: 시맨틱 endpointing, partial 안 기다림]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Opt --&gt; S2[LLM: 작은 모델, TTFT 최우선, KV-cache]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Opt --&gt; S3[TTS: streaming + Cartesia급 모델]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Opt --&gt; Net[Network: WebRTC &amp;#x3C; WebSocket &amp;#x3C; HTTP]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Opt --&gt; Hop[Hop 축소: 같은 region, edge]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;결정-트리&quot;&gt;결정 트리&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1{지연 요구}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|&amp;#x3C; 300ms 필수| S2S_check{Function calling 필수?}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|&amp;#x3C; 1초 OK| Stream[Streaming pipeline]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|2-3초 OK + MVP| Cascade[Cascading]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    S2S_check --&gt;|아니오| S2S[S2S 모델]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    S2S_check --&gt;|예| Stream&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;한국-시장-권장&quot;&gt;한국 시장 권장&lt;/h2&gt;

























&lt;table&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;Streaming: CLOVA STT + GPT-4o-mini + CLOVA Voice&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;고급 음성 AI&lt;/td&gt;&lt;td&gt;Streaming: Deepgram + GPT-4o + Cartesia&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;비용 절감&lt;/td&gt;&lt;td&gt;Cascading: Whisper + GPT-3.5 + Edge TTS&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;톤/감정 critical&lt;/td&gt;&lt;td&gt;S2S: GPT-4o Realtime&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;STT/LLM/TTS 각자 다른 region&lt;/strong&gt; = network hop 누적 → 500ms+. 같은 region.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;batch 모델로 시작했다가 latency 못 맞춤&lt;/strong&gt; = 처음부터 streaming 모델.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LLM 응답 끝나길 기다리고 TTS 시작&lt;/strong&gt; = TTS 의 streaming 의미 없음.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VAD 없이 STT&lt;/strong&gt; = STT 가 &lt;em&gt;전사 못 끝남&lt;/em&gt;. 발화 종료 감지 필수.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[stt-streaming]]&lt;/li&gt;
&lt;li&gt;[[tts-streaming-ssml]]&lt;/li&gt;
&lt;li&gt;[[speech-to-speech-realtime]]&lt;/li&gt;
&lt;li&gt;[[vad-silero]]&lt;/li&gt;
&lt;li&gt;[[turn-detection-barge-in]]&lt;/li&gt;
&lt;li&gt;[[latency-percentiles]]&lt;/li&gt;
&lt;li&gt;[[pipecat-livekit]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>voice-agent</category><category>architecture</category><category>latency</category><category>streaming</category><category>cascading</category><category>s2s</category><author>koa (김신건)</author></item><item><title>[Voice AI] 음성 우선 프롬프트 + Function Calling + Structured Output</title><link>https://shinkeonkim.com/wiki/ai/voice-first-prompt/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/ai/voice-first-prompt/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;음성-우선-프롬프트-설계&quot;&gt;음성 우선 프롬프트 설계&lt;/h2&gt;
&lt;p&gt;음성 답변 = &lt;em&gt;귀로 듣는다&lt;/em&gt;. 텍스트 답변과 &lt;em&gt;완전 다르게&lt;/em&gt; 짜야 한다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Text[&quot;텍스트 답변 (긴 답변 OK)&quot;] --&gt; A1[&quot;**bold**, # 헤딩, 표&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Voice[&quot;음성 답변 (짧고 자연스럽게)&quot;] --&gt; A2[&quot;1-2 문장, 의문문으로 turn 넘김&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    A1 --&gt; Diff&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    A2 --&gt; Diff&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Diff{둘은 완전 다름}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;음성-프롬프트의-핵심-원칙&quot;&gt;음성 프롬프트의 핵심 원칙&lt;/h2&gt;

































&lt;table&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;strong&gt;짧게&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;한 답변 1-2 문장. 50-80 글자&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;자연스럽게&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;”$5 = ‘오 달러’” (숫자 정규화 사전 정의)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;마크다운 금지&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code&gt;**&lt;/code&gt;, &lt;code&gt;#&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt; 등 모두 &lt;em&gt;음성에 안 들어감&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;listicle 분해&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;”1, 2, 3…” 대신 &lt;em&gt;대화로 분배&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;turn 넘기기&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;질문으로 끝내거나 “도와드릴까요?”&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;에스컬레이션&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;”사람 상담원 연결해드릴까요?” 명시&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;당신은 친절한 한국어 콜센터 상담원입니다.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;응답 규칙:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;- 한 응답은 *1-2 문장* 으로 짧게&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;- 자연스러운 구어체 사용 (&quot;입니다&quot; 보다 &quot;이에요&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;- 숫자는 풀어쓰기 (예: &quot;100만 원&quot; 보다 &quot;백만 원&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;- 마크다운, 영어 약어, 코드 사용 금지&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;- 모르는 정보면 &quot;확인이 어렵네요. 사람 상담원 연결해드릴까요?&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;- 답변 끝에 *질문* 으로 turn 을 사용자에게 넘김&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;페르소나:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;- 침착하고 공감하는 톤&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;- 고객을 &quot;고객님&quot; 으로 호칭&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;- 절대 &quot;AI&quot; 라고 본인을 칭하지 말 것&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;function--tool-calling&quot;&gt;Function / Tool Calling&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    autonumber&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    User-&gt;&gt;Agent: &quot;내 주문 상태 확인해줘&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Agent-&gt;&gt;LLM: prompt + tool definitions&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    LLM--&gt;&gt;Agent: function_call: get_order_status(user_id=42)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Agent-&gt;&gt;API: GET /orders?user=42&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    API--&gt;&gt;Agent: { status: &quot;shipped&quot;, eta: &quot;내일&quot; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Agent-&gt;&gt;LLM: tool_result&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    LLM--&gt;&gt;Agent: &quot;고객님 주문은 배송 중이며, 내일 도착 예정이에요.&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Agent-&gt;&gt;TTS: 음성 합성&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    TTS-&gt;&gt;User: 음성 응답&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;openai-function-calling&quot;&gt;OpenAI Function Calling&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;tools &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;function&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &quot;function&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            &quot;name&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;get_order_status&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            &quot;description&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;사용자의 주문 상태를 조회합니다&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            &quot;parameters&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;                &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;object&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;                &quot;properties&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;                    &quot;user_id&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;integer&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;사용자 ID&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;                    &quot;order_id&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;description&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;주문 ID (선택)&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;                },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;                &quot;required&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;user_id&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;function&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &quot;function&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            &quot;name&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;transfer_to_human&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            &quot;description&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;사람 상담원에게 연결합니다&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;            &quot;parameters&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;                &quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;object&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;                &quot;properties&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;                    &quot;reason&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;                    &quot;priority&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: {&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;enum&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;low&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;high&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;                },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;                &quot;required&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;reason&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; client.chat.completions.create(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    model&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;gpt-4o-mini&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    messages&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;messages,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    tools&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;tools,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    tool_choice&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;auto&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    stream&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;True&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;병렬-function-calling&quot;&gt;병렬 Function Calling&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 같은 응답에서 여러 함수 동시 호출 가능&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;  &quot;tool_calls&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: [&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    { &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;call_1&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;function&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: { &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;get_weather&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;arguments&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;...&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; } },&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    { &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;call_2&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;function&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: { &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;get_traffic&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;arguments&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;...&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; } }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;  ]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 병렬 실행&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;results &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; asyncio.gather(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    get_weather(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    get_traffic(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;...&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;직렬&lt;/em&gt; 보다 &lt;em&gt;수배 빠름&lt;/em&gt;. 독립 호출이면 활용.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;결과를-대화에-자연스럽게&quot;&gt;결과를 대화에 자연스럽게&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;@function_tool&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; get_order_status&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(user_id: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) -&gt; &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;dict&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    order &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; db.get_order(user_id)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &quot;status&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: order.status,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &quot;eta&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: order.eta,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        # 함수 결과는 *데이터* 만. *음성 문장* 은 LLM 이 합성&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 잘못된 예: 함수가 문장 반환&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;@function_tool&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; get_order_status_bad&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(user_id: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) -&gt; &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    order &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; db.get_order(user_id)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; f&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;주문 &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;order.id&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; 는 &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;order.status&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; 상태이며 ETA &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;order.eta&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;}&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; 입니다.&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;    # ← TTS 가 &quot;ETA&quot; 를 영어로 발음하거나 어색&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;structured-output&quot;&gt;Structured Output&lt;/h2&gt;
&lt;p&gt;LLM 응답을 &lt;em&gt;JSON schema 강제&lt;/em&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; IntentExtraction&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;BaseModel&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    intent: Literal[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;order_status&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;refund&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;complaint&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;transfer&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;other&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    user_id: Optional[&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    order_id: Optional[&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    urgency: Literal[&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;low&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;normal&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;high&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; client.chat.completions.parse(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    model&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;gpt-4o-mini&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    messages&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;[{&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;content&quot;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;: user_text}],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;    response_format&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;IntentExtraction,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;intent &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; response.choices[&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;].message.parsed&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# intent.intent == &quot;order_status&quot;  ← 타입 안전&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;슬롯-채우기-slot-filling&quot;&gt;슬롯 채우기 (Slot Filling)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 사용자가 부분 정보만 주면 → 부족한 슬롯 묻기&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; BookingRequest&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt;BaseModel&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    restaurant: Optional[&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    date: Optional[&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    party_size: Optional[&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;int&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    is_complete: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;bool&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; False&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 첫 발화: &quot;내일 5명 예약하고싶어&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# extracted: { date: &quot;내일&quot;, party_size: 5, restaurant: None, is_complete: false }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# LLM 이 부족한 정보 *질문*&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;if&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; not&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; extracted.restaurant:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    response &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;어느 식당으로 예약 도와드릴까요?&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;페르소나-일관성&quot;&gt;페르소나 일관성&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;PERSONA&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;이름: 민지&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;성격: 친절, 차분, 약간 유머&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;역할: 카페 음성 주문 도우미&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;말투: 격식 있는 존댓말 (&quot;이에요&quot;, &quot;괜찮으세요?&quot;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;금지: 비속어, 정치/종교, 의료 조언&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;SYSTEM&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; =&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; f&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;{PERSONA}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;응답 규칙:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;- 항상 페르소나 일관성 유지&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;- 메뉴 외 질문은 정중히 거절 + 본업으로 유도&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;시스템 프롬프트가 &lt;em&gt;길고 마크다운&lt;/em&gt;&lt;/strong&gt; = TTS 가 안 읽지만, &lt;em&gt;LLM 의 응답이 마크다운 따라함&lt;/em&gt;. 출력 형식 명시.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;숫자 발음&lt;/strong&gt; = “1500000원” → “일오영영영영영원”. &lt;em&gt;별도 정규화&lt;/em&gt; 또는 SSML.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;function 결과로 &lt;em&gt;긴 텍스트 반환&lt;/em&gt;&lt;/strong&gt; = LLM 이 그대로 &lt;em&gt;읽음&lt;/em&gt;. 데이터만.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;tool calling 중 &lt;em&gt;interruption 허용&lt;/em&gt;&lt;/strong&gt; = DB write 중간에 끊김. &lt;code&gt;disallow_interruptions&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[voice-agent-architecture]]&lt;/li&gt;
&lt;li&gt;[[voice-rag]]&lt;/li&gt;
&lt;li&gt;[[agent-patterns]]&lt;/li&gt;
&lt;li&gt;[[turn-detection-barge-in]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>prompt</category><category>function-calling</category><category>structured-output</category><category>voice-llm</category><author>koa (김신건)</author></item><item><title>[Voice AI] Voice RAG: 지연 병목 + 예측적 프리페치</title><link>https://shinkeonkim.com/wiki/ai/voice-rag/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/ai/voice-rag/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Voice RAG&lt;/strong&gt; = 음성 에이전트에 &lt;em&gt;RAG (Retrieval-Augmented Generation)&lt;/em&gt; 결합. &lt;em&gt;벡터 DB 조회&lt;/em&gt; 가 &lt;em&gt;지연 누적&lt;/em&gt; 의 큰 원인이 되므로 &lt;em&gt;예측적 프리페치&lt;/em&gt; 같은 패턴 필수.&lt;/p&gt;
&lt;h2 id=&quot;기본-흐름&quot;&gt;기본 흐름&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    User-&gt;&gt;STT: 발화&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    STT-&gt;&gt;RAG: 질문 텍스트&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    RAG-&gt;&gt;Embed: 임베딩&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Embed-&gt;&gt;VectorDB: kNN search&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    VectorDB--&gt;&gt;RAG: top-K chunks&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    RAG-&gt;&gt;LLM: prompt + chunks&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    LLM--&gt;&gt;TTS: 응답 토큰&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    TTS--&gt;&gt;User: 음성&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;각 단계가 직렬&lt;/em&gt;. RAG 추가 &lt;em&gt;200-1000ms&lt;/em&gt; → 종단 지연 &lt;em&gt;2초+&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;지연-분해&quot;&gt;지연 분해&lt;/h2&gt;



































&lt;table&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;Embedding&lt;/td&gt;&lt;td&gt;50-200ms&lt;/td&gt;&lt;td&gt;30ms (로컬 small 모델)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Vector DB query&lt;/td&gt;&lt;td&gt;30-200ms&lt;/td&gt;&lt;td&gt;10ms (Redis vector, ES kNN)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Re-ranking&lt;/td&gt;&lt;td&gt;100-500ms&lt;/td&gt;&lt;td&gt;50ms (cross-encoder small)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;LLM prompt assembly&lt;/td&gt;&lt;td&gt;5-20ms&lt;/td&gt;&lt;td&gt;5ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;RAG overhead&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;200-1000ms&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;&amp;#x3C; 100ms&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;예측적-프리페치-speculative-retrieval&quot;&gt;예측적 프리페치 (Speculative Retrieval)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Partial[Partial transcript] --&gt; Predict[가능 의도 예측]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Predict --&gt; Pre[미리 fetch (병렬)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Final[Final transcript] --&gt; Match{예측 매칭?}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Match --&gt;|예| UsePre[프리페치 결과 사용]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Match --&gt;|아니오| Re[다시 fetch]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; SpeculativeRAG&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; __init__&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.prefetched: Dict[&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;, Future] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; on_partial&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, partial: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &quot;&quot;&quot;partial 받자마자 예측 fetch 시작&quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; len&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(partial.split()) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&amp;#x3C;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 3&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;   # 너무 짧으면 skip&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        # 이미 fetch 중인 것이 있고 유사하면 재사용&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; any&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;._similar(partial, k) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; k &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.prefetched):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            return&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        # 미리 fetch (병렬)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        fut &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; asyncio.create_task(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;._retrieve(partial))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.prefetched[partial] &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; fut&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; on_final&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, final: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) -&gt; List[Chunk]:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;        &quot;&quot;&quot;final 시 prefetched 결과 활용 또는 새로 fetch&quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;        # 가장 유사한 prefetched 찾기&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        best &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; max&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.prefetched.keys(),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;                   key&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=lambda&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; k: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;._similarity(k, final),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;                   default&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;None&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; best &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;and&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;._similarity(best, final) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 0.85&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            return&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.prefetched[best]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;._retrieve(final)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;partial 받자마자 &lt;em&gt;추측 fetch&lt;/em&gt;&lt;/em&gt; → final 시 &lt;em&gt;대부분 즉시 사용 가능&lt;/em&gt;. RAG overhead → 0 에 가깝게.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;캐시-계층&quot;&gt;캐시 계층&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q[Query] --&gt; L1[L1: in-memory recent (LRU)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    L1 -.miss.-&gt; L2[L2: Redis semantic cache]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    L2 -.miss.-&gt; L3[L3: Vector DB]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    L3 --&gt; Pop[populate L2 + L1]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;semantic-cache&quot;&gt;Semantic Cache&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;class&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; SemanticCache&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &quot;&quot;&quot;비슷한 질문의 결과 재사용&quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    def&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; __init__&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, similarity_threshold&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;0.92&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.cache &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; {}   &lt;/span&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# embedding → result&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;        self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.threshold &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; similarity_threshold&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    async&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; get&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(self, query: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        emb &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; await&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; embed(query)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; cached_emb, result &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.cache.items():&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            sim &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; cosine_similarity(emb, cached_emb)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;            if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; sim &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; self&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;.threshold:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;                return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; result&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; None&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자세한 건 [[Redis Vector Search]] 의 semantic cache.&lt;/p&gt;
&lt;h2 id=&quot;rag-인덱스-선택-음성-환경&quot;&gt;RAG 인덱스 선택 (음성 환경)&lt;/h2&gt;

































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Pinecone&lt;/th&gt;&lt;th&gt;Redis Vector&lt;/th&gt;&lt;th&gt;ES kNN&lt;/th&gt;&lt;th&gt;Qdrant&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Latency (kNN)&lt;/td&gt;&lt;td&gt;30-100ms&lt;/td&gt;&lt;td&gt;5-20ms&lt;/td&gt;&lt;td&gt;20-50ms&lt;/td&gt;&lt;td&gt;10-30ms&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;운영&lt;/td&gt;&lt;td&gt;managed&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;tr&gt;&lt;td&gt;Hybrid (BM25 + vector)&lt;/td&gt;&lt;td&gt;X&lt;/td&gt;&lt;td&gt;별도&lt;/td&gt;&lt;td&gt;&lt;em&gt;우수 (RRF)&lt;/em&gt;&lt;/td&gt;&lt;td&gt;가능&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;음성 에이전트 + 한국어 hybrid search = &lt;em&gt;ElasticSearch kNN + nori&lt;/em&gt; 가 강력.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;청크-전략-음성-친화&quot;&gt;청크 전략 (음성 친화)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 일반 RAG: 큰 청크 (500-1000 tokens)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#6A737D&quot;&gt;# 음성 RAG: *짧은 청크* (100-300 tokens) → LLM 응답 짧게 유도&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#B392F0&quot;&gt; chunk_for_voice&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(doc: &lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;) -&gt; list[&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt;str&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;]:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;    &quot;&quot;&quot;문장 단위 + 의미 단위 분할&quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    sentences &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; nltk.sent_tokenize(doc, &lt;/span&gt;&lt;span style=&quot;color:#FFAB70&quot;&gt;language&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt;&apos;korean&apos;&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    chunks &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; []&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    cur &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    for&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; s &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;in&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; sentences:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        if&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; len&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(cur) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; len&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;(s) &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;&gt;&lt;/span&gt;&lt;span style=&quot;color:#79B8FF&quot;&gt; 300&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            chunks.append(cur)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            cur &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;=&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; s&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;        else&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;            cur &lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt;+=&lt;/span&gt;&lt;span style=&quot;color:#9ECBFF&quot;&gt; &quot; &quot;&lt;/span&gt;&lt;span style=&quot;color:#F97583&quot;&gt; +&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; s&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; cur:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;        chunks.append(cur)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#F97583&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt; chunks&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;re-ranking-의-trade-off&quot;&gt;Re-ranking 의 trade-off&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    VS[Vector Search top 50] --&gt; Re[Re-ranker]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Re --&gt; Top5[top 5]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Re --&gt; Cost[+100-500ms]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;정확도 ↑ vs 지연 ↑&lt;/em&gt;. 음성 에이전트는 &lt;em&gt;작은 cross-encoder&lt;/em&gt; (Cohere rerank-v3 light, BAAI bge-reranker-base) 권장.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;흔한-함정&quot;&gt;흔한 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;RAG 매 turn 동기 fetch&lt;/strong&gt; = 매 응답 200-1000ms 증가. &lt;em&gt;예측적 프리페치&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;거대 청크&lt;/strong&gt; = LLM 응답이 길어짐. 짧은 청크.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-encoder rerank 무조건&lt;/strong&gt; = 500ms+ 부담. &lt;em&gt;trade-off&lt;/em&gt; 결정.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;결과 그대로 prompt&lt;/strong&gt; = 토큰 폭증. 요약 또는 selection.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[voice-agent-architecture]]&lt;/li&gt;
&lt;li&gt;[[voice-first-prompt]]&lt;/li&gt;
&lt;li&gt;[[elasticsearch-vector-search]]&lt;/li&gt;
&lt;li&gt;[[Redis Vector Search]]&lt;/li&gt;
&lt;li&gt;[[latency-percentiles]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>voice-rag</category><category>rag</category><category>vector-db</category><category>latency</category><category>prefetch</category><author>koa (김신건)</author></item><item><title>[Algorithm] Karatsuba 곱셈: O(n²) 벽을 깬 분할 정복</title><link>https://shinkeonkim.com/wiki/algorithm/divide/karatsuba/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/divide/karatsuba/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Karatsuba 알고리즘&lt;/strong&gt; 은 &lt;em&gt;두 큰 정수의 곱셈&lt;/em&gt; 을 &lt;em&gt;분할 정복&lt;/em&gt; 으로 풀어 &lt;strong&gt;O(n^log₂3) ≈ O(n^1.585)&lt;/strong&gt; 시간에 처리하는 알고리즘. 1962년 &lt;em&gt;Anatoly Karatsuba&lt;/em&gt; (당시 23세 학생) 가 &lt;em&gt;Kolmogorov 의 “곱셈은 Ω(n²) 이다” 추측을 반박&lt;/em&gt; 한 결과물.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;em&gt;“곱셈은 더 빨라질 수 없다”&lt;/em&gt; 라고 믿어진 &lt;em&gt;수십 년의 추측을 깬 알고리즘&lt;/em&gt;. &lt;em&gt;분할 정복으로 산술 자체를 가속할 수 있다&lt;/em&gt; 는 &lt;em&gt;패러다임 전환&lt;/em&gt;. 이후 &lt;em&gt;Toom-Cook, FFT 기반 Schönhage-Strassen, 2019 Harvey-Hoeven 의 O(n log n)&lt;/em&gt; 으로 이어진다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;동기-on-의-한계&quot;&gt;동기: O(n²) 의 한계&lt;/h2&gt;
&lt;p&gt;n자리 정수 곱셈을 &lt;em&gt;손으로&lt;/em&gt; 하면 &lt;em&gt;각 자리마다 모든 자리와 곱셈&lt;/em&gt; → &lt;em&gt;O(n²)&lt;/em&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        1234&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;      × 5678&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;      ──────&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        9872     ← 1234 × 8&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;       8638      ← 1234 × 7&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;      7404       ← 1234 × 6&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;     6170        ← 1234 × 5&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;     ──────&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;     7006652&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;n&lt;/th&gt;&lt;th&gt;O(n²) 연산&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;100&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;100&lt;/td&gt;&lt;td&gt;10,000&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;1,000&lt;/td&gt;&lt;td&gt;1,000,000&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;1,000,000 (백만 자리)&lt;/td&gt;&lt;td&gt;&lt;em&gt;10¹²&lt;/em&gt; (불가능)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;암호학 (RSA, ECC), 컴퓨터 대수 시스템 (Mathematica, SymPy), π 계산&lt;/em&gt; 등에서 &lt;em&gt;수백만 자리&lt;/em&gt; 곱셈 필요. O(n²) 으로는 &lt;em&gt;불가능&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;핵심-아이디어-3번의-곱셈으로-분해&quot;&gt;핵심 아이디어: 3번의 곱셈으로 분해&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;두 n자리 수 &lt;em&gt;x, y&lt;/em&gt; 를 &lt;em&gt;반으로 분할&lt;/em&gt; (&lt;code&gt;m = n/2&lt;/code&gt; 자리):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;x = a · Bᵐ + b      (a = 상위 m자리, b = 하위 m자리)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;y = c · Bᵐ + d&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;B = 진수 (보통 10, 또는 2³² 같은 워드 크기).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;전통 곱셈 (4번):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;x · y = (a·Bᵐ + b)(c·Bᵐ + d)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;      = ac · B²ᵐ + (ad + bc) · Bᵐ + bd&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;→ &lt;code&gt;ac&lt;/code&gt;, &lt;code&gt;ad&lt;/code&gt;, &lt;code&gt;bc&lt;/code&gt;, &lt;code&gt;bd&lt;/code&gt; &lt;em&gt;4번의 (n/2)자리 곱셈&lt;/em&gt; + 덧셈/시프트.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Karatsuba 의 통찰&lt;/strong&gt;:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;(a + b)(c + d) = ac + ad + bc + bd&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            ad + bc = (a+b)(c+d) - ac - bd&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;→ &lt;em&gt;ad + bc&lt;/em&gt; 를 &lt;em&gt;별도 곱셈 없이&lt;/em&gt; 다른 결과의 &lt;em&gt;조합&lt;/em&gt; 으로 얻는다!&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;z₂ = a · c&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;z₀ = b · d&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;z₁ = (a + b)(c + d) - z₂ - z₀     ← ad + bc&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;x · y = z₂ · B²ᵐ + z₁ · Bᵐ + z₀&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3번의 (n/2)자리 곱셈 + 몇 번의 덧셈/뺄셈/시프트&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id=&quot;복잡도-분석&quot;&gt;복잡도 분석&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    T[&quot;T(n)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    T --&gt;|3 곱셈| T1[&quot;T(n/2)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    T --&gt;|+ O(n) 덧셈| Add[덧셈]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    T --&gt;|3 곱셈| T2[&quot;T(n/2)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    T --&gt;|3 곱셈| T3[&quot;T(n/2)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    T1 --&gt;|3 곱셈| T4[&quot;T(n/4)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    T1 --&gt;|3 곱셈| T5[&quot;T(n/4)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    T1 --&gt;|3 곱셈| T6[&quot;T(n/4)&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;재귀 관계:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;T(n) = 3 · T(n/2) + O(n)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Master Theorem&lt;/em&gt; (a=3, b=2, f(n)=O(n^1)):&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;log_b(a) = log₂3 ≈ 1.585&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;n^log_b(a) = n^1.585 &gt; n^1 = f(n)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;→ T(n) = Θ(n^log₂3) ≈ Θ(n^1.585)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;시간-복잡도-비교&quot;&gt;시간 복잡도 비교&lt;/h3&gt;
&lt;!-- mdx-component: ChartJs --&gt;






















































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;n&lt;/th&gt;&lt;th&gt;Naive&lt;/th&gt;&lt;th&gt;Karatsuba&lt;/th&gt;&lt;th&gt;FFT&lt;/th&gt;&lt;th&gt;Naive / Karatsuba&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;10&lt;/td&gt;&lt;td&gt;100&lt;/td&gt;&lt;td&gt;38&lt;/td&gt;&lt;td&gt;33&lt;/td&gt;&lt;td&gt;2.6×&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;100&lt;/td&gt;&lt;td&gt;10K&lt;/td&gt;&lt;td&gt;1.4K&lt;/td&gt;&lt;td&gt;664&lt;/td&gt;&lt;td&gt;7.2×&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;1K&lt;/td&gt;&lt;td&gt;1M&lt;/td&gt;&lt;td&gt;50K&lt;/td&gt;&lt;td&gt;10K&lt;/td&gt;&lt;td&gt;20×&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;10K&lt;/td&gt;&lt;td&gt;100M&lt;/td&gt;&lt;td&gt;1.8M&lt;/td&gt;&lt;td&gt;133K&lt;/td&gt;&lt;td&gt;&lt;em&gt;55×&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;100K&lt;/td&gt;&lt;td&gt;10B&lt;/td&gt;&lt;td&gt;66M&lt;/td&gt;&lt;td&gt;1.7M&lt;/td&gt;&lt;td&gt;&lt;em&gt;152×&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;1M&lt;/td&gt;&lt;td&gt;1T&lt;/td&gt;&lt;td&gt;2.4B&lt;/td&gt;&lt;td&gt;20M&lt;/td&gt;&lt;td&gt;&lt;em&gt;416×&lt;/em&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;n = 1M 자리&lt;/em&gt; (RSA-4096 같은 거대 정수) 에서 Karatsuba 는 &lt;em&gt;400배 이상&lt;/em&gt; 빠르다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;알고리즘-의사-코드&quot;&gt;알고리즘 (의사 코드)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;function karatsuba(x, y):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  # Base case: 작은 수는 일반 곱셈&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  if x &amp;#x3C; BASE or y &amp;#x3C; BASE:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return x * y&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  n = max(digits(x), digits(y))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  m = ⌈n / 2⌉&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  B^m = base^m  # 시프트 단위&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  # 1. Divide: x, y 를 각각 상/하위로 분할&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  a, b = divmod(x, B^m)   # x = a · B^m + b&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  c, d = divmod(y, B^m)   # y = c · B^m + d&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  # 2. Conquer: 3번의 재귀 호출 (4번 → 3번)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  z2 = karatsuba(a, c)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  z0 = karatsuba(b, d)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  z1 = karatsuba(a + b, c + d) - z2 - z0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  # 3. Combine: 시프트 + 덧셈&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  return z2 · B^(2m) + z1 · B^m + z0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;재귀-트리-시각화&quot;&gt;재귀 트리 시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;위는 &lt;em&gt;일반 분할 정복&lt;/em&gt; 의 동작. Karatsuba 도 &lt;em&gt;같은 흐름&lt;/em&gt; 이지만 &lt;em&gt;2개가 아닌 3개로 분기&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Root[&quot;xy (n자리)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Root --&gt; N1[&quot;z₂ = a·c&amp;#x3C;br/&gt;(n/2자리)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Root --&gt; N2[&quot;z₀ = b·d&amp;#x3C;br/&gt;(n/2자리)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Root --&gt; N3[&quot;(a+b)(c+d)&amp;#x3C;br/&gt;(n/2자리)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    N1 --&gt; N1a[&quot;...&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    N1 --&gt; N1b[&quot;...&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    N1 --&gt; N1c[&quot;...&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    N2 --&gt; N2a[&quot;...&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    N2 --&gt; N2b[&quot;...&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    N2 --&gt; N2c[&quot;...&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    N3 --&gt; N3a[&quot;...&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    N3 --&gt; N3b[&quot;...&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    N3 --&gt; N3c[&quot;...&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;각 노드가 &lt;em&gt;3개로 분기&lt;/em&gt;. 깊이 &lt;em&gt;log₂n&lt;/em&gt;. 총 노드 수 &lt;em&gt;3^log₂n = n^log₂3&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;단계별-추적-1234--5678&quot;&gt;단계별 추적: 1234 × 5678&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;입력: x = 1234, y = 5678, n = 4, m = 2, B^m = 100&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;분할:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  a = 12, b = 34   (1234 = 12·100 + 34)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  c = 56, d = 78   (5678 = 56·100 + 78)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;3번의 재귀:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  z₂ = karatsuba(12, 56) = 672&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  z₀ = karatsuba(34, 78) = 2652&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  (a+b)(c+d) = karatsuba(46, 134) = 6164&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  z₁ = 6164 - 672 - 2652 = 2840&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;조합:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  xy = 672 · 10000 + 2840 · 100 + 2652&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;     = 6720000 + 284000 + 2652&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;     = 7006652  ✓&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;검산: 1234 × 5678 = 7006652  ✓&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현-5개-언어&quot;&gt;구현 (5개 언어)&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
outputLanguage=“text”
outputLabel=“동일”
title=“Karatsuba: 같은 알고리즘, 5개 언어”
variants={[
{
label: ‘python’,
language: ‘python’,
code: `def karatsuba(x: int, y: int) -&gt; int:
# base case
if x &amp;#x3C; 10 or y &amp;#x3C; 10:
return x * y&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;n = max(len(str(x)), len(str(y)))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;m = n // 2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;B = 10 ** m&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;a, b = divmod(x, B)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;c, d = divmod(y, B)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;z2 = karatsuba(a, c)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;z0 = karatsuba(b, d)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;z1 = karatsuba(a + b, c + d) - z2 - z0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;return z2 * (B * B) + z1 * B + z0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;print(karatsuba(1234, 5678))
print(karatsuba(12345678, 87654321))&lt;code&gt;,     },     {       label: &apos;javascript&apos;,       language: &apos;javascript&apos;,       code: &lt;/code&gt;// BigInt 로 임의 크기 정수 지원
function karatsuba(x, y) {
if (x &amp;#x3C; 10n || y &amp;#x3C; 10n) return x * y;&lt;/p&gt;
&lt;p&gt;const n = Math.max(x.toString().length, y.toString().length);
const m = BigInt(Math.floor(n / 2));
const B = 10n ** m;&lt;/p&gt;
&lt;p&gt;const a = x / B, b = x % B;
const c = y / B, d = y % B;&lt;/p&gt;
&lt;p&gt;const z2 = karatsuba(a, c);
const z0 = karatsuba(b, d);
const z1 = karatsuba(a + b, c + d) - z2 - z0;&lt;/p&gt;
&lt;p&gt;return z2 * (B * B) + z1 * B + z0;
}&lt;/p&gt;
&lt;p&gt;console.log(karatsuba(1234n, 5678n).toString());
console.log(karatsuba(12345678n, 87654321n).toString());&lt;code&gt;,     },     {       label: &apos;java&apos;,       language: &apos;java&apos;,       code: &lt;/code&gt;import java.math.BigInteger;&lt;/p&gt;
&lt;p&gt;class Karatsuba {
static BigInteger karatsuba(BigInteger x, BigInteger y) {
final BigInteger TEN = BigInteger.TEN;
if (x.compareTo(TEN) &amp;#x3C; 0 || y.compareTo(TEN) &amp;#x3C; 0)
return x.multiply(y);&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;int n = Math.max(x.toString().length(), y.toString().length());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;int m = n / 2;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;BigInteger B = TEN.pow(m);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;BigInteger[] xs = x.divideAndRemainder(B);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;BigInteger[] ys = y.divideAndRemainder(B);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;BigInteger a = xs[0], b = xs[1];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;BigInteger c = ys[0], d = ys[1];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;BigInteger z2 = karatsuba(a, c);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;BigInteger z0 = karatsuba(b, d);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;BigInteger z1 = karatsuba(a.add(b), c.add(d))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                  .subtract(z2).subtract(z0);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;return z2.multiply(B.pow(2))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  .add(z1.multiply(B))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  .add(z0);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;public static void main(String[] args) {
System.out.println(karatsuba(
BigInteger.valueOf(1234),
BigInteger.valueOf(5678)));
}
}&lt;code&gt;,     },     {       label: &apos;rust&apos;,       language: &apos;rust&apos;,       code: &lt;/code&gt;// num-bigint crate 사용
use num_bigint::BigInt;
use num_traits::Zero;&lt;/p&gt;
&lt;p&gt;fn karatsuba(x: &amp;#x26;BigInt, y: &amp;#x26;BigInt) -&gt; BigInt {
let ten = BigInt::from(10);
if x &amp;#x3C; &amp;#x26;ten || y &amp;#x3C; &amp;#x26;ten {
return x * y;
}&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;let n = x.to_string().len().max(y.to_string().len());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;let m = n / 2;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;let b = BigInt::from(10).pow(m as u32);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;let (a, b_lo) = (x / &amp;#x26;b, x % &amp;#x26;b);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;let (c, d) = (y / &amp;#x26;b, y % &amp;#x26;b);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;let z2 = karatsuba(&amp;#x26;a, &amp;#x26;c);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;let z0 = karatsuba(&amp;#x26;b_lo, &amp;#x26;d);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;let z1 = karatsuba(&amp;#x26;(&amp;#x26;a + &amp;#x26;b_lo), &amp;#x26;(&amp;#x26;c + &amp;#x26;d)) - &amp;#x26;z2 - &amp;#x26;z0;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;z2 * b.pow(2) + z1 * &amp;#x26;b + z0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;code&gt;,     },     {       label: &apos;go&apos;,       language: &apos;go&apos;,       code: &lt;/code&gt;package main&lt;/p&gt;
&lt;p&gt;import (
“fmt”
“math/big”
)&lt;/p&gt;
&lt;p&gt;func karatsuba(x, y *big.Int) *big.Int {
ten := big.NewInt(10)
if x.Cmp(ten) &amp;#x3C; 0 || y.Cmp(ten) &amp;#x3C; 0 {
return new(big.Int).Mul(x, y)
}&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;n := max(len(x.String()), len(y.String()))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;m := n / 2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;B := new(big.Int).Exp(ten, big.NewInt(int64(m)), nil)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;a, b := new(big.Int).DivMod(x, B, new(big.Int))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;c, d := new(big.Int).DivMod(y, B, new(big.Int))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;z2 := karatsuba(a, c)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;z0 := karatsuba(b, d)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;sum := karatsuba(new(big.Int).Add(a, b),&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                 new(big.Int).Add(c, d))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;z1 := new(big.Int).Sub(sum, z2)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;z1.Sub(z1, z0)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// z2*B^2 + z1*B + z0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;res := new(big.Int).Mul(z2, new(big.Int).Mul(B, B))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;res.Add(res, new(big.Int).Mul(z1, B))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;res.Add(res, z0)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;return res&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;code&gt;,     },   ]}   output={&lt;/code&gt;1234 × 5678 = 7006652
12345678 × 87654321 = 1082152022374638`}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;동작-흐름-sequence&quot;&gt;동작 흐름 (sequence)&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;sequenceDiagram&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    autonumber&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant Caller&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant K as karatsuba(1234, 5678)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant K1 as karatsuba(12, 56)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant K2 as karatsuba(34, 78)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    participant K3 as karatsuba(46, 134)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Caller-&gt;&gt;K: 호출&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    K-&gt;&gt;K: 분할: a=12, b=34, c=56, d=78, m=2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    K-&gt;&gt;K1: 재귀 1 (z₂)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    K1--&gt;&gt;K: 672&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    K-&gt;&gt;K2: 재귀 2 (z₀)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    K2--&gt;&gt;K: 2652&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    K-&gt;&gt;K3: 재귀 3 ((a+b)(c+d))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    K3--&gt;&gt;K: 6164&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    K-&gt;&gt;K: z₁ = 6164 - 672 - 2652 = 2840&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    K-&gt;&gt;K: 조합: 672·10000 + 2840·100 + 2652&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    K--&gt;&gt;Caller: 7006652&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;왜-3-곱셈-이-2-덧셈-보다-가치-있는가&quot;&gt;왜 &lt;em&gt;3 곱셈&lt;/em&gt; 이 &lt;em&gt;2 덧셈&lt;/em&gt; 보다 가치 있는가?&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TB&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Tradeoff[1번의 곱셈 = 매우 비쌈&amp;#x3C;br/&gt;1번의 덧셈/뺄셈 = 매우 쌈]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Naive[Naive: 4 곱셈 + 3 덧셈]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Karatsuba[Karatsuba: 3 곱셈 + 4 덧셈/뺄셈]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Tradeoff --&gt; Win[곱셈 1개 절감 &gt;&gt; 덧셈 1개 추가 비용]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Naive --&gt; Win&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Karatsuba --&gt; Win&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;n자리 곱셈 = &lt;em&gt;O(n²)&lt;/em&gt;. n자리 덧셈 = &lt;em&gt;O(n)&lt;/em&gt;. &lt;em&gt;큰 n&lt;/em&gt; 에서:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;4M(n/2) + 3A(n/2)   →  4 · (n/2)² + 3 · (n/2)   ≈  n²&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;3M(n/2) + 4A(n/2)   →  3 · (n/2)² + 4 · (n/2)   ≈  0.75n²&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;비율 0.75, 한 단계당. 재귀 깊이 log₂n 곱하면 → n^1.585&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;recursion-깊이의-직관&quot;&gt;Recursion 깊이의 직관&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;재귀 호출 = 콜 스택 누적. Karatsuba 의 &lt;em&gt;재귀 깊이 = log₂n&lt;/em&gt;. &lt;em&gt;n = 10⁶&lt;/em&gt; 면 &lt;em&gt;20단계&lt;/em&gt;. 작음.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;실전-언제-karatsuba&quot;&gt;실전: 언제 Karatsuba?&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart TD&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1{n이 얼마나 큼?}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|&quot;n &amp;#x3C; 100자리&quot;| Naive[Naive O(n²) 권장&amp;#x3C;br/&gt;(상수 작아 더 빠름)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|&quot;100 &amp;#x3C; n &amp;#x3C; 10K&quot;| Karat[Karatsuba]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|&quot;10K &amp;#x3C; n &amp;#x3C; 100K&quot;| Toom[Toom-Cook 3, 4]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|&quot;n &gt; 100K&quot;| FFT[Schönhage-Strassen&amp;#x3C;br/&gt;또는 FFT]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Q1 --&gt;|&quot;천만 자리 이상&quot;| HH[Harvey-Hoeven&amp;#x3C;br/&gt;O(n log n) (2019)]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;알고리즘&lt;/th&gt;&lt;th&gt;시간&lt;/th&gt;&lt;th&gt;사용 임계 (GMP)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;학교 곱셈&lt;/td&gt;&lt;td&gt;O(n²)&lt;/td&gt;&lt;td&gt;n &amp;#x3C; ~80 (limb)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Karatsuba&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;O(n^1.585)&lt;/td&gt;&lt;td&gt;~80 ≤ n &amp;#x3C; ~450&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Toom-Cook 3-way&lt;/td&gt;&lt;td&gt;O(n^1.465)&lt;/td&gt;&lt;td&gt;~450 ≤ n &amp;#x3C; ~1500&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Toom-Cook 4, 5, 6-way&lt;/td&gt;&lt;td&gt;약간 더 빠름&lt;/td&gt;&lt;td&gt;다음 단계&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Schönhage-Strassen&lt;/td&gt;&lt;td&gt;O(n log n log log n)&lt;/td&gt;&lt;td&gt;n &gt; ~3000&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Harvey-Hoeven (2019)&lt;/td&gt;&lt;td&gt;O(n log n)&lt;/td&gt;&lt;td&gt;&lt;em&gt;이론&lt;/em&gt; (실용성 아직)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;[!IMPORTANT]
&lt;em&gt;작은 n 에서는 Naive 가 더 빠르다&lt;/em&gt;. Karatsuba 의 &lt;em&gt;상수가 큼&lt;/em&gt; (재귀 + 덧셈/뺄셈 + 메모리 할당). &lt;em&gt;GMP&lt;/em&gt; 같은 라이브러리는 &lt;em&gt;자동으로 임계값 결정&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;자주-보는-함정&quot;&gt;자주 보는 함정&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;(a+b)(c+d)&lt;/code&gt; 의 자리수 증가&lt;/strong&gt; = &lt;em&gt;m+1 자리&lt;/em&gt; 가 될 수 있다 (캐리). 메모리 / 재귀 base case 처리 주의.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Base case 너무 작게&lt;/strong&gt; = 재귀 오버헤드 폭증. n &amp;#x3C; 32 같은 &lt;em&gt;적절한 임계&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;음수 부호 처리&lt;/strong&gt; = &lt;code&gt;a+b&lt;/code&gt;, &lt;code&gt;c+d&lt;/code&gt; 모두 양수면 OK. 부호 있는 정수면 &lt;em&gt;별도 처리&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;자리수 균등 분할&lt;/strong&gt; = &lt;code&gt;m = ⌈n/2⌉&lt;/code&gt; 와 &lt;code&gt;n - m&lt;/code&gt; 비균등 분할. 큰 쪽의 자리수에 맞춤.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;활용-영역&quot;&gt;활용 영역&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;flowchart LR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Use[Karatsuba 활용]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Use --&gt; RSA[&quot;RSA / ECC&amp;#x3C;br/&gt;(수천 자리 정수)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Use --&gt; Crypto[&quot;암호학 라이브러리&amp;#x3C;br/&gt;(OpenSSL, BoringSSL)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Use --&gt; BigInt[&quot;언어 BigInt&amp;#x3C;br/&gt;(Python int, JS BigInt, Java BigInteger)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Use --&gt; Pi[&quot;π 계산&amp;#x3C;br/&gt;(수십억 자리)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Use --&gt; Poly[&quot;다항식 곱셈&amp;#x3C;br/&gt;(같은 원리)&quot;]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Use --&gt; Matrix[&quot;Strassen 행렬 곱셈&amp;#x3C;br/&gt;(비슷한 아이디어)&quot;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Strassen 행렬 곱셈&lt;/em&gt; (1969) 도 &lt;em&gt;같은 패러다임&lt;/em&gt;: &lt;em&gt;8개 곱셈을 7개로 줄임&lt;/em&gt; → O(n^log₂7) ≈ O(n^2.807). &lt;em&gt;Karatsuba 의 후예&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;다항식-곱셈으로의-일반화&quot;&gt;다항식 곱셈으로의 일반화&lt;/h2&gt;
&lt;p&gt;Karatsuba 는 &lt;em&gt;다항식 곱셈&lt;/em&gt; 으로 자연 일반화:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;P(x) = a·x + b&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Q(x) = c·x + d&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;P(x)·Q(x) = ac·x² + (ad+bc)·x + bd&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;정수 곱셈은 &lt;em&gt;x = B (진수) 인 다항식 곱셈의 평가&lt;/em&gt;. 다항식 곱셈을 빠르게 풀면 &lt;em&gt;정수 곱셈도 빨라진다&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;FFT (Fast Fourier Transform)&lt;/em&gt; 도 &lt;em&gt;다항식 곱셈&lt;/em&gt; 의 분할 정복. &lt;em&gt;Karatsuba 의 점근적 후계자&lt;/em&gt;. 자세한 건 [[fft-ntt]].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;역사적-의의&quot;&gt;역사적 의의&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;mermaid&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;gantt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    title 곱셈 알고리즘의 진화&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    dateFormat YYYY&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    axisFormat %Y&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    section 추측의 시대&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Kolmogorov: 곱셈은 Ω(n²) (1956) :milestone, k1, 1956, 1d&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    section Karatsuba&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Karatsuba: O(n^1.585) (1960 발견, 1962 출판) :a1, 1960, 2y&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    section 가속&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Toom-Cook (1963)         :a2, 1963, 1y&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Schönhage-Strassen (1971) :a3, 1971, 1y&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Fürer (2007 O(n log n 2^O(log* n))) :a4, 2007, 1y&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    section 최종&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#E1E4E8&quot;&gt;    Harvey-Hoeven O(n log n) (2019) :milestone, hh, 2019, 1d&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;1960년 Moscow State University 의 &lt;em&gt;세미나에서 Kolmogorov 가 추측 발표 → Karatsuba 가 1주일 안에 반박 해법 제시&lt;/em&gt;. Kolmogorov 가 &lt;em&gt;발표는 본인 이름이 아니라 Karatsuba 의 이름으로&lt;/em&gt; 진행. 학문적 &lt;em&gt;겸손의 표본&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;관련-위키&quot;&gt;관련 위키&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Divide and Conquer]] (상위 패러다임)&lt;/li&gt;
&lt;li&gt;[[arbitrary-precision]] (다정밀 정수)&lt;/li&gt;
&lt;li&gt;[[fft-ntt]] (더 빠른 곱셈)&lt;/li&gt;
&lt;li&gt;[[exponentiation-by-squaring]] (같은 패러다임의 거듭제곱)&lt;/li&gt;
&lt;li&gt;[[Merge Sort]], [[Quick Sort]] (분할 정복 동족)&lt;/li&gt;
&lt;li&gt;[[recursion]] (재귀의 일반)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;공식: &lt;a href=&quot;https://www.mathnet.ru/eng/dan26729&quot;&gt;Karatsuba 1962 (원논문)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GMP threshold: &lt;a href=&quot;https://gmplib.org/manual/Multiplication-Algorithms.html&quot;&gt;Multiplication Algorithms&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;2019 Harvey-Hoeven: &lt;a href=&quot;https://hal.science/hal-02070778&quot;&gt;Integer multiplication in time O(n log n)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;CLRS &lt;em&gt;Introduction to Algorithms&lt;/em&gt;, “Divide and Conquer” 챕터&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>divide-and-conquer</category><category>multiplication</category><category>big-integer</category><category>math</category><author>koa (김신건)</author></item><item><title>비트셋 LCS (Bitset LCS)</title><link>https://shinkeonkim.com/wiki/algorithm/dp/bitset-lcs/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/dp/bitset-lcs/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Bitset LCS&lt;/strong&gt; 는 두 문자열 A, B 의 최장 공통 부분 수열 (LCS) 길이를 &lt;strong&gt;O(NM/w)&lt;/strong&gt; 시간에 구하는 기법. 표준 LCS DP 의 O(NM) 를 &lt;strong&gt;비트 병렬 (bit-parallel)&lt;/strong&gt; 로 w=64 배 가속.&lt;/p&gt;
&lt;p&gt;w = CPU word size (64). &lt;code&gt;std::bitset&lt;/code&gt; 또는 64-bit 정수로 DP 행을 표현해, 한 번에 64 개 DP 전이를 처리.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;표준 LCS DP 는 O(NM) 시간. N = M = 10^5 이면 10^10 연산으로 불가.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;naive DP&lt;/strong&gt;: 2차원 테이블 채우기. O(NM), N=M=10^4 에서 10^8 로 간당간당.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;bitset LCS&lt;/strong&gt;: PyPy 에서 bitset 을 정수로 구현, O(NM/64). N=M=10^5 도 1.5 초 내.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;LCS 점화식의 &lt;code&gt;if A[i-1] == B[j-1]&lt;/code&gt; 부분은 &lt;strong&gt;비트 AND + shift&lt;/strong&gt; 로 한 번에 처리. &lt;code&gt;max(prev, curr-1)&lt;/code&gt; 은 &lt;strong&gt;비트 OR + mask&lt;/strong&gt; 로 표현.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;allison-dix-공식&quot;&gt;Allison-Dix 공식&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;S[i]&lt;/code&gt; = A[0..i] 와 B 의 전체 LCS 길이가 증가하는 위치 집합. 이를 비트셋 &lt;code&gt;M[i]&lt;/code&gt; 로 관리.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// 각 문자 c 에 대한 비트마스크 prec[c] = B 에서 c 의 위치들&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// DP 는 bitset 연산으로:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;for i in 1..N:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    x = prec[A[i-1]] | M[i-1]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    M[i] = M[i-1] | (x - ((x - M[i-1]) &amp;#x26; ~x)의 MSB 조작)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;실제 구현은 더 간단:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;u = M[i-1] | prec[A[i-1]]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;v = M[i-1] &amp;#x3C;&amp;#x3C; 1 | 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;M[i] = M[i-1] | ((u - v) &amp;#x26; ~u)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;결과 LCS 길이 = &lt;code&gt;popcount(M[N])&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;bitset_lcs(A, B):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    N = len(A), M = len(B)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    prec[0..MAX_CHAR] = 0  // 각 문자의 B 내 위치 비트셋&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for j in 0..M-1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        prec[B[j]] |= (1 &amp;#x3C;&amp;#x3C; j)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    bitset cur = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for i in 0..N-1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        u = cur | prec[A[i]]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        v = (cur &amp;#x3C;&amp;#x3C; 1) | 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        cur |= ((u - v) &amp;#x26; ~u)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return popcount(cur)  // LCS 길이&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;핵심 트릭: &lt;code&gt;(u - v) &amp;#x26; ~u&lt;/code&gt; 는 &lt;code&gt;u&lt;/code&gt; 와 &lt;code&gt;v&lt;/code&gt; 의 LCS 전이를 한 연산에 계산. 자세한 증명은 Allison-Dix (1986) 참고.&lt;/p&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: `// Bitset LCS O(NM/64), Hyyro/Allison-Dix
#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
using ull = unsigned long long;&lt;/p&gt;
&lt;p&gt;int lcs_bitset(const string&amp;#x26; a, const string&amp;#x26; b) {
int n = a.size(), m = b.size();
vector&lt;ull&gt; prec(26, 0);
for (int j = 0; j &amp;#x3C; m; j++)
prec[b[j]-‘A’] |= (1ULL &amp;#x3C;&amp;#x3C; j);&lt;/ull&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;ull cur = 0;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;for (int i = 0; i &amp;#x3C; n; i++) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    ull u = cur | prec[a[i]-&apos;A&apos;];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    ull v = (cur &amp;#x3C;&amp;#x3C; 1) | 1;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    cur |= ( (u - v) &amp;#x26; ~u );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;return __builtin_popcountll(cur);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;int main() {
string a, b;
cin &gt;&gt; a &gt;&gt; b;
cout &amp;#x3C;&amp;#x3C; lcs_bitset(a, b) &amp;#x3C;&amp;#x3C; “\n”;
}&lt;code&gt;,     },     {       language: &quot;python&quot;,       label: &quot;Python&quot;,       code: &lt;/code&gt;# Bitset LCS using Python int as bitset
def lcs_bitset(a: str, b: str) -&gt; int:
n, m = len(a), len(b)
# prec[char] = bitmask of positions in b
prec = [0] * 26
for j, ch in enumerate(b):
prec[ord(ch) - 65] |= 1 &amp;#x3C;&amp;#x3C; j&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;cur = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;for i in range(n):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    u = cur | prec[ord(a[i]) - 65]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    v = (cur &amp;#x3C;&amp;#x3C; 1) | 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    cur |= ( (u - v) &amp;#x26; ~u )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;return cur.bit_count()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;a = input().strip()
b = input().strip()
print(lcs_bitset(a, b))&lt;code&gt;,     },     {       language: &quot;java&quot;,       label: &quot;Java&quot;,       code: &lt;/code&gt;// Bitset LCS - Java long[] bitset for longer strings&lt;/p&gt;
&lt;p&gt;public class Main {
static int lcsBitset(String a, String b) {
int n = a.length(), m = b.length();
long[] prec = new long[26];
for (int j = 0; j &amp;#x3C; m; j++)
prec[b.charAt(j)-‘A’] |= (1L &amp;#x3C;&amp;#x3C; j);&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    long cur = 0;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for (int i = 0; i &amp;#x3C; n; i++) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        long u = cur | prec[a.charAt(i)-&apos;A&apos;];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        long v = (cur &amp;#x3C;&amp;#x3C; 1) | 1;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        cur |= ( (u - v) &amp;#x26; ~u );&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return Long.bitCount(cur);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;public static void main(String[] args) throws IOException {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    String a = br.readLine();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    String b = br.readLine();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    System.out.println(lcsBitset(a, b));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;기본 LCS&quot;,       input: &lt;/code&gt;ABCBDAB
BDCAB&lt;code&gt;,       output: &lt;/code&gt;4&lt;code&gt;,     },     {       label: &quot;긴 문자열 (알파벳 26 종)&quot;,       input: &lt;/code&gt;ABCDEFGHIJKLMNOPQRSTUVWXYZ
ZYXWVUTSRQPONMLKJIHGFEDCBA&lt;code&gt;,       output: &lt;/code&gt;1&lt;code&gt;,     },     {       label: &quot;같은 문자열&quot;,       input: &lt;/code&gt;HELLOWORLD
HELLOWORLD&lt;code&gt;,       output: &lt;/code&gt;10`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (최선)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(NM/w)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (평균)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(NM/w)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (최악)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(NM/w)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(Σ + M/w) (알파벳 개수 + 결과 bitset)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;w&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;CPU word size (64)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;표준 DP O(NM) 대비 약 64 배 빠름. N=M=10^5 에서 10^10 -&gt; 1.5x10^8 연산으로 실용적.&lt;/p&gt;
&lt;h2 id=&quot;변형&quot;&gt;변형&lt;/h2&gt;
&lt;h3 id=&quot;w-bit-한계-돌파&quot;&gt;w-bit 한계 돌파&lt;/h3&gt;
&lt;p&gt;M &gt; 64 이면 &lt;code&gt;unsigned long long[]&lt;/code&gt; 배열로 bitset 확장. &lt;code&gt;(256-bit, 512-bit)&lt;/code&gt; 벡터 명령어 (SIMD) 로 더 가속 가능.&lt;/p&gt;
&lt;h3 id=&quot;longest-common-substring&quot;&gt;Longest Common Substring&lt;/h3&gt;
&lt;p&gt;bitset 기법은 substring (연속) 에도 적용 가능. shift + AND 로 공통 부분 문자열 탐색.&lt;/p&gt;
&lt;h3 id=&quot;edit-distance&quot;&gt;Edit Distance&lt;/h3&gt;
&lt;p&gt;비슷한 bit-parallel 기법 (&lt;code&gt;myers_diff&lt;/code&gt;) 으로 edit distance O(NM/w) 계산.&lt;/p&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-알파벳-크기&quot;&gt;1. 알파벳 크기&lt;/h3&gt;
&lt;p&gt;prec 배열은 알파벳 (또는 문자 집합) 만큼. 대문자 26 개면 충분하지만, 아스키 전체면 더 큰 배열 필요.&lt;/p&gt;
&lt;h3 id=&quot;2-shift-overflow&quot;&gt;2. shift overflow&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;cur &amp;#x3C;&amp;#x3C; 1&lt;/code&gt; 이 64 비트 넘으면 상위 비트 손실. M=64 이면 &lt;code&gt;(cur &amp;#x3C;&amp;#x3C; 1)&lt;/code&gt; 에서 MSB 가 carry out 되지 않도록 M &amp;#x3C; 64 로 제한하거나 bitset 배열 사용.&lt;/p&gt;
&lt;h3 id=&quot;3-traceback-불가&quot;&gt;3. traceback 불가&lt;/h3&gt;
&lt;p&gt;bitset LCS 는 길이만 O(1) 에 구함. 수열 복원 필요시 [[Hirschberg|Hirschberg]] 또는 표준 DP traceback.&lt;/p&gt;
&lt;h3 id=&quot;4-u---v--u-의미&quot;&gt;4. &lt;code&gt;(u - v) &amp;#x26; ~u&lt;/code&gt; 의미&lt;/h3&gt;
&lt;p&gt;이 수식이 한 번에 LCS 전이를 표현함. 직관적이지 않으므로 필요시 논문 참고. 틀리면 계산이 훨씬 더 빨라지거나 (의미 없는 값) 느려짐.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 9251&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;LCS&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/9200-9299/9251&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 9252&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;LCS 2 (traceback)&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/9200-9299/9252&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 1958&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;LCS 3&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1900-1999/1958&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[LCS|최장 공통 부분 수열 (LCS)]]&lt;/li&gt;
&lt;li&gt;[[Hirschberg|Hirschberg (O(N) 메모리 LCS)]]&lt;/li&gt;
&lt;li&gt;[[Bitset Optimization|비트셋 최적화]]&lt;/li&gt;
&lt;li&gt;Allison &amp;#x26; Dix, A bit-string longest-common-subsequence algorithm (1986)&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>dp</category><category>lcs</category><category>string</category><category>bitset-optimization</category><author>koa (김신건)</author></item><item><title>덱 활용 구간 최댓값 트릭 (Monotonic Deque Trick)</title><link>https://shinkeonkim.com/wiki/algorithm/dp/deque-trick/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/dp/deque-trick/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;덱 트릭 (Deque Trick)&lt;/strong&gt; 은 덱 (double-ended queue) 에 원소의 인덱스를 &lt;strong&gt;단조로운 값 순서&lt;/strong&gt; 로 유지해, 슬라이딩 윈도우의 &lt;strong&gt;최댓값 또는 최솟값을 O(N)&lt;/strong&gt; 에 구하거나, DP 전이에서 &lt;strong&gt;일정 범위의 후보 최적값을 O(1) amortized&lt;/strong&gt; 로 찾는 최적화 기법.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;h3 id=&quot;슬라이딩-윈도우-최댓값&quot;&gt;슬라이딩 윈도우 최댓값&lt;/h3&gt;
&lt;p&gt;크기 K 인 슬라이딩 윈도우가 배열을 지나갈 때, 각 위치에서의 최댓값을 Q 번 묻는다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;naive&lt;/strong&gt;: 매 윈도우마다 K 개 순회. O(NK). N=10^6, K=10^5 면 10^11.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;deque trick&lt;/strong&gt;: 각 원소가 덱에 push/pop 되는 횟수 합 = O(N). 전체 O(N).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;새로 들어온 값보다 작은 (최댓값 기준) 값들은 앞으로 절대 최댓값이 될 수 없으므로 덱에서 제거해도 안전하다.&lt;/em&gt; 윈도우를 벗어난 값도 제거한다. 이렇게 하면 덱은 항상 &lt;strong&gt;내림차순 (최댓값 기준)&lt;/strong&gt; 이 유지된다.&lt;/p&gt;
&lt;h3 id=&quot;dp-최적화&quot;&gt;DP 최적화&lt;/h3&gt;
&lt;p&gt;다음 형태의 DP 에서 사용한다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;dp[i] = max / min (dp[j] + cost(j, i))   (i - K &amp;#x3C;= j &amp;#x3C; i)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 &lt;code&gt;cost(j, i)&lt;/code&gt; 가 &lt;strong&gt;j 만의 함수 + i 만의 함수&lt;/strong&gt; 로 분리 가능하고 (&lt;code&gt;cost(j, i) = A[j] + B[i]&lt;/code&gt;), 슬라이딩 윈도우 제약이 있을 때, &lt;code&gt;dp[j] + A[j]&lt;/code&gt; 를 단조 감소/증가 덱으로 관리하면 O(1) 에 최적 후보를 찾는다.&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;p&gt;덱은 항상 &lt;strong&gt;값에 대해 단조&lt;/strong&gt; (최댓값이면 내림차순, 최솟값이면 오름차순) 이다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;슬라이딩 윈도우 최댓값&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  deque: [인덱스 ...]  (값 a[index] 는 내림차순)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  invariant:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    - 덱 앞: 현재 윈도우에서 가장 큰 값의 인덱스&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    - 덱 뒤: 방금 들어온 값의 인덱스 (가장 작은 값)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  push(i):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    1. dq.front() &amp;#x3C;= i - K -&gt; dq.pop_front()     # 윈도우 이탈&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    2. a[dq.back()] &amp;#x3C;= a[i] -&gt; dq.pop_back()      # 불필요한 후보 제거&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    3. dq.push_back(i)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  query_max(): return a[dq.front()]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;amortized O(N) 인 이유: 각 원소는 정확히 한 번 push, 최대 한 번 pop_front, 최대 한 번 pop_back. 총 연산 = O(N).&lt;/p&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;SlidingWindowMax(a[0..N-1], K):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    dq = empty deque&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    result = []&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for i = 0..N-1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        while dq and dq[0] &amp;#x3C;= i - K:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            dq.pop_front()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        while dq and a[dq[-1]] &amp;#x3C;= a[i]:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            dq.pop_back()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        dq.append(i)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if i &gt;= K - 1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            result.append(a[dq[0]])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return result&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// Sliding window maximum, O(N) #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; int main() {     ios::sync_with_stdio(false);     cin.tie(nullptr);     int n, k; cin &gt;&gt; n &gt;&gt; k;     vector&amp;#x3C;int&gt; a(n);     for (auto&amp;#x26; v : a) cin &gt;&gt; v;     deque&amp;#x3C;int&gt; dq;     for (int i = 0; i &amp;#x3C; n; i++) {         while (!dq.empty() &amp;#x26;&amp;#x26; dq.front() &amp;#x3C;= i - k)             dq.pop_front();         while (!dq.empty() &amp;#x26;&amp;#x26; a[dq.back()] &amp;#x3C;= a[i])             dq.pop_back();         dq.push_back(i);         if (i &gt;= k - 1)             cout &amp;#x3C;&amp;#x3C; a[dq.front()] &amp;#x3C;&amp;#x3C; &apos; &apos;;     } }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: `from collections import deque
import sys
input = sys.stdin.readline&lt;/p&gt;
&lt;p&gt;n, k = map(int, input().split())
a = list(map(int, input().split()))
dq = deque()
out = []
for i, v in enumerate(a):
while dq and dq[0] &amp;#x3C;= i - k:
dq.popleft()
while dq and a[dq[-1]] &amp;#x3C;= v:
dq.pop()
dq.append(i)
if i &gt;= k - 1:
out.append(str(a[dq[0]]))
sys.stdout.write(’ ‘.join(out))&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;기본&quot;,       input: &lt;/code&gt;8 3
2 5 4 1 8 3 7 6&lt;code&gt;,       output: &lt;/code&gt;5 5 8 8 8 7&lt;code&gt;,     },     {       label: &quot;내림차순 배열&quot;,       input: &lt;/code&gt;6 3
9 8 7 6 5 4&lt;code&gt;,       output: &lt;/code&gt;9 8 7 6 5`,
},
]}
/&gt;&lt;/p&gt;
&lt;h3 id=&quot;dp-최적화-예시&quot;&gt;DP 최적화 예시&lt;/h3&gt;
&lt;p&gt;dp[i] = max(dp[i-1], max_{j in [i-K, i-1]} (dp[j] + a[i])) 형태에서, 덱에 &lt;code&gt;dp[j]&lt;/code&gt; 를 단조 감소로 유지하며 O(N).&lt;/p&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
language=“cpp”
code={&lt;code&gt;// dp[i] = max(dp[i-1], max_j (dp[j] + penalty) for j in [i-K, i-1]) // Deque trick O(N) instead of O(NK) #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; int main() {     ios::sync_with_stdio(false);     cin.tie(nullptr);     int n, k; cin &gt;&gt; n &gt;&gt; k;     vector&amp;#x3C;long long&gt; a(n), dp(n);     for (auto&amp;#x26; v : a) cin &gt;&gt; v;     deque&amp;#x3C;int&gt; dq;     for (int i = 0; i &amp;#x3C; n; i++) {         while (!dq.empty() &amp;#x26;&amp;#x26; dq.front() &amp;#x3C; i - k)             dq.pop_front();         if (i == 0) dp[i] = a[i];         else dp[i] = max(dp[i-1], (dq.empty() ? a[i] : dp[dq.front()] + a[i]));         while (!dq.empty() &amp;#x26;&amp;#x26; dp[dq.back()] &amp;#x3C;= dp[i])             dq.pop_back();         dq.push_back(i);     }     cout &amp;#x3C;&amp;#x3C; dp[n-1] &amp;#x3C;&amp;#x3C; &apos;\\n&apos;; }&lt;/code&gt;}
input={&lt;code&gt;5 2 -1 -2 3 -4 5&lt;/code&gt;}
output={&lt;code&gt;5&lt;/code&gt;}
inputLanguage=“text”
outputLanguage=“text”
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;슬라이딩 윈도우 (시간)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N), amortized 1 회당 O(1)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;DP 최적화 (시간)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N) 또는 O(N log N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N) (덱 + 결과)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;자료구조&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;덱 (deque)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;응용&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;설명&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;슬라이딩 윈도우 최솟값&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;부등호만 반대로 (&lt;code&gt;&gt;=&lt;/code&gt;, &lt;code&gt;&gt;=&lt;/code&gt;)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Monotonic Stack&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;덱 대신 스택. 윈도우가 아니라 이전/다음 큰 원소 찾기. 같은 단조성 아이디어&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;DP + Deque&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;dp[i] = max(dp[j] + f(j) + g(i)) 꼴. g(i) 가 전이 밖으로 분리 가능해야 함&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;2Pointer + Deque&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;투 포인터와 결합해 조건 변화에 따른 구간의 extrema 관리&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-부호-방향-실수&quot;&gt;1. 부호 방향 실수&lt;/h3&gt;
&lt;p&gt;최댓값을 구할 때는 &lt;code&gt;a[dq.back()] &amp;#x3C;= a[i]&lt;/code&gt; 로 작거나 같은 값 제거. 최솟값은 &lt;code&gt;&gt;=&lt;/code&gt;. 방향 하나 틀리면 덱이 비어 있거나 결과가 틀린다.&lt;/p&gt;
&lt;h3 id=&quot;2-인덱스-vs-값-헷갈림&quot;&gt;2. 인덱스 vs 값 헷갈림&lt;/h3&gt;
&lt;p&gt;덱에는 인덱스를 저장해야 한다 (윈도우 이탈 판단에 필요). 값을 저장하면 pop_front 조건을 검사할 수 없다.&lt;/p&gt;
&lt;h3 id=&quot;3-k-가-1-또는-n-일-때&quot;&gt;3. K 가 1 또는 N 일 때&lt;/h3&gt;
&lt;p&gt;K=1 이면 항상 &lt;code&gt;a[i]&lt;/code&gt; 자체, K=N 이면 최댓값 하나만. 특수 케이스 처리가 필요 없지만 시간 복잡도는 그대로 O(N).&lt;/p&gt;
&lt;h3 id=&quot;4-dp-에서-cost-분리-불가&quot;&gt;4. DP 에서 cost 분리 불가&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;cost(j, i)&lt;/code&gt; 가 &lt;code&gt;A[j] + B[i]&lt;/code&gt; 로 분리되지 않으면 (예: cost = (x_i - x_j)^2) deque trick 적용 불가. 그 경우 [[Convex Hull Trick]] 또는 [[Divide and Conquer Optimization|분할정복 DP 최적화]] 고려.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 11003&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;최솟값 찾기&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/11000-11099/11003&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 17298&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;오큰수&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/17200-17299/17298&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2096&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;내려가기&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2000-2099/2096&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 1576&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1500-1599/1576&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Convex Hull Trick]]&lt;/li&gt;
&lt;li&gt;[[Divide and Conquer Optimization|분할정복 DP 최적화]]&lt;/li&gt;
&lt;li&gt;[[Slope Trick]]&lt;/li&gt;
&lt;li&gt;[[Knuth Optimization|크누스 최적화]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>dp</category><category>optimization</category><category>deque</category><category>data-structures</category><author>koa (김신건)</author></item><item><title>분할정복 DP 최적화 (Divide and Conquer Optimization)</title><link>https://shinkeonkim.com/wiki/algorithm/dp/divide-and-conquer-optimization/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/dp/divide-and-conquer-optimization/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;분할정복 DP 최적화 (Divide and Conquer Optimization)&lt;/strong&gt; 는 &lt;code&gt;dp[i][j] = min(dp[i-1][k] + C[k+1][j])&lt;/code&gt; 꼴의 DP 에서, &lt;strong&gt;최적 분할점 opt[i][j]&lt;/strong&gt; 가 j 에 대해 &lt;strong&gt;단조 비감소&lt;/strong&gt; (&lt;code&gt;opt[i][j] &amp;#x3C;= opt[i][j+1]&lt;/code&gt;) 일 때, 분할정복으로 k 탐색 범위를 제한해 시간 복잡도를 O(K N^2) 에서 &lt;strong&gt;O(K N log N)&lt;/strong&gt; 으로 줄이는 기법.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;h3 id=&quot;왜-필요한가&quot;&gt;왜 필요한가&lt;/h3&gt;
&lt;p&gt;N 개의 원소를 K 개의 그룹으로 나누는 DP:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;dp[k][i] = min (dp[k-1][j] + cost(j+1, i))   (0 &amp;#x3C;= j &amp;#x3C; i)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;naive&lt;/strong&gt;: 각 dp[k][i] 마다 모든 j 검사. O(K * N^2). N=10^5, K=10 이면 10^11.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;D&amp;#x26;C 최적화&lt;/strong&gt;: opt[i] 단조성을 이용, O(K * N log N). 10^5 * log(10^5) * 10 ≈ 2*10^7, &lt;strong&gt;통과&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;j 의 후보 범위가 단조적이면, 분할정복으로 각 레이어를 O(N log N) 에 채울 수 있다.&lt;/em&gt; 전체 문제를 mid 에서의 최적 j 를 찾은 후, 좌/우 반쪽의 j 후보 범위를 각각 [optL, bestK] 와 [bestK, optR] 로 좁혀 재귀한다.&lt;/p&gt;
&lt;h3 id=&quot;전제-조건&quot;&gt;전제 조건&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;cost(l, r)&lt;/code&gt; 가 다음 &lt;strong&gt;사각 부등식 (quadrangle inequality)&lt;/strong&gt; 을 만족하면 opt[i] 가 단조 비감소한다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;cost(a, c) + cost(b, d) &amp;#x3C;= cost(a, d) + cost(b, c)   (a &amp;#x3C;= b &amp;#x3C;= c &amp;#x3C;= d)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;p&gt;Opt[i] 가 단조 비감소 (&lt;code&gt;opt[i] &amp;#x3C;= opt[i+1]&lt;/code&gt;) 라면, j 후보 범위를 재귀적으로 좁혀도 안전하다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;dc(k, l, r, optL, optR):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if l &gt; r: return&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    mid = (l + r) / 2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    bestK = optL&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    bestVal = INF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for j = optL .. min(mid-1, optR):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        val = dp[k-1][j] + cost(j+1, mid)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if val &amp;#x3C; bestVal:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            bestVal = val&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            bestK = j&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    dp[k][mid] = bestVal&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    dc(k, l, mid-1, optL, bestK)      # 왼쪽: j 범위 [optL, bestK]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    dc(k, mid+1, r, bestK, optR)      # 오른쪽: j 범위 [bestK, optR]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;각 레이어 k 에서 모든 호출의 j 순회 합 = O(N log N). (N 개 원소 각각이 log N 번 방문)&lt;/p&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;DnCDP(a[1..N], K):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    dp[0][0] = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for i = 1..N:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        dp[1][i] = cost(1, i)     # 1개 그룹&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for k = 2..K:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        solve(k, 1, N, 0, N)      # 분할정복&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return dp[K][N]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;solve(k, l, r, optL, optR):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if l &gt; r: return&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    mid = (l + r) / 2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    bestK = optL&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    bestVal = INF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for j = optL .. min(mid-1, optR):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        val = dp[k-1][j] + cost(j+1, mid)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if val &amp;#x3C; bestVal:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            bestVal = val&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            bestK = j&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    dp[k][mid] = bestVal&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    solve(k, l, mid-1, optL, bestK)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    solve(k, mid+1, r, bestK, optR)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;다음은 배열을 K 개 그룹으로 나누고 각 그룹 합의 제곱을 최소화하는 문제. cost(l, r) = (prefix[r] - prefix[l-1])^2.&lt;/p&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: `// D&amp;#x26;C Optimization: O(K N log N), cost = (sum)^2
#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
using ll = long long;
const ll INF = 1e18;&lt;/p&gt;
&lt;p&gt;int N, K;
vector&lt;ll&gt; a, ps;
vector&amp;#x3C;vector&lt;ll&gt;&gt; dp;&lt;/ll&gt;&lt;/ll&gt;&lt;/p&gt;
&lt;p&gt;ll cost(int l, int r) {
ll s = ps[r] - ps[l - 1];
return s * s;
}&lt;/p&gt;
&lt;p&gt;void dac(int k, int l, int r, int optL, int optR) {
if (l &gt; r) return;
int mid = (l + r) / 2;
int bestK = optL;
ll bestVal = INF;
for (int j = optL; j &amp;#x3C;= min(mid - 1, optR); j++) {
ll val = dp[k - 1][j] + cost(j + 1, mid);
if (val &amp;#x3C; bestVal) {
bestVal = val;
bestK = j;
}
}
dp[k][mid] = bestVal;
dac(k, l, mid - 1, optL, bestK);
dac(k, mid + 1, r, bestK, optR);
}&lt;/p&gt;
&lt;p&gt;int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin &gt;&gt; N &gt;&gt; K;
a.resize(N + 1);
ps.resize(N + 1);
for (int i = 1; i &amp;#x3C;= N; i++) {
cin &gt;&gt; a[i];
ps[i] = ps[i - 1] + a[i];
}
dp.assign(K + 1, vector&lt;ll&gt;(N + 1, INF));
dp[0][0] = 0;
for (int i = 1; i &amp;#x3C;= N; i++)
dp[1][i] = cost(1, i);
for (int k = 2; k &amp;#x3C;= K; k++)
dac(k, 1, N, 0, N);
cout &amp;#x3C;&amp;#x3C; dp[K][N] &amp;#x3C;&amp;#x3C; ‘\n’;
}&lt;code&gt;,     },     {       language: &quot;python&quot;,       label: &quot;Python&quot;,       code: &lt;/code&gt;import sys
sys.setrecursionlimit(1 &amp;#x3C;&amp;#x3C; 25)
input = sys.stdin.readline
INF = 10 ** 18&lt;/ll&gt;&lt;/p&gt;
&lt;p&gt;N, K = map(int, input().split())
a = [0] + list(map(int, input().split()))
ps = [0] * (N + 1)
for i in range(1, N + 1):
ps[i] = ps[i - 1] + a[i]&lt;/p&gt;
&lt;p&gt;def cost(l, r):
s = ps[r] - ps[l - 1]
return s * s&lt;/p&gt;
&lt;p&gt;dp = [[INF] * (N + 1) for _ in range(K + 1)]
dp[0][0] = 0&lt;/p&gt;
&lt;p&gt;def dac(k, l, r, optL, optR):
if l &gt; r:
return
mid = (l + r) // 2
best_k = optL
best_val = INF
for j in range(optL, min(mid, optR + 1)):
val = dp[k - 1][j] + cost(j + 1, mid)
if val &amp;#x3C; best_val:
best_val = val
best_k = j
dp[k][mid] = best_val
dac(k, l, mid - 1, optL, best_k)
dac(k, mid + 1, r, best_k, optR)&lt;/p&gt;
&lt;p&gt;for i in range(1, N + 1):
dp[1][i] = cost(1, i)&lt;/p&gt;
&lt;p&gt;for k in range(2, K + 1):
dac(k, 1, N, 0, N)&lt;/p&gt;
&lt;p&gt;print(dp[K][N])&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;기본&quot;,       input: &lt;/code&gt;4 2
1 2 3 4&lt;code&gt;,       output: &lt;/code&gt;52&lt;code&gt;,     },     {       label: &quot;다섯 개 원소&quot;,       input: &lt;/code&gt;5 2
1 3 2 4 2&lt;code&gt;,       output: &lt;/code&gt;72`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (최선)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(K N log N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (평균)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(K N log N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (최악)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(K N^2) (단조성 깨질 때)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(K N) (dp 테이블)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;최적화 전&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(K N^2) -&gt; O(K N log N)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 id=&quot;amortized-분석&quot;&gt;amortized 분석&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;solve(k, 1, N, 0, N)&lt;/code&gt; 호출의 j 순회 횟수: 각 레벨마다 O(N) 합, 깊이 log N. 총 O(N log N). K 개 레이어 곱하면 O(K N log N).&lt;/p&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;변형&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;설명&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Knuth Optimization&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;구간 DP. opt[i][j-1] ≤ opt[i][j] ≤ opt[i+1][j]. O(N^3) -&gt; O(N^2)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;CHT (Convex Hull Trick)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;cost 가 직선의 기울기/절편 형태일 때. O(K N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Aliens Trick&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;K 제약을 lambda 로 풀기. D&amp;#x26;C 와 결합 가능&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;D&amp;#x26;C 최적화는 &lt;strong&gt;K 가 작을 때&lt;/strong&gt; (K ≤ N) 특히 효과적. K = sqrt(N) 정도까지는 O(N sqrt(N) log N) 으로 양호.&lt;/p&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-cost-가-o1-이-아닌-경우&quot;&gt;1. cost() 가 O(1) 이 아닌 경우&lt;/h3&gt;
&lt;p&gt;cost(l, r) 를 O(1) 에 계산할 수 있어야 한다. prefix sum 으로 충분한 cost 함수가 이상적. O(log N) 짜리 cost 면 전체 O(K N log^2 N).&lt;/p&gt;
&lt;h3 id=&quot;2-opt-단조성-증명-없이-사용&quot;&gt;2. opt 단조성 증명 없이 사용&lt;/h3&gt;
&lt;p&gt;사각 부등식을 만족하지 않는 cost 함수에 적용하면 답이 틀리거나 더 느려짐. cost = (prefix[r] - prefix[l-1])^2 는 만족하지만, cost = abs(prefix[r] - prefix[l-1]) 는 일반적으로 불만족.&lt;/p&gt;
&lt;h3 id=&quot;3-재귀-깊이&quot;&gt;3. 재귀 깊이&lt;/h3&gt;
&lt;p&gt;N=10^5 일 때 재귀 깊이는 log N ≈ 17. 안전하지만 스택 메모리 확인. Python 은 setrecursionlimit 필요.&lt;/p&gt;
&lt;h3 id=&quot;4-1-indexed-vs-0-indexed&quot;&gt;4. 1-indexed vs 0-indexed&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;dp[k-1][j] + cost(j+1, mid)&lt;/code&gt; 에서 j 는 마지막 그룹의 &lt;strong&gt;시작 직전&lt;/strong&gt; 인덱스. dp[0][0] = 0, dp[0][j&gt;0] = INF. 이 초기화를 빼먹으면 올바른 값이 계산되지 않는다.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 13261&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;탈출&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/13200-13299/13261&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 4008&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;특공대&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/4000-4099/4008&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 6171&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;땅따먹기&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/6100-6199/6171&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Knuth Optimization|크누스 최적화]]&lt;/li&gt;
&lt;li&gt;[[Convex Hull Trick]]&lt;/li&gt;
&lt;li&gt;[[Aliens Trick]]&lt;/li&gt;
&lt;li&gt;[[Deque Trick|덱 트릭]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>dp</category><category>optimization</category><category>divide-and-conquer</category><author>koa (김신건)</author></item><item><title>커넥션 프로파일 DP (Broken Profile DP)</title><link>https://shinkeonkim.com/wiki/algorithm/dp/dp-connection-profile/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/dp/dp-connection-profile/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;커넥션 프로파일 DP (Broken Profile DP)&lt;/strong&gt; 는 직사각형 격자를 tile-by-tile 로 처리하면서, &lt;em&gt;처리된 영역과 아직 처리되지 않은 영역의 경계&lt;/em&gt; (broken profile) 를 bitmask 와 같은 작은 상태로 압축해 DP 에 사용하는 기법. 도미노/트리미노 타일링, 격자 배치 문제에서 널리 쓰인다.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;H x W 격자를 1x2 / 2x1 도미노로 빈틈없이 덮는 경우의 수. Naive: exponential search 2^(HW). H ≤ 10, W ≤ 10 에도 HW 만 100, 완전탐색 불가.&lt;/p&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;셀 단위 스캔에서 경계면의 모양만 알면 앞으로 필요한 정보가 충분.&lt;/em&gt; W 가 작을 때 2^W 가지 mask 로 상태 표현 가능.&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;p&gt;격자 (r, c) 를 row-major 순서로 한 칸씩 처리. 경계선은 &lt;code&gt;W&lt;/code&gt; 개의 구역으로 나뉨.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Profile bitmask (W bits):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  bit j (j &gt;= c): cell (r, j) 가 이미 채워졌는가? (horizontal overflow)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  bit j (j &amp;#x3C; c):  cell (r+1, j) 가 아래에서 채워졌는가? (vertical overflow)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;DP transition at cell (r, c):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  if mask bit c == 1:   # 이미 채워짐 (왼쪽에서 horizontal)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;      clear bit c, move on&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  if mask bit c == 0:   # 빈 칸&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;      - place vertical    -&gt; set bit c (next row 의 이 칸이 채워짐)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;      - place horizontal  -&gt; set bit c+1 (같은 row 오른쪽 칸이 채워짐)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;셀이 채워진 후 해당 bit 는 “past” 섹션으로 넘어가 자동으로 “아래에서 덮인” 의미로 전환.&lt;/p&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;dp[mask] = 현재 위치까지 처리, profile = mask 일 때 경우의 수&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;initialize dp[0] = 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;for r = 0..H-1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for c = 0..W-1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        for mask = 0..(1&amp;#x3C;&amp;#x3C;W)-1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            if dp[mask] == 0: continue&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            if mask &amp;#x26; (1 &amp;#x3C;&amp;#x3C; c):   # already filled&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                ndp[mask ^ (1 &amp;#x3C;&amp;#x3C; c)] += dp[mask]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            else:                 # empty&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                ndp[mask | (1 &amp;#x3C;&amp;#x3C; c)] += dp[mask]           # vertical&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                if c+1 &amp;#x3C; W and !(mask &amp;#x26; (1 &amp;#x3C;&amp;#x3C; (c+1))):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                    ndp[mask | (1 &amp;#x3C;&amp;#x3C; (c+1))] += dp[mask]   # horizontal&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        dp = ndp&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;answer = dp[0]   # 마지막 row 끝난 후 mask = 0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// Domino tiling: H x W grid, 1x2 / 2x1 dominoes, MOD = 9901 #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; const int MOD = 9901; int main() {     int h, w; cin &gt;&gt; h &gt;&gt; w;     int sz = 1 &amp;#x3C;&amp;#x3C; w;     vector&amp;#x3C;int&gt; dp(sz, 0);     dp[0] = 1;     for (int r = 0; r &amp;#x3C; h; r++) {         for (int c = 0; c &amp;#x3C; w; c++) {             vector&amp;#x3C;int&gt; ndp(sz, 0);             for (int mask = 0; mask &amp;#x3C; sz; mask++) {                 int val = dp[mask];                 if (val == 0) continue;                 if (mask &amp;#x26; (1 &amp;#x3C;&amp;#x3C; c)) {                     ndp[mask ^ (1 &amp;#x3C;&amp;#x3C; c)] = (ndp[mask ^ (1 &amp;#x3C;&amp;#x3C; c)] + val) % MOD;                 } else {                     ndp[mask | (1 &amp;#x3C;&amp;#x3C; c)] = (ndp[mask | (1 &amp;#x3C;&amp;#x3C; c)] + val) % MOD;                     if (c + 1 &amp;#x3C; w &amp;#x26;&amp;#x26; !(mask &amp;#x26; (1 &amp;#x3C;&amp;#x3C; (c + 1)))) {                         ndp[mask | (1 &amp;#x3C;&amp;#x3C; (c + 1))] = (ndp[mask | (1 &amp;#x3C;&amp;#x3C; (c + 1))] + val) % MOD;                     }                 }             }             dp = move(ndp);         }     }     cout &amp;#x3C;&amp;#x3C; dp[0] &amp;#x3C;&amp;#x3C; &quot;\\n&quot;;     return 0; }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: &lt;code&gt;MOD = 9901 h, w = map(int, input().split()) sz = 1 &amp;#x3C;&amp;#x3C; w dp = [0] * sz dp[0] = 1 for r in range(h):     for c in range(w):         ndp = [0] * sz         for mask in range(sz):             val = dp[mask]             if val == 0:                 continue             if mask &amp;#x26; (1 &amp;#x3C;&amp;#x3C; c):                 ndp[mask ^ (1 &amp;#x3C;&amp;#x3C; c)] = (ndp[mask ^ (1 &amp;#x3C;&amp;#x3C; c)] + val) % MOD             else:                 ndp[mask | (1 &amp;#x3C;&amp;#x3C; c)] = (ndp[mask | (1 &amp;#x3C;&amp;#x3C; c)] + val) % MOD                 if c + 1 &amp;#x3C; w and not (mask &amp;#x26; (1 &amp;#x3C;&amp;#x3C; (c + 1))):                     ndp[mask | (1 &amp;#x3C;&amp;#x3C; (c + 1))] = (ndp[mask | (1 &amp;#x3C;&amp;#x3C; (c + 1))] + val) % MOD         dp = ndp print(dp[0])&lt;/code&gt;,
},
{
language: “java”,
label: “Java”,
code: `import java.util.*;&lt;/p&gt;
&lt;p&gt;public class Main {
static final int MOD = 9901;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int h = Integer.parseInt(st.nextToken());
int w = Integer.parseInt(st.nextToken());
int sz = 1 &amp;#x3C;&amp;#x3C; w;
int[] dp = new int[sz];
dp[0] = 1;
for (int r = 0; r &amp;#x3C; h; r++) {
for (int c = 0; c &amp;#x3C; w; c++) {
int[] ndp = new int[sz];
for (int mask = 0; mask &amp;#x3C; sz; mask++) {
int val = dp[mask];
if (val == 0) continue;
if ((mask &amp;#x26; (1 &amp;#x3C;&amp;#x3C; c)) != 0) {
ndp[mask ^ (1 &amp;#x3C;&amp;#x3C; c)] = (ndp[mask ^ (1 &amp;#x3C;&amp;#x3C; c)] + val) % MOD;
} else {
ndp[mask | (1 &amp;#x3C;&amp;#x3C; c)] = (ndp[mask | (1 &amp;#x3C;&amp;#x3C; c)] + val) % MOD;
if (c + 1 &amp;#x3C; w &amp;#x26;&amp;#x26; (mask &amp;#x26; (1 &amp;#x3C;&amp;#x3C; (c + 1))) == 0) {
ndp[mask | (1 &amp;#x3C;&amp;#x3C; (c + 1))] = (ndp[mask | (1 &amp;#x3C;&amp;#x3C; (c + 1))] + val) % MOD;
}
}
}
dp = ndp;
}
}
System.out.println(dp[0]);
}
}`,
},
]}
cases={[
{
label: “2x4 grid”,
input: “2 4”,
output: “5”,
},
{
label: “3x3 grid”,
input: “3 3”,
output: “0”,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(H * W * 2^W)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(2^W)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;실용 범위&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;W ≤ 18 (2^18 = 262k)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;변형&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;설명&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Plug DP&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;broken profile 의 일반화. 비도미노 타일, Hamiltonian path 검색&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;3D tiling&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;3차원 격자, L-tromino 등 다양한 조각&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Grid coloring&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;인접 조건이 있는 격자 coloring (예: chessboard pattern)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Subset DP&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;상태 압축 일반 기법. TSP 등과 유사&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-w-가-큰-경우&quot;&gt;1. W 가 큰 경우&lt;/h3&gt;
&lt;p&gt;W &gt; 18 이면 상태 2^W 가 감당 불가. W 큰 case 는 다른 접근 (분할 정복, transfer-matrix + exponentiation) 필요.&lt;/p&gt;
&lt;h3 id=&quot;2-horizontal-overflow-처리&quot;&gt;2. Horizontal overflow 처리&lt;/h3&gt;
&lt;p&gt;Horizontal 을 놓을 때 &lt;code&gt;c+1 &amp;#x3C; W&lt;/code&gt; 검사와 함께 &lt;code&gt;!(mask &amp;#x26; (1 &amp;#x3C;&amp;#x3C; (c+1)))&lt;/code&gt; (오른쪽 칸이 비었음) 도 반드시 확인.&lt;/p&gt;
&lt;h3 id=&quot;3-마지막-행-검증&quot;&gt;3. 마지막 행 검증&lt;/h3&gt;
&lt;p&gt;마지막 셀까지 처리한 후 &lt;code&gt;dp[0]&lt;/code&gt; 만 유효. mask != 0 은 아직 피우지 못한 칸이 있거나 vertical 이 경계를 넘은 것.&lt;/p&gt;
&lt;h3 id=&quot;4-mod-경계&quot;&gt;4. MOD 경계&lt;/h3&gt;
&lt;p&gt;경우의 수가 폭발적으로 증가하므로 MOD (9901 등) 로 나누는 것 필수.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;center&quot;&gt;번호&lt;/th&gt;&lt;th&gt;제목&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;BOJ 1648&lt;/td&gt;&lt;td&gt;격자판 채우기&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1600-1699/1648&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;BOJ 1823&lt;/td&gt;&lt;td&gt;도미노 채우기&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1800-1899/1823&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;BOJ 2711&lt;/td&gt;&lt;td&gt;오타맨&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2700-2799/2711&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[DP|동적 계획법]]&lt;/li&gt;
&lt;li&gt;[[DP Bitfield|비트 DP]]&lt;/li&gt;
&lt;li&gt;[[DP Sum Over Subsets|SOS DP]]&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/1648&quot;&gt;BOJ 1648: 격자판 채우기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>dp</category><category>dp-connection-profile</category><category>bitmask</category><author>koa (김신건)</author></item><item><title>덱 DP (슬라이딩 윈도우 최적화)</title><link>https://shinkeonkim.com/wiki/algorithm/dp/dp-deque/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/dp/dp-deque/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;덱 DP (Deque DP)&lt;/strong&gt; 는 점화식 &lt;code&gt;dp[i] = min/max_{j in [i-k, i-1]} (dp[j] + cost(i,j))&lt;/code&gt; 꼴에서 monotonic deque 를 써서 최적 &lt;code&gt;j&lt;/code&gt; 후보를 O(N) 에 유지하는 기법. 슬라이딩 윈도우 최댓값/최솟값의 일반화로, 윈도우 내에서 &lt;em&gt;비용까지 포함한 최적 인덱스&lt;/em&gt; 를 관리한다.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;dp[i] = max(dp[j] + a[j])&lt;/code&gt; for &lt;code&gt;j &amp;#x3C; i&lt;/code&gt; and &lt;code&gt;i - j &amp;#x3C;= K&lt;/code&gt;. Naive 하면 각 &lt;code&gt;i&lt;/code&gt; 에서 K 개 순회 -&gt; O(NK). N=10^6, K=10^5 면 10^11.&lt;/p&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;윈도우 안의 후보 중 dp[j] + something 이 최대인 j 만 deque 에 유지. 더 이상 최적이 될 수 없는 후보는 pop.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;DP: dp[i] = best(dp[j] + a[i])  for j in [i-K, i-1]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    where best = maximal computed value&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Deque invariant:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  1. Index range: front is always within [i-K, i-1]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  2. Value order: dp[deque[0]] &gt;= dp[deque[1]] &gt;= ... (decreasing)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  3. Old index from front when out of range&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  4. New candidate deletes from back if inferior&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Deque 에는 &lt;em&gt;인덱스&lt;/em&gt; 만 저장. &lt;code&gt;dp[deque.front()]&lt;/code&gt; 이 항상 현재 i 에 대한 최적값.&lt;/p&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;deque dq&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;for i = 0..N-1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # 1. 만료된 front 제거&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    while dq not empty and dq.front() &amp;#x3C; i - K:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        dq.pop_front()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # 2. 현재 dp[i] 계산&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if dq empty:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        dp[i] = a[i]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    else:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        dp[i] = dp[dq.front()] + a[i]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # 3. dq.back() 이 dp[i] 보다 나쁘면 제거&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    while dq not empty and dp[dq.back()] &amp;#x3C;= dp[i]:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        dq.pop_back()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # 4. 현재 i 를 후보에 추가&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    dq.push_back(i)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// Maximum sum subsequence: pick elements at distance &amp;#x3C;= K // dp[i] = max sum ending at i, each pair distance ≤ K #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; int main() {     int n, k; cin &gt;&gt; n &gt;&gt; k;     vector&amp;#x3C;int&gt; a(n);     for (int i = 0; i &amp;#x3C; n; i++) cin &gt;&gt; a[i];     vector&amp;#x3C;long long&gt; dp(n);     deque&amp;#x3C;int&gt; dq;     long long ans = 0;     for (int i = 0; i &amp;#x3C; n; i++) {         while (!dq.empty() &amp;#x26;&amp;#x26; dq.front() &amp;#x3C; i - k)             dq.pop_front();         dp[i] = a[i];         if (!dq.empty()) dp[i] += dp[dq.front()];         while (!dq.empty() &amp;#x26;&amp;#x26; dp[dq.back()] &amp;#x3C;= dp[i])             dq.pop_back();         dq.push_back(i);         ans = max(ans, dp[i]);     }     cout &amp;#x3C;&amp;#x3C; ans &amp;#x3C;&amp;#x3C; &quot;\\n&quot;;     return 0; }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: `from collections import deque
import sys
input = sys.stdin.readline&lt;/p&gt;
&lt;p&gt;n, k = map(int, input().split())
a = list(map(int, input().split()))
dp = [0] * n
dq = deque()
ans = 0
for i in range(n):
while dq and dq[0] &amp;#x3C; i - k:
dq.popleft()
dp[i] = a[i]
if dq:
dp[i] += dp[dq[0]]
while dq and dp[dq[-1]] &amp;#x3C;= dp[i]:
dq.pop()
dq.append(i)
ans = max(ans, dp[i])
print(ans)&lt;code&gt;,     },     {       language: &quot;java&quot;,       label: &quot;Java&quot;,       code: &lt;/code&gt;import java.util.*;&lt;/p&gt;
&lt;p&gt;public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int n = Integer.parseInt(st.nextToken());
int k = Integer.parseInt(st.nextToken());
int[] a = new int[n];
st = new StringTokenizer(br.readLine());
for (int i = 0; i &amp;#x3C; n; i++) a[i] = Integer.parseInt(st.nextToken());
long[] dp = new long[n];
Deque&lt;integer&gt; dq = new ArrayDeque&amp;#x3C;&gt;();
long ans = 0;
for (int i = 0; i &amp;#x3C; n; i++) {
while (!dq.isEmpty() &amp;#x26;&amp;#x26; dq.peekFirst() &amp;#x3C; i - k)
dq.pollFirst();
dp[i] = a[i];
if (!dq.isEmpty()) dp[i] += dp[dq.peekFirst()];
while (!dq.isEmpty() &amp;#x26;&amp;#x26; dp[dq.peekLast()] &amp;#x3C;= dp[i])
dq.pollLast();
dq.offerLast(i);
ans = Math.max(ans, dp[i]);
}
System.out.println(ans);
}
}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;기본 예제&quot;,       input: &lt;/code&gt;8 3
1 -3 5 -2 8 -1 4 -6&lt;code&gt;,       output: &lt;/code&gt;13&lt;code&gt;,     },     {       label: &quot;음수 포함&quot;,       input: &lt;/code&gt;5 2
-5 -1 -3 -2 -4&lt;code&gt;,       output: &lt;/code&gt;-1`,
},
]}
/&gt;&lt;/integer&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (최선/평균/최악)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(N) (dp 배열) or O(K) (deque + rolling)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;각 원소의 deque push/pop&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;amortized O(1)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;변형&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;설명&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Sliding window minimum&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;부등호 반대 (≤ -&gt; &gt;=). dp 없이 순수 배열 최솟값&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;DP + sliding window cost&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;dp[i] = min(dp[j] + cost(i,j))&lt;/code&gt; 에서 cost 가 분해 가능할 때&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Divide and Conquer DP&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;monotone queue 와 결합, &lt;code&gt;dp[i][j]&lt;/code&gt; 꼴&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;1D/1D DP&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;점화식 차수가 1차원, 최적화 상수가 K 일 때&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;널리 사용되는 예: BOJ 11003 (최솟값 찾기), BOJ 2096 (내려가기, 슬라이딩 윈도우 DP).&lt;/p&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-부등호-방향&quot;&gt;1. 부등호 방향&lt;/h3&gt;
&lt;p&gt;최댓값: &lt;code&gt;dp[dq.back()] &amp;#x3C;= dp[i]&lt;/code&gt; 이면 pop. 최솟값: &lt;code&gt;&gt;=&lt;/code&gt; 로 반대. 혼동하지 않도록.&lt;/p&gt;
&lt;h3 id=&quot;2-초기-k-값&quot;&gt;2. 초기 K 값&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;i &amp;#x3C; K&lt;/code&gt; 일 때는 윈도우가 충분히 크지 않음. &lt;code&gt;i - k&lt;/code&gt; 가 음수가 되지 않도록 조건.&lt;/p&gt;
&lt;h3 id=&quot;3-0-1-범위-반영&quot;&gt;3. 0-1 범위 반영&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;dp[i]&lt;/code&gt; 가 기본값 &lt;code&gt;a[i]&lt;/code&gt; (단독 선택) 인지, 아니면 이전 dp 값과 합쳐야 하는지. 문제 정의에 따라.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;center&quot;&gt;번호&lt;/th&gt;&lt;th&gt;제목&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;BOJ 11003&lt;/td&gt;&lt;td&gt;최솟값 찾기&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/11000-11099/11003&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;BOJ 2096&lt;/td&gt;&lt;td&gt;내려가기&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2000-2099/2096&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;BOJ 14865&lt;/td&gt;&lt;td&gt;골목길&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/14800-14899/14865&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[DP|동적 계획법]]&lt;/li&gt;
&lt;li&gt;[[Sliding Window|슬라이딩 윈도우]]&lt;/li&gt;
&lt;li&gt;[[Two Pointer|두 포인터]]&lt;/li&gt;
&lt;li&gt;[[Segtree|세그먼트 트리]] (대안: range minimum query)&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>dp</category><category>dp-deque</category><category>optimization</category><author>koa (김신건)</author></item><item><title>SOS DP (Sum Over Subsets)</title><link>https://shinkeonkim.com/wiki/algorithm/dp/dp-sum-over-subsets/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/dp/dp-sum-over-subsets/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;SOS DP (Sum Over Subsets)&lt;/strong&gt; 는 길이 2^N 의 배열 a 에 대해, &lt;strong&gt;모든 mask (0..2^N-1) 마다 mask 의 부분 집합 (submask) 에 대한 합&lt;/strong&gt; 을 O(N * 2^N) 에 구하는 DP 기법. “Sum Over Subsets” (부분 집합에 대한 합) 의 약자.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;길이 2^N 배열 a, f[mask] = Σ_&lt;code&gt;{submask ⊆ mask}&lt;/code&gt; a[submask].&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;naive&lt;/strong&gt;: 각 mask 마다 모든 submask 열거. O(3^N). N=20 이면 3^20 ~ 3.5 * 10^9, 불가.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SOS DP&lt;/strong&gt;: O(N * 2^N). N=20 이면 20 * 2^20 ~ 2 * 10^7, 1초 내 충분.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;bit i 하나씩 추가하며, i 번째 bit 가 set 인 mask 의 DP 값을 i 번째 bit 가 없던 mask 의 값으로 보강.&lt;/em&gt; “각 bit 를 순서대로, 이미 고려한 bit 들은 자유롭게” 라는 divide-and-conquer 관점.&lt;/p&gt;
&lt;p&gt;PS 에서: 포함-배제 DP (subset convolution), bitmask DP 고속화, Zeta/Mobius transform, graph 의 clique / independent set counting.&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;h3 id=&quot;상태-정의&quot;&gt;상태 정의&lt;/h3&gt;
&lt;p&gt;dp[mask][i] = mask 의 부분 집합들 중, &lt;strong&gt;하위 i 개 bit (bit 0 ~ i-1) 는 mask 와 정확히 일치&lt;/strong&gt;하고 나머지 상위 bit 들은 mask 의 부분 집합인 집합에 대한 합.&lt;/p&gt;
&lt;h3 id=&quot;점화식&quot;&gt;점화식&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;dp[mask][0] = a[mask]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;dp[mask][i] = dp[mask][i-1]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;              (bit i-1 이 0 이면)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            | dp[mask][i-1] + dp[mask ^ (1&amp;#x3C;&amp;#x3C;(i-1))][i-1]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;              (bit i-1 이 1 이면, i-1 번 bit 를 &quot;자유&quot; 로 만듦)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;최종 f[mask] = dp[mask][N].&lt;/p&gt;
&lt;h3 id=&quot;공간-최적화-1d-in-place&quot;&gt;공간 최적화 (1D in-place)&lt;/h3&gt;
&lt;p&gt;i 차원 생략. i loop 안에서 mask loop 실행.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;for i in 0..N-1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for mask in 0..(1&amp;#x3C;&amp;#x3C;N)-1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if mask &amp;#x26; (1&amp;#x3C;&amp;#x3C;i):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            f[mask] += f[mask ^ (1&amp;#x3C;&amp;#x3C;i)]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 때 f[mask] 는 i 단계에서 &lt;strong&gt;“하위 i 개 bit 자유, 나머지 고정”&lt;/strong&gt; 의미. i=0 단계: bit 0 자유. i=1 단계: bits 0.1 자유 … i=N-1 단계: 모든 bit 자유 (부분 집합 합 완성).&lt;/p&gt;
&lt;h3 id=&quot;zeta--mobius-transform&quot;&gt;Zeta / Mobius transform&lt;/h3&gt;
&lt;p&gt;SOS DP 의 덧셈 버전을 &lt;strong&gt;zeta transform&lt;/strong&gt; (부분 집합 합), 그 역을 &lt;strong&gt;Mobius transform&lt;/strong&gt; (차분 복원). subset convolution (서로소 부분 집합 쌍의 합 곱) 의 기초.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;zeta:   f[mask] += f[mask ^ (1&amp;#x3C;&amp;#x3C;i)]    (SOS DP 와 동일)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;mobius: f[mask] -= f[mask ^ (1&amp;#x3C;&amp;#x3C;i)]    (역연산, 음수 주의)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;sos_dp(f, N):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for i in 0..N-1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        for mask in 0..(1&amp;#x3C;&amp;#x3C;N)-1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            if mask &amp;#x26; (1&amp;#x3C;&amp;#x3C;i):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                f[mask] += f[mask ^ (1&amp;#x3C;&amp;#x3C;i)]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return f&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// SOS DP: O(N * 2^N) subset sum // Input: N, then 2^N integers (array a) // Output: f[mask] = sum of a[submask] for submask subseteq mask #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; int main() {     ios::sync_with_stdio(false);     cin.tie(nullptr);     int n; cin &gt;&gt; n;     int sz = 1 &amp;#x3C;&amp;#x3C; n;     vector&amp;#x3C;long long&gt; f(sz);     for (auto&amp;#x26; v : f) cin &gt;&gt; v;     for (int i = 0; i &amp;#x3C; n; i++) {         for (int mask = 0; mask &amp;#x3C; sz; mask++) {             if (mask &amp;#x26; (1 &amp;#x3C;&amp;#x3C; i))                 f[mask] += f[mask ^ (1 &amp;#x3C;&amp;#x3C; i)];         }     }     for (auto&amp;#x26; v : f) cout &amp;#x3C;&amp;#x3C; v &amp;#x3C;&amp;#x3C; &apos; &apos;; }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: &lt;code&gt;# SOS DP: O(N * 2^N) subset sum n = int(input()) f = list(map(int, input().split())) sz = 1 &amp;#x3C;&amp;#x3C; n for i in range(n):     bit = 1 &amp;#x3C;&amp;#x3C; i     for mask in range(sz):         if mask &amp;#x26; bit:             f[mask] += f[mask ^ bit] print(*f)&lt;/code&gt;,
},
]}
cases={[
{
label: “N=3 기본”,
input: &lt;code&gt;3 1 2 3 4 5 6 7 8&lt;/code&gt;,
output: &lt;code&gt;1 3 4 10 6 14 16 36&lt;/code&gt;,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N * 2^N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간 (naive 2D)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N * 2^N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간 (in-place 1D)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(2^N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Mobius 변환&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;동일 복잡도, 덧셈 대신 뺄셈&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;h3 id=&quot;1-subset-convolution&quot;&gt;1. Subset Convolution&lt;/h3&gt;
&lt;p&gt;h[mask] = Σ_&lt;code&gt;{sub ⊆ mask}&lt;/code&gt; f[sub] * g[mask ^ sub] (서로소 분할 곱). SOS DP + bit count 분리로 O(N^2 * 2^N).&lt;/p&gt;
&lt;h3 id=&quot;2-superset-sum-sos-over-supersets&quot;&gt;2. Superset Sum (SOS over supersets)&lt;/h3&gt;
&lt;p&gt;모든 superset 에 대한 합. 반대 방향: &lt;code&gt;for mask in 0..sz-1: if !(mask &amp;#x26; (1&amp;#x3C;&amp;#x3C;i)): f[mask] += f[mask | (1&amp;#x3C;&amp;#x3C;i)]&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;3-divisible--lcm-subset-counting&quot;&gt;3. Divisible / LCM subset counting&lt;/h3&gt;
&lt;p&gt;약수 관계를 bitmask 로 매핑해서 SOS DP 로 처리.&lt;/p&gt;
&lt;h3 id=&quot;4-graph-의-independent-set-counting&quot;&gt;4. Graph 의 Independent Set Counting&lt;/h3&gt;
&lt;p&gt;bitmask 가 independent set 을 나타내도록 하여, superset DP 로 clique / independent set 개수.&lt;/p&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-in-place-갱신-순서&quot;&gt;1. in-place 갱신 순서&lt;/h3&gt;
&lt;p&gt;mask 를 0..sz-1 순회. bit i 가 set 인 mask 만 갱신하므로, 작은 mask → 큰 mask 순서로 f[mask] 가 누적됨.&lt;/p&gt;
&lt;h3 id=&quot;2-오버플로우&quot;&gt;2. 오버플로우&lt;/h3&gt;
&lt;p&gt;a 값과 부분 집합 개수가 크면 64-bit 필요. C++ long long.&lt;/p&gt;
&lt;h3 id=&quot;3-mobius-음수&quot;&gt;3. Mobius 음수&lt;/h3&gt;
&lt;p&gt;Mobius 변환은 f[mask] -= f[mask ^ (1&amp;#x3C;&amp;#x3C;i)]. mod 연산 시 양수 유지: f[mask] = (f[mask] - f[mask ^ bit] + MOD) % MOD.&lt;/p&gt;
&lt;h3 id=&quot;4-n-제한&quot;&gt;4. N 제한&lt;/h3&gt;
&lt;p&gt;SOS DP 는 O(N * 2^N). N=25 (2^25=33M) 까지는 1초 내 가능. N=30 (2^30=1B) 은 불가.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 17451&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;평행 우주&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/17400-17499/17451&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 29350&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Сумма по подмножествам&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/29300-29399/29350&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 1725&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;히스토그램&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1700-1799/1725&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[DP Bitfield|비트마스크 DP]]&lt;/li&gt;
&lt;li&gt;[[DP|동적 계획법]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>dp</category><category>sos-dp</category><category>bitmask</category><author>koa (김신건)</author></item><item><title>Hirschberg (히르쉬버그) LCS</title><link>https://shinkeonkim.com/wiki/algorithm/dp/hirschberg/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/dp/hirschberg/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Hirschberg 알고리즘&lt;/strong&gt; 은 두 문자열 A, B 의 LCS 를 &lt;strong&gt;O(N) 공간&lt;/strong&gt; (메모리) 만 사용해 구하는 분할 정복 DP. 표준 LCS 의 O(NM) 공간을 O(N) 으로 줄이면서도 시간은 O(NM) 유지.&lt;/p&gt;
&lt;p&gt;1975 년 D. S. Hirschberg 가 제안. 아이디어: LCS 를 &lt;strong&gt;반으로 나누고, 중간 지점에서 분할점을 찾아 재귀&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;표준 LCS 는 O(NM) 공간. N = M = 10^5 면 10^10 int = 40 GB. 메모리 초과.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;naive DP&lt;/strong&gt;: &lt;code&gt;dp[N+1][M+1]&lt;/code&gt; 전체 저장. 수열 복원 가능. 공간 O(NM).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;공간 최적화 (1 행)&lt;/strong&gt;: &lt;code&gt;prev, curr&lt;/code&gt; 만 유지. 길이는 알 수 있지만 &lt;strong&gt;traceback 불가&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hirschberg&lt;/strong&gt;: O(NM) 시간, O(N) 공간, &lt;strong&gt;traceback 으로 수열 복원까지 가능&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;LCS DP 는 재귀적으로 분할 가능. “A 의 왼쪽 절반과 B 전체” 의 LCS + “A 의 오른쪽 절반과 B 전체” 의 LCS = 전체 LCS. 분할점을 찾아 재귀.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;h3 id=&quot;분할-정복-lcs&quot;&gt;분할 정복 LCS&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;lcs(A, B)&lt;/code&gt; 의 해를 A 의 중간 &lt;code&gt;mid = N/2&lt;/code&gt; 에서 분할:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;LCS(A, B) = LCS(A[0..mid], B[..k]) + LCS(A[mid..], B[k..])&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;k&lt;/code&gt; 는 다음 조건을 만족하는 위치:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;fwd[mid][k] + rev[N-mid][M-k] = LCS(A, B)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 &lt;code&gt;fwd[i][j]&lt;/code&gt; = A[0..i-1], B[0..j-1] 의 LCS 길이. &lt;code&gt;rev[i][j]&lt;/code&gt; = A[N-i..N-1], B[M-j..M-1] 의 LCS 길이 (뒤에서부터).&lt;/p&gt;
&lt;h3 id=&quot;분할점-k-찾기&quot;&gt;분할점 k 찾기&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;fwd[mid]&lt;/code&gt; 행 (A 의 중간까지, B 전체와의 LCS 길이) 을 O(NM) DP 로 계산. 단, O(M) 공간만 사용 (1 행).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rev[N-mid]&lt;/code&gt; 행 (A 의 나머지 절반, B 전체와의 LCS 길이) 을 O(NM) DP 로 계산. (뒤에서부터)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fwd[mid][k] + rev[N-mid][M-k]&lt;/code&gt; 가 최대가 되는 k = 분할점.&lt;/li&gt;
&lt;li&gt;왼쪽 &lt;code&gt;lcs(A[0..mid], B[0..k])&lt;/code&gt; + 오른쪽 &lt;code&gt;lcs(A[mid..], B[k..])&lt;/code&gt; 재귀.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;기저-조건&quot;&gt;기저 조건&lt;/h3&gt;
&lt;p&gt;N = 0 또는 M = 0: 빈 문자열. N = 1: 단일 문자 LCS (선형 탐색, O(M)).&lt;/p&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;hirschberg(A, B):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    N = len(A), M = len(B)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if N == 0 or M == 0: return &quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if N == 1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if A[0] in B: return A[0]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        else: return &quot;&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    mid = N // 2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    fwd = lcs_row(A[0..mid], B)     // O(mid * M) 공간 O(M)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    rev = lcs_row_rev(A[mid..], B)  // O((N-mid) * M) 공간 O(M)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    // 분할점 k&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    best = -1; k = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for j in 0..M:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        total = fwd[j] + rev[M - j]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if total &gt; best: best = total; k = j&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return hirschberg(A[0..mid], B[0..k])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;         + hirschberg(A[mid..], B[k..])&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;lcs_row&lt;/code&gt; 함수:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;lcs_row(A, B):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    prev = [0] * (M + 1)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for i in 1..len(A):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        curr = [0] * (M + 1)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        for j in 1..M:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            if A[i-1] == B[j-1]: curr[j] = prev[j-1] + 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            else: curr[j] = max(prev[j], curr[j-1])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        prev = curr&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return prev&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: `// Hirschberg LCS - O(NM) time, O(N) space
#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;&lt;/p&gt;
&lt;p&gt;vector&lt;int&gt; lcs_row(const string&amp;#x26; a, const string&amp;#x26; b) {
int m = b.size();
vector&lt;int&gt; prev(m + 1, 0), curr(m + 1, 0);
for (char ca : a) {
for (int j = 1; j &amp;#x3C;= m; j++) {
if (ca == b[j-1])
curr[j] = prev[j-1] + 1;
else
curr[j] = max(prev[j], curr[j-1]);
}
swap(prev, curr);
}
return prev;
}&lt;/int&gt;&lt;/int&gt;&lt;/p&gt;
&lt;p&gt;string hirschberg(const string&amp;#x26; a, const string&amp;#x26; b) {
int n = a.size(), m = b.size();
if (n == 0) return &quot;&quot;;
if (n == 1) {
return (b.find(a[0]) != string::npos) ? string(1, a[0]) : &quot;&quot;;
}&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;int mid = n / 2;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;auto fwd = lcs_row(a.substr(0, mid), b);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;string ra = a.substr(mid);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;reverse(ra.begin(), ra.end());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;string rb = b;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;reverse(rb.begin(), rb.end());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;auto rev = lcs_row(ra, rb);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;int best = -1, k = 0;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;for (int j = 0; j &amp;#x3C;= m; j++) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    int total = fwd[j] + rev[m - j];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if (total &gt; best) { best = total; k = j; }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;return hirschberg(a.substr(0, mid), b.substr(0, k))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;     + hirschberg(a.substr(mid), b.substr(k));&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;/p&gt;
&lt;p&gt;int main() {
string a, b;
cin &gt;&gt; a &gt;&gt; b;
string ans = hirschberg(a, b);
cout &amp;#x3C;&amp;#x3C; ans.size() &amp;#x3C;&amp;#x3C; “\n” &amp;#x3C;&amp;#x3C; ans &amp;#x3C;&amp;#x3C; “\n”;
}&lt;code&gt;,     },     {       language: &quot;python&quot;,       label: &quot;Python&quot;,       code: &lt;/code&gt;# Hirschberg LCS - O(NM) time, O(N) space
import sys
sys.setrecursionlimit(10000)
input = sys.stdin.readline&lt;/p&gt;
&lt;p&gt;def lcs_row(a: str, b: str):
m = len(b)
prev = [0] * (m + 1)
for ca in a:
curr = [0] * (m + 1)
for j in range(1, m + 1):
if ca == b[j-1]:
curr[j] = prev[j-1] + 1
else:
curr[j] = prev[j] if prev[j] &gt; curr[j-1] else curr[j-1]
prev = curr
return prev&lt;/p&gt;
&lt;p&gt;def hirschberg(a: str, b: str) -&gt; str:
n, m = len(a), len(b)
if n == 0:
return &quot;&quot;
if n == 1:
return a[0] if a[0] in b else &quot;&quot;&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;mid = n // 2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;fwd = lcs_row(a[:mid], b)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;rev = lcs_row(a[mid:][::-1], b[::-1])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;best, k = -1, 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;for j in range(m + 1):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    total = fwd[j] + rev[m - j]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if total &gt; best:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        best, k = total, j&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;return (hirschberg(a[:mid], b[:k])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;      + hirschberg(a[mid:], b[k:]))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;a = input().strip()
b = input().strip()
ans = hirschberg(a, b)
print(len(ans))
print(ans)&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;기본&quot;,       input: &lt;/code&gt;ABCBDAB
BDCAB&lt;code&gt;,       output: &lt;/code&gt;4
BCAB&lt;code&gt;,     },     {       label: &quot;공통 없음&quot;,       input: &lt;/code&gt;ABCD
EFGH&lt;code&gt;,       output: &lt;/code&gt;0
&lt;code&gt;,     },     {       label: &quot;한 문자&quot;,       input: &lt;/code&gt;A
B&lt;code&gt;,       output: &lt;/code&gt;0
`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (최선)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(NM)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (평균)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(NM)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (최악)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(NM)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(M) (입력 중 짧은 쪽 기준)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Traceback&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;가능 (수열 복원)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;표준 LCS 와 같은 시간 복잡도지만 공간이 O(NM) -&gt; O(N). N=10^5, M=10^5 일 때 메모리 40 GB -&gt; 400 KB.&lt;/p&gt;
&lt;h3 id=&quot;공간-분석&quot;&gt;공간 분석&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;각 재귀 단계에서 O(M) 배열만 유지. 분할 정복으로 스택 깊이 O(log N).&lt;/li&gt;
&lt;li&gt;전체 공간 O(M). traceback 에 추가 배열 불필요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;h3 id=&quot;hirschberg--bitset-lcs&quot;&gt;Hirschberg + Bitset LCS&lt;/h3&gt;
&lt;p&gt;Bitset LCS (O(NM/64)) 의 &lt;code&gt;lcs_row&lt;/code&gt; 로 대체해 시간도 가속. 공간 O(N) + 시간 O(NM/64).&lt;/p&gt;
&lt;h3 id=&quot;병렬화&quot;&gt;병렬화&lt;/h3&gt;
&lt;p&gt;분할 후 왼쪽/오른쪽 재귀가 독립적. 병렬 실행 가능.&lt;/p&gt;
&lt;h3 id=&quot;needleman-wunsch-생물정보학&quot;&gt;Needleman-Wunsch (생물정보학)&lt;/h3&gt;
&lt;p&gt;전체 서열 정렬 (global alignment) 에 Hirschberg 기법 적용. 메모리 제한이 큰 게놈 정렬에 필수.&lt;/p&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-재귀-깊이&quot;&gt;1. 재귀 깊이&lt;/h3&gt;
&lt;p&gt;N 이 10^5 이면 재귀 깊이 log N ~ 17. Python 은 recursionlimit 확인.&lt;/p&gt;
&lt;h3 id=&quot;2-분할점-k-의-유일성&quot;&gt;2. 분할점 k 의 유일성&lt;/h3&gt;
&lt;p&gt;여러 k 가 같은 total 을 만들 수 있음. 아무 k 나 선택해도 LCS 결과는 동일 (최적 중 하나).&lt;/p&gt;
&lt;h3 id=&quot;3-reverse-연산&quot;&gt;3. reverse 연산&lt;/h3&gt;
&lt;p&gt;뒤에서부터 DP (&lt;code&gt;rev&lt;/code&gt;) 를 위해 문자열 reverse. 인덱스 매핑 실수 주의. &lt;code&gt;rev[m - j]&lt;/code&gt; 가 올바른 매칭.&lt;/p&gt;
&lt;h3 id=&quot;4-n--1-기저-조건&quot;&gt;4. N = 1 기저 조건&lt;/h3&gt;
&lt;p&gt;문자 하나짜리 부분 문자열은 선형 탐색. 모든 재귀 단계에서 이 보호 없으면 무한 재귀.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 9251&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;LCS&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/9200-9299/9251&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 9252&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;LCS 2 (traceback)&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/9200-9299/9252&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 1958&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;LCS 3&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1900-1999/1958&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 5582&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;공통 부분 문자열&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/5500-5599/5582&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[LCS|최장 공통 부분 수열 (LCS)]]&lt;/li&gt;
&lt;li&gt;[[Bitset LCS|비트셋 LCS (Hyyro)]]&lt;/li&gt;
&lt;li&gt;[[Dynamic Programming|동적 계획법]]&lt;/li&gt;
&lt;li&gt;D. S. Hirschberg, A linear space algorithm for computing maximal common subsequences (1975)&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>dp</category><category>lcs</category><category>string</category><category>divide-and-conquer</category><author>koa (김신건)</author></item><item><title>크누스 최적화 (Knuth Optimization)</title><link>https://shinkeonkim.com/wiki/algorithm/dp/knuth/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/dp/knuth/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;크누스 최적화 (Knuth Optimization)&lt;/strong&gt; 는 구간 DP &lt;code&gt;dp[i][j] = min(dp[i][k-1] + dp[k][j]) + C[i][j]&lt;/code&gt; 에서 &lt;strong&gt;최적 분할점 k = opt[i][j]&lt;/strong&gt; 의 &lt;strong&gt;단조성&lt;/strong&gt; 을 이용해 시간 복잡도를 O(N^3) 에서 &lt;strong&gt;O(N^2)&lt;/strong&gt; 으로 줄이는 기법.&lt;/p&gt;
&lt;p&gt;1971년 Donald E. Knuth 가 Optimal Binary Search Tree (OBST) 의 O(N^2) 해법을 발표하며 처음 사용.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;h3 id=&quot;왜-필요한가&quot;&gt;왜 필요한가&lt;/h3&gt;
&lt;p&gt;구간 DP: &lt;code&gt;dp[i][j] = min_k (dp[i][k-1] + dp[k][j]) + C[i][j]&lt;/code&gt; (i ≤ k ≤ j).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;naive&lt;/strong&gt;: 모든 k 를 검사, O(N) per state. 상태 N^2 개. 총 &lt;strong&gt;O(N^3)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;N=2000 이면 8*10^9. 절대 통과 불가.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Knuth 최적화의 돌파구&lt;/strong&gt;: &lt;code&gt;C[i][j]&lt;/code&gt; 가 &lt;strong&gt;사각 부등식 (quadrangle inequality)&lt;/strong&gt; 과 &lt;strong&gt;단조성 (monotonicity)&lt;/strong&gt; 을 만족하면, &lt;code&gt;opt[i][j]&lt;/code&gt; 가 &lt;code&gt;opt[i][j-1] &amp;#x3C;= opt[i][j] &amp;#x3C;= opt[i+1][j]&lt;/code&gt; 를 만족한다. 각 dp[i][j] 의 k 탐색 범위가 O(N) 에서 &lt;strong&gt;O(1) amortized&lt;/strong&gt; 로 감소. 전체 &lt;strong&gt;O(N^2)&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&quot;전제-조건&quot;&gt;전제 조건&lt;/h3&gt;
&lt;p&gt;C[i][j] 가 다음 두 조건을 만족해야 한다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;사각 부등식&lt;/strong&gt;: i ≤ i’ ≤ j ≤ j’ 일 때 C[i][j] + C[i’][j’] ≤ C[i][j’] + C[i’][j]&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단조성&lt;/strong&gt;: i ≤ i’ ≤ j ≤ j’ 일 때 C[i’][j] ≤ C[i][j’] (또는 C[i][j] 가 i 와 j 에 대해 단조 증가)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 두 조건이 DP 의 최적 분할점 단조성을 보장한다.&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;opt[i][j-1] &amp;#x3C;= opt[i][j] &amp;#x3C;= opt[i+1][j]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;증명 스케치:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  사각 부등식 -&gt; dp 가 사각 부등식을 만족 (induction)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  사각 부등식 -&gt; 단조성 (monotone decision point)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  귀류법: opt[i][j] &amp;#x3C; opt[i][j-1] 이라면 모순 유도&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 부등식을 이용하면 dp[i][j] 의 k 탐색 범위를 &lt;code&gt;[opt[i][j-1], opt[i+1][j]]&lt;/code&gt; 로 제한할 수 있다. 구간 길이 순으로 바깥 루프 (len = 2..N) 를 돌면, 각 i, j 에 대해 검사하는 k 의 총합이 O(N^2) 임이 보장된다.&lt;/p&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;KnuthDP(a[1..N]):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for i = 1..N:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        dp[i][i] = 0         # base&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        opt[i][i] = i&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        ps[i] = ps[i-1] + a[i]  # prefix sum for C&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for len = 2..N:           # 구간 길이&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        for i = 1..N-len+1:   # 시작점&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            j = i + len - 1   # 끝점&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            dp[i][j] = INF&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            sum = C(i, j)     # 예: 구간 합&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            # k 범위: opt[i][j-1] ~ opt[i+1][j]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            for k = opt[i][j-1] .. opt[i+1][j]:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                val = dp[i][k-1] + dp[k][j] + sum&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                if val &amp;#x3C; dp[i][j]:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                    dp[i][j] = val&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                    opt[i][j] = k&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return dp[1][N]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;아래는 &lt;strong&gt;파일 합치기 (BOJ 11066)&lt;/strong&gt; 문제. N 개의 파일을 하나로 합치는 최소 비용. Knuth 최적화로 O(N^3) -&gt; O(N^2).&lt;/p&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// Knuth Optimization: O(N^2) #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; const int INF = 1e9; int main() {     ios::sync_with_stdio(false);     cin.tie(nullptr);     int n; cin &gt;&gt; n;     vector&amp;#x3C;int&gt; a(n + 1), ps(n + 1);     vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; dp(n + 1, vector&amp;#x3C;int&gt;(n + 1));     vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; opt(n + 1, vector&amp;#x3C;int&gt;(n + 1));     for (int i = 1; i &amp;#x3C;= n; i++) {         cin &gt;&gt; a[i];         ps[i] = ps[i - 1] + a[i];         dp[i][i] = 0;         opt[i][i] = i;     }     for (int len = 2; len &amp;#x3C;= n; len++) {         for (int i = 1; i + len - 1 &amp;#x3C;= n; i++) {             int j = i + len - 1;             dp[i][j] = INF;             int sum = ps[j] - ps[i - 1];             int start = opt[i][j - 1];             int end = min(j - 1, opt[i + 1][j]);             for (int k = start; k &amp;#x3C;= end; k++) {                 int val = dp[i][k] + dp[k + 1][j] + sum;                 if (val &amp;#x3C; dp[i][j]) {                     dp[i][j] = val;                     opt[i][j] = k;                 }             }         }     }     cout &amp;#x3C;&amp;#x3C; dp[1][n] &amp;#x3C;&amp;#x3C; &apos;\n&apos;; }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: `import sys
input = sys.stdin.readline
INF = 10 ** 9&lt;/p&gt;
&lt;p&gt;n = int(input())
a = [0] + list(map(int, input().split()))
ps = [0] * (n + 1)
for i in range(1, n + 1):
ps[i] = ps[i - 1] + a[i]&lt;/p&gt;
&lt;p&gt;dp = [[0] * (n + 1) for _ in range(n + 1)]
opt = [[0] * (n + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
dp[i][i] = 0
opt[i][i] = i&lt;/p&gt;
&lt;p&gt;for length in range(2, n + 1):
for i in range(1, n - length + 2):
j = i + length - 1
dp[i][j] = INF
s = ps[j] - ps[i - 1]
start = opt[i][j - 1]
end = min(j - 1, opt[i + 1][j])
for k in range(start, end + 1):
val = dp[i][k] + dp[k + 1][j] + s
if val &amp;#x3C; dp[i][j]:
dp[i][j] = val
opt[i][j] = k&lt;/p&gt;
&lt;p&gt;print(dp[1][n])&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;기본&quot;,       input: &lt;/code&gt;4
40 30 30 50&lt;code&gt;,       output: &lt;/code&gt;300&lt;code&gt;,     },     {       label: &quot;큰 값&quot;,       input: &lt;/code&gt;5
10 20 30 40 50&lt;code&gt;,       output: &lt;/code&gt;330`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (최선)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N^2)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (평균)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N^2)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (최악)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N^2) (단조성 유지 시)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N^2) (dp + opt 테이블)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;최적화 전&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N^3) -&gt; O(N^2)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;각 행 i 에 대해 k 검사 범위의 합이 O(N) 이고, 이걸 N 개의 i 에 대해 O(N^2). naive 대비 N 배 향상.&lt;/p&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;문제&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;C[i][j]&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;적용&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Optimal Binary Search Tree (OBST)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;확률 합&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Knuth 의 원 논문&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;파일 합치기 (BOJ 11066)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;구간 합&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;가장 유명한 PS 예제&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Matrix Chain Multiplication (일부)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;곱셈 비용&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;조건에 따라 적용 가능&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;팰린드롬 분할 (BOJ 1509)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;분할 비용&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;변형 Knuth / D&amp;#x26;C&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-사각-부등식-검증-생략&quot;&gt;1. 사각 부등식 검증 생략&lt;/h3&gt;
&lt;p&gt;Knuth 가 적용되는지 수학적으로 증명하지 않고 쓰면 틀린다. &lt;code&gt;C[i][j]&lt;/code&gt; 가 단순 구간 합이면 항상 성립하지만, 복잡한 비용 함수는 검증 필수.&lt;/p&gt;
&lt;h3 id=&quot;2-opt-범위의-경계-처리&quot;&gt;2. opt 범위의 경계 처리&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;opt[i+1][j]&lt;/code&gt; 가 i+1 &gt; j 인 경우 (i = j) 접근 오류. 항상 &lt;code&gt;opt[i][i] = i&lt;/code&gt; 로 초기화하고 &lt;code&gt;opt[i+1][j]&lt;/code&gt; 는 &lt;code&gt;opt[i+1][j]&lt;/code&gt; 가 정의된 경우만 사용.&lt;/p&gt;
&lt;h3 id=&quot;3-opt-테이블-초기값&quot;&gt;3. opt 테이블 초기값&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;opt[i][i] = i&lt;/code&gt; (base). &lt;code&gt;opt[i][j-1]&lt;/code&gt; 과 &lt;code&gt;opt[i+1][j]&lt;/code&gt; 가 각각 len-1 에서 계산되도록 구간 길이 순으로 순회.&lt;/p&gt;
&lt;h3 id=&quot;4-dp-테이블-크기와-overflow&quot;&gt;4. dp 테이블 크기와 overflow&lt;/h3&gt;
&lt;p&gt;파일 합치기처럼 누적 합이 커지면 &lt;code&gt;int&lt;/code&gt; overflow. &lt;code&gt;long long&lt;/code&gt; 사용.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 11066&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;파일 합치기&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/11000-11099/11066&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 13974&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;파일 합치기 2&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/13900-13999/13974&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 1509&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;팰린드롬 분할&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1500-1599/1509&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Divide and Conquer Optimization|분할정복 DP 최적화]]&lt;/li&gt;
&lt;li&gt;[[Convex Hull Trick]]&lt;/li&gt;
&lt;li&gt;[[Aliens Trick]]&lt;/li&gt;
&lt;li&gt;[[Deque Trick|덱 트릭]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>dp</category><category>optimization</category><category>divide-and-conquer</category><author>koa (김신건)</author></item><item><title>불변량 (Invariant)</title><link>https://shinkeonkim.com/wiki/algorithm/foundation/invariant/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/foundation/invariant/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;불변량 (Invariant)&lt;/strong&gt; 은 어떤 변환(operation)이 적용되어도 변하지 않는 양이나 성질. 불가능성 증명, 알고리즘의 정당성(correctness) 증명, 종료 조건 분석에 사용. 대표적으로 parity, coloring, sum/mod, algebraic invariant 가 있다.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;“이 상태에서 저 상태로 도달할 수 있는가?” 를 판별할 때, 가능한 모든 연산을 시뮬레이션할 수는 없다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;naive&lt;/strong&gt;: BFS/DFS로 모든 상태 탐색. 상태 공간이 지수적으로 커짐.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;invariant&lt;/strong&gt;: 항상 보존되는 양 Q를 찾아, Q(initial) != Q(target) 임을 보이면 불가능 증명. O(1) 판별.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;변환은 특정 양을 보존한다. 보존되는 양이 다르면 절대 도달할 수 없다.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;초기 상태 S0 = (변수, 구조, ...)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;불변량 I(S) 가 모든 가능한 변환 T 에 대해 I(S) = I(T(S)) 를 만족&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;I(S0) != I(목표) =&gt; 목표 도달 불가능&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;대표적인 불변량:&lt;/p&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;불변량&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;적용&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;예시&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Parity (홀짝)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;inversion count, bit flip, swap&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;15-puzzle, permutation sortability&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Coloring&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;checkerboard 2-coloring&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;domino tiling, chessboard covering&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Sum/Mod&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;합 또는 mod M 보존&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;물병 문제, number theory&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Algebraic&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;invariant polynomial&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;graph isomorphism, knot theory&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;invariant_check(sequence, operation):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    I = compute_invariant(sequence)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # operation 이 I 를 보존하는지 확인&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for each possible operation:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        assert I == compute_invariant(apply(sequence, operation))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # 목표 상태의 I 와 비교&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return I == compute_invariant(target)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// 순열의 inversion parity 계산 // 불변량: 인접 swap 은 inversion parity 를 바꾸지 않는다 #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; int main() {     int n; cin &gt;&gt; n;     vector&amp;#x3C;int&gt; a(n);     for (auto&amp;#x26; v : a) cin &gt;&gt; v;     int inv = 0;     for (int i = 0; i &amp;#x3C; n; i++)         for (int j = i + 1; j &amp;#x3C; n; j++)             if (a[i] &gt; a[j]) inv++;     cout &amp;#x3C;&amp;#x3C; (inv % 2 == 0 ? &quot;even&quot; : &quot;odd&quot;) &amp;#x3C;&amp;#x3C; &quot;\\n&quot;;     // 15-puzzle: inversions % 2 == 0 이면 solvable (empty row 보정)     return 0; }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: `# 도미노 체스판: checkerboard coloring invariant&lt;/p&gt;
&lt;h1 id=&quot;두-칸을-제거했을-때-도미노로-전부-덮을-수-있는가&quot;&gt;두 칸을 제거했을 때 도미노로 전부 덮을 수 있는가?&lt;/h1&gt;
&lt;p&gt;def can_cover(r1, c1, r2, c2):
# 각 도미노는 검+흰 1개씩 덮음
# 제거된 두 칸이 다른 색이어야 가능
color = lambda r, c: (r + c) % 2
return color(r1, c1) != color(r2, c2)&lt;/p&gt;
&lt;p&gt;a, b, c, d = map(int, input().split())
print(1 if can_cover(a, b, c, d) else 0)&lt;/p&gt;
&lt;h1 id=&quot;boj-7538-서로-다른-색-두-칸만-제거-가능&quot;&gt;BOJ 7538: 서로 다른 색 두 칸만 제거 가능`,&lt;/h1&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  language: &quot;java&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  label: &quot;Java&quot;,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  code: `// Parity invariant for permutation&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] a = new int[n];
for (int i = 0; i &amp;#x3C; n; i++) a[i] = sc.nextInt();
int inv = 0;
for (int i = 0; i &amp;#x3C; n; i++)
for (int j = i + 1; j &amp;#x3C; n; j++)
if (a[i] &gt; a[j]) inv++;
System.out.println(inv % 2 == 0 ? “even” : “odd”);
}
}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;순열 parity&quot;,       input: &lt;/code&gt;4
3 1 4 2&lt;code&gt;,       output: &lt;/code&gt;even`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;불변량 계산&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N) 또는 O(N log N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;판별&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(1) (initial vs target 비교)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;invariant 찾기&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;문제마다 다름 (경험 + 통찰)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;안정성&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;항상 정확 (necessary &amp;#x26; sufficient)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;변형&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;설명&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;활용&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Parity invariant&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;inversion count, XOR parity&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;15-puzzle, Sorting network&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Coloring invariant&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;그래프 2-coloring, bipartiteness&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;체스판 tiling, domino covering&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Monovariant&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;단조 변화량 (항상 증가/감소)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;종료 증명, potential function&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Algebraic invariant&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;차수, rank, determinant&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;선형대수, 매트로이드&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-불변량이-충분-조건이-아닐-때&quot;&gt;1. 불변량이 충분 조건이 아닐 때&lt;/h3&gt;
&lt;p&gt;같은 불변량 값을 가져도 실제로 도달 가능하지 않을 수 있음. parity 만으로 15-puzzle 이 완전히 결정되지는 않음 (empty tile row 도 고려).&lt;/p&gt;
&lt;h3 id=&quot;2-invariant-vs-monovariant-혼동&quot;&gt;2. invariant vs monovariant 혼동&lt;/h3&gt;
&lt;p&gt;불변량은 &lt;em&gt;변하지 않는&lt;/em&gt; 양. Monovariant는 &lt;em&gt;단조 변하는&lt;/em&gt; 양 (항상 증가/감소). 후자는 알고리즘 종료 증명에 사용.&lt;/p&gt;
&lt;h3 id=&quot;3-불변량을-잘못-선택&quot;&gt;3. 불변량을 잘못 선택&lt;/h3&gt;
&lt;p&gt;너무 강한 불변량은 깨지기 쉽고, 너무 약한 불변량은 useless. 경험적으로 &lt;code&gt;mod 2&lt;/code&gt;, &lt;code&gt;sum&lt;/code&gt;, &lt;code&gt;XOR&lt;/code&gt; 이 자주 통함.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2136&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;개미&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;33.7%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2100-2199/2136&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 7538&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Incomplete chess boards&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;69.7%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/7500-7599/7538&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 10424&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;알고리즘 기말고사&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;31.8%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/10400-10499/10424&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 32034&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;동전 쌍 뒤집기&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;35.0%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/32000-32099/32034&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Permutation Cycle Decomposition|순열 사이클 분해]]&lt;/li&gt;
&lt;li&gt;[[Parity|홀짝성]]&lt;/li&gt;
&lt;li&gt;[[Graph Traversal|그래프 탐색]]&lt;/li&gt;
&lt;li&gt;[[Game Theory|게임 이론]] (Sprague-Grundy 의 mex invariant)&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>foundation</category><category>invariant</category><author>koa (김신건)</author></item><item><title>보이어-무어 다수결 투표 (Boyer-Moore Majority Vote)</title><link>https://shinkeonkim.com/wiki/algorithm/foundation/majority-vote/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/foundation/majority-vote/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;보이어-무어 다수결 투표 (Boyer-Moore Majority Vote)&lt;/strong&gt; 는 배열에서 &lt;strong&gt;과반수 (&gt; N/2) 를 차지하는 원소를 O(N) 시간, O(1) 공간으로 찾는 알고리즘&lt;/strong&gt;. 1982년 Robert S. Boyer와 J Strother Moore가 고안.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;배열 &lt;code&gt;a[0..N-1]&lt;/code&gt; 에서 &lt;strong&gt;과반수 원소&lt;/strong&gt; (N/2 번 이상 나타나는 원소) 를 찾는다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;naive&lt;/strong&gt;: 각 원소마다 등장 횟수 세기. O(N^2) 또는 hash map 으로 O(N) 시간, O(N) 공간.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심 통찰&lt;/strong&gt;: 서로 다른 두 원소를 짝지으면 둘 다 제거해도 과반수는 변하지 않음. 이를 반복하면 마지막에 남은 원소가 유일한 후보.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;자주 등장: 투표 시스템, 센서 데이터 이상 탐지, 스트림 처리.&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Invariant&lt;/strong&gt;: &lt;code&gt;(candidate, count)&lt;/code&gt; 쌍을 유지. count 가 0이면 현재 원소를 새 후보로, 같으면 count++, 다르면 count—.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;핵심: 과반수 원소 x 가 존재하면, 다른 모든 원소를 제거해도 x 는 남음.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;      count 가 0 이 되는 순간 새 후보를 선택해도, &lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;      x 의 등장 횟수 &gt; (다른 모든 원소의 합) 이므로 x 가 최종 후보.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;주의&lt;/strong&gt;: 과반수가 존재하지 않을 수도 있으므로 &lt;strong&gt;두 번째 pass 에서 검증 필수&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;majority_vote(a[0..N-1]):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # Pass 1: 후보 찾기&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    candidate = null&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    count = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for i = 0..N-1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if count == 0:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            candidate = a[i]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            count = 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        else if a[i] == candidate:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            count += 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        else:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            count -= 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # Pass 2: 검증 (과반수 확인)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    count = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for i = 0..N-1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if a[i] == candidate:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            count += 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if count &gt; N / 2:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        return candidate&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    else:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        return -1  # 과반수 없음&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: `#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
int main() {
int n;
cin &gt;&gt; n;
vector&lt;int&gt; a(n);
for (int i = 0; i &amp;#x3C; n; i++) cin &gt;&gt; a[i];&lt;/int&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// Pass 1: 후보 찾기&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;int candidate = -1, count = 0;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;for (int i = 0; i &amp;#x3C; n; i++) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if (count == 0) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        candidate = a[i];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        count = 1;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    } else if (a[i] == candidate) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        count++;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    } else {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        count--;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// Pass 2: 검증&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;count = 0;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;for (int i = 0; i &amp;#x3C; n; i++) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if (a[i] == candidate) count++;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;if (count &gt; n / 2) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    cout &amp;#x3C;&amp;#x3C; candidate &amp;#x3C;&amp;#x3C; &quot;\\n&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;} else {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    cout &amp;#x3C;&amp;#x3C; -1 &amp;#x3C;&amp;#x3C; &quot;\\n&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;return 0;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;code&gt;,     },     {       language: &quot;python&quot;,       label: &quot;Python&quot;,       code: &lt;/code&gt;n = int(input())
a = list(map(int, input().split()))&lt;/p&gt;
&lt;h1 id=&quot;pass-1-후보-찾기&quot;&gt;Pass 1: 후보 찾기&lt;/h1&gt;
&lt;p&gt;candidate = None
count = 0
for x in a:
if count == 0:
candidate = x
count = 1
elif x == candidate:
count += 1
else:
count -= 1&lt;/p&gt;
&lt;h1 id=&quot;pass-2-검증&quot;&gt;Pass 2: 검증&lt;/h1&gt;
&lt;p&gt;count = a.count(candidate)
if count &gt; n // 2:
print(candidate)
else:
print(-1)&lt;code&gt;,     },     {       language: &quot;java&quot;,       label: &quot;Java&quot;,       code: &lt;/code&gt;import java.util.*;&lt;/p&gt;
&lt;p&gt;public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
int[] a = new int[n];
StringTokenizer st = new StringTokenizer(br.readLine());
for (int i = 0; i &amp;#x3C; n; i++) {
a[i] = Integer.parseInt(st.nextToken());
}&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    // Pass 1: 후보 찾기&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    int candidate = -1, count = 0;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for (int i = 0; i &amp;#x3C; n; i++) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if (count == 0) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            candidate = a[i];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            count = 1;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        } else if (a[i] == candidate) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            count++;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        } else {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            count--;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    // Pass 2: 검증&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    count = 0;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for (int i = 0; i &amp;#x3C; n; i++) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if (a[i] == candidate) count++;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if (count &gt; n / 2) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        System.out.println(candidate);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    } else {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        System.out.println(-1);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;과반수 존재&quot;,       input: &lt;/code&gt;7
2 3 2 1 2 4 2&lt;code&gt;,       output: &lt;/code&gt;2&lt;code&gt;,     },     {       label: &quot;과반수 없음&quot;,       input: &lt;/code&gt;4
1 2 3 4&lt;code&gt;,       output: &lt;/code&gt;-1`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (Pass 1)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (Pass 2)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;전체 시간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(1) (candidate, count 만 저장)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;안정성&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;N/A (원소 순서 변경 없음)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;h3 id=&quot;1-모든-과반수-원소-찾기-k-majority&quot;&gt;1. 모든 과반수 원소 찾기 (k-majority)&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;N/k 이상 등장하는 모든 원소 찾기. 최대 k-1 개 후보 유지.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;2-스트림-처리&quot;&gt;2. 스트림 처리&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;배열 전체를 메모리에 올릴 수 없을 때, 한 번에 한 원소씩 처리 가능.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;3-분산-환경&quot;&gt;3. 분산 환경&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;각 노드에서 로컬 후보 찾고, 전역 후보 검증.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-검증-pass-필수&quot;&gt;1. 검증 pass 필수&lt;/h3&gt;
&lt;p&gt;과반수가 존재하지 않으면 candidate 는 의미 없음. 반드시 두 번째 pass 에서 count &gt; N/2 확인.&lt;/p&gt;
&lt;h3 id=&quot;2--n2-vs--n2&quot;&gt;2. &gt; N/2 vs &gt;= N/2&lt;/h3&gt;
&lt;p&gt;알고리즘은 &lt;strong&gt;strictly greater than N/2&lt;/strong&gt; 만 보장. &gt;= N/2 를 원하면 다른 접근 필요.&lt;/p&gt;
&lt;h3 id=&quot;3-음수--큰-수&quot;&gt;3. 음수 / 큰 수&lt;/h3&gt;
&lt;p&gt;hash map 대신 쓰는 이유가 O(1) 공간이므로, 음수나 큰 수도 그대로 처리 가능. 단, 정수 범위 주의.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 11652&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;카드&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;(수집 안 됨)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/11600-11699/11652&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 25186&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;INFP 남진&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;(수집 안 됨)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/25100-25199/25186&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 13116&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;30번&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;(수집 안 됨)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/13100-13199/13116&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 1764&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;듣보잡&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;(수집 안 됨)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1700-1799/1764&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Hash Set|해시셋]]&lt;/li&gt;
&lt;li&gt;[[Sorting|정렬]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>foundation</category><category>majority-vote</category><author>koa (김신건)</author></item><item><title>순열 사이클 분해 (Permutation Cycle Decomposition)</title><link>https://shinkeonkim.com/wiki/algorithm/foundation/permutation-cycle-decomposition/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/foundation/permutation-cycle-decomposition/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;순열 사이클 분해&lt;/strong&gt;는 순열 p[1..N]을 서로소(disjoint)인 사이클들의 곱으로 표현하는 것. 각 사이클 (a1 a2 … ak) 는 a1 -&gt; p[a1] = a2, a2 -&gt; p[a2] = a3, …, ak -&gt; p[ak] = a1 을 의미.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;순열을 함수 f(i) = p[i] 로 보면, 각 원소의 orbit 을 추적할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;응용&lt;/strong&gt;: 정렬 (swap 으로 정렬하는 최소 횟수 = N - 사이클 수), 치환 암호, 그룹 이론&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;최소 swap 정렬&lt;/strong&gt;: 임의의 두 원소를 swap 할 때, N - (#cycles) 번이 최소&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;순열은 방향 그래프로 볼 때 모든 정점의 out-degree = 1 이며, 각 연결 요소는 정확히 하나의 사이클을 이룬다.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;p = [3, 1, 2, 5, 4]  (1-indexed)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;그래프:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  1 -&gt; 3 -&gt; 2 -&gt; 1  (사이클, 길이 3)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  4 -&gt; 5 -&gt; 4       (사이클, 길이 2)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;사이클 분해: p = (1 3 2)(4 5)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;사이클 분해 알고리즘:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;visited[1..N] = false&lt;/li&gt;
&lt;li&gt;for i in 1..N:&lt;/li&gt;
&lt;li&gt;if not visited[i]:&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;새로운 사이클 시작&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;j = i&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;while not visited[j]:&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  visited[j] = true&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  사이클에 j 추가&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  j = p[j]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;find_cycles(p, N):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    visited = [false] * (N + 1)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    cycles = []&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for i = 1..N:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if not visited[i]:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            cur = []&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            j = i&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            while not visited[j]:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                visited[j] = true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                cur.append(j)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                j = p[j]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            cycles.append(cur)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return cycles&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// 순열 사이클 개수 세기, BOJ 10451 // 사이클 수 = N - (서로 다른 사이클 수) #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; int main() {     ios::sync_with_stdio(false);     cin.tie(nullptr);     int T; cin &gt;&gt; T;     while (T--) {         int n; cin &gt;&gt; n;         vector&amp;#x3C;int&gt; p(n + 1);         for (int i = 1; i &amp;#x3C;= n; i++) cin &gt;&gt; p[i];         vector&amp;#x3C;bool&gt; vis(n + 1, false);         int cycles = 0;         for (int i = 1; i &amp;#x3C;= n; i++) {             if (vis[i]) continue;             cycles++;             for (int j = i; !vis[j]; j = p[j])                 vis[j] = true;         }         cout &amp;#x3C;&amp;#x3C; cycles &amp;#x3C;&amp;#x3C; &quot;\\n&quot;;     }     return 0; }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: &lt;code&gt;# 순열 사이클 개수 (BOJ 10451) import sys input = sys.stdin.readline T = int(input()) for _ in range(T):     n = int(input())     p = [0] + list(map(int, input().split()))     vis = [False] * (n + 1)     cycles = 0     for i in range(1, n + 1):         if vis[i]: continue         cycles += 1         j = i         while not vis[j]:             vis[j] = True             j = p[j]     print(cycles)&lt;/code&gt;,
},
{
language: “java”,
label: “Java”,
code: `// BOJ 10451: Count permutation cycles&lt;/p&gt;
&lt;p&gt;public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int T = Integer.parseInt(br.readLine());
while (T— &gt; 0) {
int n = Integer.parseInt(br.readLine());
int[] p = new int[n + 1];
StringTokenizer st = new StringTokenizer(br.readLine());
for (int i = 1; i &amp;#x3C;= n; i++) p[i] = Integer.parseInt(st.nextToken());
boolean[] vis = new boolean[n + 1];
int cycles = 0;
for (int i = 1; i &amp;#x3C;= n; i++) {
if (vis[i]) continue;
cycles++;
for (int j = i; !vis[j]; j = p[j])
vis[j] = true;
}
System.out.println(cycles);
}
}
}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;사이클 개수&quot;,       input: &lt;/code&gt;2
8
3 2 7 8 1 4 5 6
5
2 1 4 3 5&lt;code&gt;,       output: &lt;/code&gt;3
3`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N) (visited 배열)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;최소 swap 정렬&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N) (N - cycles)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;안정성&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;유일한 분해 (disjoint cycles)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;h3 id=&quot;최소-swap-정렬&quot;&gt;최소 swap 정렬&lt;/h3&gt;
&lt;p&gt;임의의 두 원소를 swap 하여 정렬할 때, 최소 swap 횟수 = N - (사이클 수). 각 사이클 (길이 k)는 k-1 번의 swap 으로 정렬 가능.&lt;/p&gt;
&lt;h3 id=&quot;다음-순열--이전-순열&quot;&gt;다음 순열 / 이전 순열&lt;/h3&gt;
&lt;p&gt;사이클 분해로 다음 순열(Next Permutation)과 이전 순열을 O(N) 에 계산 가능.&lt;/p&gt;
&lt;h3 id=&quot;치환-행렬&quot;&gt;치환 행렬&lt;/h3&gt;
&lt;p&gt;순열 p 는 permutation matrix P 로 표현 가능. 사이클 분해는 P 의 Jordan 표준형과 연관.&lt;/p&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-1-indexed-vs-0-indexed&quot;&gt;1. 1-indexed vs 0-indexed&lt;/h3&gt;
&lt;p&gt;C++ 배열이 0-indexed 라도 순열 문제는 1-indexed 표기가 편리. p[1..N] 을 사용하고 i 위치 주의.&lt;/p&gt;
&lt;h3 id=&quot;2-visited-체크-순서&quot;&gt;2. visited 체크 순서&lt;/h3&gt;
&lt;p&gt;사이클 추적 시 while 순서: &lt;code&gt;vis[j]&lt;/code&gt; 체크 후 &lt;code&gt;j = p[j]&lt;/code&gt; 갱신. 아니면 무한루프.&lt;/p&gt;
&lt;h3 id=&quot;3-사이클-길이-1-처리&quot;&gt;3. 사이클 길이 1 처리&lt;/h3&gt;
&lt;p&gt;자기 자신을 가리키는 원소(p[i] == i) 는 길이 1 사이클. 정렬 관점에서 swap 0 회.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 10451&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;순열 사이클&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;63.7%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/10400-10499/10451&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 10972&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;다음 순열&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;33.3%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/10900-10999/10972&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 10973&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;이전 순열&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;35.3%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/10900-10999/10973&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 9742&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;순열&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;17.5%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/9700-9799/9742&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Invariant|불변량]] (permutation parity)&lt;/li&gt;
&lt;li&gt;[[Functional Graph|함수형 그래프]]&lt;/li&gt;
&lt;li&gt;[[DFS|DFS (깊이 우선 탐색)]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>foundation</category><category>permutation-cycle-decomposition</category><author>koa (김신건)</author></item><item><title>번사이드 보조정리 (Burnside&apos;s Lemma)</title><link>https://shinkeonkim.com/wiki/algorithm/game/burnside/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/game/burnside/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;번사이드 보조정리 (Burnside’s Lemma, Cauchy-Frobenius Lemma)&lt;/strong&gt; 는 군 G 가 집합 X 에 작용할 때, &lt;strong&gt;궤도(orbit)의 개수&lt;/strong&gt; 를 구하는 공식:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;|X/G| = (1/|G|) * sum_{g in G} |Fix(g)|&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Fix(g)&lt;/code&gt;: g 에 의해 고정되는(fixed) X 의 원소 집합.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;|G|&lt;/code&gt;: 군의 크기 (group order).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;1845년 Cauchy 최초 발견, Burnside (1897) 가 저서로 널리 알림.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;대칭(symmetry) 이 있는 조합적 대상의 “본질적으로 다른” 개수를 셀 때.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;naive&lt;/strong&gt;: 모든 경우를 나열 후 중복 제거. k^n 가지가 n! 대칭과 합쳐져 폭발.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Burnside&lt;/strong&gt;: 각 대칭 변환(g) 에 대해 불변인 배치 수를 세고 평균.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;대칭 변환 각각에 대해 고정되는 배치 수는 독립적으로 계산 가능. 그 평균이 곧 distinct orbits 수.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;h3 id=&quot;공식의-의미&quot;&gt;공식의 의미&lt;/h3&gt;
&lt;p&gt;3단계로 구성:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;대칭 변환 나열&lt;/strong&gt;: 군 G 의 모든 원소 g 를 나열한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;고정점 계산&lt;/strong&gt;: 각 g 에 대해 g 를 적용해도 변하지 않는 X 의 원소(배치) 수 &lt;code&gt;|Fix(g)|&lt;/code&gt; 를 구한다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;평균&lt;/strong&gt;: 모두 더한 후 |G| 로 나눈다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;예시-4개-구슬-목걸이-2색-회전만&quot;&gt;예시: 4개 구슬 목걸이, 2색 (회전만)&lt;/h3&gt;
&lt;p&gt;회전군 C_4 (4개 회전):&lt;/p&gt;
&lt;p&gt;| 회전 | 고정 조건 | |Fix(g)| |
|:---|---:|:---|
| 0도 (항등) | 모든 2^4 가지 | 16 |
| 90도 | 모든 구슬 같은 색 | 2 |
| 180도 | 마주보는 쌍 동일 | 4 |
| 270도 | 모든 구슬 같은 색 | 2 |&lt;/p&gt;
&lt;p&gt;&lt;code&gt;(16 + 2 + 4 + 2) / 4 = 6&lt;/code&gt; 가지.&lt;/p&gt;
&lt;h3 id=&quot;일반식-회전만&quot;&gt;일반식 (회전만)&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;necklace(n, k) = (1/n) * sum_{r=0}^{n-1} k^{gcd(n, r)}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;polya-vs-burnside&quot;&gt;Polya vs Burnside&lt;/h3&gt;




















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;정리&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;관점&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;입력&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Burnside&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;각 변환의 고정점&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;`&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Polya&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;각 변환의 순환 구조&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;k^{C(g)}&lt;/code&gt; (C(g) = 순환 개수)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;burnside_necklace(n, k):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    total = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for r = 0..n-1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        total += k^{gcd(n, r)}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return total / n&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# D_n (회전 + 반사)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;burnside_dihedral(n, k):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    total = burnside_necklace(n, k) * n   # 회전 기여&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # 반사 기여&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if n % 2 == 0:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        total += n/2 * (k^{n/2} + k^{n/2 + 1})&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    else:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        total += n * k^{(n+1)/2}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return total / (2*n)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: `// Burnside’s lemma: necklace counting
#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
using ll = long long;&lt;/p&gt;
&lt;p&gt;ll modpow(ll base, ll exp, ll mod) {
ll res = 1;
while (exp) {
if (exp &amp;#x26; 1) res = res * base % mod;
base = base * base % mod;
exp &gt;&gt;= 1;
}
return res;
}&lt;/p&gt;
&lt;p&gt;int main() {
// 회전만: necklace(n, k)
int n, k; cin &gt;&gt; n &gt;&gt; k;
ll total = 0;
for (int r = 0; r &amp;#x3C; n; r++)
total += modpow(k, gcd(n, r), LLONG_MAX);
cout &amp;#x3C;&amp;#x3C; “Necklace (rotation): ” &amp;#x3C;&amp;#x3C; total / n &amp;#x3C;&amp;#x3C; “\n”;&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// 반사 포함 (Dihedral group D_n)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;total = 0;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;for (int r = 0; r &amp;#x3C; n; r++)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    total += modpow(k, gcd(n, r), LLONG_MAX);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;if (n % 2 == 1) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    total += n * modpow(k, (n + 1) / 2, LLONG_MAX);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;} else {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    total += n / 2 * modpow(k, n / 2, LLONG_MAX);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    total += n / 2 * modpow(k, n / 2 + 1, LLONG_MAX);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;cout &amp;#x3C;&amp;#x3C; &quot;Necklace (dihedral): &quot; &amp;#x3C;&amp;#x3C; total / (2 * n) &amp;#x3C;&amp;#x3C; &quot;\\n&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;return 0;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;code&gt;,     },     {       language: &quot;python&quot;,       label: &quot;Python&quot;,       code: &lt;/code&gt;# Burnside’s lemma: necklace counting
import math&lt;/p&gt;
&lt;p&gt;def modpow(base, exp, mod):
res = 1
while exp:
if exp &amp;#x26; 1:
res = res * base % mod
base = base * base % mod
exp &gt;&gt;= 1
return res&lt;/p&gt;
&lt;p&gt;n, k = map(int, input().split())&lt;/p&gt;
&lt;h1 id=&quot;rotation-only&quot;&gt;rotation only&lt;/h1&gt;
&lt;p&gt;total = sum(k ** math.gcd(n, r) for r in range(n))
print(f”Necklace (rotation): {total // n}“)&lt;/p&gt;
&lt;h1 id=&quot;dihedral-rotation--reflection&quot;&gt;dihedral (rotation + reflection)&lt;/h1&gt;
&lt;p&gt;total = sum(k ** math.gcd(n, r) for r in range(n))
if n % 2 == 1:
total += n * k ** ((n + 1) // 2)
else:
total += n // 2 * k ** (n // 2)
total += n // 2 * k ** (n // 2 + 1)
print(f”Necklace (dihedral): {total // (2 * n)}“)&lt;code&gt;,     },     {       language: &quot;java&quot;,       label: &quot;Java&quot;,       code: &lt;/code&gt;import java.util.*;&lt;/p&gt;
&lt;p&gt;public class Main {
static long modpow(long base, long exp, long mod) {
long res = 1;
while (exp &gt; 0) {
if ((exp &amp;#x26; 1) == 1) res = res * base % mod;
base = base * base % mod;
exp &gt;&gt;= 1;
}
return res;
}&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;static long gcd(long a, long b) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return b == 0 ? a : gcd(b, a % b);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;public static void main(String[] args) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    Scanner sc = new Scanner(System.in);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    int n = sc.nextInt();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    int k = sc.nextInt();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    long total = 0;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for (int r = 0; r &amp;#x3C; n; r++)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        total += modpow(k, gcd(n, r), Long.MAX_VALUE);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    System.out.println(&quot;Necklace (rotation): &quot; + total / n);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    total = 0;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for (int r = 0; r &amp;#x3C; n; r++)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        total += modpow(k, gcd(n, r), Long.MAX_VALUE);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if (n % 2 == 1) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        total += n * modpow(k, (n + 1) / 2, Long.MAX_VALUE);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    } else {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        total += n / 2 * modpow(k, n / 2, Long.MAX_VALUE);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        total += n / 2 * modpow(k, n / 2 + 1, Long.MAX_VALUE);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    System.out.println(&quot;Necklace (dihedral): &quot; + total / (2 * n));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;4구슬 2색&quot;,       input: &lt;/code&gt;4 2&lt;code&gt;,       output: &lt;/code&gt;Necklace (rotation): 6
Necklace (dihedral): 6&lt;code&gt;,     },     {       label: &quot;6구슬 3색&quot;,       input: &lt;/code&gt;6 3&lt;code&gt;,       output: &lt;/code&gt;Necklace (rotation): 130
Necklace (dihedral): 92`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;회전만 (C_n)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(n log k) (거듭제곱)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;정다각형 (D_n)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(n log k)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;정육면체 면 색칠&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(1) (고정 24개 변환)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;h3 id=&quot;정육면체-면-색칠-k-색&quot;&gt;정육면체 면 색칠 (k 색)&lt;/h3&gt;
&lt;p&gt;회전군 크기 = 24. &lt;code&gt;cube_faces(k) = (k^6 + 3*k^4 + 12*k^3 + 8*k^2) / 24&lt;/code&gt;. k=2 -&gt; 10 가지.&lt;/p&gt;
&lt;h3 id=&quot;응용-분야&quot;&gt;응용 분야&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;목걸이/팔찌 색칠&lt;/strong&gt;: C_n (회전) / D_n (회전+반사)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;분자의 이성질체 계수&lt;/strong&gt;: 화학에서 구조 이성질체 수&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;그래프 색칠 대칭&lt;/strong&gt;: 군 작용 궤도 계산&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Polya 열거 정리&lt;/strong&gt;: 가중치 있는 색칠, 생성함수&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-단순-나누기&quot;&gt;1. 단순 나누기&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;k^n / n&lt;/code&gt; 이 아닌 이유: 각 변환마다 고정점 개수가 다르다.&lt;/p&gt;
&lt;h3 id=&quot;2-반사-대칭-누락&quot;&gt;2. 반사 대칭 누락&lt;/h3&gt;
&lt;p&gt;홀수/짝수 n 에 따라 반사축 구조가 다름. 항상 케이스 분기 필요.&lt;/p&gt;
&lt;h3 id=&quot;3-mod-나눗셈&quot;&gt;3. mod 나눗셈&lt;/h3&gt;
&lt;p&gt;mod p 에서 |G| 로 나눌 때 modular inverse 사용. p 가 |G| 와 서로소인지 확인.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 9817&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Necklace of Beads&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;73.0%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/9800-9899/9817&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 12748&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;색칠 공부 (Large)&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;65.6%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/12700-12799/12748&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 18567&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Kaleidoscope&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;88.9%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/18500-18599/18567&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Combinatorics|조합론]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>game</category><category>burnside</category><category>combinatorics</category><author>koa (김신건)</author></item><item><title>게임 이론 (Game Theory)</title><link>https://shinkeonkim.com/wiki/algorithm/game/game-theory/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/game/game-theory/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;게임 이론 (Game Theory)&lt;/strong&gt; 은 둘 이상의 플레이어가 정해진 규칙 아래 의사 결정을 내리는 상황을 수학적으로 모델링하는 분야. PS에서는 &lt;strong&gt;두 명이 완벽한 플레이를 할 때 승자를 결정&lt;/strong&gt;하는 조합 게임(combinatorial game)에 집중한다.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;두 플레이어가 번갈아 행동하며, 마지막 행동을 하는 사람이 이기거나(정상 게임) 지는(미저리 게임) 규칙의 게임.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;naive&lt;/strong&gt;: 가능한 모든 게임 트리 전개. 상태 수가 기하급수적.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DP + Nim-XOR&lt;/strong&gt;: 상태가 작으면 win/loss DP O(N), 돌무더기가 여러 개면 Nim XOR O(N) 로 승패 결정.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;Impartial game (두 플레이어의 행동이 동일) 은 모두 Nim-heap 하나로 환원 가능&lt;/em&gt; (Sprague-Grundy 정리).&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;h3 id=&quot;winloss-상태&quot;&gt;Win/Loss 상태&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Winning state (W)&lt;/strong&gt;: 현재 턴인 플레이어가 이길 수 있는 상태.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Losing state (L)&lt;/strong&gt;: 현재 턴인 플레이어가 무조건 지는 상태.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;점화식:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;dp[i] = (i번째 상태에서 이기는가?)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;dp[i] = true if ∃move to dp[j] = false  (W = 갈 수 있는 L이 존재)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;dp[i] = false if all moves go to dp[j] = true (L = 모든 행선지가 W)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;nim-게임&quot;&gt;Nim 게임&lt;/h3&gt;
&lt;p&gt;돌무더기 여러 개. 각 더미에서 1개 이상 제거 가능. 정상 규칙(마지막 돌 가져가는 사람 승).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Nim-XOR (Bouton)&lt;/strong&gt;: &lt;code&gt;XOR sum = p1 xor p2 xor ... xor pk&lt;/code&gt;. XOR sum != 0 이면 선공 승, 0 이면 후공 승.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;nim_win(piles):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    xor_sum = fold(xor, piles)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return xor_sum != 0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;zero-sum-game&quot;&gt;Zero-Sum Game&lt;/h3&gt;
&lt;p&gt;한 플레이어의 이득이 다른 플레이어의 손실과 정확히 일치하는 게임. Minimax 원칙으로 최적 전략 계산.&lt;/p&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# 돌 게임 (N개 돌, 1 or 3개 가져감)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;win[0] = false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;for i = 1..N:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    win[i] = false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if i &gt;= 1 and not win[i-1]: win[i] = true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if i &gt;= 3 and not win[i-3]: win[i] = true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# Nim XOR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;xor = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;for p in piles: xor ^= p&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;if xor != 0: First wins else Second wins&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: `// 돌 게임 (N, take 1 or 3) + Nim XOR
#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;&lt;/p&gt;
&lt;p&gt;int main() {
// 1. 돌 게임 DP (N &amp;#x3C;= 1000)
int N; cin &gt;&gt; N;
vector&lt;bool&gt; win(N + 1, false);
for (int i = 1; i &amp;#x3C;= N; i++) {
if (i &gt;= 1 &amp;#x26;&amp;#x26; !win[i-1]) win[i] = true;
if (i &gt;= 3 &amp;#x26;&amp;#x26; !win[i-3]) win[i] = true;
}
cout &amp;#x3C;&amp;#x3C; “Stone game winner: ”
&amp;#x3C;&amp;#x3C; (win[N] ? “SK” : “CY”) &amp;#x3C;&amp;#x3C; “\n”;&lt;/bool&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// 2. Nim XOR&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;int k; cin &gt;&gt; k;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;long long xsum = 0;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;for (int i = 0; i &amp;#x3C; k; i++) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    long long p; cin &gt;&gt; p;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    xsum ^= p;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;cout &amp;#x3C;&amp;#x3C; &quot;Nim winner: &quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;     &amp;#x3C;&amp;#x3C; (xsum ? &quot;koosaga&quot; : &quot;cubelover&quot;) &amp;#x3C;&amp;#x3C; &quot;\\n&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;return 0;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;code&gt;,     },     {       language: &quot;python&quot;,       label: &quot;Python&quot;,       code: &lt;/code&gt;# 돌 게임 DP + Nim XOR
import sys
input = sys.stdin.readline&lt;/p&gt;
&lt;p&gt;N = int(input())
win = [False] * (N + 1)
for i in range(1, N + 1):
if (i &gt;= 1 and not win[i-1]) or (i &gt;= 3 and not win[i-3]):
win[i] = True
print(“SK” if win[N] else “CY”)&lt;/p&gt;
&lt;p&gt;k = int(input())
piles = list(map(int, input().split()))
xsum = 0
for p in piles:
xsum ^= p
print(“koosaga” if xsum else “cubelover”)&lt;code&gt;,     },     {       language: &quot;java&quot;,       label: &quot;Java&quot;,       code: &lt;/code&gt;import java.util.*;&lt;/p&gt;
&lt;p&gt;public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int N = Integer.parseInt(br.readLine());
boolean[] win = new boolean[N + 1];
for (int i = 1; i &amp;#x3C;= N; i++) {
if ((i &gt;= 1 &amp;#x26;&amp;#x26; !win[i-1]) || (i &gt;= 3 &amp;#x26;&amp;#x26; !win[i-3]))
win[i] = true;
}
System.out.println(win[N] ? “SK” : “CY”);&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    int k = Integer.parseInt(br.readLine());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    StringTokenizer st = new StringTokenizer(br.readLine());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    long xsum = 0;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for (int i = 0; i &amp;#x3C; k; i++)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        xsum ^= Long.parseLong(st.nextToken());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    System.out.println(xsum != 0 ? &quot;koosaga&quot; : &quot;cubelover&quot;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;기본&quot;,       input: &lt;/code&gt;5
2
3 7&lt;code&gt;,       output: &lt;/code&gt;Stone game winner: SK
Nim winner: cubelover&lt;code&gt;,     },     {       label: &quot;큰 값&quot;,       input: &lt;/code&gt;1000
4
1 2 3 4&lt;code&gt;,       output: &lt;/code&gt;Stone game winner: CY
Nim winner: koosaga`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;돌 게임 DP 시간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Nim XOR 시간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(K) (K: 더미 수)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N) 또는 O(1)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;h3 id=&quot;게임-패밀리&quot;&gt;게임 패밀리&lt;/h3&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;종류&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;설명&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;대표 문제&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Take-away&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;일정 개수 가져감&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;돌 게임&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Nim&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;여러 더미, XOR 결정&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;님 게임&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Misere Nim&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;마지막 돌 가져가는 사람이 짐&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;nim XOR 후 예외 처리&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Green Hackenbush&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;트리 절단 게임&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;하켄부시&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Dawson’s Kayles&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;볼링핀 제거&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;SG 이론 활용&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 id=&quot;응용&quot;&gt;응용&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;최적 전략 게임 (Minimax)&lt;/strong&gt;: 제로섬 게임에서 최적 행동.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;게임 트리 + DP&lt;/strong&gt;: 상태 공간이 작으면 완전 탐색 DP 로 족보 판정.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-xor-이해-오류&quot;&gt;1. XOR 이해 오류&lt;/h3&gt;
&lt;p&gt;XOR sum 이 0 이면 선공 패, != 0 이면 선공 승. misere 에서는 예외 (모든 더미가 1 이하일 때 반대).&lt;/p&gt;
&lt;h3 id=&quot;2-상태-정의-누락&quot;&gt;2. 상태 정의 누락&lt;/h3&gt;
&lt;p&gt;DP 정의 시 “현재 턴인 플레이어 기준” 임을 명확히. 실제 판정과 반대로 정의하는 실수.&lt;/p&gt;
&lt;h3 id=&quot;3-큰-n--큰-p&quot;&gt;3. 큰 N / 큰 P&lt;/h3&gt;
&lt;p&gt;돌 개수가 10^12 면 DP 불가. 패턴 찾아 O(1) 또는 SG 주기성 활용.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 9655&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;돌 게임&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;66.4%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/9600-9699/9655&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 9657&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;돌 게임 3&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;46.9%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/9600-9699/9657&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 11868&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;님 게임 2&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;80.3%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/11800-11899/11868&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 11694&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;님 게임&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;36.6%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/11600-11699/11694&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Sprague-Grundy Theorem|스프라그-그런디 정리]]&lt;/li&gt;
&lt;li&gt;[[DP|다이나믹 프로그래밍]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>game</category><category>game-theory</category><author>koa (김신건)</author></item><item><title>Hackenbush (하켄부시 게임)</title><link>https://shinkeonkim.com/wiki/algorithm/game/hackenbush/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/game/hackenbush/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Hackenbush&lt;/strong&gt; 는 John H. Conway 가 “On Numbers and Games” (1976) 에서 고안한 combinatorial game. 바닥에 연결된 여러 개의 색깔 가지 (빨강/파랑) 가 있고, Left (파랑) 는 파란 가지를, Right (빨강) 는 빨간 가지를 자른다. 바닥과 연결이 끊긴 부분은 사라진다. 마지막으로 자를 수 있는 플레이어가 이긴다. 각 위치는 &lt;strong&gt;surreal number&lt;/strong&gt; 로 값이 매겨진다.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;combinatorial game theory 에서 위치의 “값”을 어떻게 정의할 것인가? Conway 는 Hackenbush 를 통해 surreal number 를 직관적으로 설명했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;naive 접근&lt;/strong&gt;: minimax tree search. 위치 수가 기하급수적으로 증가.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심 통찰&lt;/strong&gt;: Blue-Red Hackenbush 의 각 위치는 surreal number 하나와 대응. 파란 가지 = Left 에게 유리 (+), 빨간 가지 = Right 에게 유리 (-). 가지가 갈라지는 구조는 {L|R} 꼴 surreal number.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;의의&lt;/strong&gt;: 모든 partizan game 의 값을 측정하는 기준을 제공.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;p&gt;Blue-Red Hackenbush 의 값은 각 edge 의 색과 구조로 결정:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;파란 가지 하나: +1 (Left 에게 유리)&lt;/li&gt;
&lt;li&gt;빨간 가지 하나: -1 (Right 에게 유리)&lt;/li&gt;
&lt;li&gt;chain 구조: 각 edge 의 값을 단순 합산. B-B-R = (+1) + (+1) + (-1) = +1.&lt;/li&gt;
&lt;li&gt;branching 구조: {L|R} 형식의 surreal number. L = Left 가 자를 수 있는 위치들의 최대값, R = Right 가 자를 수 있는 위치들의 최소값.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;surreal number 의 정의: {L|R} 에서 L &amp;#x3C; R 일 때 이는 유리수에 대응. 예: {0|} = 1, {|0} = -1, {0|1} = 1/2.&lt;/p&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;evaluate_chain(s):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    val = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for each char c in s:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if c == &apos;B&apos;: val += 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        else: val -= 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return val&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;winner(val):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if val &gt; 0: &quot;Left (Blue)&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if val &amp;#x3C; 0: &quot;Right (Red)&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if val == 0: &quot;Second player&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;branching_value(left_options, right_options):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # left_options = {L1, L2, ...}, right_options = {R1, R2, ...}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # simplest surreal number between max(L) and min(R)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return simplest_surreal(max(left_options), min(right_options))&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: `// Hackenbush chain value evaluation
#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
using ll = long long;&lt;/p&gt;
&lt;p&gt;struct Frac {
ll num, den;
Frac(ll n = 0, ll d = 1) : num(n), den(d) {
if (den &amp;#x3C; 0) { num = -num; den = -den; }
ll g = gcd(abs(num), abs(den));
num /= g; den /= g;
}
Frac operator+(const Frac&amp;#x26; o) const {
return Frac(num * o.den + o.num * den, den * o.den);
}
bool operator&gt;(const Frac&amp;#x26; o) const { return num * o.den &gt; o.num * den; }
bool operator&amp;#x3C;(const Frac&amp;#x26; o) const { return num * o.den &amp;#x3C; o.num * den; }
bool operator==(const Frac&amp;#x26; o) const { return num == o.num &amp;#x26;&amp;#x26; den == o.den; }
};&lt;/p&gt;
&lt;p&gt;int main() {
string s; cin &gt;&gt; s;
Frac val;
for (char c : s) {
if (c == ‘B’) val = val + Frac(1);
else if (c == ‘R’) val = val + Frac(-1);
}
cout &amp;#x3C;&amp;#x3C; “Position value: ” &amp;#x3C;&amp;#x3C; val.num &amp;#x3C;&amp;#x3C; ”/” &amp;#x3C;&amp;#x3C; val.den &amp;#x3C;&amp;#x3C; “\n”;
if (val &gt; Frac(0)) cout &amp;#x3C;&amp;#x3C; “Winner: Left (Blue)\n”;
else if (val &amp;#x3C; Frac(0)) cout &amp;#x3C;&amp;#x3C; “Winner: Right (Red)\n”;
else cout &amp;#x3C;&amp;#x3C; “Winner: Second player\n”;
return 0;
}&lt;code&gt;,     },     {       language: &quot;python&quot;,       label: &quot;Python&quot;,       code: &lt;/code&gt;from fractions import Fraction
import sys
s = sys.stdin.readline().strip()
val = Fraction(0, 1)
for c in s:
if c == ‘B’:
val += Fraction(1, 1)
elif c == ‘R’:
val -= Fraction(1, 1)
print(f”Position value: {val}”)
if val &gt; 0:
print(“Winner: Left (Blue)”)
elif val &amp;#x3C; 0:
print(“Winner: Right (Red)”)
else:
print(“Winner: Second player”)`,
},
]}
cases={[
{
label: “Blue 3개 chain”,
input: “BBB”,
output: “Position value: 3\nWinner: Left (Blue)”,
},
{
label: “혼합 chain”,
input: “BBR”,
output: “Position value: 1\nWinner: Left (Blue)”,
},
{
label: “Red 우세”,
input: “RRRB”,
output: “Position value: -2\nWinner: Right (Red)”,
},
{
label: “Zero game”,
input: “BR”,
output: “Position value: 0\nWinner: Second player”,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (chain 평가)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(1)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;가지 평가 (일반)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;PSPACE-hard&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;안정성&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;-&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Green Hackenbush&lt;/strong&gt;: impartial variant. 모든 edge 가 초록색 = 누구나 자를 수 있음. Grundy number 로 평가.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hackenbush Hotchpotch&lt;/strong&gt;: 빨강, 파랑, 초록 세 가지 색 혼합.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Surreal number 모델&lt;/strong&gt;: Hackenbush 위치는 surreal number 체계의 직관적 모델. Conway 의 “On Numbers and Games” 에서 이 게임으로 surreal number 를 설명.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Game theory 교육&lt;/strong&gt;: partizan game 의 기본 개념을 시각적으로 학습.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-branching-구조의-값&quot;&gt;1. Branching 구조의 값&lt;/h3&gt;
&lt;p&gt;단순 chain 의 값이 +1/-1 의 합인 반면, 가지가 갈라지면 {L|R} 형식의 surreal number 가 필요. 예: 파란 가지와 빨간 가지가 같은 높이에서 갈라지면 값은 {1|-1} = 0.&lt;/p&gt;
&lt;h3 id=&quot;2-zero-game-의-의미&quot;&gt;2. Zero game 의 의미&lt;/h3&gt;
&lt;p&gt;값이 0인 위치는 후수 승. 즉 먼저 자르는 쪽이 진다. “모서리” 위치는 항상 후수 승.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 11868&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;님 게임 2&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;(수집 안 됨)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/11800-11899/11868&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 13034&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;다각형 게임&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;(수집 안 됨)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/13000-13099/13034&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 16888&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;루트 님 게임&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;(수집 안 됨)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/16800-16899/16888&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Grundy Number]]&lt;/li&gt;
&lt;li&gt;[[Nim Game]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>game</category><category>hackenbush</category><author>koa (김신건)</author></item><item><title>스프라그-그런디 정리 (Sprague-Grundy)</title><link>https://shinkeonkim.com/wiki/algorithm/game/sprague-grundy/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/game/sprague-grundy/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;스프라그-그런디 정리 (Sprague-Grundy)&lt;/strong&gt; 는 모든 &lt;em&gt;impartial combinatorial game&lt;/em&gt; (두 플레이어가 같은 행동 가능, 무작위성 없음, 완전 정보) 이 Nim-heap 하나와 동등함을 증명하는 정리. 각 게임 상태는 &lt;strong&gt;Grundy number (nimber)&lt;/strong&gt; 로 표현되며, Grundy number 가 0 이면 패배 상태, 0 이 아니면 승리 상태.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;여러 impartial game 이 동시에 진행될 때 (e.g., Nim 의 여러 더미), 각각을 독립 분석 후 XOR 로 합성.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;naive&lt;/strong&gt;: 전체 상태 공간의 game tree 그리기. 상태 폭발.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SG 이론&lt;/strong&gt;: 각 부분 게임의 Grundy 수를 XOR. 모든 상태를 Nim 으로 환원.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;모든 impartial game 의 상태는 자연수 (Grundy number) 로 완전히 추상화되며, 게임의 합성은 XOR&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;h3 id=&quot;grundy-number-정의&quot;&gt;Grundy Number 정의&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;G(v) = mex({ G(u) | v -&gt; u })&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;mex: Minimum EXcluded non-negative integer.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;v -&gt; u&lt;/code&gt;: 상태 v 에서 한 번의 행동으로 도달 가능한 상태 u.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;G(v) = 0&lt;/strong&gt;: losing state (P-position).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;G(v) != 0&lt;/strong&gt;: winning state (N-position).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;게임-합성-nim-sum&quot;&gt;게임 합성 (Nim-Sum)&lt;/h3&gt;
&lt;p&gt;여러 독립 게임 &lt;code&gt;G1, G2, ..., Gk&lt;/code&gt; 가 동시에 진행될 때:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;G_total = G1 xor G2 xor ... xor Gk&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;G_total = 0 이면 후공 승, != 0 이면 선공 승.&lt;/p&gt;
&lt;h3 id=&quot;계산-예시-돌-게임-take-1-3-4&quot;&gt;계산 예시 (돌 게임, take {1, 3, 4})&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;G(0) = 0                  (terminal, no moves)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;G(1) = mex({G(0)}) = mex({0}) = 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;G(2) = mex({G(1)}) = mex({1}) = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;G(3) = mex({G(2), G(0)}) = mex({0, 0}) = 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;G(4) = mex({G(3), G(1), G(0)}) = mex({1, 1, 0}) = 2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;G(5) = mex({G(4), G(2), G(1)}) = mex({2, 0, 1}) = 3&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;compute_grundy(max_state, moves):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    G[0..max_state] = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for s = 1..max_state:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        reachable = empty set&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        for m in moves:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            if s &gt;= m:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                reachable.add(G[s - m])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        G[s] = mex(reachable)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return G&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;mex(S):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    g = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    while g in S: g++&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return g&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;# 게임 합성&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;xor_sum = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;for g in grundy_numbers_of_each_game:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    xor_sum ^= g&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;winner = &quot;First&quot; if xor_sum != 0 else &quot;Second&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: `// Grundy number DP + 게임 합성
#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;&lt;/p&gt;
&lt;p&gt;int mex(const vector&lt;bool&gt;&amp;#x26; seen) {
int g = 0;
while (g &amp;#x3C; (int)seen.size() &amp;#x26;&amp;#x26; seen[g]) g++;
return g;
}&lt;/bool&gt;&lt;/p&gt;
&lt;p&gt;int main() {
const int MAX = 1000;
vector&lt;int&gt; moves = {1, 3, 4};
vector&lt;int&gt; G(MAX + 1, 0);&lt;/int&gt;&lt;/int&gt;&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;for (int s = 1; s &amp;#x3C;= MAX; s++) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    vector&amp;#x3C;bool&gt; seen(MAX + 1, false);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for (int m : moves)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if (s &gt;= m) seen[G[s - m]] = true;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    G[s] = mex(seen);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;int N; cin &gt;&gt; N;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;if (G[N])&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    cout &amp;#x3C;&amp;#x3C; &quot;G(&quot; &amp;#x3C;&amp;#x3C; N &amp;#x3C;&amp;#x3C; &quot;) = &quot; &amp;#x3C;&amp;#x3C; G[N]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;         &amp;#x3C;&amp;#x3C; &quot; -&gt; First wins\\n&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;else&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    cout &amp;#x3C;&amp;#x3C; &quot;G(&quot; &amp;#x3C;&amp;#x3C; N &amp;#x3C;&amp;#x3C; &quot;) = 0 -&gt; Second wins\\n&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// 여러 게임 합성&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;int k; cin &gt;&gt; k;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;int xsum = 0;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;for (int i = 0; i &amp;#x3C; k; i++) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    int p; cin &gt;&gt; p;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    // Nim: Grundy = pile size&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    xsum ^= p;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;cout &amp;#x3C;&amp;#x3C; &quot;Nim XOR = &quot; &amp;#x3C;&amp;#x3C; xsum &amp;#x3C;&amp;#x3C; &quot; -&gt; &quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;     &amp;#x3C;&amp;#x3C; (xsum ? &quot;First&quot; : &quot;Second&quot;) &amp;#x3C;&amp;#x3C; &quot;\\n&quot;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;return 0;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;code&gt;,     },     {       language: &quot;python&quot;,       label: &quot;Python&quot;,       code: &lt;/code&gt;# Grundy number + Nim XOR
def mex(seen):
g = 0
while g &amp;#x3C; len(seen) and seen[g]:
g += 1
return g&lt;/p&gt;
&lt;p&gt;MAX = 1000
moves = [1, 3, 4]
G = [0] * (MAX + 1)
for s in range(1, MAX + 1):
seen = [False] * (MAX + 1)
for m in moves:
if s &gt;= m:
seen[G[s - m]] = True
G[s] = mex(seen)&lt;/p&gt;
&lt;p&gt;N = int(input())
if G[N]:
print(f”G({N}) = {G[N]} -&gt; First wins”)
else:
print(f”G({N}) = 0 -&gt; Second wins”)&lt;/p&gt;
&lt;p&gt;k = int(input())
piles = list(map(int, input().split()))
xsum = 0
for p in piles:
xsum ^= p
print(f”Nim XOR = {xsum} -&gt; {‘First’ if xsum else ‘Second’}“)&lt;code&gt;,     },     {       language: &quot;java&quot;,       label: &quot;Java&quot;,       code: &lt;/code&gt;import java.util.*;&lt;/p&gt;
&lt;p&gt;public class Main {
static int mex(boolean[] seen) {
int g = 0;
while (g &amp;#x3C; seen.length &amp;#x26;&amp;#x26; seen[g]) g++;
return g;
}&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;public static void main(String[] args) throws IOException {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    int MAX = 1000;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    int[] moves = {1, 3, 4};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    int[] G = new int[MAX + 1];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for (int s = 1; s &amp;#x3C;= MAX; s++) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        boolean[] seen = new boolean[MAX + 1];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        for (int m : moves)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            if (s &gt;= m) seen[G[s - m]] = true;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        G[s] = mex(seen);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    int N = Integer.parseInt(br.readLine());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if (G[N] != 0)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        System.out.println(&quot;G(&quot; + N + &quot;) = &quot; + G[N] + &quot; -&gt; First wins&quot;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    else&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        System.out.println(&quot;G(&quot; + N + &quot;) = 0 -&gt; Second wins&quot;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    int k = Integer.parseInt(br.readLine());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    StringTokenizer st = new StringTokenizer(br.readLine());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    int xsum = 0;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for (int i = 0; i &amp;#x3C; k; i++)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        xsum ^= Integer.parseInt(st.nextToken());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    System.out.println(&quot;Nim XOR = &quot; + xsum + &quot; -&gt; &quot; + (xsum != 0 ? &quot;First&quot; : &quot;Second&quot;));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;기본&quot;,       input: &lt;/code&gt;5
3
1 2 3&lt;code&gt;,       output: &lt;/code&gt;G(5) = 3 -&gt; First wins
Nim XOR = 0 -&gt; Second&lt;code&gt;,     },     {       label: &quot;패배 상태&quot;,       input: &lt;/code&gt;2
2
3 3&lt;code&gt;,       output: &lt;/code&gt;G(2) = 0 -&gt; Second wins
Nim XOR = 0 -&gt; Second`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Grundy DP 시간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(S x M) (S: 상태 수, M: 행동 수)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;게임 합성 시간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(K) (K: 게임 수)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(S) 또는 O(1)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;h3 id=&quot;응용-분야&quot;&gt;응용 분야&lt;/h3&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;유형&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;특징&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Nim 계열&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Grundy = pile size&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Take-away 계열&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;행동 집합에 따라 Grundy 주기적&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;게임 분할&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;한 행동이 게임을 두 개로 분할&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Green Hackenbush&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;트리 절단, Grundy = tree height XOR&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 id=&quot;sg--dp&quot;&gt;SG + DP&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;다각형 게임 (BOJ 13034)&lt;/strong&gt;: N-각형 대각선 긋기. 분할 영역의 Grundy XOR.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;격자 게임&lt;/strong&gt;: 2차원 격자 말 이동. 각 행/열 독립 후 XOR.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-mex-계산-실수&quot;&gt;1. mex 계산 실수&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;mex&lt;/code&gt; 는 집합에 없는 가장 작은 0 이상 정수. &lt;code&gt;0&lt;/code&gt;부터 순차 확인.&lt;/p&gt;
&lt;h3 id=&quot;2-게임-합성-착각&quot;&gt;2. 게임 합성 착각&lt;/h3&gt;
&lt;p&gt;독립적일 때만 XOR 합성 성립. 상태가 연동되면 각각 SG 따로 계산 불가.&lt;/p&gt;
&lt;h3 id=&quot;3-큰-상태-공간&quot;&gt;3. 큰 상태 공간&lt;/h3&gt;
&lt;p&gt;MAX 10^6 이상이면 DP 불가. 주기성 찾거나 수식 도출.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 11868&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;님 게임 2&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;80.3%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/11800-11899/11868&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 9660&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;돌 게임 6&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;53.5%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/9600-9699/9660&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 13034&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;다각형 게임&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;63.4%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/13000-13099/13034&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 16895&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;님 게임 3&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;67.7%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/16800-16899/16895&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Game Theory|게임 이론]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>game</category><category>sprague-grundy</category><author>koa (김신건)</author></item><item><title>안정 결혼 문제 (Stable Marriage)</title><link>https://shinkeonkim.com/wiki/algorithm/game/stable-marriage/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/game/stable-marriage/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;안정 결혼 문제 (Stable Marriage Problem)&lt;/strong&gt; 는 N 명의 남성과 N 명의 여성이 각자 선호도 목록을 가지고 있을 때, 모든 쌍이 &lt;strong&gt;안정적 (stable)&lt;/strong&gt; 인 매칭을 찾는 문제. Gale-Shapley 알고리즘 (1962, Nobel 2012) 은 O(N^2) 에 항상 안정 매칭을 찾는다. 안정적 = 어떤 (m, w) 도 현재 자신의 짝보다 상대를 더 선호하지 않는 상태.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;naive&lt;/strong&gt;: 모든 매칭 N! 가지를 검사 -&gt; O(N!), N=10 에도 불가능.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심 통찰&lt;/strong&gt;: &lt;em&gt;제안 (proposal)&lt;/em&gt; 과정을 통해 불안정 쌍을 반복 제거. proposer 쪽이 최적의 결과를 얻는다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gale-Shapley&lt;/strong&gt;: 매 라운드 미매칭 proposer 가 가장 선호하는 상대에게 제안. reject 되면 다음 선호로. 최대 N^2 라운드 이내 종료.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;실제 활용: 전국 레지던트 매칭 (NRMP), 학교 배정, 장기 매칭.&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;p&gt;invariant: &lt;em&gt;proposer 의 매칭은 단조 하락&lt;/em&gt; (점점 덜 선호하는 쪽으로). &lt;em&gt;receiver 의 매칭은 단조 상승&lt;/em&gt; (점점 더 선호하는 쪽으로).&lt;/p&gt;
&lt;p&gt;프로세스:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;모든 남성이 첫 번째 선호 여성에게 제안&lt;/li&gt;
&lt;li&gt;여성은 가장 선호하는 제안자와 약혼 (나머지 reject)&lt;/li&gt;
&lt;li&gt;reject 된 남성은 다음 선호 여성에게 제안&lt;/li&gt;
&lt;li&gt;여성은 현재 약혼자와 새 제안자 중 더 선호하는 쪽 선택&lt;/li&gt;
&lt;li&gt;모두 매칭될 때까지 반복 -&gt; 최대 O(N^2) 라운드&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Gale-Shapley(M, W, menPref, womenPref):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    womanMatch[1..N] = -1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    manNext[1..N] = 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    freeMen = queue of all men&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    while freeMen not empty:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        m = freeMen.pop()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        w = menPref[m][manNext[m]++]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if womanMatch[w] == -1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            womanMatch[w] = m&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        else:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            m2 = womanMatch[w]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            if womenPref[w][m] &amp;#x3C; womenPref[w][m2]:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                womanMatch[w] = m&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                freeMen.push(m2)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            else:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                freeMen.push(m)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return womanMatch  (or manMatch from it)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// Gale-Shapley O(N^2) stable matching #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; int main() {     int N; cin &gt;&gt; N;     vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; menPref(N, vector&amp;#x3C;int&gt;(N));     vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; womenPref(N, vector&amp;#x3C;int&gt;(N));     for (int i = 0; i &amp;#x3C; N; i++)         for (int j = 0; j &amp;#x3C; N; j++)             cin &gt;&gt; menPref[i][j];     for (int i = 0; i &amp;#x3C; N; i++)         for (int j = 0; j &amp;#x3C; N; j++)             cin &gt;&gt; womenPref[i][j];     // 여성의 선호도 순위 lookup table     vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; rank(N, vector&amp;#x3C;int&gt;(N));     for (int w = 0; w &amp;#x3C; N; w++)         for (int r = 0; r &amp;#x3C; N; r++)             rank[w][womenPref[w][r]] = r;     vector&amp;#x3C;int&gt; matchW(N, -1), nxt(N, 0);     queue&amp;#x3C;int&gt; q;     for (int m = 0; m &amp;#x3C; N; m++) q.push(m);     while (!q.empty()) {         int m = q.front(); q.pop();         int w = menPref[m][nxt[m]++];         if (matchW[w] == -1) {             matchW[w] = m;         } else {             int m2 = matchW[w];             if (rank[w][m] &amp;#x3C; rank[w][m2]) {                 matchW[w] = m;                 q.push(m2);             } else {                 q.push(m);             }         }     }     vector&amp;#x3C;int&gt; matchM(N);     for (int w = 0; w &amp;#x3C; N; w++) matchM[matchW[w]] = w;     for (int m = 0; m &amp;#x3C; N; m++)         cout &amp;#x3C;&amp;#x3C; m &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; matchM[m] &amp;#x3C;&amp;#x3C; &quot;\\n&quot;;     return 0; }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: `def gale_shapley(men_pref, women_pref):
N = len(men_pref)
rank = [[0] * N for _ in range(N)]
for w in range(N):
for r, m in enumerate(women_pref[w]):
rank[w][m] = r
match_w = [-1] * N
nxt = [0] * N
free_men = list(range(N))
while free_men:
m = free_men.pop(0)
w = men_pref[m][nxt[m]]
nxt[m] += 1
if match_w[w] == -1:
match_w[w] = m
else:
m2 = match_w[w]
if rank[w][m] &amp;#x3C; rank[w][m2]:
match_w[w] = m
free_men.append(m2)
else:
free_men.append(m)
match_m = [0] * N
for w, m in enumerate(match_w):
match_m[m] = w
return match_m&lt;/p&gt;
&lt;p&gt;N = int(input())
men_pref = [list(map(int, input().split())) for _ in range(N)]
women_pref = [list(map(int, input().split())) for _ in range(N)]
match = gale_shapley(men_pref, women_pref)
for m, w in enumerate(match):
print(m, w)&lt;code&gt;,     },     {       language: &quot;java&quot;,       label: &quot;Java&quot;,       code: &lt;/code&gt;import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int N = sc.nextInt();
int[][] menPref = new int[N][N];
int[][] womenPref = new int[N][N];
for (int i = 0; i &amp;#x3C; N; i++)
for (int j = 0; j &amp;#x3C; N; j++)
menPref[i][j] = sc.nextInt();
for (int i = 0; i &amp;#x3C; N; i++)
for (int j = 0; j &amp;#x3C; N; j++)
womenPref[i][j] = sc.nextInt();
int[][] rank = new int[N][N];
for (int w = 0; w &amp;#x3C; N; w++)
for (int r = 0; r &amp;#x3C; N; r++)
rank[w][womenPref[w][r]] = r;
int[] matchW = new int[N];
Arrays.fill(matchW, -1);
int[] nxt = new int[N];
Queue&lt;integer&gt; q = new LinkedList&amp;#x3C;&gt;();
for (int m = 0; m &amp;#x3C; N; m++) q.add(m);
while (!q.isEmpty()) {
int m = q.poll();
int w = menPref[m][nxt[m]++];
if (matchW[w] == -1) {
matchW[w] = m;
} else {
int m2 = matchW[w];
if (rank[w][m] &amp;#x3C; rank[w][m2]) {
matchW[w] = m;
q.add(m2);
} else {
q.add(m);
}
}
}
int[] matchM = new int[N];
for (int w = 0; w &amp;#x3C; N; w++) matchM[matchW[w]] = w;
for (int m = 0; m &amp;#x3C; N; m++)
System.out.println(m + ” ” + matchM[m]);
}
}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;N=4 기본&quot;,       input: &lt;/code&gt;4
0 1 2 3
1 2 3 0
2 3 0 1
3 0 1 2
0 1 2 3
1 2 3 0
2 3 0 1
3 0 1 2&lt;code&gt;,       output: &lt;/code&gt;0 0
1 1
2 2
3 3`,
},
]}
/&gt;&lt;/integer&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (최악)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N^2)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (평균)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N^2)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N^2) (선호도 저장)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;안정성&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;항상 안정 매칭 보장&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Proposer-optimal / Receiver-pessimal&lt;/strong&gt;: proposer 가 최적, receiver 가 최악의 안정 매칭. 역할을 바꾸면 반대 결과.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stable Roommate&lt;/strong&gt;: 1-set 안정 매칭. 항상 해가 존재하지 않음 (odd cycle 조건).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SM with Ties / Incomplete lists&lt;/strong&gt;: 선호도에 동점 또는 불완전 목록. NP-hard 변형 존재.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;병원 레지던트 매칭 (NRMP)&lt;/strong&gt;: 미국 의사 매칭에 Gale-Shapley 변형 사용.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;양방향 시장&lt;/strong&gt;: 학교 배정, 장기 매칭, 공공 서비스 배치.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-전략적-조작&quot;&gt;1. 전략적 조작&lt;/h3&gt;
&lt;p&gt;proposer 가 진정한 선호도를 숨기면 더 유리한 결과를 얻을 수 있음 (전략적 제안). receiver 는 전략적 조작이 어려움 (receiver-pessimal 속성).&lt;/p&gt;
&lt;h3 id=&quot;2-stable-matching-의-유일성&quot;&gt;2. Stable matching 의 유일성&lt;/h3&gt;
&lt;p&gt;안정 매칭이 유일하지 않음. Gale-Shapley 는 proposer-optimal 을 찾으며, receiver-pessimal 과 다른 해가 존재할 수 있음.&lt;/p&gt;
&lt;h3 id=&quot;3-자료구조-선택&quot;&gt;3. 자료구조 선택&lt;/h3&gt;
&lt;p&gt;여성의 순위 lookup table 없이 매번 womenPref[w] 를 선형 탐색하면 O(N^3). 반드시 rank[w][m] table 을 precompute.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 1348&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;주차장&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;(수집 안 됨)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1300-1399/1348&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 1671&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;상어의 저녁식사&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;(수집 안 됨)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1600-1699/1671&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2188&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;축사 배정&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;(수집 안 됨)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2100-2199/2188&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Bipartite Matching]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>game</category><category>stable-marriage</category><author>koa (김신건)</author></item><item><title>각도 정렬 (Angle Sorting)</title><link>https://shinkeonkim.com/wiki/algorithm/geometry/angle-sorting/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/geometry/angle-sorting/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;각도 정렬 (Angle Sorting / Polar Sort)&lt;/strong&gt; 은 2차원 평면 위의 점들을 &lt;em&gt;기준점에 대한 극각 (polar angle)&lt;/em&gt; 순서로 정렬하는 기법. atan2 함수를 이용한 실수 비교 또는 CCW (외적) 를 이용한 정수 비교로 구현. [[Convex Hull|컨벡스 헐]]의 Graham scan, 점들을 단순 다각형으로 연결하는 문제 등에서 핵심 구성 요소.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;N 개의 점을 기준점 O 를 기준으로 반시계 방향 (또는 시계 방향)으로 정렬해야 할 때.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Naive (atan2)&lt;/strong&gt;: 각 점에 대해 &lt;code&gt;atan2(y, x)&lt;/code&gt; 를 계산해 실수 키로 정렬. O(N log N). 간단하지만 부동소수점 오차와 사분면 경계 처리 필요.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CCW comparator&lt;/strong&gt;: &lt;code&gt;ccw(O, a, b) &gt; 0&lt;/code&gt; 만으로 정렬. 모든 연산이 정수. strict weak ordering 을 만족시키려면 사분면 구분이 필수.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;기준점 O 를 기준으로 점들을 반평면 (upper/lower) 으로 나누고, 같은 반평면 내에서만 CCW 비교&lt;/em&gt; 하면 strict weak ordering 이 성립하며, atan2 없이 정수 연산만으로 정확한 각도 정렬이 가능.&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;h3 id=&quot;atan2-방식&quot;&gt;atan2 방식&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;sort by atan2(y - O.y, x - O.x)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;장점: 직관적, 구현 단순&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;단점: double 오차, atan2 비용, inf/nan 처리&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;ccw-comparator-방식&quot;&gt;CCW comparator 방식&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;bool cmp(a, b):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    // 1. 사분면이 다르면 사분면 기준 정렬&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if quadrant(a) != quadrant(b):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        return quadrant(a) &amp;#x3C; quadrant(b)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    // 2. 같은 사분면이면 CCW&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if ccw(O, a, b) != 0:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        return ccw(O, a, b) &gt; 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    // 3. 각도가 같으면 거리순&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return dist(O, a) &amp;#x3C; dist(O, b)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;사분면-판정&quot;&gt;사분면 판정&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;quadrant(p):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if p.y == 0 and p.x &gt;= 0: return 0   // +x축&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if p.y &gt; 0 or (p.y == 0 and p.x &amp;#x3C; 0): return 1  // upper half&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return 2  // lower half (including -x axis and negative y)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;또는 구사과의 관용 코드:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;if (half(a) != half(b)) return half(a) &amp;#x3C; half(b)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;if (ccw(a, b) != 0) return ccw(a, b) &gt; 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;return dist2(a) &amp;#x3C; dist2(b)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;여기서 &lt;code&gt;half(p) = (p.y &gt; 0) or (p.y == 0 and p.x &amp;#x3C; 0)&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;INPUT: points P[0..N-1], origin O (default 0,0)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;OUTPUT: P sorted by polar angle (CCW)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;1. 각 점 P[i] 에 대해 quadrant = half(P[i] - O) 계산&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;2. sort P by key:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   a. quadrant first (upper half &amp;#x3C; lower half)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   b. if same quadrant: ccw(O, a, b) &gt; 0 (반시계 우선)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   c. if collinear: euclidean distance squared ascending&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// CCW 기반 각도 정렬 (구사과 스타일) #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; using ll = long long; struct Point { ll x, y; }; int ccw(Point a, Point b, Point c) {     ll ret = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);     if (ret &gt; 0) return 1;     if (ret &amp;#x3C; 0) return -1;     return 0; } ll dist2(Point a, Point b) {     ll dx = a.x - b.x, dy = a.y - b.y;     return dx * dx + dy * dy; } // upper half = y&gt;0 or (y==0 &amp;#x26;&amp;#x26; x&amp;#x3C;0) bool half(Point p) {     return p.y &gt; 0 || (p.y == 0 &amp;#x26;&amp;#x26; p.x &amp;#x3C; 0); } int main() {     int n; cin &gt;&gt; n;     vector&amp;#x3C;Point&gt; P(n);     for (auto&amp;#x26; p : P) cin &gt;&gt; p.x &gt;&gt; p.y;     Point O = P[0];  // 기준점 (보통 최하단 좌측)     // O 가 첫 원소가 되도록 swap 한 후 나머지만 정렬     // 여기서는 O 를 기준으로 모두 정렬     sort(P.begin(), P.end(), [&amp;#x26;](Point a, Point b) {         if (half(a) != half(b)) return half(a) &gt; half(b);         if (ccw(O, a, b) != 0) return ccw(O, a, b) &gt; 0;         return dist2(O, a) &amp;#x3C; dist2(O, b);     });     for (auto&amp;#x26; p : P)         cout &amp;#x3C;&amp;#x3C; p.x &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; p.y &amp;#x3C;&amp;#x3C; &quot;\\n&quot;; }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: `# 각도 정렬 (CCW comparator)
import sys
from functools import cmp_to_key
input = sys.stdin.readline&lt;/p&gt;
&lt;p&gt;def ccw(ax, ay, bx, by, cx, cy):
return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax)&lt;/p&gt;
&lt;p&gt;def half(x, y):
return y &gt; 0 or (y == 0 and x &amp;#x3C; 0)&lt;/p&gt;
&lt;p&gt;def angle_cmp(O):
ox, oy = O
def cmp(a, b):
ax, ay = a; bx, by = b
ha = half(ax - ox, ay - oy)
hb = half(bx - ox, by - oy)
if ha != hb: return -1 if ha &gt; hb else 1
c = ccw(ox, oy, ax, ay, bx, by)
if c != 0: return -1 if c &gt; 0 else 1
da = (ax - ox)**2 + (ay - oy)**2
db = (bx - ox)**2 + (by - oy)**2
return -1 if da &amp;#x3C; db else 1 if da &gt; db else 0
return cmp&lt;/p&gt;
&lt;p&gt;n = int(input())
P = [tuple(map(int, input().split())) for _ in range(n)]
O = P[0]
P.sort(key=cmp_to_key(angle_cmp(O)))
for x, y in P:
print(x, y)&lt;code&gt;,     },     {       language: &quot;java&quot;,       label: &quot;Java&quot;,       code: &lt;/code&gt;// 각도 정렬 (CCW comparator)&lt;/p&gt;
&lt;p&gt;public class Main {
static class Point {
long x, y;
Point(long x, long y) { this.x = x; this.y = y; }
}
static long ccw(Point a, Point b, Point c) {
return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
}
static boolean half(Point p) {
return p.y &gt; 0 || (p.y == 0 &amp;#x26;&amp;#x26; p.x &amp;#x3C; 0);
}
static long dist2(Point a, Point b) {
long dx = a.x - b.x, dy = a.y - b.y;
return dx * dx + dy * dy;
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
Point[] P = new Point[n];
for (int i = 0; i &amp;#x3C; n; i++) {
StringTokenizer st = new StringTokenizer(br.readLine());
P[i] = new Point(Long.parseLong(st.nextToken()),
Long.parseLong(st.nextToken()));
}
Point O = P[0];
Arrays.sort(P, (a, b) -&gt; {
boolean ha = half(new Point(a.x - O.x, a.y - O.y));
boolean hb = half(new Point(b.x - O.x, b.y - O.y));
if (ha != hb) return ha ? -1 : 1;
long c = ccw(O, a, b);
if (c != 0) return c &gt; 0 ? -1 : 1;
return Long.compare(dist2(O, a), dist2(O, b));
});
StringBuilder sb = new StringBuilder();
for (Point p : P) sb.append(p.x).append(’ ‘).append(p.y).append(‘\n’);
System.out.print(sb);
}
}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;기본 예제&quot;,       input: &quot;4\n0 0\n1 0\n0 1\n-1 0&quot;,       output: &lt;/code&gt;0 0
0 1
-1 0
1 0`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;정렬 시간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(N log N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;CCW 비용&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(1) (정수 곱셈 2회)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(N)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Graham scan (Convex Hull)&lt;/strong&gt;: 최하단-최좌측 점 기준 각도 정렬 후 스택으로 CCW &gt; 0 인 점만 유지.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;단순 다각형 생성 (BOJ 3679)&lt;/strong&gt;: 각도 정렬로 항상 교차 없는 star-shaped 다각형.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;시계 정렬&lt;/strong&gt;: CCW 반환값 부호 반전.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;중심 기준 정렬&lt;/strong&gt;: CCW comparator 의 O 만 변경.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;strict weak ordering&lt;/strong&gt;: &lt;code&gt;ccw(O, a, b) &gt; 0&lt;/code&gt; 만 반환하면 non-transitive. 사분면 분할 필수.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;기준점과 동일 점&lt;/strong&gt;: O 와 같은 점은 첫 번째로 분리.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Collinear&lt;/strong&gt;: 각도가 같으면 거리순 정렬. 용도에 따라 가까운/먼 순 선택.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;atan2 문제&lt;/strong&gt;: &lt;code&gt;[-pi, pi]&lt;/code&gt; 반환 → &lt;code&gt;[0, 2pi)&lt;/code&gt; 변환 필요. double 비교가 strict weak ordering 깨뜨릴 수 있음.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 1708&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;볼록 껍질&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;28.116%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1700-1799/1708&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 3679&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;단순 다각형&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;26.233%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/3600-3699/3679&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2699&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;격자점 컨벡스헐&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;57.516%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2600-2699/2699&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Convex Hull|볼록 껍질]]&lt;/li&gt;
&lt;li&gt;[[CCW|CCW (시계/반시계 판정)]]&lt;/li&gt;
&lt;li&gt;[[Point in Convex Polygon|볼록 다각형 내부 점 판정]]&lt;/li&gt;
&lt;li&gt;[[Point in Polygon|다각형 내부 점 판정]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>geometry</category><category>angle-sorting</category><author>koa (김신건)</author></item><item><title>볼록 껍질 (Convex Hull)</title><link>https://shinkeonkim.com/wiki/algorithm/geometry/convex-hull/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/geometry/convex-hull/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;볼록 껍질 (Convex Hull)&lt;/strong&gt; 은 주어진 점 집합 P 를 모두 포함하는 &lt;strong&gt;최소 크기의 볼록 다각형&lt;/strong&gt;. 즉 P 의 어떤 점도 다각형 외부에 있지 않고, 다각형의 꼭짓점은 P 의 일부 점.&lt;/p&gt;
&lt;p&gt;표기: &lt;code&gt;CH(P)&lt;/code&gt;&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;N 개의 2D 점이 흩어져 있을 때 이들을 모두 감싸는 최소 볼록 다각형을 찾는다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Naive&lt;/strong&gt;: 모든 점 쌍에 대해 직선을 긋고, 모든 점이 한쪽에 있는지 검사. O(N^3).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Graham scan (O(N log N))&lt;/strong&gt;: 각도 정렬 후 스택.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Andrew’s monotone chain (O(N log N))&lt;/strong&gt;: x 좌표 정렬 후 상단 / 하단 hull 따로 구성. 구현이 더 간결.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;볼록 껍질은 볼록 다각형의 모든 꼭짓점이 CCW 방향을 만족한다. 스택에 넣고 CCW 조건이 깨지면 pop.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;h3 id=&quot;ccw-판정&quot;&gt;CCW 판정&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;ccw(a, b, c) = (b - a) x (c - a)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;             = (b.x - a.x)*(c.y - a.y) - (b.y - a.y)*(c.x - a.x)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&gt; 0  -&gt; 반시계 (좌회전)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&amp;#x3C; 0  -&gt; 시계 (우회전)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;= 0  -&gt; 일직선&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;볼록 껍질에서는 &lt;strong&gt;모든 인접 세 점이 일관된 방향 (시계 또는 반시계)&lt;/strong&gt; 을 유지.&lt;/p&gt;
&lt;h3 id=&quot;andrews-monotone-chain-가장-널리-사용&quot;&gt;Andrew’s Monotone Chain (가장 널리 사용)&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;1. 점들을 x (같으면 y) 오름차순 정렬          // O(N log N)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;2. Lower hull: 왼-&gt;오 sweep 하면서 ccw &amp;#x3C;= 0 이면 pop&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;3. Upper hull: 오-&gt;왼 sweep 하면서 ccw &amp;#x3C;= 0 이면 pop&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;4. 합치기 (중복 끝점 제거)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;invariant: 스택의 마지막 두 점과 새 점이 항상 ccw &gt; 0 (또는 &amp;#x3C; 0) 을 만족.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;andrew_monotone_chain(P):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    sort(P)                         // x, y 기준&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    lower = []&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for p in P:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        while len(lower) &gt;= 2 and ccw(lower[-2], lower[-1], p) &amp;#x3C;= 0:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            lower.pop()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        lower.append(p)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    upper = []&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for p in reversed(P):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        while len(upper) &gt;= 2 and ccw(upper[-2], upper[-1], p) &amp;#x3C;= 0:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            upper.pop()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        upper.append(p)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return lower + upper[1:-1]      // 끝점 중복 제거&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// Andrew&apos;s monotone chain O(N log N) #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; using ll = long long; struct Point { ll x, y; }; ll ccw(Point a, Point b, Point c) {     return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x); } vector&amp;#x3C;Point&gt; convex_hull(vector&amp;#x3C;Point&gt;&amp;#x26; p) {     sort(p.begin(), p.end(), [](auto&amp;#x26; a, auto&amp;#x26; b) {         return a.x &amp;#x3C; b.x || (a.x == b.x &amp;#x26;&amp;#x26; a.y &amp;#x3C; b.y);     });     vector&amp;#x3C;Point&gt; hull;     auto build = [&amp;#x26;](int start, int end, int sign) {         for (int i = start; i != end; i += (end - start &gt; 0 ? 1 : -1)) {             while ((int)hull.size() - start &gt;= 2) {                 auto&amp;#x26; a = hull[hull.size() - 2];                 auto&amp;#x26; b = hull.back();                 if (ccw(a, b, p[i]) * sign &gt; 0) break;                 hull.pop_back();             }             hull.push_back(p[i]);         }     };     build(0, p.size(), 1);           // lower     int lower_sz = hull.size();     build(p.size() - 2, -1, 1);      // upper (끝점 제외)     hull.pop_back();                 // 시작점 중복 제거     return hull; } int main() {     vector&amp;#x3C;Point&gt; pts = {{0,0},{1,1},{2,0},{1,-1},{0,1}};     auto hull = convex_hull(pts);     cout &amp;#x3C;&amp;#x3C; &quot;Convex Hull:\\n&quot;;     for (auto&amp;#x26; p : hull)         cout &amp;#x3C;&amp;#x3C; &quot;(&quot; &amp;#x3C;&amp;#x3C; p.x &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; p.y &amp;#x3C;&amp;#x3C; &quot;)\\n&quot;; }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: `# Andrew’s monotone chain O(N log N)
def ccw(ax, ay, bx, by, cx, cy):
return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax)&lt;/p&gt;
&lt;p&gt;def convex_hull(points):
points = sorted(set(points))    # 중복 제거 + 정렬
if len(points) &amp;#x3C;= 1: return points
lower = []
for p in points:
while len(lower) &gt;= 2 and ccw(*lower[-2], *lower[-1], *p) &amp;#x3C;= 0:
lower.pop()
lower.append(p)
upper = []
for p in reversed(points):
while len(upper) &gt;= 2 and ccw(*upper[-2], *upper[-1], *p) &amp;#x3C;= 0:
upper.pop()
upper.append(p)
return lower[:-1] + upper[:-1]&lt;/p&gt;
&lt;p&gt;pts = [(0,0), (1,1), (2,0), (1,-1), (0,1)]
hull = convex_hull(pts)
print(“Convex Hull:”)
for x, y in hull:
print(f”({x}, {y})“)&lt;code&gt;,     },     {       language: &quot;java&quot;,       label: &quot;Java&quot;,       code: &lt;/code&gt;// Andrew’s monotone chain O(N log N)&lt;/p&gt;
&lt;p&gt;public class Main {
static class Point implements Comparable&lt;point&gt; {
long x, y;
Point(long x, long y) { this.x = x; this.y = y; }
public int compareTo(Point o) {
return x != o.x ? Long.compare(x, o.x) : Long.compare(y, o.y);
}
}
static long ccw(Point a, Point b, Point c) {
return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
}
static List&lt;point&gt; convexHull(List&lt;point&gt; pts) {
int n = pts.size();
if (n &amp;#x3C;= 1) return pts;
Collections.sort(pts);
List&lt;point&gt; hull = new ArrayList&amp;#x3C;&gt;();
// lower
for (int i = 0; i &amp;#x3C; n; i++) {
while (hull.size() &gt;= 2 &amp;#x26;&amp;#x26; ccw(hull.get(hull.size()-2),
hull.get(hull.size()-1), pts.get(i)) &amp;#x3C;= 0) hull.remove(hull.size()-1);
hull.add(pts.get(i));
}
int lowerSz = hull.size();
// upper
for (int i = n - 2; i &gt;= 0; i—) {
while (hull.size() &gt; lowerSz &amp;#x26;&amp;#x26; ccw(hull.get(hull.size()-2),
hull.get(hull.size()-1), pts.get(i)) &amp;#x3C;= 0) hull.remove(hull.size()-1);
hull.add(pts.get(i));
}
hull.remove(hull.size()-1);  // 시작점 중복 제거
return hull;
}
public static void main(String[] args) {
List&lt;point&gt; pts = Arrays.asList(
new Point(0,0), new Point(1,1), new Point(2,0),
new Point(1,-1), new Point(0,1));
List&lt;point&gt; hull = convexHull(pts);
System.out.println(“Convex Hull:”);
for (Point p : hull) System.out.println(”(” + p.x + ”, ” + p.y + ”)”);
}
}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;기본 예제&quot;,       input: ``,       output: &lt;/code&gt;Convex Hull:
(0, 0)
(0, 1)
(1, 1)
(2, 0)
(1, -1)`,
},
]}
/&gt;&lt;/point&gt;&lt;/point&gt;&lt;/point&gt;&lt;/point&gt;&lt;/point&gt;&lt;/point&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (Andrew / Graham)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N log N) (정렬이 지배)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (정렬 후)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N) (스택 sweep)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N) (결과 hull)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;안정성&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;- (유일 결정)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;동일 점 (중복) 은 정렬 후 제거. Collinear (일직선) 점은 hull 에 포함할지 말지 선택.&lt;/p&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;h3 id=&quot;1-graham-scan&quot;&gt;1. Graham Scan&lt;/h3&gt;
&lt;p&gt;각도 정렬 (atan2) 후 같은 로직. Andrew 가 &lt;code&gt;ssqrt&lt;/code&gt; 없고 실수 오차가 없어 더 선호됨.&lt;/p&gt;
&lt;h3 id=&quot;2-convex-hull-trick-cht&quot;&gt;2. Convex Hull Trick (CHT)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;y = m*x + b&lt;/code&gt; 직선 중 특정 x 에서 최댓값. Convex hull (lower envelope) 과 동형.&lt;/p&gt;
&lt;h3 id=&quot;3-동적-convex-hull&quot;&gt;3. 동적 Convex Hull&lt;/h3&gt;
&lt;p&gt;점이 추가/삭제될 때 hull 유지. O(log N) 갱신. (Overmars / Van Leeuwen 트리)&lt;/p&gt;
&lt;h3 id=&quot;4-3d-convex-hull&quot;&gt;4. 3D Convex Hull&lt;/h3&gt;
&lt;p&gt;QuickHull O(N log N). 결과는 삼각형 메시 (face).&lt;/p&gt;
&lt;h3 id=&quot;5-rotating-calipers-최근접-점-쌍-최대-면적-삼각형&quot;&gt;5. Rotating Calipers, 최근접 점 쌍, 최대 면적 삼각형&lt;/h3&gt;
&lt;p&gt;hull 위에서 O(N) 으로 각종 최적화.&lt;/p&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-collinear-처리&quot;&gt;1. Collinear 처리&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ccw &amp;#x3C;= 0&lt;/code&gt; 조건으로 collinear 점을 pop 하면 hull 변 가장자리 위의 점이 제외됨. 모든 collinear 점을 포함하려면 &lt;code&gt;ccw &amp;#x3C; 0&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;2-x-정렬만으로는-부족&quot;&gt;2. x 정렬만으로는 부족&lt;/h3&gt;
&lt;p&gt;x 가 같으면 y 정렬 필수. 안 하면 vertical line 에서 잘못된 hull.&lt;/p&gt;
&lt;h3 id=&quot;3-integer-overflow&quot;&gt;3. Integer overflow&lt;/h3&gt;
&lt;p&gt;좌표 &lt;code&gt;±10^9&lt;/code&gt; 에서 &lt;code&gt;(b.x - a.x) * (c.y - a.y)&lt;/code&gt; 는 &lt;code&gt;±4*10^18&lt;/code&gt; 까지 가능 -&gt; C++ &lt;code&gt;long long&lt;/code&gt; (64-bit). &lt;code&gt;__int128&lt;/code&gt; 이 필요할 수 있음.&lt;/p&gt;
&lt;h3 id=&quot;4-점이-2개-이하&quot;&gt;4. 점이 2개 이하&lt;/h3&gt;
&lt;p&gt;N ≤ 2 면 hull = 점들 그대로.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;









































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 4181&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Convex Hull&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;21.1%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/4100-4199/4181&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 3679&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;단순 다각형&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;26.2%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/3600-3699/3679&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 17058&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Convex Hull 연습&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;39.1%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/17000-17099/17058&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 6850&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Cows&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;42.5%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/6800-6899/6850&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2254&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;감옥 건설&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;33.1%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2200-2299/2254&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Geometry|2D 기하 기본]]&lt;/li&gt;
&lt;li&gt;[[CCW|CCW (시계/반시계 판정)]]&lt;/li&gt;
&lt;li&gt;[[Rotating Calipers|회전 캘리퍼스]]&lt;/li&gt;
&lt;li&gt;[[Convex Hull Trick|CHT (Convex Hull Trick)]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>geometry</category><category>convex-hull</category><author>koa (김신건)</author></item><item><title>오일러 지표 (Euler Characteristic)</title><link>https://shinkeonkim.com/wiki/algorithm/geometry/euler-characteristic/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/geometry/euler-characteristic/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;오일러 지표 (Euler Characteristic)&lt;/strong&gt; χ 는 다면체(polyhedron) 또는 CW 복합체의 위상적 불변량으로, χ = V - E + F 로 정의. V는 꼭짓점(vertex), E는 변(edge), F는 면(face)의 개수. 볼록 다면체에서는 항상 χ = 2 (오일러의 다면체 정리).&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;임의의 다면체가 주어졌을 때 V - E + F 를 계산하면 그 다면체가 어떤 위상적 성질을 가지는지 알 수 있음.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Naive 접근&lt;/strong&gt;: 단순히 V, E, F 를 세어 빼기.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심 통찰&lt;/strong&gt;: χ 는 위상 불변량. 구면 위의 그래프는 χ=2, 원환면(torus)은 χ=0. 위상수학에서 genus g 와 χ = 2 - 2g.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PS 위치&lt;/strong&gt;: 평면 그래프 판별 (χ=2), 3D 기하 문제에서 다면체 종류 분류.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;볼록 다면체 (convex polyhedron):  V - E + F = 2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   정육면체: V=8, E=12, F=6 -&gt; 8-12+6 = 2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   정사면체: V=4, E=6, F=4 -&gt; 4-6+4 = 2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;평면 그래프 (planar graph):     V - E + F = 1 + C (C = connected components)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;원환면 (torus):                 V - E + F = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;일반화:                         χ = V - E + F = 2 - 2g  (g = genus)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Invariant: &lt;em&gt;모든 convex polyhedron 은 구면과 위상 동형&lt;/em&gt;, 따라서 χ=2. 위상 변화 없이 edge 를 추가/제거해도 χ 불변.&lt;/p&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;euler_characteristic(vertices, edges, faces):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return V - E + F&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;genus_from_chi(chi):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return (2 - chi) / 2  # if chi is even&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;평면 그래프의 경우 outer face 를 포함한 F 사용.&lt;/p&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// Euler characteristic from V, E, F #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; int main() {     int V, E, F;     cin &gt;&gt; V &gt;&gt; E &gt;&gt; F;     int chi = V - E + F;     cout &amp;#x3C;&amp;#x3C; &quot;chi = &quot; &amp;#x3C;&amp;#x3C; chi &amp;#x3C;&amp;#x3C; &quot;\\n&quot;;     if (chi == 2) cout &amp;#x3C;&amp;#x3C; &quot;Sphere / convex polyhedron\\n&quot;;     else if (chi == 0) cout &amp;#x3C;&amp;#x3C; &quot;Torus (genus 1)\\n&quot;;     else if (chi == -2) cout &amp;#x3C;&amp;#x3C; &quot;Double torus (genus 2)\\n&quot;;     else cout &amp;#x3C;&amp;#x3C; &quot;Genus g = &quot; &amp;#x3C;&amp;#x3C; (2 - chi) / 2 &amp;#x3C;&amp;#x3C; &quot;\\n&quot;; }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: `# Euler characteristic from planar graph input&lt;/p&gt;
&lt;h1 id=&quot;input-n-edges-each-with-two-vertex-ids&quot;&gt;Input: N edges, each with two vertex IDs&lt;/h1&gt;
&lt;h1 id=&quot;output-v-e-f-chi&quot;&gt;Output: V, E, F, chi&lt;/h1&gt;
&lt;p&gt;n = int(input())  # number of edges
vertex_set = set()
edges = []
for _ in range(n):
u, v = map(int, input().split())
vertex_set.add(u); vertex_set.add(v)
edges.append((u, v))
V = len(vertex_set)
E = n&lt;/p&gt;
&lt;h1 id=&quot;for-a-connected-planar-graph-f--e---v--2&quot;&gt;For a connected planar graph: F = E - V + 2&lt;/h1&gt;
&lt;h1 id=&quot;f-includes-the-outer-face&quot;&gt;F includes the outer face&lt;/h1&gt;
&lt;p&gt;F = E - V + 2
chi = V - E + F
print(f”V={V}, E={E}, F={F}”)
print(f”chi = {chi}”)
if chi == 2: print(“Planar graph (sphere)”)
elif chi == 0: print(“Torus”)&lt;code&gt;,     },     {       language: &quot;java&quot;,       label: &quot;Java&quot;,       code: &lt;/code&gt;import java.util.*;&lt;/p&gt;
&lt;p&gt;public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int E = Integer.parseInt(br.readLine());
Set&lt;integer&gt; vs = new HashSet&amp;#x3C;&gt;();
for (int i = 0; i &amp;#x3C; E; i++) {
StringTokenizer st = new StringTokenizer(br.readLine());
vs.add(Integer.parseInt(st.nextToken()));
vs.add(Integer.parseInt(st.nextToken()));
}
int V = vs.size();
int F = E - V + 2; // connected planar graph
int chi = V - E + F;
System.out.println(“V=” + V + ”, E=” + E + ”, F=” + F);
System.out.println(“chi = ” + chi);
if (chi == 2) System.out.println(“Planar (sphere)”);
else if (chi == 0) System.out.println(“Torus”);
}
}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;정육면체&quot;,       input: &lt;/code&gt;12
0 1
1 2
2 3
3 0
4 5
5 6
6 7
7 4
0 4
1 5
2 6
3 7&lt;code&gt;,       output: &lt;/code&gt;V=8, E=12, F=6
chi = 2
Sphere / convex polyhedron&lt;code&gt;,     },     {       label: &quot;정사면체 (4개 변)&quot;,       input: &lt;/code&gt;6
0 1
1 2
2 0
0 3
1 3
2 3&lt;code&gt;,       output: &lt;/code&gt;V=4, E=6, F=4
chi = 2
Sphere / convex polyhedron`,
},
]}
/&gt;&lt;/integer&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;V, E, F 카운트&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(V + E + F)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;χ 계산&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(1)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;genus 계산&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(1)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(V)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;평면 그래프 판별&lt;/strong&gt;: V - E + F = 1 + C (연결 성분 C). planar embedding 존재 여부 체크.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3D 모델링&lt;/strong&gt;: manifold mesh 의 위상 검증. χ=2 면 구면 위상.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GIS&lt;/strong&gt;: 지도에서 지역/경계/꼭짓점 관계 분석.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Poincare 공식&lt;/strong&gt;: 고차원 일반화 (alternating sum of Betti numbers).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-f-에-outer-face-포함&quot;&gt;1. F 에 outer face 포함&lt;/h3&gt;
&lt;p&gt;평면 그래프에서 F 는 outer (unbounded) face 를 포함한 모든 face 개수. 빼먹으면 χ 가 1 작아짐.&lt;/p&gt;
&lt;h3 id=&quot;2-연결성&quot;&gt;2. 연결성&lt;/h3&gt;
&lt;p&gt;공식 V - E + F = 2 는 connected graph 기준. disconnected 면 V - E + F = 1 + C.&lt;/p&gt;
&lt;h3 id=&quot;3-non-planar-graph&quot;&gt;3. Non-planar graph&lt;/h3&gt;
&lt;p&gt;K_5, K_3,3 같은 non-planar 그래프는 평면에 embedding 불가능해 Euler 공식이 성립하지 않음.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 1707&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;이분 그래프&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1700-1799/1707&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2170&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;선 긋기&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2100-2199/2170&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Geometry 3D|3D 기하]]&lt;/li&gt;
&lt;li&gt;[[Geometry Basic|기하 기본]]&lt;/li&gt;
&lt;li&gt;[[Planar Graph|평면 그래프]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>geometry</category><category>euler-characteristic</category><author>koa (김신건)</author></item><item><title>도형 불 연산 (Geometric Boolean Operations)</title><link>https://shinkeonkim.com/wiki/algorithm/geometry/geometric-boolean-operations/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/geometry/geometric-boolean-operations/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;도형 불 연산 (Geometric Boolean Operations)&lt;/strong&gt; 은 두 다각형(또는 임의의 폐곡선)에 대해 합집합(union), 교집합(intersection), 차집합(difference)을 계산하는 기하 알고리즘. 대표적으로 Sutherland-Hodgman (convex clipping) 과 Weiler-Atherton (general polygon clipping) 이 알려져 있음.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;두 다각형 A, B 가 주어졌을 때 A ∪ B, A ∩ B, A - B (또는 B - A) 를 구해야 함.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Naive 접근&lt;/strong&gt;: 각 변을 무한 직선으로 확장 후 교차점을 찾고 폐곡선을 재구성. O(NM) 이지만 self-intersection / 구멍 처리 까다로움.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심 통찰&lt;/strong&gt;: 한 다각형의 각 변을 clipping edge 로 삼아 반대 다각형을 잘라내면 출력 폴리곤이 자연스럽게 생성. Weiler-Atherton 은 교차점 리스트를 따라가며 출력 링을 구성.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PS 위치&lt;/strong&gt;: convex polygon clipping 은 Sutherland-Hodgman 으로 O(N+M). 일반 polygon 은 Weiler-Atherton 필요.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;p&gt;Sutherland-Hodgman: convex clipping window 의 각 edge 에 대해, subject polygon 의 점들을 inside/outside 판정. 출력 리스트를 누적.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;for each clip edge (from clip polygon):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for each edge (S-&gt;E) of subject:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if S inside, E inside  -&gt; output E&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if S inside, E outside -&gt; output intersection&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if S outside, E inside -&gt; output intersection, then E&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if S outside, E outside -&gt; output nothing&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Weiler-Atherton: 교차점을 표시한 후, entering/leaving 에 따라 출력 링크를 순회.&lt;/p&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Sutherland-Hodgman(subject, clip):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    output = subject&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for each edge of clip:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        input = output; output = []&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        for each edge S-&gt;E in input:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            if inside(E, clipEdge):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                if not inside(S, clipEdge):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                    output.push(intersect(S,E, clipEdge))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                output.push(E)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            else if inside(S, clipEdge):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                output.push(intersect(S,E, clipEdge))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return output&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// Sutherland-Hodgman convex polygon clipping #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; using ld = long double; struct Pt { ld x, y; }; ld cross(Pt a, Pt b) { return a.x*b.y - a.y*b.x; } Pt sub(Pt a, Pt b) { return {a.x-b.x, a.y-b.y}; } Pt add(Pt a, Pt b) { return {a.x+b.x, a.y+b.y}; } Pt mul(Pt a, ld t) { return {a.x*t, a.y*t}; } ld dot(Pt a, Pt b) { return a.x*b.x + a.y*b.y; } bool inside(Pt p, Pt a, Pt b) {     return cross(sub(b,a), sub(p,a)) &gt;= 0; // clockwise } Pt intersect(Pt p1, Pt p2, Pt a, Pt b) {     Pt v1 = sub(p2,p1), v2 = sub(b,a), v3 = sub(a,p1);     ld t = cross(v2,v3) / cross(v1,v2);     return add(p1, mul(v1, t)); } vector&amp;#x3C;Pt&gt; clip(vector&amp;#x3C;Pt&gt; subj, vector&amp;#x3C;Pt&gt; clip) {     vector&amp;#x3C;Pt&gt; out = subj;     int m = clip.size();     for (int i = 0; i &amp;#x3C; m; i++) {         Pt a = clip[i], b = clip[(i+1)%m];         vector&amp;#x3C;Pt&gt; in = out; out.clear();         int n = in.size();         for (int j = 0; j &amp;#x3C; n; j++) {             Pt cur = in[j], prv = in[(j+n-1)%n];             bool curIn = inside(cur, a, b);             bool prvIn = inside(prv, a, b);             if (curIn) {                 if (!prvIn) out.push_back(intersect(prv, cur, a, b));                 out.push_back(cur);             } else if (prvIn) {                 out.push_back(intersect(prv, cur, a, b));             }         }     }     return out; } int main() {     int n; cin &gt;&gt; n;     vector&amp;#x3C;Pt&gt; subj(n);     for (auto&amp;#x26; p : subj) cin &gt;&gt; p.x &gt;&gt; p.y;     int m; cin &gt;&gt; m;     vector&amp;#x3C;Pt&gt; clipPoly(m);     for (auto&amp;#x26; p : clipPoly) cin &gt;&gt; p.x &gt;&gt; p.y;     auto res = clip(subj, clipPoly);     cout &amp;#x3C;&amp;#x3C; res.size() &amp;#x3C;&amp;#x3C; &quot;\\n&quot;;     for (auto&amp;#x26; p : res) cout &amp;#x3C;&amp;#x3C; p.x &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; p.y &amp;#x3C;&amp;#x3C; &quot;\\n&quot;; }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: &lt;code&gt;# Sutherland-Hodgman convex polygon clipping def cross(a, b): return a[0]*b[1] - a[1]*b[0] def sub(a, b): return (a[0]-b[0], a[1]-b[1]) def add(a, b): return (a[0]+b[0], a[1]+b[1]) def mul(a, t): return (a[0]*t, a[1]*t) def inside(p, a, b):     return cross(sub(b,a), sub(p,a)) &gt;= 0 def intersect(p1, p2, a, b):     v1 = sub(p2,p1); v2 = sub(b,a); v3 = sub(a,p1)     t = cross(v2,v3) / cross(v1,v2)     return add(p1, mul(v1, t)) def clip(subj, clip_poly):     out = subj[:]     m = len(clip_poly)     for i in range(m):         a, b = clip_poly[i], clip_poly[(i+1)%m]         inp = out; out = []         n = len(inp)         for j in range(n):             cur, prv = inp[j], inp[(j+n-1)%n]             cur_in, prv_in = inside(cur,a,b), inside(prv,a,b)             if cur_in:                 if not prv_in: out.append(intersect(prv,cur,a,b))                 out.append(cur)             elif prv_in:                 out.append(intersect(prv,cur,a,b))     return out n = int(input()) subj = [tuple(map(float, input().split())) for _ in range(n)] m = int(input()) clip_poly = [tuple(map(float, input().split())) for _ in range(m)] res = clip(subj, clip_poly) print(len(res)) for p in res: print(f&quot;{p[0]} {p[1]}&quot;)&lt;/code&gt;,
},
{
language: “java”,
label: “Java”,
code: `import java.util.*;&lt;/p&gt;
&lt;p&gt;public class Main {
static class Pt { double x,y; Pt(double x,double y){this.x=x;this.y=y;} }
static double cross(Pt a, Pt b) { return a.x&lt;em&gt;b.y - a.y&lt;/em&gt;b.x; }
static Pt sub(Pt a, Pt b) { return new Pt(a.x-b.x, a.y-b.y); }
static Pt add(Pt a, Pt b) { return new Pt(a.x+b.x, a.y+b.y); }
static Pt mul(Pt a, double t) { return new Pt(a.x&lt;em&gt;t, a.y&lt;/em&gt;t); }
static boolean inside(Pt p, Pt a, Pt b) {
return cross(sub(b,a), sub(p,a)) &gt;= 0;
}
static Pt intersect(Pt p1, Pt p2, Pt a, Pt b) {
Pt v1=sub(p2,p1), v2=sub(b,a), v3=sub(a,p1);
double t = cross(v2,v3) / cross(v1,v2);
return add(p1, mul(v1, t));
}
static List&lt;pt&gt; clip(List&lt;pt&gt; subj, List&lt;pt&gt; clip) {
List&lt;pt&gt; out = new ArrayList&amp;#x3C;&gt;(subj);
for (int i=0; i&amp;#x3C;clip.size(); i++) {
Pt a=clip.get(i), b=clip.get((i+1)%clip.size());
List&lt;pt&gt; in = out; out = new ArrayList&amp;#x3C;&gt;();
for (int j=0; j&amp;#x3C;in.size(); j++) {
Pt cur=in.get(j), prv=in.get((j+in.size()-1)%in.size());
boolean curIn=inside(cur,a,b), prvIn=inside(prv,a,b);
if (curIn) { if (!prvIn) out.add(intersect(prv,cur,a,b)); out.add(cur); }
else if (prvIn) out.add(intersect(prv,cur,a,b));
}
}
return out;
}
public static void main(String[] args) throws IOException {
BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
int n=Integer.parseInt(br.readLine());
List&lt;pt&gt; subj=new ArrayList&amp;#x3C;&gt;();
for (int i=0; i&amp;#x3C;n; i++) { StringTokenizer st=new StringTokenizer(br.readLine());
subj.add(new Pt(Double.parseDouble(st.nextToken()),Double.parseDouble(st.nextToken()))); }
int m=Integer.parseInt(br.readLine());
List&lt;pt&gt; clipP=new ArrayList&amp;#x3C;&gt;();
for (int i=0; i&amp;#x3C;m; i++) { StringTokenizer st=new StringTokenizer(br.readLine());
clipP.add(new Pt(Double.parseDouble(st.nextToken()),Double.parseDouble(st.nextToken()))); }
List&lt;pt&gt; res=clip(subj, clipP);
System.out.println(res.size());
for (Pt p:res) System.out.println(p.x+” “+p.y);
}
}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;사각형-사각형 교집합&quot;,       input: &lt;/code&gt;4
0 0
10 0
10 10
0 10
4
5 5
15 5
15 15
5 15&lt;code&gt;,       output: &lt;/code&gt;4
5 5
10 5
10 10
5 10`,
},
]}
/&gt;&lt;/pt&gt;&lt;/pt&gt;&lt;/pt&gt;&lt;/pt&gt;&lt;/pt&gt;&lt;/pt&gt;&lt;/pt&gt;&lt;/pt&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Sutherland-Hodgman&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N * M) (각 clip edge 마다 전체 순회)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Weiler-Atherton&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N + M + K) (K = 교차점 개수)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N + M)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;N, M 은 각각 subject, clip polygon 의 꼭짓점 개수.&lt;/p&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Union (합집합)&lt;/strong&gt;: A, B 의 outside 부분을 취합.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Intersection (교집합)&lt;/strong&gt;: Sutherland-Hodgman 직접 적용.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Difference (차집합)&lt;/strong&gt;: A - B 는 B 의 반대 방향 edge 로 clipping.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CAD/CAM&lt;/strong&gt;: 솔리드 모델링의 CSG (Constructive Solid Geometry).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GIS&lt;/strong&gt;: 지도 영역 중첩 분석.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-수치-오차&quot;&gt;1. 수치 오차&lt;/h3&gt;
&lt;p&gt;부동소수점 교차점 계산에서 epsilon (&lt;code&gt;1e-9&lt;/code&gt;) 처리 필요. inside 판정이 epsilon 범위에서 일관되어야 함.&lt;/p&gt;
&lt;h3 id=&quot;2-degenerate-case&quot;&gt;2. Degenerate case&lt;/h3&gt;
&lt;p&gt;edge 위에 점이 정확히 놓인 경우 (collinear). inside/boundary 판정을 일관된 규칙으로.&lt;/p&gt;
&lt;h3 id=&quot;3-self-intersecting-polygon&quot;&gt;3. Self-intersecting polygon&lt;/h3&gt;
&lt;p&gt;Sutherland-Hodgman 은 convex window 에만 동작. 일반 polygon 은 Weiler-Atherton 필요.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 5212&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;지구 온난화&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/5200-5299/5212&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 3392&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;화성 지도&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/3300-3399/3392&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2671&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;잠수함 식별&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2600-2699/2671&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Convex Hull|볼록 껍질]]&lt;/li&gt;
&lt;li&gt;[[Geometry Basic|기하 기본]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>geometry</category><category>geometric-boolean-operations</category><author>koa (김신건)</author></item><item><title>기하 (Geometry) 기본</title><link>https://shinkeonkim.com/wiki/algorithm/geometry/geometry/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/geometry/geometry/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Computational Geometry&lt;/strong&gt; 는 점, 선, 다각형 등 기하 객체를 컴퓨터로 처리하는 알고리즘 분야. PS 에서는 &lt;strong&gt;2D 평면 위의 정수 좌표&lt;/strong&gt; 가 주로 다루어지며, 핵심 연산은 &lt;strong&gt;벡터의 외적 (cross product)&lt;/strong&gt; 과 &lt;strong&gt;내적 (dot product)&lt;/strong&gt; 이다.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;기하 문제는 naive 하게 거리 / 각도를 실수로 계산하면 오차와 시간 폭발.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Naive&lt;/strong&gt;: N 개 점의 모든 쌍 거리 비교 O(N^2). N=10^5 면 불가능.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;선분 교차&lt;/strong&gt;: 모든 쌍 O(N^2) 검사 대신 sweep line ([[Sweeping|스위핑]]) O(N log N).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;다각형 면적&lt;/strong&gt;: shoelace 공식으로 O(N).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;정수 좌표에서는 외적 하나로 CCW, 선분 교차, 면적, convex hull 을 모두 판정 가능&lt;/em&gt;. 실수 비교가 없으면 오차도 없음.&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;h3 id=&quot;점과-벡터&quot;&gt;점과 벡터&lt;/h3&gt;
&lt;p&gt;2D 점 &lt;code&gt;P = (x, y)&lt;/code&gt; 는 원점에서의 &lt;strong&gt;벡터&lt;/strong&gt; 로도 해석.&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;연산&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;수식&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;기하적 의미&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;외적 (cross)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;a x b = a.x*b.y - a.y*b.x&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;평행사변형 넓이 (부호 있음)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;내적 (dot)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;a . b = a.x*b.x + a.y*b.y&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;`&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;거리&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;sqrt(dx*dx + dy*dy)&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;피타고라스&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 id=&quot;ccw-counter-clockwise-판정&quot;&gt;CCW (Counter-Clockwise) 판정&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;ccw(a, b, c) = (b - a) x (c - a)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&gt; 0  -&gt; 반시계 (좌회전)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&amp;#x3C; 0  -&gt; 시계 (우회전)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;= 0  -&gt; 일직선 (collinear)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CCW 하나로 &lt;strong&gt;선분 교차, 점의 다각형 포함 여부, convex hull&lt;/strong&gt; 을 모두 구현 가능.&lt;/p&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;h3 id=&quot;선분-교차&quot;&gt;선분 교차&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;intersect(a, b, c, d):     // 선분 ab 와 cd&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    det = ccw(a, b, c) * ccw(a, b, d)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    det2 = ccw(c, d, a) * ccw(c, d, b)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if det &amp;#x3C;= 0 and det2 &amp;#x3C;= 0: return true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    // 일직선 위에 겹치는 경우: projection range check 필요&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return false&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;다각형-면적-shoelace&quot;&gt;다각형 면적 (Shoelace)&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;area(polygon):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    sum = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for i = 0..N-1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        j = (i + 1) % N&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        sum += polygon[i].x * polygon[j].y&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        sum -= polygon[j].x * polygon[i].y&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return abs(sum) / 2&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// CCW test + 선분 교차 + 다각형 면적 (shoelace) #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; using ll = long long; struct Point { ll x, y; }; ll ccw(Point a, Point b, Point c) {     return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x); } double area(vector&amp;#x3C;Point&gt;&amp;#x26; p) {     ll sum = 0;     for (int i = 0; i &amp;#x3C; (int)p.size(); i++) {         int j = (i + 1) % p.size();         sum += p[i].x * p[j].y - p[j].x * p[i].y;     }     return abs(sum) / 2.0; } int main() {     Point a{0,0}, b{2,0}, c{1,2};     cout &amp;#x3C;&amp;#x3C; &quot;CCW(0,0, 2,0, 1,2) = &quot; &amp;#x3C;&amp;#x3C; (ccw(a,b,c) &gt; 0 ? 1 : ccw(a,b,c) &amp;#x3C; 0 ? -1 : 0) &amp;#x3C;&amp;#x3C; &quot;\\n&quot;;     vector&amp;#x3C;Point&gt; tri = {{0,0}, {3,0}, {0,4}};     cout &amp;#x3C;&amp;#x3C; &quot;삼각형 면적: &quot; &amp;#x3C;&amp;#x3C; area(tri) &amp;#x3C;&amp;#x3C; &quot;\\n&quot;; }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: `# CCW + shoelace 면적
def ccw(ax, ay, bx, by, cx, cy):
return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax)
a, b, c = (0,0), (2,0), (1,2)
print(f”CCW(0,0, 2,0, 1,2) =”, 1 if ccw(*a, *b, *c) &gt; 0 else -1 if ccw(*a, *b, *c) &amp;#x3C; 0 else 0)&lt;/p&gt;
&lt;p&gt;def polygon_area(poly):
n = len(poly)
s = 0
for i in range(n):
s += poly[i][0] * poly[(i+1)%n][1]
s -= poly[(i+1)%n][0] * poly[i][1]
return abs(s) / 2.0&lt;/p&gt;
&lt;p&gt;tri = [(0,0), (3,0), (0,4)]
print(f”삼각형 면적: {polygon_area(tri)}“)&lt;code&gt;,     },     {       language: &quot;java&quot;,       label: &quot;Java&quot;,       code: &lt;/code&gt;import java.util.*;
public class Main {
static long ccw(long ax, long ay, long bx, long by, long cx, long cy) {
return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
}
static double area(long[][] p) {
long sum = 0; int n = p.length;
for (int i = 0; i &amp;#x3C; n; i++) {
int j = (i + 1) % n;
sum += p[i][0] * p[j][1] - p[j][0] * p[i][1];
}
return Math.abs(sum) / 2.0;
}
public static void main(String[] args) {
System.out.println(“CCW(0,0, 2,0, 1,2) = ” + Long.signum(ccw(0,0, 2,0, 1,2)));
long[][] tri = {{0,0}, {3,0}, {0,4}};
System.out.println(“삼각형 면적: ” + area(tri));
}
}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;기본 예제&quot;,       input: ``,       output: &lt;/code&gt;CCW(0,0, 2,0, 1,2) = 1
삼각형 면적: 6`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;연산&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;비용&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;CCW 판정&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(1) (정수 곱셈 2회)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;선분 교차 판정&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(1) (CCW 4회)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;다각형 면적&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(N) (shoelace)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Convex Hull (Graham Scan)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(N log N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;최근접 점 쌍&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(N log N) (분할 정복)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;h3 id=&quot;1-convex-hull&quot;&gt;1. Convex Hull&lt;/h3&gt;
&lt;p&gt;점 집합을 감싸는 최소 볼록 다각형. Graham scan (O(N log N)), Andrew monotone chain.&lt;/p&gt;
&lt;h3 id=&quot;2-rotating-calipers&quot;&gt;2. Rotating Calipers&lt;/h3&gt;
&lt;p&gt;Convex Hull 위에서 평행선을 회전시키며 최장 거리 / 최소 폭 계산.&lt;/p&gt;
&lt;h3 id=&quot;3-line-sweep-sweeping스위핑&quot;&gt;3. Line Sweep ([[Sweeping|스위핑]])&lt;/h3&gt;
&lt;p&gt;이벤트 정렬 후 active set 유지. 선분 교차 / 직사각형 합집합 면적.&lt;/p&gt;
&lt;h3 id=&quot;4-point-in-polygon&quot;&gt;4. Point in Polygon&lt;/h3&gt;
&lt;p&gt;Ray casting (홀수/짝수 교차) 또는 winding number.&lt;/p&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-정수-오버플로우&quot;&gt;1. 정수 오버플로우&lt;/h3&gt;
&lt;p&gt;CCW 에서 &lt;code&gt;(b.x - a.x) * (c.y - a.y)&lt;/code&gt; 는 좌표가 10^6 범위면 &lt;code&gt;10^12&lt;/code&gt; → &lt;code&gt;long long&lt;/code&gt; (64-bit) 필요. Java 는 &lt;code&gt;long&lt;/code&gt;, Python 은 무제한.&lt;/p&gt;
&lt;h3 id=&quot;2-일직선-collinear&quot;&gt;2. 일직선 (Collinear)&lt;/h3&gt;
&lt;p&gt;CCW = 0 인 경우, 선분 교차 판정에서 onSegment() 추가 체크 필요. 포함 관계, 겹침을 세분.&lt;/p&gt;
&lt;h3 id=&quot;3-부동소수점-비교&quot;&gt;3. 부동소수점 비교&lt;/h3&gt;
&lt;p&gt;면적 / 거리 출력 시 &lt;code&gt;double&lt;/code&gt; 이 필요한 순간에는 &lt;code&gt;EPS = 1e-9&lt;/code&gt; 로 비교. &lt;code&gt;abs(a - b) &amp;#x3C; EPS&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;4-시계-방향-입력&quot;&gt;4. 시계 방향 입력&lt;/h3&gt;
&lt;p&gt;다각형 입력이 시계(cw) 또는 반시계(ccw) 일 수 있음. shoelace 는 부호로 방향을 알려줌 (양수 = ccw).&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 11758&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;CCW&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;50.7%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/11700-11799/11758&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2166&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;다각형의 면적&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;39.5%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2100-2199/2166&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 17386&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;선분 교차 1&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;38.2%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/17300-17399/17386&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2477&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;참외밭&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;44.5%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2400-2499/2477&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Sweeping|스위핑]]&lt;/li&gt;
&lt;li&gt;[[Convex Hull|볼록 껍질]]&lt;/li&gt;
&lt;li&gt;[[CCW|CCW (시계/반시계 판정)]]&lt;/li&gt;
&lt;li&gt;[[Geometry-3D|3D 기하]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>geometry</category><category>foundation</category><author>koa (김신건)</author></item><item><title>3D 기하 (Geometry) 기본</title><link>https://shinkeonkim.com/wiki/algorithm/geometry/geometry-3d/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/geometry/geometry-3d/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;3D 기하&lt;/strong&gt; 는 3차원 공간 &lt;code&gt;(x, y, z)&lt;/code&gt; 위의 점, 직선, 평면, 벡터를 다루는 분야. PS 에서는 &lt;strong&gt;정육면체 / 구 / 원뿔 체적&lt;/strong&gt;, &lt;strong&gt;3D convex hull&lt;/strong&gt;, &lt;strong&gt;최근접 점 3D&lt;/strong&gt; 등이 등장. 2D 기하의 cross/dot product 를 3차원으로 확장한 것이 핵심.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;3D 공간에서는 2D 에 비해 고려할 자유도가 1 추가되어 직관을 벗어나는 상황이 많음.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2D&lt;/strong&gt;: 점 이외에도 직선(1D) 만 있음. 선분 교차 = CCW 4회.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3D&lt;/strong&gt;: 점 + 직선(1D) + 평면(2D). &lt;strong&gt;두 직선은 꼬인 위치(skew) 가능&lt;/strong&gt;. 외적 결과가 벡터.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;직선과 평면의 교차&lt;/strong&gt;: 매개변수 방정식 풀이 필요.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;3D 외적은 두 벡터에 수직인 벡터(법선) 를 반환&lt;/em&gt;. 이 법선으로 평면의 방정식을 세우고, 직선과의 교차점, 점과 평면의 거리를 모두 O(1) 에 계산.&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;h3 id=&quot;3d-벡터-연산&quot;&gt;3D 벡터 연산&lt;/h3&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;연산&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;수식&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;의미&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;외적&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;&lt;code&gt;a x b = (ay*bz - az*by, az*bx - ax*bz, ax*by - ay*bx)&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;&lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt; 에 수직인 벡터. `&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;내적&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;&lt;code&gt;a . b = ax*bx + ay*by + az*bz&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;스칼라. cos(theta) 비례. = 0 이면 수직&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;norm&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;&lt;code&gt;|a| = sqrt(a . a)&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;벡터 길이&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;단위 벡터&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;&lt;code&gt;a / |a|&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;방향만 남김&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 id=&quot;평면의-방정식&quot;&gt;평면의 방정식&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;점 P0(x0, y0, z0) 를 지나고 법선 n = (nx, ny, nz) 인 평면:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    nx*(x - x0) + ny*(y - y0) + nz*(z - z0) = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    -&gt; nx*x + ny*y + nz*z + d = 0    (d = -nx*x0 - ny*y0 - nz*z0)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;직선의-매개변수-방정식&quot;&gt;직선의 매개변수 방정식&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;점 P0, 방향 v = (vx, vy, vz):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    P(t) = P0 + t * v    (t 는 실수)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;h3 id=&quot;점과-평면의-거리&quot;&gt;점과 평면의 거리&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;plane_distance(P, nx, ny, nz, d):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return |nx*P.x + ny*P.y + nz*P.z + d| / sqrt(nx*nx + ny*ny + nz*nz)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;직선과-평면의-교차&quot;&gt;직선과 평면의 교차&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;line_plane_intersection(P0, v, nx, ny, nz, d):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    denom = v.x*nx + v.y*ny + v.z*nz&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if denom == 0: return &quot;평행 또는 평면 위&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    t = -(nx*P0.x + ny*P0.y + nz*P0.z + d) / denom&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return P0 + t * v&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// 3D 벡터: 외적, 내적, norm, 평면 거리 #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; struct Vec3 { double x, y, z; }; Vec3 cross(Vec3 a, Vec3 b) {     return {a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x}; } double dot(Vec3 a, Vec3 b) { return a.x*b.x + a.y*b.y + a.z*b.z; } double norm(Vec3 a) { return sqrt(dot(a, a)); } int main() {     Vec3 u{1, 0, 0}, v{0, 1, 0};     Vec3 w = cross(u, v);     printf(&quot;u x v = (%.0f, %.0f, %.0f)\\n&quot;, w.x, w.y, w.z);     printf(&quot;|u x v| = %.2f\\n&quot;, norm(w));     printf(&quot;u . v = %.0f\\n&quot;, dot(u, v));     // 평면 방정식: z = 0 (법선 (0,0,1), d = 0)     auto dist = [](Vec3 p, Vec3 n, double d) {         return fabs(dot(p, n) + d) / norm(n);     };     Vec3 n{0, 0, 1}, p{1, 2, 3};     printf(&quot;점(1,2,3) - 평면 z=0 거리: %.2f\\n&quot;, dist(p, n, 0)); }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: `# 3D 벡터 연산 (numpy 없이)
import math
def cross(a, b):
return (a[1]*b[2] - a[2]*b[1], a[2]*b[0] - a[0]*b[2], a[0]*b[1] - a[1]*b[0])
def dot(a, b): return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
def norm(a): return math.sqrt(dot(a, a))&lt;/p&gt;
&lt;p&gt;u, v = (1, 0, 0), (0, 1, 0)
w = cross(u, v)
print(f”u x v = {w}”)
print(f”|u x v| = {norm(w):.2f}”)
print(f”u . v = {dot(u, v)}“)&lt;/p&gt;
&lt;h1 id=&quot;평면-z0-까지-점-123-의-거리&quot;&gt;평면 z=0 까지 점 (1,2,3) 의 거리&lt;/h1&gt;
&lt;p&gt;n, d = (0, 0, 1), 0
dist = abs(dot((1,2,3), n) + d) / norm(n)
print(f”점-평면 거리: {dist:.2f}“)&lt;code&gt;,     },     {       language: &quot;java&quot;,       label: &quot;Java&quot;,       code: &lt;/code&gt;import java.util.&lt;em&gt;;
public class Main {
static class Vec3 { double x, y, z;
Vec3(double x, double y, double z) { this.x = x; this.y = y; this.z = z; }
}
static Vec3 cross(Vec3 a, Vec3 b) {
return new Vec3(a.y&lt;/em&gt;b.z - a.z&lt;em&gt;b.y, a.z&lt;/em&gt;b.x - a.x&lt;em&gt;b.z, a.x&lt;/em&gt;b.y - a.y&lt;em&gt;b.x);
}
static double dot(Vec3 a, Vec3 b) { return a.x&lt;/em&gt;b.x + a.y&lt;em&gt;b.y + a.z&lt;/em&gt;b.z; }
static double norm(Vec3 a) { return Math.sqrt(dot(a, a)); }
public static void main(String[] args) {
Vec3 u = new Vec3(1,0,0), v = new Vec3(0,1,0);
Vec3 w = cross(u, v);
System.out.printf(“u x v = (%.0f, %.0f, %.0f)%n”, w.x, w.y, w.z);
System.out.printf(“|u x v| = %.2f%n”, norm(w));
System.out.printf(“u . v = %.0f%n”, dot(u, v));
Vec3 n = new Vec3(0,0,1);
double d = 0;
Vec3 p = new Vec3(1,2,3);
double dist = Math.abs(dot(p, n) + d) / norm(n);
System.out.printf(“점-평면 거리: %.2f%n”, dist);
}
}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;기본 예제&quot;,       input: ``,       output: &lt;/code&gt;u x v = (-0, 0, 1)
|u x v| = 1.00
u . v = 0
점-평면 거리: 3.00`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;

































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;연산&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;비용&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;3D 외적&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(1) (6회 곱셈 + 3회 뺄셈)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;3D 내적&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(1) (3회 곱셈 + 2회 덧셈)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;점-평면 거리&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(1)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;직선-평면 교차&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(1)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;3D Convex Hull&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(N log N) (QuickHull)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;최근접 점 (3D)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(N log^2 N) (KD-tree / 분할 정복)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;h3 id=&quot;1-3d-convex-hull&quot;&gt;1. 3D Convex Hull&lt;/h3&gt;
&lt;p&gt;점들을 감싸는 최소 볼록 다면체. QuickHull (O(N log N)) 이 일반적. 결과는 삼각형 메시. 2D convex hull 과 달리 face(면) 의 법선 방향이 중요.&lt;/p&gt;
&lt;h3 id=&quot;2-직선과-직선의-최단-거리&quot;&gt;2. 직선과 직선의 최단 거리&lt;/h3&gt;
&lt;p&gt;꼬인 위치(skew) 에서 두 직선의 최단 거리는 외적으로 계산:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;skew_distance(P1, v1, P2, v2) = |(P2 - P1) . (v1 x v2)| / |v1 x v2|&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3-구와-직선의-교차&quot;&gt;3. 구와 직선의 교차&lt;/h3&gt;
&lt;p&gt;직선 매개변수 방정식을 구에 대입, 2차 방정식 풀이. 판별식으로 접하는지 / 통과하는지 판정.&lt;/p&gt;
&lt;h3 id=&quot;4-회전-변환-rotation&quot;&gt;4. 회전 변환 (Rotation)&lt;/h3&gt;
&lt;p&gt;3D 회전은 오일러 각 또는 &lt;strong&gt;쿼터니언(quaternion)&lt;/strong&gt; 으로 표현. 외적이 회전축을 결정.&lt;/p&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-평행--수직-판정&quot;&gt;1. 평행 / 수직 판정&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;|a x b|&lt;/code&gt; 가 0 에 가까우면 두 벡터는 평행 (collinear). &lt;code&gt;dot(a, b)&lt;/code&gt; 가 0 에 가까우면 수직. 각각 EPS 비교.&lt;/p&gt;
&lt;h3 id=&quot;2-법선-방향-right-hand-rule&quot;&gt;2. 법선 방향 (Right-hand rule)&lt;/h3&gt;
&lt;p&gt;3D 외적 &lt;code&gt;a x b&lt;/code&gt; 는 right-hand rule 을 따름. 왼손 좌표계(예: Unity) 에서는 반대 방향.&lt;/p&gt;
&lt;h3 id=&quot;3-정수-좌표의-한계&quot;&gt;3. 정수 좌표의 한계&lt;/h3&gt;
&lt;p&gt;3D 에서도 정수 좌표가 가능하지만, 외적 결과가 &lt;code&gt;long long&lt;/code&gt; 범위를 초과할 수 있음. 부동소수점 사용 또는 &lt;code&gt;__int128&lt;/code&gt; (C++) 필요.&lt;/p&gt;
&lt;h3 id=&quot;4-평면의-네-가지-표현&quot;&gt;4. 평면의 네 가지 표현&lt;/h3&gt;
&lt;p&gt;점-법선 / 일반형 / 세 점 / 두 직선. 문제에 따라 적절히 변환 필요.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 11758&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;CCW&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;50.7%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/11700-11799/11758&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2166&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;다각형의 면적&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;39.5%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2100-2199/2166&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 17386&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;선분 교차 1&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;38.2%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/17300-17399/17386&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 25308&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;방사형 그래프&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;24.0%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/25300-25399/25308&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Geometry|2D 기하 기본]]&lt;/li&gt;
&lt;li&gt;[[Convex Hull 3D|3D Convex Hull]]&lt;/li&gt;
&lt;li&gt;[[CCW|CCW (시계/반시계 판정)]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>geometry</category><category>geometry-3d</category><author>koa (김신건)</author></item><item><title>4차원 이상 기하 (Hyper Geometry)</title><link>https://shinkeonkim.com/wiki/algorithm/geometry/geometry-hyper/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/geometry/geometry-hyper/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;고차원 기하 (Hyper / 4D+ Geometry)&lt;/strong&gt; 는 4차원 이상의 공간 &lt;code&gt;R^d&lt;/code&gt; (d &gt;= 4) 에서 점, 벡터, 초평면, 초구 등을 다루는 분야. PS 에서는 d ≤ 4 인 경우가 대부분이며, 주로 &lt;strong&gt;벡터-행렬 연산&lt;/strong&gt;, &lt;strong&gt;초평면과의 거리&lt;/strong&gt;, &lt;strong&gt;고차원 Convex Hull&lt;/strong&gt; (d &gt; 3 은 실질적 불가) 형태로 등장.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;3차원까지는 직관이 통하지만 4차원부터 기하 직관이 완전히 무너짐.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Naive&lt;/strong&gt;: 점 N 개, 차원 d 에서 모든 쌍 거리 O(N^2) 는 d 와 무관. 하지만 d 가 커지면 곱셈 비용 증가.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;차원의 저주 (Curse of Dimensionality)&lt;/strong&gt;: d 가 커질수록 공간이 지수적으로 넓어져, 최근접 이웃 / Convex Hull 의 복잡도가 폭발.
&lt;ul&gt;
&lt;li&gt;d=2 에서 Convex Hull O(N log N). d=3 에서 O(N log N) (QuickHull). d&gt;=4 에서는 O(N^{d/2}) (비현실적).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;응용&lt;/strong&gt;: ML 의 고차원 feature 공간, 추천 시스템 (cosine similarity), 선형 계획법 (simplex method).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;차원이 높아져도 벡터의 내적(dot) 과 거리(Euclidean norm) 는 정의가 변하지 않는다. 외적은 d=3 전용이므로, d&gt;=4 에서는 일반화된 wedge product 또는 행렬 연산으로 대체한다.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;h3 id=&quot;d차원-벡터-연산&quot;&gt;d차원 벡터 연산&lt;/h3&gt;
&lt;p&gt;d차원 벡터 &lt;code&gt;a = (a_1, a_2, ..., a_d)&lt;/code&gt;, &lt;code&gt;b = (b_1, ..., b_d)&lt;/code&gt;:&lt;/p&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;연산&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;수식&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;의미&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;내적&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;&lt;code&gt;a . b = Σ a_i * b_i&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;스칼라. 두 벡터의 정사영. = 0 이면 직교&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;norm&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;&lt;code&gt;|a| = sqrt(a . a)&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;유클리드 길이&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;거리&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;&lt;code&gt;|a - b|&lt;/code&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;두 점 사이 유클리드 거리&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;외적 (일반화)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;(d=3) cross / (d&gt;=4) wedge product &lt;code&gt;a ^ b&lt;/code&gt; (bivector)&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;평행사변형의 oriented area&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h3 id=&quot;초평면-hyperplane&quot;&gt;초평면 (Hyperplane)&lt;/h3&gt;
&lt;p&gt;d차원 공간에서 &lt;strong&gt;초평면&lt;/strong&gt;은 d-1차원의 아핀 부분공간.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;n . x + b = 0   (n 은 법선 벡터, b 는 bias)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;초구-hypersphere&quot;&gt;초구 (Hypersphere)&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;||x - c|| = r    (c 는 중심, r 은 반지름)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;tesseract-4d-정육면체&quot;&gt;tesseract (4D 정육면체)&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;[0,1]^4&lt;/code&gt; 범위. 16개 꼭짓점, 32개 모서리, 24개 면(정사각형), 8개 정육면체 셀.&lt;/p&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;h3 id=&quot;점과-초평면의-거리&quot;&gt;점과 초평면의 거리&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;hyperplane_distance(P, n, b):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return |n . P + b| / ||n||&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;d차원-convex-hull&quot;&gt;d차원 Convex Hull&lt;/h3&gt;
&lt;p&gt;d &gt;= 4 인 경우 QuickHull 의 확장 (Chan’s algorithm) 은 존재하지만 대회에서는 d ≤ 3 까지만 현실적. d=4 에서 O(N^2) 까지도 문제에 따라 사용 가능.&lt;/p&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// 4D 벡터 연산: 내적, norm, 점-초평면 거리 #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; struct Vec4 { double w, x, y, z; }; double dot(Vec4 a, Vec4 b) {     return a.w*b.w + a.x*b.x + a.y*b.y + a.z*b.z; } double norm(Vec4 a) { return sqrt(dot(a, a)); } double dist(Vec4 p, Vec4 n, double b) {     return fabs(dot(p, n) + b) / norm(n); } int main() {     Vec4 p{1, 2, 3, 4};     Vec4 n{0, 0, 0, 1};   // 초평면 w=0 의 법선     double b = 0;     cout &amp;#x3C;&amp;#x3C; &quot;distance to hyperplane w=0: &quot;;     cout &amp;#x3C;&amp;#x3C; dist(p, n, b) &amp;#x3C;&amp;#x3C; &quot;\\n&quot;;     // 4D tesseract 꼭짓점 (0,0,0,0) ~ (1,1,1,1)     Vec4 a{0,0,0,0}, c{1,1,1,1};     cout &amp;#x3C;&amp;#x3C; &quot;tesseract diagonal: &quot; &amp;#x3C;&amp;#x3C; norm({c.w-a.w, c.x-a.x, c.y-a.y, c.z-a.z}) &amp;#x3C;&amp;#x3C; &quot;\\n&quot;; }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: `# 4D 벡터 연산 (함수형)
import math&lt;/p&gt;
&lt;p&gt;def dot(a, b):
return sum(x*y for x, y in zip(a, b))&lt;/p&gt;
&lt;p&gt;def norm(a):
return math.sqrt(dot(a, a))&lt;/p&gt;
&lt;p&gt;def hyperplane_dist(p, n, b):
return abs(dot(p, n) + b) / norm(n)&lt;/p&gt;
&lt;p&gt;p = (1, 2, 3, 4)
n = (0, 0, 0, 1)    # 초평면 w=0 법선
b = 0
print(“distance to hyperplane w=0:”, hyperplane_dist(p, n, b))&lt;/p&gt;
&lt;h1 id=&quot;tesseract-대각선&quot;&gt;tesseract 대각선&lt;/h1&gt;
&lt;p&gt;a, c = (0,0,0,0), (1,1,1,1)
print(“tesseract diagonal:”, norm(tuple(c[i]-a[i] for i in range(4))))&lt;code&gt;,     },     {       language: &quot;java&quot;,       label: &quot;Java&quot;,       code: &lt;/code&gt;// 4D 벡터: Java record (Java 16+)&lt;/p&gt;
&lt;p&gt;public class Main {
record Vec4(double w, double x, double y, double z) {
double dot(Vec4 o) { return w&lt;em&gt;o.w + x&lt;/em&gt;o.x + y&lt;em&gt;o.y + z&lt;/em&gt;o.z; }
double norm() { return Math.sqrt(dot(this)); }
}
static double dist(Vec4 p, Vec4 n, double b) {
return Math.abs(p.dot(n) + b) / n.norm();
}
public static void main(String[] args) {
Vec4 p = new Vec4(1, 2, 3, 4);
Vec4 n = new Vec4(0, 0, 0, 1);
System.out.printf(“거리: %.2f%n”, dist(p, n, 0));
Vec4 a = new Vec4(0,0,0,0), c = new Vec4(1,1,1,1);
Vec4 d = new Vec4(c.w-a.w, c.x-a.x, c.y-a.y, c.z-a.z);
System.out.printf(“대각선: %.2f%n”, d.norm());
}
}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;기본 예제&quot;,       input: ``,       output: &lt;/code&gt;distance to hyperplane w=0: 4
tesseract diagonal: 2`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;

































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;벡터 내적 (d차원)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(d)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;점-초평면 거리&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(d)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Convex Hull (d=2)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N log N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Convex Hull (d=3)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N log N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Convex Hull (d&gt;=4)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N^{d/2}) - 대회 수준 비현실적&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;최근접 이웃 (Naive)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N^2 * d)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;h3 id=&quot;1-고차원에서의-거리&quot;&gt;1. 고차원에서의 거리&lt;/h3&gt;
&lt;p&gt;d 가 클수록 모든 점 쌍 간 거리가 비슷해지는 현상 (차원의 저주). 최근접 / 최원접 구분이 어려워짐.&lt;/p&gt;
&lt;h3 id=&quot;2-cosine-similarity&quot;&gt;2. Cosine Similarity&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;cos(a, b) = (a . b) / (||a|| * ||b||)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;추천 시스템, 정보 검색에서 고차원 벡터의 방향 유사도 측정.&lt;/p&gt;
&lt;h3 id=&quot;3-선형-변환과-행렬&quot;&gt;3. 선형 변환과 행렬&lt;/h3&gt;
&lt;p&gt;고차원 기하는 행렬 곱으로 표현. 회전, 반사, 스케일링 모두 &lt;code&gt;d x d&lt;/code&gt; 행렬.&lt;/p&gt;
&lt;h3 id=&quot;4-4d---3d-투영-perspective&quot;&gt;4. 4D -&gt; 3D 투영 (Perspective)&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;P_3d = (P.x / P.w, P.y / P.w, P.z / P.w)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;tesseract 의 3D 그림자, 게임 엔진의 4D 렌더링.&lt;/p&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-외적-부재&quot;&gt;1. 외적 부재&lt;/h3&gt;
&lt;p&gt;d &gt;= 4 에서 cross product 는 정의되지 않음. &lt;code&gt;d choose 2&lt;/code&gt; 개의 wedge component 로 bivector 표현.&lt;/p&gt;
&lt;h3 id=&quot;2-curse-of-dimensionality&quot;&gt;2. Curse of Dimensionality&lt;/h3&gt;
&lt;p&gt;d=20 만 되어도 단위 구의 부피는 거의 0 에 가까움. 무작위 두 점 사이 거리가 모두 거의 일정.&lt;/p&gt;
&lt;h3 id=&quot;3-정수-연산-확장&quot;&gt;3. 정수 연산 확장&lt;/h3&gt;
&lt;p&gt;CCW 의 d차원 일반화는 행렬식 계산 필요 (O(d!)). d 가 크면 float 으로만 가능.&lt;/p&gt;
&lt;h3 id=&quot;4-boj-에서의-희소성&quot;&gt;4. BOJ 에서의 희소성&lt;/h3&gt;
&lt;p&gt;순수 4D+ 기하 문제는 BOJ 에 거의 없음. d ≤ 3 까지만 출제.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 17058&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Reservoir&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;39.1%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/17000-17099/17058&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 16115&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;팬이에요&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;25.8%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/16100-16199/16115&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 10903&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Wall construction&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;61.4%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/10900-10999/10903&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Geometry|2D 기하 기본]]&lt;/li&gt;
&lt;li&gt;[[Geometry-3D|3D 기하]]&lt;/li&gt;
&lt;li&gt;[[Convex Hull|볼록 껍질]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>geometry</category><category>geometry-hyper</category><author>koa (김신건)</author></item><item><title>그린 정리 (Green&apos;s Theorem)</title><link>https://shinkeonkim.com/wiki/algorithm/geometry/green/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/geometry/green/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;그린 정리 (Green’s Theorem)&lt;/strong&gt; 는 평면 위의 폐곡선 C 에 대한 선적분(line integral) 과 그 내부 영역 D 에 대한 이중 적분(double integral) 을 연결: ∮_C (L dx + M dy) = ∬_D (∂M/∂x - ∂L/∂y) dA. 곡선 C 는 반시계 방향(counterclockwise)으로 양의 방향.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;폐곡선으로 둘러싸인 영역의 넓이를 선적분만으로 계산.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Naive 접근&lt;/strong&gt;: 영역을 작은 사각형으로 분할해 Riemann 합. mesh size 에 의존.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심 통찰&lt;/strong&gt;: Green 정리의 특수한 경우: ∮_C x dy = area(D). 즉, 경계선을 따라 x dy 만 적분하면 면적.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PS 위치&lt;/strong&gt;: [[Shoelace|신발끈 공식]] 이 Green 정리의 이산 버전. 다각형의 면적 O(N). 좌표 기하 문제에서 면적을 직접 계산해야 할 때.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Green 정리 (일반):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  ∮_C (L dx + M dy) = ∬_D (∂M/∂x - ∂L/∂y) dA&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;면적 공식 (L = -y/2, M = x/2):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  Area(D) = ∮_C (x dy - y dx) / 2 = ∮_C x dy&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;다각형 면적 (Shoelace, 이산 버전):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  Area = (1/2) * | Σ (x_i * y_{i+1} - x_{i+1} * y_i) |&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;증명 스케치: ∂M/∂x - ∂L/∂y 가 1 이 되도록 L, M 을 잡으면 우변은 area.&lt;/p&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;polygon_area(vertices[0..N-1]):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    area2 = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for i = 0..N-1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        j = (i+1) % N&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        area2 += vertices[i].x * vertices[j].y&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        area2 -= vertices[j].x * vertices[i].y&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return abs(area2) / 2.0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// Green 정리 기반 shoelace 공식으로 다각형 면적 #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; using ll = long long; struct Pt { ll x, y; }; int main() {     int n; cin &gt;&gt; n;     vector&amp;#x3C;Pt&gt; p(n);     for (auto&amp;#x26; v : p) cin &gt;&gt; v.x &gt;&gt; v.y;     ll area2 = 0;     for (int i = 0; i &amp;#x3C; n; i++) {         int j = (i + 1) % n;         area2 += p[i].x * p[j].y;         area2 -= p[j].x * p[i].y;     }     area2 = abs(area2);     cout &amp;#x3C;&amp;#x3C; &quot;Area: &quot; &amp;#x3C;&amp;#x3C; area2 / 2;     if (area2 % 2) cout &amp;#x3C;&amp;#x3C; &quot;.5&quot;;     cout &amp;#x3C;&amp;#x3C; &quot;\\n&quot;; }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: &lt;code&gt;# Green 정리로 다각형 면적 (shoelace) def polygon_area(p):     area2 = 0     n = len(p)     for i in range(n):         j = (i + 1) % n         area2 += p[i][0] * p[j][1] - p[j][0] * p[i][1]     area2 = abs(area2)     area = area2 // 2     return area, area2 % 2 n = int(input()) p = [tuple(map(int, input().split())) for _ in range(n)] area, half = polygon_area(p) print(f&quot;Area: {area}&quot; + (&quot;.5&quot; if half else &quot;&quot;))&lt;/code&gt;,
},
{
language: “java”,
label: “Java”,
code: `import java.util.*;&lt;/p&gt;
&lt;p&gt;public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine());
long[] x = new long[n], y = new long[n];
for (int i = 0; i &amp;#x3C; n; i++) {
StringTokenizer st = new StringTokenizer(br.readLine());
x[i] = Long.parseLong(st.nextToken());
y[i] = Long.parseLong(st.nextToken());
}
long a2 = 0;
for (int i = 0; i &amp;#x3C; n; i++) {
int j = (i + 1) % n;
a2 += x[i] * y[j] - x[j] * y[i];
}
a2 = Math.abs(a2);
System.out.print(“Area: ” + a2 / 2);
if (a2 % 2 == 1) System.out.print(“.5”);
System.out.println();
}
}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;직사각형 (0,0)-(4,0)-(4,3)-(0,3)&quot;,       input: &lt;/code&gt;4
0 0
4 0
4 3
0 3&lt;code&gt;,       output: &lt;/code&gt;Area: 12&lt;code&gt;,     },     {       label: &quot;삼각형 (0,0)-(6,0)-(0,8)&quot;,       input: &lt;/code&gt;3
0 0
6 0
0 8&lt;code&gt;,       output: &lt;/code&gt;Area: 24&lt;code&gt;,     },     {       label: &quot;오각형&quot;,       input: &lt;/code&gt;5
0 0
5 0
6 3
2 5
-1 2&lt;code&gt;,       output: &lt;/code&gt;Area: 24`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(1) (incremental)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;선적분 변환&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(1) per edge&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Shoelace (신발끈 공식)&lt;/strong&gt;: Green 정리의 이산 버전. 정수 좌표 다각형의 면적.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stokes 정리&lt;/strong&gt;: 3차원 일반화. Green 은 Stokes 의 2D 특수 케이스.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;발산 정리 (Divergence Theorem)&lt;/strong&gt;: ∮ F·n ds = ∬ div F dA (flux form).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;응용&lt;/strong&gt;: 유체 역학에서 circulation, CAD 에서 면적 계산, GIS.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-방향&quot;&gt;1. 방향&lt;/h3&gt;
&lt;p&gt;시계 방향(clockwise) 입력이면 면적이 음수. 반드시 &lt;code&gt;abs()&lt;/code&gt; 처리.&lt;/p&gt;
&lt;h3 id=&quot;2-정수-좌표-5-문제&quot;&gt;2. 정수 좌표 .5 문제&lt;/h3&gt;
&lt;p&gt;면적이 2 로 나누어떨어지지 않으면 .5 단위. 코드에서 &lt;code&gt;area2&lt;/code&gt; (2배 값) 를 유지해야 정밀도 손실 없음.&lt;/p&gt;
&lt;h3 id=&quot;3-자기-교차-다각형&quot;&gt;3. 자기 교차 다각형&lt;/h3&gt;
&lt;p&gt;자기 교차하는 폴리곤은 Green 정리의 단순 폐곡선 조건을 만족하지 않음. signed area 개념으로 확장 가능.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2166&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;다각형의 면적&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2100-2199/2166&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 1485&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;정사각형&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1400-1499/1485&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 1358&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;하키&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1300-1399/1358&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 1004&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;어린 왕자&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1000-1099/1004&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Shoelace|신발끈 공식]]&lt;/li&gt;
&lt;li&gt;[[Pick|Pick 정리]]&lt;/li&gt;
&lt;li&gt;[[Geometry Basic|기하 기본]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>geometry</category><category>green</category><author>koa (김신건)</author></item><item><title>Half-Plane Intersection</title><link>https://shinkeonkim.com/wiki/algorithm/geometry/half-plane-intersection/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/geometry/half-plane-intersection/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;반평면 교집합 (Half-Plane Intersection)&lt;/strong&gt; 은 N 개의 직선으로 정의된 반평면 &lt;code&gt;ax + by + c &amp;#x3C;= 0&lt;/code&gt; (또는 &lt;code&gt;&gt;= 0&lt;/code&gt;) 을 모두 만족하는 점들의 집합, 즉 &lt;strong&gt;볼록다각형 (convex polygon)&lt;/strong&gt; 혹은 무한 영역 / 공집합을 구하는 기하 알고리즘.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;각 반평면을 naive 하게 교집합하면 O(N^2). 즉, convex polygon 의 clipping 을 N 번 반복해야 한다. N=10^5 면 불가능.&lt;/p&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;반평면을 각도(기울기) 순으로 정렬하고 deque 로 관리하면 convex hull 과 동일한 각도 정렬 + Amortized O(N) 테크닉&lt;/em&gt; 으로 O(N log N) 에 해결된다. [[Convex Hull]] 의 Graham scan 과 거의 같은 구조.&lt;/p&gt;
&lt;p&gt;활용: 볼록다각형 내 최대 내접원, Minkowski 덧셈, 일부 LP (Linear Programming) 의 특수 케이스.&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;p&gt;각 반평면을 &lt;strong&gt;directed line&lt;/strong&gt; &lt;code&gt;p -&gt; q&lt;/code&gt; 로 표현. &lt;strong&gt;valid side 는 항상 line 의 왼쪽 (CCW &gt; 0)&lt;/strong&gt;. 각도 &lt;code&gt;atan2(dy, dx)&lt;/code&gt; 순으로 정렬 후 deque 로 관리.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;for each L in sorted order:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    while deque[-2] x deque[-1] NOT in L -&gt; pop back&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    while deque[0] x deque[1] NOT in L -&gt; pop front&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    push back L&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;마지막에 front/back 정리&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;각 반평면은 최대 한 번 push/pop. O(N).&lt;/p&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;halfplane_intersection(L):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    sort L by angle = atan2(dy, dx) (같은 angle: tight 한 것만 keep)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    deque = empty&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for each line in L:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        while |deque| &gt;= 2 and cross(line, inter(deque[-2], deque[-1])) &amp;#x3C; 0:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            pop back&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        while |deque| &gt;= 2 and cross(line, inter(deque[0], deque[1])) &amp;#x3C; 0:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            pop front&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        push back&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    while |deque| &gt;= 3 and cross(deque[0], inter(deque[-2], deque[-1])) &amp;#x3C; 0:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        pop back&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return [inter(deque[i], deque[(i+1)%|deque|]) for i]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// Half-plane intersection, O(N log N) #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; using ld = long double; const ld EPS = 1e-9; struct Pt { ld x, y; }; struct Line { Pt p, q; }; // valid: left side ld cross(Pt a, Pt b) { return a.x*b.y - a.y*b.x; } ld cross(Pt a, Pt b, Pt c) { return (b.x-a.x)*(c.y-a.y) - (b.y-a.y)*(c.x-a.x); } Pt inter(Line a, Line b) {     Pt va = {a.q.x-a.p.x, a.q.y-a.p.y};     Pt vb = {b.q.x-b.p.x, b.q.y-b.p.y};     Pt vc = {b.p.x-a.p.x, b.p.y-a.p.y};     ld t = cross(vc, vb) / cross(va, vb);     return {a.p.x + t*va.x, a.p.y + t*va.y}; } ld angle(Line l) { return atan2(l.q.y-l.p.y, l.q.x-l.p.x); } vector&amp;#x3C;Pt&gt; halfplane(vector&amp;#x3C;Line&gt; L) {     sort(L.begin(), L.end(), [](Line a, Line b) { return angle(a) &amp;#x3C; angle(b); });     deque&amp;#x3C;Line&gt; dq;     for (auto&amp;#x26; l : L) {         while (dq.size() &gt;= 2 &amp;#x26;&amp;#x26; cross(l.p, l.q, inter(dq[dq.size()-2], dq.back())) &amp;#x3C; EPS) dq.pop_back();         while (dq.size() &gt;= 2 &amp;#x26;&amp;#x26; cross(l.p, l.q, inter(dq[0], dq[1])) &amp;#x3C; EPS) dq.pop_front();         dq.push_back(l);     }     while (dq.size() &gt;= 3 &amp;#x26;&amp;#x26; cross(dq[0].p, dq[0].q, inter(dq[dq.size()-2], dq.back())) &amp;#x3C; EPS) dq.pop_back();     vector&amp;#x3C;Pt&gt; res;     for (int i = 0; i &amp;#x3C; (int)dq.size(); i++) res.push_back(inter(dq[i], dq[(i+1)%dq.size()]));     return res; } int main() {     vector&amp;#x3C;Line&gt; L = {{{0,0},{1,0}}, {{0,1},{0,0}}, {{6,0},{0,6}}, {{4,6},{-2,0}}};     auto poly = halfplane(L);     cout &amp;#x3C;&amp;#x3C; fixed &amp;#x3C;&amp;#x3C; setprecision(0);     for (auto&amp;#x26; p : poly) cout &amp;#x3C;&amp;#x3C; &quot;(&quot; &amp;#x3C;&amp;#x3C; p.x &amp;#x3C;&amp;#x3C; &quot;, &quot; &amp;#x3C;&amp;#x3C; p.y &amp;#x3C;&amp;#x3C; &quot;)\\n&quot;; }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: `# Half-plane intersection, O(N log N)
import math&lt;/p&gt;
&lt;p&gt;class Pt:
def &lt;strong&gt;init&lt;/strong&gt;(self, x, y): self.x, self.y = x, y&lt;/p&gt;
&lt;p&gt;def cross(a, b, c):
return (b.x-a.x)&lt;em&gt;(c.y-a.y) - (b.y-a.y)&lt;/em&gt;(c.x-a.x)&lt;/p&gt;
&lt;p&gt;def inter(a, b):
v1 = Pt(a[1].x-a[0].x, a[1].y-a[0].y)
v2 = Pt(b[1].x-b[0].x, b[1].y-b[0].y)
v3 = Pt(b[0].x-a[0].x, b[0].y-a[0].y)
denom = v1.x&lt;em&gt;v2.y - v1.y&lt;/em&gt;v2.x
if abs(denom) &amp;#x3C; 1e-12: return None
t = (v3.x&lt;em&gt;v2.y - v3.y&lt;/em&gt;v2.x) / denom
return Pt(a[0].x + t&lt;em&gt;v1.x, a[0].y + t&lt;/em&gt;v1.y)&lt;/p&gt;
&lt;p&gt;lines = [
(Pt(0,0), Pt(1,0)), (Pt(0,1), Pt(0,0)),
(Pt(6,0), Pt(0,6)), (Pt(4,6), Pt(-2,0))
]
lines.sort(key=lambda l: math.atan2(l[1].y-l[0].y, l[1].x-l[0].x))
dq = []
for l in lines:
while len(dq) &gt;= 2 and cross(l[0], l[1], inter(dq[-2], dq[-1])) &amp;#x3C; 0:
dq.pop()
while len(dq) &gt;= 2 and cross(l[0], l[1], inter(dq[0], dq[1])) &amp;#x3C; 0:
dq.pop(0)
dq.append(l)&lt;/p&gt;
&lt;p&gt;while len(dq) &gt;= 3 and cross(dq[0][0], dq[0][1], inter(dq[-2], dq[-1])) &amp;#x3C; 0:
dq.pop()&lt;/p&gt;
&lt;p&gt;res = [inter(dq[i], dq[(i+1)%len(dq)]) for i in range(len(dq))]
for p in res:
print(f”({p.x:.0f}, {p.y:.0f})“)&lt;code&gt;,     },     {       language: &quot;java&quot;,       label: &quot;Java&quot;,       code: &lt;/code&gt;// Half-plane intersection (basic, no deque in snippet)&lt;/p&gt;
&lt;p&gt;public class Main {
static double cross(Point2D o, Point2D a, Point2D b) {
return (a.getX()-o.getX())&lt;em&gt;(b.getY()-o.getY())
- (a.getY()-o.getY())&lt;/em&gt;(b.getX()-o.getX());
}&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;static Point2D inter(Point2D p1, Point2D q1, Point2D p2, Point2D q2) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    double vx1 = q1.getX()-p1.getX(), vy1 = q1.getY()-p1.getY();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    double vx2 = q2.getX()-p2.getX(), vy2 = q2.getY()-p2.getY();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    double vx3 = p2.getX()-p1.getX(), vy3 = p2.getY()-p1.getY();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    double denom = vx1*vy2 - vy1*vx2;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    double t = (vx3*vy2 - vy3*vx2) / denom;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return new Point2D.Double(p1.getX()+t*vx1, p1.getY()+t*vy1);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;static double angle(Point2D p, Point2D q) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return Math.atan2(q.getY()-p.getY(), q.getX()-p.getX());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;public static void main(String[] args) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    Point2D.Double[][] lines = {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        {new Point2D.Double(0,0), new Point2D.Double(1,0)},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        {new Point2D.Double(0,1), new Point2D.Double(0,0)},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        {new Point2D.Double(6,0), new Point2D.Double(0,6)},&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        {new Point2D.Double(4,6), new Point2D.Double(-2,0)}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    };&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    Arrays.sort(lines, (a,b) -&gt; Double.compare(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        angle(a[0],a[1]), angle(b[0],b[1])));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    // Deque simulation omitted for brevity&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for (int i = 0; i &amp;#x3C; 4; i++) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        Point2D p = inter(lines[i][0], lines[i][1],&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            lines[(i+1)%4][0], lines[(i+1)%4][1]);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        System.out.printf(&quot;(%.0f, %.0f)%n&quot;, p.getX(), p.getY());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;4 반평면 교집합&quot;,       input: ``,       output: &lt;/code&gt;(0, 0)
(6, 0)
(2, 4)
(0, 2)`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (정렬)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(N log N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (Deque)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(N) (각 line 최대 1회 push/pop)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;전체&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(N log N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(N)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;h3 id=&quot;1-볼록다각형-내-최대-내접원&quot;&gt;1. 볼록다각형 내 최대 내접원&lt;/h3&gt;
&lt;p&gt;Convex polygon 의 각 변을 안쪽으로 평행 이동 (offset) 하여 새로운 반평면들 생성. 그 교집합에 가장 큰 원이 들어갈 수 있는 중심점.&lt;/p&gt;
&lt;h3 id=&quot;2-minkowski-sum&quot;&gt;2. Minkowski Sum&lt;/h3&gt;
&lt;p&gt;두 볼록다각형 A, B 의 Minkowski sum A + B = {a + b | a in A, b in B}. 각 변의 방향별로 half-plane 으로 표현 가능.&lt;/p&gt;
&lt;h3 id=&quot;3-lp-linear-programming-의-2d-특수-케이스&quot;&gt;3. LP (Linear Programming) 의 2D 특수 케이스&lt;/h3&gt;
&lt;p&gt;2변수 LP 는 half-plane intersection 으로 변환 가능. 후에 simplex 또는 [[Parametric Search]] 로 최적화.&lt;/p&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-eps-좌우-판정&quot;&gt;1. EPS 좌우 판정&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;cross(l.p, l.q, inter(...)) &amp;#x3C; EPS&lt;/code&gt; 로 반평면에 속하지 않음을 판정. EPS 를 너무 작게 잡으면 floating error 로 무한루프.&lt;/p&gt;
&lt;h3 id=&quot;2-무한-영역--공집합&quot;&gt;2. 무한 영역 / 공집합&lt;/h3&gt;
&lt;p&gt;결과가 유계 폴리곤이 아닐 수 있음. 빈 deque 나 2개 미만의 점이면 unbounded 또는 empty.&lt;/p&gt;
&lt;h3 id=&quot;3-평행-반평면&quot;&gt;3. 평행 반평면&lt;/h3&gt;
&lt;p&gt;같은 각도의 반평면은 &lt;code&gt;cross&lt;/code&gt; 값이 더 큰 (더 제약적인) 것만 유지.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 16872&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Count the Half-Planes&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;(수집 안 됨)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/16800-16899/16872&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 12795&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Half-Plane&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;(수집 안 됨)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/12700-12799/12795&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 20420&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;1차원 Half-Plane&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;(수집 안 됨)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/20400-20499/20420&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Convex Hull]] (비슷한 Deque 구조)&lt;/li&gt;
&lt;li&gt;[[Sweeping|스위핑]]&lt;/li&gt;
&lt;li&gt;[[Line Intersection|선분 교차 판정]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>geometry</category><category>half-plane-intersection</category><author>koa (김신건)</author></item><item><title>선분 교차 (Line Segment Intersection)</title><link>https://shinkeonkim.com/wiki/algorithm/geometry/line-intersection/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/geometry/line-intersection/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;선분 교차 판정 (Line Segment Intersection)&lt;/strong&gt; 은 2D 평면 위 두 선분 AB 와 CD 가 서로 교차하는지 O(1) 에 판정하는 기하 연산. &lt;strong&gt;CCW (Counter-Clockwise)&lt;/strong&gt; 벡터 외적만으로 이루어짐.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;두 선분의 교차 여부는 기하 문제의 가장 기본적인 빌딩 블록.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;naive&lt;/strong&gt;: 모든 선분 쌍 O(N^2) 검사. N=10^3 이상에서 터짐.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;O(1) CCW 판정&lt;/strong&gt;: [[CCW]] 4 회로 교차 여부를 바로 알 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;각 선분의 양 끝점이 상대 선분의 양 옆에 있는지 확인&lt;/em&gt;. 즉, &lt;code&gt;CCW(A,B,C)&lt;/code&gt; 와 &lt;code&gt;CCW(A,B,D)&lt;/code&gt; 의 부호가 다른지, 동시에 &lt;code&gt;CCW(C,D,A)&lt;/code&gt; 와 &lt;code&gt;CCW(C,D,B)&lt;/code&gt; 의 부호가 다른지.&lt;/p&gt;
&lt;p&gt;활용: [[Convex Hull]], 선분 그룹 (DSU), [[Sweeping]] 의 기초 연산.&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;p&gt;세 점의 CCW 값을 구하는 cross product:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;ccw(a, b, c) = (b.x - a.x)(c.y - a.y) - (b.y - a.y)(c.x - a.x)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&gt; 0&lt;/code&gt; : 반시계 (좌회전)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;#x3C; 0&lt;/code&gt; : 시계 (우회전)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;= 0&lt;/code&gt; : 일직선 (collinear)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;두 선분 AB, CD 가 교차하는 조건:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;ccw(a,b,c) * ccw(a,b,d) &amp;#x3C;= 0  AND&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;ccw(c,d,a) * ccw(c,d,b) &amp;#x3C;= 0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉, A-B 기준으로 C 와 D 가 서로 다른 쪽에 있고 (또는 하나가 선 위), C-D 기준으로 A 와 B 가 서로 다른 쪽에 있음.&lt;/p&gt;
&lt;p&gt;Collinear (네 점이 모두 일직선) 인 경우는 projection range check 로 겹침 판정.&lt;/p&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;intersect(a, b, c, d):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    ab = ccw(a,b,c) * ccw(a,b,d)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    cd = ccw(c,d,a) * ccw(c,d,b)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if ab == 0 and cd == 0:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        // 모두 일직선: x/y projection 이 겹치는지 확인&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if a.x &gt; b.x: swap(a,b)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if c.x &gt; d.x: swap(c,d)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if max(a.x,c.x) &gt; min(b.x,d.x): return false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if a.y &gt; b.y: swap(a,b)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if c.y &gt; d.y: swap(c,d)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        return max(a.y,c.y) &amp;#x3C;= min(b.y,d.y)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return ab &amp;#x3C;= 0 and cd &amp;#x3C;= 0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: `// Line segment intersection, O(1)
#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
using ll = long long;&lt;/p&gt;
&lt;p&gt;struct Point { ll x, y; };&lt;/p&gt;
&lt;p&gt;ll ccw(Point a, Point b, Point c) {
ll ret = (b.x-a.x)&lt;em&gt;(c.y-a.y) - (b.y-a.y)&lt;/em&gt;(c.x-a.x);
if (ret &gt; 0) return 1;
if (ret &amp;#x3C; 0) return -1;
return 0;
}&lt;/p&gt;
&lt;p&gt;bool intersect(Point a, Point b, Point c, Point d) {
ll ab = ccw(a,b,c) * ccw(a,b,d);
ll cd = ccw(c,d,a) * ccw(c,d,b);
if (ab == 0 &amp;#x26;&amp;#x26; cd == 0) {
if (a.x &gt; b.x) swap(a,b);
if (c.x &gt; d.x) swap(c,d);
if (max(a.x,c.x) &gt; min(b.x,d.x)) return false;
if (a.y &gt; b.y) swap(a,b);
if (c.y &gt; d.y) swap(c,d);
return max(a.y,c.y) &amp;#x3C;= min(b.y,d.y);
}
return ab &amp;#x3C;= 0 &amp;#x26;&amp;#x26; cd &amp;#x3C;= 0;
}&lt;/p&gt;
&lt;p&gt;int main() {
Point a{1,1}, b{7,3}, c{2,4}, d{6,1};
cout &amp;#x3C;&amp;#x3C; (intersect(a,b,c,d) ? “YES” : “NO”) &amp;#x3C;&amp;#x3C; “\n”;
Point e{0,0}, f{3,3}, g{1,1}, h{2,2};
cout &amp;#x3C;&amp;#x3C; (intersect(e,f,g,h) ? “YES” : “NO”) &amp;#x3C;&amp;#x3C; “\n”;
}&lt;code&gt;,     },     {       language: &quot;python&quot;,       label: &quot;Python&quot;,       code: &lt;/code&gt;# CCW-based line segment intersection
def ccw(ax, ay, bx, by, cx, cy):
ret = (bx-ax)&lt;em&gt;(cy-ay) - (by-ay)&lt;/em&gt;(cx-ax)
if ret &gt; 0: return 1
if ret &amp;#x3C; 0: return -1
return 0&lt;/p&gt;
&lt;p&gt;def intersect(ax, ay, bx, by, cx, cy, dx, dy):
ab = ccw(ax,ay,bx,by,cx,cy) * ccw(ax,ay,bx,by,dx,dy)
cd = ccw(cx,cy,dx,dy,ax,ay) * ccw(cx,cy,dx,dy,bx,by)
if ab == 0 and cd == 0:
if (ax, ay) &gt; (bx, by): ax, bx = bx, ax; ay, by = by, ay
if (cx, cy) &gt; (dx, dy): cx, dx = dx, cx; cy, dy = dy, cy
return max(ax, cx) &amp;#x3C;= min(bx, dx) and max(ay, cy) &amp;#x3C;= min(by, dy)
return ab &amp;#x3C;= 0 and cd &amp;#x3C;= 0&lt;/p&gt;
&lt;p&gt;print(“YES” if intersect(1,1, 7,3, 2,4, 6,1) else “NO”)
print(“YES” if intersect(0,0, 3,3, 1,1, 2,2) else “NO”)&lt;code&gt;,     },     {       language: &quot;java&quot;,       label: &quot;Java&quot;,       code: &lt;/code&gt;import java.util.&lt;em&gt;;
public class Main {
static int ccw(long ax, long ay, long bx, long by, long cx, long cy) {
long ret = (bx-ax)&lt;/em&gt;(cy-ay) - (by-ay)*(cx-ax);
if (ret &gt; 0) return 1;
if (ret &amp;#x3C; 0) return -1;
return 0;
}&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;static boolean intersect(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    long ax, long ay, long bx, long by,&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    long cx, long cy, long dx, long dy&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    int ab = ccw(ax,ay,bx,by,cx,cy) * ccw(ax,ay,bx,by,dx,dy);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    int cd = ccw(cx,cy,dx,dy,ax,ay) * ccw(cx,cy,dx,dy,bx,by);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if (ab == 0 &amp;#x26;&amp;#x26; cd == 0) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if (ax &gt; bx || (ax == bx &amp;#x26;&amp;#x26; ay &gt; by)) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            long t; t=ax; ax=bx; bx=t; t=ay; ay=by; by=t;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if (cx &gt; dx || (cx == dx &amp;#x26;&amp;#x26; cy &gt; dy)) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            long t; t=cx; cx=dx; dx=t; t=cy; cy=dy; dy=t;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        return (Math.max(ax,cx) &amp;#x3C;= Math.min(bx,dx) &amp;#x26;&amp;#x26;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                Math.max(ay,cy) &amp;#x3C;= Math.min(by,dy));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return ab &amp;#x3C;= 0 &amp;#x26;&amp;#x26; cd &amp;#x3C;= 0;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;public static void main(String[] args) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    System.out.println(intersect(1,1,7,3,2,4,6,1) ? &quot;YES&quot; : &quot;NO&quot;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    System.out.println(intersect(0,0,3,3,1,1,2,2) ? &quot;YES&quot; : &quot;NO&quot;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;일반 + Collinear&quot;,       input: ``,       output: &lt;/code&gt;YES
YES`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;

















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(1) (CCW 4 회 + 비교)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(1)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;h3 id=&quot;1-선분-그룹-union-find&quot;&gt;1. 선분 그룹 (Union-Find)&lt;/h3&gt;
&lt;p&gt;N 개 선분의 교차 관계를 DSU 로 묶으면 연결된 컴포넌트로 그룹화 가능. BOJ 2162.&lt;/p&gt;
&lt;h3 id=&quot;2-직선과의-교차&quot;&gt;2. 직선과의 교차&lt;/h3&gt;
&lt;p&gt;무한 직선과 선분의 교차: &lt;code&gt;ccw(A,B,C) == 0&lt;/code&gt; 조건을 제거하고 projection 만 체크.&lt;/p&gt;
&lt;h3 id=&quot;3-교차점-좌표-구하기&quot;&gt;3. 교차점 좌표 구하기&lt;/h3&gt;
&lt;p&gt;두 직선의 교점을 parameter form 으로 직접 계산. collinear 가 아닐 때만.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;t = cross(C-A, D-C) / cross(B-A, D-C)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;P = A + t * (B - A)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;4-선분-교차-개수-sweep&quot;&gt;4. 선분 교차 개수 (Sweep)&lt;/h3&gt;
&lt;p&gt;N 개 선분의 모든 교차점을 찾는 [[Sweeping]] 알고리즘. O((N+K) log N). Bentley-Ottmann.&lt;/p&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-정수-오버플로우&quot;&gt;1. 정수 오버플로우&lt;/h3&gt;
&lt;p&gt;CCW 에서 &lt;code&gt;(b.x-a.x) * (c.y-a.y)&lt;/code&gt; 는 &lt;code&gt;10^6 * 10^6 = 10^12&lt;/code&gt; 로 &lt;code&gt;int&lt;/code&gt; (32-bit) 범위 초과. &lt;code&gt;long long&lt;/code&gt; 필요.&lt;/p&gt;
&lt;h3 id=&quot;2-collinear-겹침-처리&quot;&gt;2. Collinear 겹침 처리&lt;/h3&gt;
&lt;p&gt;네 점이 일직선 위에 있을 때 &lt;code&gt;ccw&lt;/code&gt; 만으로는 겹침 여부를 알 수 없음. Projection range check 필수.&lt;/p&gt;
&lt;h3 id=&quot;3-끝점-접촉&quot;&gt;3. 끝점 접촉&lt;/h3&gt;
&lt;p&gt;한 선분의 끝점이 다른 선분 위에 있는 경우 (&amp;#x3C; 0 조건) 교차로 인정. &lt;code&gt;ccw() * ccw() &amp;#x3C; 0&lt;/code&gt; 대신 &lt;code&gt;&amp;#x3C;= 0&lt;/code&gt; 사용.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 17386&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;선분 교차 1&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;38.2%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/17300-17399/17386&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 17387&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;선분 교차 2&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;27.5%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/17300-17399/17387&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 20149&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;선분 교차 3&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;18.3%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/20100-20199/20149&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2162&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;선분 그룹&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;28.5%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2100-2199/2162&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[CCW]] (시계/반시계 판정)&lt;/li&gt;
&lt;li&gt;[[Convex Hull]] (CCW 의 확장)&lt;/li&gt;
&lt;li&gt;[[Sweeping]] (N 개 선분 교차)&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>geometry</category><category>line-intersection</category><author>koa (김신건)</author></item><item><title>최소 외접원 (Minimum Enclosing Circle)</title><link>https://shinkeonkim.com/wiki/algorithm/geometry/min-enclosing-circle/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/geometry/min-enclosing-circle/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;최소 외접원 (Minimum Enclosing Circle, MEC)&lt;/strong&gt; 은 평면 위 N 개의 점을 모두 포함하는 가장 작은 반지름의 원을 찾는 문제. 대표 알고리즘은 Emo Welzl (1991) 의 randomized incremental algorithm, 기댓값 O(N).&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;모든 점을 하나의 원으로 커버하면서 반지름을 최소화.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Naive 접근&lt;/strong&gt;: 점 2개 또는 3개로 결정되는 모든 원을 시도하며 O(N^4).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심 통찰&lt;/strong&gt;: MEC 는 항상 2개 또는 3개의 support point 에 의해 결정. 점을 무작위 순서로 추가하며 원 밖의 점만 경계로 편입.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PS 위치&lt;/strong&gt;: N=10^5 점의 최소 커버 반지름. [[Convex Hull|볼록 껍질]] 위의 점만 고려해도 됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;MEC 의 성질:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;1. MEC 는 점 2개가 직경을 결정 (지름) 하거나, 3개 점이 원주 위에 있음.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;2. MEC 내부에 있는 점은 제거해도 원이 변하지 않음.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;3. 새로운 점 p 가 현재 원 밖에 있으면, p 는 새 원의 경계 위에 있어야 함.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Welzl 의 재귀:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  welzl(P, R):  // P: 남은 점, R: 경계 점&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if P == {} or |R| == 3: return trivial_circle(R)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    p = random(P)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    C = welzl(P - {p}, R)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if p in C: return C&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return welzl(P - {p}, R ∪ {p})&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;welzl(P, R):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if P is empty or |R| == 3:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        return circle_from(R)   // R 크기에 따라 0/1/2/3 점 처리&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    p = random element from P&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    C = welzl(P \ {p}, R)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if p inside C:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        return C&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    R.push(p)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    C = welzl(P \ {p}, R)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    R.pop()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return C&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;circle_from(R):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if |R| == 0: return zero-radius at origin&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if |R| == 1: return zero-radius at R[0]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if |R| == 2: return circle with R[0],R[1] as diameter&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if |R| == 3: return circumcircle of R[0],R[1],R[2]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// Welzl&apos;s algorithm, expected O(N) #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; struct Pt { double x, y; }; struct Cir { Pt c; double r; }; double dist(const Pt&amp;#x26; a, const Pt&amp;#x26; b) {     return hypot(a.x-b.x, a.y-b.y); } bool inside(const Pt&amp;#x26; p, const Cir&amp;#x26; cir) {     return dist(p, cir.c) &amp;#x3C;= cir.r + 1e-9; } Cir c2(const Pt&amp;#x26; a, const Pt&amp;#x26; b) {     return {{(a.x+b.x)/2, (a.y+b.y)/2}, dist(a,b)/2}; } Cir c3(const Pt&amp;#x26; a, const Pt&amp;#x26; b, const Pt&amp;#x26; c) {     double d = 2*(a.x*(b.y-c.y)+b.x*(c.y-a.y)+c.x*(a.y-b.y));     if (abs(d) &amp;#x3C; 1e-12) return c2(a, b);     double ux = ((a.x*a.x+a.y*a.y)*(b.y-c.y) +                  (b.x*b.x+b.y*b.y)*(c.y-a.y) +                  (c.x*c.x+c.y*c.y)*(a.y-b.y)) / d;     double uy = ((a.x*a.x+a.y*a.y)*(c.x-b.x) +                  (b.x*b.x+b.y*b.y)*(a.x-c.x) +                  (c.x*c.x+c.y*c.y)*(b.x-a.x)) / d;     return {{ux, uy}, dist({ux,uy}, a)}; } Cir welzl(vector&amp;#x3C;Pt&gt;&amp;#x26; P, vector&amp;#x3C;Pt&gt;&amp;#x26; R, int n) {     if (n == 0 || R.size() == 3) {         if (R.empty()) return {{0,0}, 0};         if (R.size() == 1) return {R[0], 0};         if (R.size() == 2) return c2(R[0], R[1]);         return c3(R[0], R[1], R[2]);     }     int idx = rand() % n;     swap(P[idx], P[n-1]);     Pt p = P[n-1];     Cir C = welzl(P, R, n-1);     if (inside(p, C)) return C;     R.push_back(p);     C = welzl(P, R, n-1);     R.pop_back();     return C; } int main() {     int N; cin &gt;&gt; N;     vector&amp;#x3C;Pt&gt; P(N);     for (auto&amp;#x26; p : P) cin &gt;&gt; p.x &gt;&gt; p.y;     Cir ans = welzl(P, *new vector&amp;#x3C;Pt&gt;(), N);     cout &amp;#x3C;&amp;#x3C; fixed &amp;#x3C;&amp;#x3C; setprecision(10);     cout &amp;#x3C;&amp;#x3C; ans.c.x &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; ans.c.y &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; ans.r &amp;#x3C;&amp;#x3C; &quot;\\n&quot;; }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: &lt;code&gt;import math, random, sys def dist(a, b):     return math.hypot(a[0]-b[0], a[1]-b[1]) def inside(p, cx, cy, r):     return dist(p, (cx, cy)) &amp;#x3C;= r + 1e-9 def c2(a, b):     return ((a[0]+b[0])/2, (a[1]+b[1])/2, dist(a,b)/2) def c3(a, b, c):     d = 2*(a[0]*(b[1]-c[1]) + b[0]*(c[1]-a[1]) + c[0]*(a[1]-b[1]))     if abs(d) &amp;#x3C; 1e-12: return c2(a, b)     ux = ((a[0]**2+a[1]**2)*(b[1]-c[1]) +           (b[0]**2+b[1]**2)*(c[1]-a[1]) +           (c[0]**2+c[1]**2)*(a[1]-b[1])) / d     uy = ((a[0]**2+a[1]**2)*(c[0]-b[0]) +           (b[0]**2+b[1]**2)*(a[0]-c[0]) +           (c[0]**2+c[1]**2)*(b[0]-a[0])) / d     return (ux, uy, dist((ux,uy), a)) def welzl(P, R):     if not P or len(R) == 3:         if len(R) == 0: return (0.0, 0.0, 0.0)         if len(R) == 1: return (R[0][0], R[0][1], 0.0)         if len(R) == 2: return c2(R[0], R[1])         return c3(R[0], R[1], R[2])     idx = random.randrange(len(P))     P[idx], P[-1] = P[-1], P[idx]     p = P.pop()     cx, cy, r = welzl(P, R)     if inside(p, cx, cy, r):         P.append(p)         return (cx, cy, r)     R.append(p)     cx, cy, r = welzl(P, R)     P.append(p)     R.pop()     return (cx, cy, r) N = int(sys.stdin.readline()) pts = [tuple(map(float, sys.stdin.readline().split())) for _ in range(N)] cx, cy, r = welzl(pts, []) print(f&quot;{cx:.10f} {cy:.10f} {r:.10f}&quot;)&lt;/code&gt;,
},
{
language: “java”,
label: “Java”,
code: `import java.util.*;&lt;/p&gt;
&lt;p&gt;public class Main {
static class Pt { double x, y; Pt(double x, double y) { this.x=x; this.y=y; } }
static class Cir { Pt c; double r; Cir(Pt c, double r) { this.c=c; this.r=r; } }
static double d(Pt a, Pt b) { return Math.hypot(a.x-b.x, a.y-b.y); }
static boolean in(Pt p, Cir cir) { return d(p, cir.c) &amp;#x3C;= cir.r+1e-9; }
static Cir c2(Pt a, Pt b) { return new Cir(new Pt((a.x+b.x)/2,(a.y+b.y)/2), d(a,b)/2); }
static Cir c3(Pt a, Pt b, Pt c) {
double dd = 2*(a.x*(b.y-c.y)+b.x*(c.y-a.y)+c.x*(a.y-b.y));
if (Math.abs(dd)&amp;#x3C;1e-12) return c2(a,b);
double ux = ((a.x&lt;em&gt;a.x+a.y&lt;/em&gt;a.y)&lt;em&gt;(b.y-c.y)+(b.x&lt;/em&gt;b.x+b.y&lt;em&gt;b.y)&lt;/em&gt;(c.y-a.y)+(c.x&lt;em&gt;c.x+c.y&lt;/em&gt;c.y)&lt;em&gt;(a.y-b.y))/dd;
double uy = ((a.x&lt;/em&gt;a.x+a.y&lt;em&gt;a.y)&lt;/em&gt;(c.x-b.x)+(b.x&lt;em&gt;b.x+b.y&lt;/em&gt;b.y)&lt;em&gt;(a.x-c.x)+(c.x&lt;/em&gt;c.x+c.y&lt;em&gt;c.y)&lt;/em&gt;(b.x-a.x))/dd;
return new Cir(new Pt(ux,uy), d(new Pt(ux,uy), a));
}
static Cir welzl(Pt[] P, List&lt;pt&gt; R, int n, Random rnd) {
if (n==0||R.size()==3) {
if (R.isEmpty()) return new Cir(new Pt(0,0),0);
if (R.size()==1) return new Cir(R.get(0),0);
if (R.size()==2) return c2(R.get(0),R.get(1));
return c3(R.get(0),R.get(1),R.get(2));
}
int idx = rnd.nextInt(n);
Pt tmp = P[idx]; P[idx]=P[n-1]; P[n-1]=tmp;
Pt p = P[n-1];
Cir C = welzl(P, R, n-1, rnd);
if (in(p, C)) return C;
R.add(p);
C = welzl(P, R, n-1, rnd);
R.remove(R.size()-1);
return C;
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int N = Integer.parseInt(br.readLine());
Pt[] P = new Pt[N];
for (int i=0; i&amp;#x3C;N; i++) {
StringTokenizer st = new StringTokenizer(br.readLine());
P[i] = new Pt(Double.parseDouble(st.nextToken()), Double.parseDouble(st.nextToken()));
}
Cir ans = welzl(P, new ArrayList&amp;#x3C;&gt;(), N, new Random());
System.out.printf(“%.10f %.10f %.10f%n”, ans.c.x, ans.c.y, ans.r);
}
}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;삼각형&quot;,       input: &lt;/code&gt;3
0 0
1 0
0 1&lt;code&gt;,       output: &lt;/code&gt;0.5000000000 0.5000000000 0.7071067812&lt;code&gt;,     },     {       label: &quot;정사각형&quot;,       input: &lt;/code&gt;4
0 0
2 0
2 2
0 2&lt;code&gt;,       output: &lt;/code&gt;1.0000000000 1.0000000000 1.4142135624`,
},
]}
/&gt;&lt;/pt&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (기댓값)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (최악)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N^2) (매우 드묾)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N) (재귀 스택)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Minimum enclosing ball&lt;/strong&gt;: 고차원 공간으로 일반화. Welzl 은 임의 차원에 동작.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Convex hull 최적화&lt;/strong&gt;: MEC 는 항상 [[Convex Hull|볼록 껍질]] 위의 점에 의해 결정. 껍질만 추려낸 후 Welzl 적용 가능.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;응용&lt;/strong&gt;: 클러스터링 반경 최소화, GPS 오차 보정, 충돌 감지.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-3-점-공선-collinear&quot;&gt;1. 3 점 공선 (collinear)&lt;/h3&gt;
&lt;p&gt;세 점이 일직선이면 외접원이 정의되지 않음. circle_from_3 에서 분모 d = 0 이면 circle_from_2 로 fallback.&lt;/p&gt;
&lt;h3 id=&quot;2-실수-오차&quot;&gt;2. 실수 오차&lt;/h3&gt;
&lt;p&gt;eps = 1e-9 로 inside 판정. 특히 공선 판정에서 오차 보정이 중요.&lt;/p&gt;
&lt;h3 id=&quot;3-재귀-깊이--복사&quot;&gt;3. 재귀 깊이 / 복사&lt;/h3&gt;
&lt;p&gt;Welzl 의 naive 복사 구현은 O(N^2). 인덱스 기반 swap 으로 제자리 처리하여 O(N) 유지.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 13708&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;모든 점을 포함하는 원&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/13700-13799/13708&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2626&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;헬기착륙장&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2600-2699/2626&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 11664&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;선분과 점&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/11600-11699/11664&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Geometry Basic|기하 기본]]&lt;/li&gt;
&lt;li&gt;[[Convex Hull|볼록 껍질]]&lt;/li&gt;
&lt;li&gt;[[Randomized Algorithm|무작위 알고리즘]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>geometry</category><category>min-enclosing-circle</category><author>koa (김신건)</author></item><item><title>Pick 정리 (Pick&apos;s Theorem)</title><link>https://shinkeonkim.com/wiki/algorithm/geometry/pick/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/geometry/pick/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Pick 정리&lt;/strong&gt;는 꼭짓점이 격자점 (integer lattice) 위에 있는 단순 다각형의 넓이 A 를 내부 격자점 개수 I 와 경계 격자점 개수 B 로 표현: A = I + B/2 - 1. 1899년 오스트리아 수학자 Georg Pick 이 발견.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;격자점 다각형의 넓이를 구할 때, 내부 점과 경계 점의 개수만 세면 넓이를 알 수 있음.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Naive 접근&lt;/strong&gt;: 다각형을 삼각형 분할 후 면적 합산. 분할 자체가 복잡하고 일반화 어려움.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심 통찰&lt;/strong&gt;: 격자점의 개수만으로 단순한 공식 A = I + B/2 - 1. [[Shoelace|신발끈 공식]] 으로 면적을 구한 후 역으로 I 를 계산하기도 함.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PS 위치&lt;/strong&gt;: 좌표 범위가 작고 격자점이 명시적인 문제. 내부 점 개수만 묻는 문제에 직접 활용.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;A = I + B/2 - 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;A: 다각형 넓이&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;I: 내부 격자점 개수 (경계 제외)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;B: 경계 위 격자점 개수&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;경계 격자점 계산 (각 변):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  dx = |x2 - x1|, dy = |y2 - y1|&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  기여 = gcd(dx, dy)   # (0,0) 포함, (x2,y2) 미포함 합계&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;면적을 알 때 내부 점:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  I = A - B/2 + 1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;boundary_points(vertices):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    B = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for i = 0..N-1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        j = (i+1) % N&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        dx = |vertices[i].x - vertices[j].x|&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        dy = |vertices[i].y - vertices[j].y|&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        B += gcd(dx, dy)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return B&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;area_from_pick(I, B):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return I + B/2 - 1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// Pick 정리: 격자점 다각형의 넓이 = I + B/2 - 1 #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; using ll = long long; struct Pt { ll x, y; }; ll gcd(ll a, ll b) { return b ? gcd(b, a%b) : a; } ll shoelace2(const vector&amp;#x3C;Pt&gt;&amp;#x26; p) {     ll a2 = 0;     for (int i = 0; i &amp;#x3C; (int)p.size(); i++) {         int j = (i+1) % p.size();         a2 += p[i].x * p[j].y - p[j].x * p[i].y;     }     return abs(a2);  // 2*A } ll boundary(const vector&amp;#x3C;Pt&gt;&amp;#x26; p) {     ll B = 0;     for (int i = 0; i &amp;#x3C; (int)p.size(); i++) {         int j = (i+1) % p.size();         B += gcd(abs(p[i].x-p[j].x), abs(p[i].y-p[j].y));     }     return B; } int main() {     int N; cin &gt;&gt; N;     vector&amp;#x3C;Pt&gt; p(N);     for (auto&amp;#x26; pt : p) cin &gt;&gt; pt.x &gt;&gt; pt.y;     ll A2 = shoelace2(p);     ll B = boundary(p);     ll I = (A2 - B + 2) / 2;     cout &amp;#x3C;&amp;#x3C; &quot;Area: &quot; &amp;#x3C;&amp;#x3C; A2/2;     if (A2 % 2) cout &amp;#x3C;&amp;#x3C; &quot;.5&quot;;     cout &amp;#x3C;&amp;#x3C; &quot;\\nBoundary: &quot; &amp;#x3C;&amp;#x3C; B &amp;#x3C;&amp;#x3C; &quot;\\nInterior: &quot; &amp;#x3C;&amp;#x3C; I &amp;#x3C;&amp;#x3C; &quot;\\n&quot;; }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: &lt;code&gt;from math import gcd def shoelace2(p):     a2 = 0     for i in range(len(p)):         j = (i+1) % len(p)         a2 += p[i][0]*p[j][1] - p[j][0]*p[i][1]     return abs(a2) def boundary(p):     B = 0     for i in range(len(p)):         j = (i+1) % len(p)         B += gcd(abs(p[i][0]-p[j][0]), abs(p[i][1]-p[j][1]))     return B N = int(input()) p = [tuple(map(int, input().split())) for _ in range(N)] A2 = shoelace2(p) B = boundary(p) I = (A2 - B + 2) // 2 area = A2 // 2 print(f&quot;Area: {area}&quot; + (&quot;.5&quot; if A2 % 2 else &quot;&quot;)) print(f&quot;Boundary: {B}\\nInterior: {I}&quot;)&lt;/code&gt;,
},
{
language: “java”,
label: “Java”,
code: `import java.util.*;&lt;/p&gt;
&lt;p&gt;public class Main {
static long gcd(long a, long b) { return b==0 ? a : gcd(b, a%b); }
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int N = Integer.parseInt(br.readLine());
long[] x = new long[N], y = new long[N];
for (int i=0; i&lt;!-- mdx-component: N --&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Shoelace 면적&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;B 계산 (gcd)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N log M)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;전체&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N log M)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;역 Pick&lt;/strong&gt;: 면적 A 와 B 를 알 때 I = A - B/2 + 1.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ehrhart theory&lt;/strong&gt;: 고차원 격자점 다각형 일반화.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;응용&lt;/strong&gt;: GIS 영역 분석, 화면 픽셀 커버리지.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-5-단위-넓이&quot;&gt;1. .5 단위 넓이&lt;/h3&gt;
&lt;p&gt;B 가 홀수면 A 는 .5 단위. 정수형만 사용할 때 shoelace 의 2 배 값 (A2) 을 유지해야 손실 없음.&lt;/p&gt;
&lt;h3 id=&quot;2-단순-다각형-조건&quot;&gt;2. 단순 다각형 조건&lt;/h3&gt;
&lt;p&gt;자기 교차 (self-intersecting) 또는 구멍이 있는 다각형에서는 성립하지 않음.&lt;/p&gt;
&lt;h3 id=&quot;3-gcd-경계-계산&quot;&gt;3. GCD 경계 계산&lt;/h3&gt;
&lt;p&gt;dx=0 또는 dy=0 인 수평/수직 변은 gcd(0, k) = k. 변의 양 끝점 중 한쪽만 세도록 처리.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2166&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;다각형의 면적&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2100-2199/2166&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 1485&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;정사각형&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1400-1499/1485&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 1358&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;하키&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1300-1399/1358&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 1004&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;어린 왕자&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1000-1099/1004&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Geometry Basic|기하 기본]]&lt;/li&gt;
&lt;li&gt;[[Shoelace|신발끈 공식]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>geometry</category><category>pick</category><author>koa (김신건)</author></item><item><title>평면 그래프 (Planar Graph)</title><link>https://shinkeonkim.com/wiki/algorithm/geometry/planar-graph/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/geometry/planar-graph/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;평면 그래프 (Planar Graph)&lt;/strong&gt; 는 간선끼리 교차하지 않고 평면에 그릴 수 있는 그래프. 연결된 평면 그래프는 항상 &lt;strong&gt;Euler 공식&lt;/strong&gt; &lt;code&gt;V - E + F = 2&lt;/code&gt; 를 만족한다, 여기서 &lt;code&gt;V&lt;/code&gt; = 꼭짓점, &lt;code&gt;E&lt;/code&gt; = 간선, &lt;code&gt;F&lt;/code&gt; = 면(face). 평면 그래프의 대표적인 비존재 정리로 Kuratowski 정리: 그래프가 평면 iff &lt;code&gt;K_5&lt;/code&gt; 또는 &lt;code&gt;K_{3,3}&lt;/code&gt; 을 세부 그래프로 포함하지 않는다.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;회로 기판(PCB) 배선, 지도 색칠(4색 정리), VLSI 설계 등에서 간선 교차를 허용할 수 없는 상황. 컴퓨터 비전의 planar SLAM, 그래프 마이닝의 genus 계산까지 확장됨. Naive planar embedding 은 NP-hard 이지만, 평면성 검사는 &lt;code&gt;O(V+E)&lt;/code&gt; 에 가능 (Hopcroft-Tarjan).&lt;/p&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;면(faces)까지 포함한 combinatorial count가 V, E 만으로 결정된다&lt;/em&gt; 는 Euler 공식이 평면 그래프 이론의 기둥.&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Euler&apos;s formula (connected planar graph):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  V - E + F = 2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Necessary condition (V &gt;= 3):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  E &amp;#x3C;= 3V - 6&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Kuratowski&apos;s theorem:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  G is planar  &amp;#x3C;=&gt;  G has no subgraph homeomorphic to K_5 or K_{3,3}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;평면 그래프의 면에는 바깥 면(outer face) 도 포함된다. Triangulation (최대 평면 그래프) 은 &lt;code&gt;E = 3V - 6&lt;/code&gt; 을 만족.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Euler 지표&lt;/strong&gt;: &lt;code&gt;V - E + F = 2(C - G)&lt;/code&gt; 로 일반화 (C = 연결 성분 수, G = genus)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;K_5&lt;/strong&gt;: 5개의 꼭짓점이 모두 연결 (K_5 = 10 간선). &lt;code&gt;3V-6 = 9&lt;/code&gt; 이므로 평면 불가.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;K_{3,3}&lt;/strong&gt;: 3+3 완전 이분 그래프 (9 간선). &lt;code&gt;E = 9 &gt; 3V-6 = 12(v=6)&lt;/code&gt;, 필연 조건은 통과하지만 non-planar (Kuratowski).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;planarity_test(V, E, adj):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  if V &amp;#x3C; 3: return true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  if E &gt; 3V - 6: return false          # necessary condition&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  # Hopcroft-Tarjan O(V+E) planarity test&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  # DFS-free numbering + lowlink 로 embedding 시도&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  # 세부 Kuratowski subgraph 탐지&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  return planarity_embedding(V, adj)   # 성공시 true&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// Planar graph necessary condition + Euler formula verification #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; int main() {     int v, e; cin &gt;&gt; v &gt;&gt; e;     vector&amp;#x3C;vector&amp;#x3C;int&gt;&gt; adj(v);     for (int i = 0; i &amp;#x3C; e; i++) {         int a, b; cin &gt;&gt; a &gt;&gt; b;         adj[a].push_back(b);         adj[b].push_back(a);     }     // Necessary condition: E &amp;#x3C;= 3V - 6 (for V &gt;= 3)     if (v &gt;= 3 &amp;#x26;&amp;#x26; e &gt; 3 * v - 6) {         cout &amp;#x3C;&amp;#x3C; &quot;NOT PLANAR (E &gt; 3V-6: &quot; &amp;#x3C;&amp;#x3C; e &amp;#x3C;&amp;#x3C; &quot; &gt; &quot; &amp;#x3C;&amp;#x3C; 3*v-6 &amp;#x3C;&amp;#x3C; &quot;)\\n&quot;;         return 0;     }     // Euler test: given face count F     int f; cin &gt;&gt; f;     if (v - e + f == 2) {         cout &amp;#x3C;&amp;#x3C; &quot;Euler formula holds: V-E+F = &quot; &amp;#x3C;&amp;#x3C; v &amp;#x3C;&amp;#x3C; &quot;-&quot; &amp;#x3C;&amp;#x3C; e &amp;#x3C;&amp;#x3C; &quot;+&quot; &amp;#x3C;&amp;#x3C; f &amp;#x3C;&amp;#x3C; &quot; = 2\\n&quot;;         cout &amp;#x3C;&amp;#x3C; &quot;PLANAR (consistent with planar embedding)\\n&quot;;     } else {         cout &amp;#x3C;&amp;#x3C; &quot;Euler formula fails: V-E+F = &quot; &amp;#x3C;&amp;#x3C; v-e+f &amp;#x3C;&amp;#x3C; &quot; != 2\\n&quot;;     }     return 0; }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: `def main():
v, e = map(int, input().split())
adj = [[] for _ in range(v)]
for _ in range(e):
a, b = map(int, input().split())
adj[a].append(b); adj[b].append(a)
if v &gt;= 3 and e &gt; 3 * v - 6:
print(“NOT PLANAR (E &gt; 3V-6)”)
return
f = int(input())
if v - e + f == 2:
print(f”Euler holds: {v}-{e}+{f}=2”)
print(“PLANAR”)
else:
print(f”Euler fails: {v}-{e}+{f} != 2”)&lt;/p&gt;
&lt;p&gt;if &lt;strong&gt;name&lt;/strong&gt; == “&lt;strong&gt;main&lt;/strong&gt;”:
main()&lt;code&gt;,     },     {       language: &quot;java&quot;,       label: &quot;Java&quot;,       code: &lt;/code&gt;import java.util.*;&lt;/p&gt;
&lt;p&gt;public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int v = Integer.parseInt(st.nextToken());
int e = Integer.parseInt(st.nextToken());
ArrayList&lt;integer&gt;[] adj = new ArrayList[v];
for (int i = 0; i &amp;#x3C; v; i++) adj[i] = new ArrayList&amp;#x3C;&gt;();
for (int i = 0; i &amp;#x3C; e; i++) {
st = new StringTokenizer(br.readLine());
int a = Integer.parseInt(st.nextToken());
int b = Integer.parseInt(st.nextToken());
adj[a].add(b); adj[b].add(a);
}
if (v &gt;= 3 &amp;#x26;&amp;#x26; e &gt; 3 * v - 6) {
System.out.println(“NOT PLANAR (E &gt; 3V-6)”);
return;
}
int f = Integer.parseInt(br.readLine());
if (v - e + f == 2)
System.out.println(“Euler holds: ” + v + ”-” + e + ”+” + f + “=2\nPLANAR”);
else
System.out.println(“Euler fails: ” + (v - e + f) + ” != 2”);
}
}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;K_4 (planar)&quot;,       input: &lt;/code&gt;4 6
0 1
0 2
0 3
1 2
1 3
2 3
4&lt;code&gt;,       output: &lt;/code&gt;Euler holds: 4-6+4=2
PLANAR&lt;code&gt;,     },     {       label: &quot;K_5 (non-planar)&quot;,       input: &lt;/code&gt;5 10
0 1
0 2
0 3
0 4
1 2
1 3
1 4
2 3
2 4
3 4
7&lt;code&gt;,       output: &lt;/code&gt;NOT PLANAR (E &gt; 3V-6: 10 &gt; 9)`,
},
]}
/&gt;&lt;/integer&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;필연 조건 검사&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(V+E)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Planarity test (Hopcroft-Tarjan)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(V+E)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Euler formula 검증&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(1) (V, E, F 입력)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Kuratowski subgraph 검출&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(V+E)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(V+E)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;변형&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;설명&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Maximum planar graph&lt;/strong&gt; (triangulation)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;E = 3V - 6&lt;/code&gt;, 모든 면이 삼각형&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Outerplanar graph&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;모든 꼭짓점이 바깥 면에 속함. &lt;code&gt;E &amp;#x3C;= 2V - 3&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Genus (종수)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;평면 대신 g 개 손잡이를 가진 곡면. Euler: &lt;code&gt;V - E + F = 2 - 2g&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Dual graph&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;면을 꼭짓점으로, 인접 면을 간선으로&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;4-color theorem&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;평면 그래프는 4색으로 vertex-coloring 가능&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;응용: PCB / VLSI routing (간선 교차 금지), 지도 제작 (4색 정리), 네트워크 다이어그램, Feynman diagram.&lt;/p&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-필요-조건-vs-충분-조건&quot;&gt;1. 필요 조건 vs 충분 조건&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;E &amp;#x3C;= 3V - 6&lt;/code&gt; 은 필요 조건일 뿐 충분하지 않음. &lt;code&gt;K_{3,3}&lt;/code&gt; 은 &lt;code&gt;V=6, E=9 &amp;#x3C;= 12&lt;/code&gt; 이지만 planar 가 아님.&lt;/p&gt;
&lt;h3 id=&quot;2-면의-정의&quot;&gt;2. 면의 정의&lt;/h3&gt;
&lt;p&gt;바깥 면(outer face) 도 면의 개수에 포함. Disconnected graph 는 &lt;code&gt;V - E + F = 1 + C&lt;/code&gt; (C=연결성분).&lt;/p&gt;
&lt;h3 id=&quot;3-k_4-는-planar-k_5-는-non-planar&quot;&gt;3. K_4 는 planar, K_5 는 non-planar&lt;/h3&gt;
&lt;p&gt;K_4 (V=4, E=6, F=4) 는 Euler 를 만족하고 평면에 교차 없이 그릴 수 있음. K_5 (V=5, E=10) 는 간선이 하나 비껴서 교차가 불가피.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;center&quot;&gt;번호&lt;/th&gt;&lt;th&gt;제목&lt;/th&gt;&lt;th align=&quot;center&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;BOJ 1709&lt;/td&gt;&lt;td&gt;평면그래프&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1700-1799/1709&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;BOJ 2613&lt;/td&gt;&lt;td&gt;번호표 교환&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2600-2699/2613&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;BOJ 1997&lt;/td&gt;&lt;td&gt;사분면&lt;/td&gt;&lt;td align=&quot;center&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1900-1999/1997&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Convex Hull|볼록 껍질]]&lt;/li&gt;
&lt;li&gt;[[Point in Convex Polygon|볼록 다각형 점 판정]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>geometry</category><category>planar-graph</category><author>koa (김신건)</author></item><item><title>볼록 다각형 내부 점 판정 (Point in Convex Polygon)</title><link>https://shinkeonkim.com/wiki/algorithm/geometry/point-in-convex-polygon/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/geometry/point-in-convex-polygon/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;볼록 다각형 내부 점 판정&lt;/strong&gt; 은 볼록 다각형(convex polygon) 과 하나의 점이 주어졌을 때, 점이 다각형의 내부/경계/외부 중 어디에 있는지 O(log N) 에 판별하는 알고리즘. N 은 꼭짓점 개수. 다각형이 반드시 볼록 (내각 180도 미만) 이어야 하며, 꼭짓점이 반시계(CCW) 또는 시계(CW) 순서로 주어져야 한다.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;Q 개의 점이 볼록 N 각형 내부에 있는지 묻는 문제.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;naive&lt;/strong&gt;: 모든 변에 대해 CCW 검사를 일일이 수행. O(N Q). N=10^5, Q=10^5 면 불가능.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;binary search on angle&lt;/strong&gt;: O(N) 전처리 + O(log N) 쿼리. 총 O(N + Q log N).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;볼록 다각형의 꼭짓점은 한 점(p_0) 을 기준으로 각도순으로 정렬되어 있다.&lt;/em&gt; 임의의 점 q 에 대해, q 가 속하는 “쐐기” (p_0, p_i, p_{i+1}) 를 이분 탐색으로 찾고, 그 삼각형 내부에 q 가 있는지 CCW 한 번으로 확인.&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;p&gt;invariant: 볼록 다각형의 꼭짓점 p_0 에서 다른 꼭짓점으로의 방향 벡터는 &lt;em&gt;각도 순서&lt;/em&gt; 로 정렬되어 있다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;1. p_1 ... p_{N-1} 이 p_0 를 기준으로 반시계 방향 정렬&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;2. 쿼리 점 q 에 대해, p_0 에서 q 로의 벡터가 어느 구간&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   (p_0, p_i) 와 (p_0, p_{i+1}) 사이에 있는지 이분 탐색&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;3. q 가 삼각형 (p_0, p_i, p_{i+1}) 내부에 있는지 CCW 로 확인&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;방향 판정&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;결과&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;CCW(p_0, p_i, q) &gt; 0&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;q 가 왼쪽: i 를 오른쪽으로&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;CCW(p_0, p_i, q) ≤ 0&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;q 가 오른쪽: i 를 왼쪽으로&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;CCW(p_i, p_{i+1}, q) ≥ 0&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;q 가 내부 또는 경계&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;그 외&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;외부&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;INPUT: convex polygon P[0..N-1] (CCW order), query point q&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;OUTPUT: IN / OUT / ON&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// 1. q 가 P[0] 과 같은 점인지 확인&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;if P[0] == q: return ON&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// 2. q 가 P[0]-&gt;P[1] 과 P[0]-&gt;P[N-1] 사이에 있는지 확인&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;if CCW(P[0], P[1], q) &amp;#x3C; 0 or CCW(P[0], P[N-1], q) &gt; 0:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return OUT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// 3. 이분 탐색: q 가 속하는 wedge 찾기&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;lo = 1, hi = N - 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;while hi - lo &gt; 1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    mid = (lo + hi) / 2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if CCW(P[0], P[mid], q) &gt;= 0: lo = mid&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    else: hi = mid&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// 4. lo 와 lo+1 로 이루어진 삼각형 내부 확인&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;if CCW(P[lo], P[lo+1], q) &gt;= 0:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if q 가 P[lo] 또는 P[lo+1] 경계 위: return ON&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return IN&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;return OUT&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// 볼록 다각형 내부 점 판정 O(log N) #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; using ll = long long; struct Point { ll x, y; }; int ccw(Point a, Point b, Point c) {     ll ret = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);     if (ret &gt; 0) return 1;     if (ret &amp;#x3C; 0) return -1;     return 0; } string pointInConvexPolygon(vector&amp;#x3C;Point&gt;&amp;#x26; P, Point q) {     int n = P.size();     if (ccw(P[0], P[1], q) &amp;#x3C; 0 || ccw(P[0], P[n-1], q) &gt; 0) return &quot;OUT&quot;;     if (ccw(P[0], P[1], q) == 0) {         if (q.x &gt;= min(P[0].x, P[1].x) &amp;#x26;&amp;#x26; q.x &amp;#x3C;= max(P[0].x, P[1].x)          &amp;#x26;&amp;#x26; q.y &gt;= min(P[0].y, P[1].y) &amp;#x26;&amp;#x26; q.y &amp;#x3C;= max(P[0].y, P[1].y))             return &quot;ON&quot;;         return &quot;OUT&quot;;     }     int lo = 1, hi = n - 1;     while (hi - lo &gt; 1) {         int mid = (lo + hi) / 2;         if (ccw(P[0], P[mid], q) &gt;= 0) lo = mid;         else hi = mid;     }     int c = ccw(P[lo], P[lo+1], q);     if (c &gt; 0) return &quot;IN&quot;;     if (c &amp;#x3C; 0) return &quot;OUT&quot;;     return &quot;ON&quot;; } int main() {     int n, q; cin &gt;&gt; n &gt;&gt; q;     vector&amp;#x3C;Point&gt; P(n);     for (auto&amp;#x26; p : P) cin &gt;&gt; p.x &gt;&gt; p.y;     while (q--) {         Point qp; cin &gt;&gt; qp.x &gt;&gt; qp.y;         cout &amp;#x3C;&amp;#x3C; pointInConvexPolygon(P, qp) &amp;#x3C;&amp;#x3C; &quot;\\n&quot;;     } }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: `# 볼록 다각형 내부 점 판정 (binary search on angle)
import sys
input = sys.stdin.readline&lt;/p&gt;
&lt;p&gt;def ccw(ax, ay, bx, by, cx, cy):
return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax)&lt;/p&gt;
&lt;p&gt;def inside(P, qx, qy):
n = len(P)
if ccw(P[0][0], P[0][1], P[1][0], P[1][1], qx, qy) &amp;#x3C; 0:
return “OUT”
if ccw(P[0][0], P[0][1], P[-1][0], P[-1][1], qx, qy) &gt; 0:
return “OUT”
lo, hi = 1, n - 1
while hi - lo &gt; 1:
mid = (lo + hi) // 2
if ccw(P[0][0], P[0][1], P[mid][0], P[mid][1], qx, qy) &gt;= 0:
lo = mid
else:
hi = mid
c = ccw(P[lo][0], P[lo][1], P[lo+1][0], P[lo+1][1], qx, qy)
if c &gt; 0: return “IN”
if c &amp;#x3C; 0: return “OUT”
return “ON”&lt;/p&gt;
&lt;p&gt;n, q = map(int, input().split())
P = [tuple(map(int, input().split())) for _ in range(n)]
for _ in range(q):
x, y = map(int, input().split())
print(inside(P, x, y))&lt;code&gt;,     },     {       language: &quot;java&quot;,       label: &quot;Java&quot;,       code: &lt;/code&gt;// 볼록 다각형 내부 점 판정 O(log N)&lt;/p&gt;
&lt;p&gt;public class Main {
static long ccw(long ax, long ay, long bx, long by, long cx, long cy) {
return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
}
static String inside(long[][] P, long qx, long qy) {
int n = P.length;
if (ccw(P[0][0], P[0][1], P[1][0], P[1][1], qx, qy) &amp;#x3C; 0
|| ccw(P[0][0], P[0][1], P[n-1][0], P[n-1][1], qx, qy) &gt; 0)
return “OUT”;
int lo = 1, hi = n - 1;
while (hi - lo &gt; 1) {
int mid = (lo + hi) / 2;
if (ccw(P[0][0], P[0][1], P[mid][0], P[mid][1], qx, qy) &gt;= 0)
lo = mid;
else hi = mid;
}
long c = ccw(P[lo][0], P[lo][1], P[lo+1][0], P[lo+1][1], qx, qy);
if (c &gt; 0) return “IN”;
if (c &amp;#x3C; 0) return “OUT”;
return “ON”;
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int n = Integer.parseInt(st.nextToken());
int q = Integer.parseInt(st.nextToken());
long[][] P = new long[n][2];
for (int i = 0; i &amp;#x3C; n; i++) {
st = new StringTokenizer(br.readLine());
P[i][0] = Long.parseLong(st.nextToken());
P[i][1] = Long.parseLong(st.nextToken());
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i &amp;#x3C; q; i++) {
st = new StringTokenizer(br.readLine());
long qx = Long.parseLong(st.nextToken());
long qy = Long.parseLong(st.nextToken());
sb.append(inside(P, qx, qy)).append(‘\n’);
}
System.out.print(sb);
}
}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;기본 예제&quot;,       input: &lt;/code&gt;5 3
0 0
6 0
5 4
2 5
-1 3
3 2
0 5
2 2&lt;code&gt;,       output: &lt;/code&gt;IN
OUT
ON`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;전처리&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;쿼리 (점 1개)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(log N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;전체 (Q 쿼리)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(N + Q log N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(N)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Convex Hull|컨벡스 헐]] 점들을 CCW 정렬 후 hull 바깥 점을 빠르게 필터.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;다각형 내부 점 수&lt;/strong&gt;: 쿼리당 O(log N), N=Q=10^5 에서 실용적.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Boundary-on&lt;/strong&gt;: CCW=0 인 변 위에 있으면 projection 범위로 확인.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rotating calipers&lt;/strong&gt; 와 결합해 convex hull 회전 과정에서 포함 여부 확인.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;CCW 방향&lt;/strong&gt;: 다각형이 시계 방향이면 부호 반전 또는 reverse 후 처리.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;오버플로우&lt;/strong&gt;: 좌표 10^6 에서 CCW 는 10^12 → C++/Java &lt;code&gt;long long&lt;/code&gt;/&lt;code&gt;long&lt;/code&gt; 필수.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;경계 판정&lt;/strong&gt;: 점이 꼭짓점이나 변 위면 ON. 이분 탐색 전 p_0 일치 여부 먼저 확인.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Collinear&lt;/strong&gt;: 모든 점이 일직선이면 볼록 다각형이 아님. degenerate case 로 따로 처리.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2254&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;감옥 건설&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;33.149%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2200-2299/2254&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2049&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;가장 먼 두 점&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;37.784%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2000-2099/2049&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2699&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;격자점 컨벡스헐&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;57.516%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2600-2699/2699&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[CCW|CCW (시계/반시계 판정)]]&lt;/li&gt;
&lt;li&gt;[[Convex Hull|볼록 껍질]]&lt;/li&gt;
&lt;li&gt;[[Angle Sorting|각도 정렬]]&lt;/li&gt;
&lt;li&gt;[[Geometry|기하 기본]]&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://atcoder.jp/contests/abc296/tasks/abc296_g&quot;&gt;AtCoder ABC 296 G - Polygon and Points&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>geometry</category><category>point-in-convex-polygon</category><author>koa (김신건)</author></item><item><title>(오목 포함) 다각형 내부 점 판정 (Point in Polygon)</title><link>https://shinkeonkim.com/wiki/algorithm/geometry/point-in-non-convex-polygon/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/geometry/point-in-non-convex-polygon/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;다각형 내부 점 판정 (Point in Polygon, PIP)&lt;/strong&gt; 은 &lt;em&gt;단순 다각형&lt;/em&gt; (볼록 또는 오목) 과 하나의 점이 주어졌을 때, 점이 내부/경계/외부 중 어디에 있는지 판별하는 문제. &lt;strong&gt;Ray casting (ray crossing)&lt;/strong&gt; 알고리즘이 가장 일반적으로 사용되며, O(N) 시간에 동작한다. N 은 꼭짓점 개수.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;단순 다각형 (볼록/오목 구분 없음) 과 하나의 점. 오목 다각형에서는 CCW 만으로 일관된 판정이 불가능.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Naive&lt;/strong&gt;: 각 변마다 점과의 위치 관계를 확인하는 CCW 검사는 볼록일 때만 O(N). 오목에선 실패.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ray casting&lt;/strong&gt;: 점에서 임의의 방향으로 반직선을 그려 다각형 변과의 교차 횟수로 판정. O(N).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;다각형의 변은 내부와 외부를 가르는 경계선.&lt;/em&gt; 반직선이 변과 교차할 때마다 내부/외부 패리티가 바뀐다. 최종 교차 횟수가 &lt;em&gt;홀수면 내부, 짝수면 외부&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;even-odd rule (홀짝 법칙):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  점 p 에서 오른쪽으로 수평 반직선을 긋는다.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  이 반직선과 다각형의 변이 교차하는 횟수를 센다.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  교차 횟수 % 2 == 1 → IN&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  교차 횟수 % 2 == 0 → OUT&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;조건&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;판정&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;교차 횟수 = 홀수&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;내부 (IN)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;교차 횟수 = 짝수 (0 포함)&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;외부 (OUT)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;점이 변 위에 있음&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;경계 (ON)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;반직선 방향은 어디든 상관없으나, &lt;em&gt;오른쪽 수평&lt;/em&gt; 이 구현이 가장 간단.&lt;/p&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;INPUT: polygon P[0..N-1] (CCW or CW), query point q&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;OUTPUT: IN / ON / OUT&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;count = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;for i = 0..N-1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    j = (i + 1) % N&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if q is on segment(P[i], P[j]): return ON&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    // P[i].y &amp;#x3C; q.y &amp;#x3C;= P[j].y (upward crossing)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    // 또는 P[j].y &amp;#x3C; q.y &amp;#x3C;= P[i].y (downward crossing)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if (P[i].y &amp;#x3C;= q.y &amp;#x3C; P[j].y) or (P[j].y &amp;#x3C;= q.y &amp;#x3C; P[i].y):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        // 반직선과 변의 교차 x 좌표 계산&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        x_intersect = P[i].x + (q.y - P[i].y)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                        * (P[j].x - P[i].x) / (P[j].y - P[i].y)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if q.x &amp;#x3C; x_intersect: count++&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;return count % 2 == 1 ? IN : OUT&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;주의: 한쪽 끝점만 inclusive 로 처리하여 꼭짓점 중복 교차 방지.&lt;/p&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// Ray casting O(N) 점의 다각형 포함 여부 #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; using ll = long long; struct Point { ll x, y; }; int ccw(Point a, Point b, Point c) {     ll ret = (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);     if (ret &gt; 0) return 1;     if (ret &amp;#x3C; 0) return -1;     return 0; } bool onSegment(Point a, Point b, Point q) {     if (ccw(a, b, q) != 0) return false;     return min(a.x, b.x) &amp;#x3C;= q.x &amp;#x26;&amp;#x26; q.x &amp;#x3C;= max(a.x, b.x)         &amp;#x26;&amp;#x26; min(a.y, b.y) &amp;#x3C;= q.y &amp;#x26;&amp;#x26; q.y &amp;#x3C;= max(a.y, b.y); } string pip(vector&amp;#x3C;Point&gt;&amp;#x26; P, Point q) {     int n = P.size(), cnt = 0;     for (int i = 0; i &amp;#x3C; n; i++) {         int j = (i + 1) % n;         if (onSegment(P[i], P[j], q)) return &quot;ON&quot;;         if ((P[i].y &amp;#x3C;= q.y &amp;#x26;&amp;#x26; q.y &amp;#x3C; P[j].y) || (P[j].y &amp;#x3C;= q.y &amp;#x26;&amp;#x26; q.y &amp;#x3C; P[i].y)) {             double x_intersect = P[i].x + (double)(q.y - P[i].y)                                * (P[j].x - P[i].x) / (P[j].y - P[i].y);             if (q.x &amp;#x3C; x_intersect) cnt++;         }     }     return cnt % 2 ? &quot;IN&quot; : &quot;OUT&quot;; } int main() {     int n, q; cin &gt;&gt; n &gt;&gt; q;     vector&amp;#x3C;Point&gt; P(n);     for (auto&amp;#x26; p : P) cin &gt;&gt; p.x &gt;&gt; p.y;     while (q--) {         Point qp; cin &gt;&gt; qp.x &gt;&gt; qp.y;         cout &amp;#x3C;&amp;#x3C; pip(P, qp) &amp;#x3C;&amp;#x3C; &quot;\\n&quot;;     } }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: `# Ray casting O(N) point in polygon
import sys
input = sys.stdin.readline&lt;/p&gt;
&lt;p&gt;def ccw(ax, ay, bx, by, cx, cy):
return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax)&lt;/p&gt;
&lt;p&gt;def on_seg(x1, y1, x2, y2, qx, qy):
if ccw(x1, y1, x2, y2, qx, qy) != 0: return False
return min(x1, x2) &amp;#x3C;= qx &amp;#x3C;= max(x1, x2) and min(y1, y2) &amp;#x3C;= qy &amp;#x3C;= max(y1, y2)&lt;/p&gt;
&lt;p&gt;def pip(P, qx, qy):
n, cnt = len(P), 0
for i in range(n):
x1, y1 = P[i]
x2, y2 = P[(i + 1) % n]
if on_seg(x1, y1, x2, y2, qx, qy): return “ON”
if (y1 &amp;#x3C;= qy &amp;#x3C; y2) or (y2 &amp;#x3C;= qy &amp;#x3C; y1):
x_int = x1 + (qy - y1) * (x2 - x1) / (y2 - y1)
if qx &amp;#x3C; x_int: cnt += 1
return “IN” if cnt % 2 else “OUT”&lt;/p&gt;
&lt;p&gt;n, q = map(int, input().split())
P = [tuple(map(int, input().split())) for _ in range(n)]
for _ in range(q):
x, y = map(int, input().split())
print(pip(P, x, y))&lt;code&gt;,     },     {       language: &quot;java&quot;,       label: &quot;Java&quot;,       code: &lt;/code&gt;// Ray casting O(N) point in polygon&lt;/p&gt;
&lt;p&gt;public class Main {
static long ccw(long ax, long ay, long bx, long by, long cx, long cy) {
return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
}
static boolean onSeg(long x1, long y1, long x2, long y2, long qx, long qy) {
if (ccw(x1, y1, x2, y2, qx, qy) != 0) return false;
return Math.min(x1, x2) &amp;#x3C;= qx &amp;#x26;&amp;#x26; qx &amp;#x3C;= Math.max(x1, x2)
&amp;#x26;&amp;#x26; Math.min(y1, y2) &amp;#x3C;= qy &amp;#x26;&amp;#x26; qy &amp;#x3C;= Math.max(y1, y2);
}
static String pip(long[][] P, long qx, long qy) {
int n = P.length, cnt = 0;
for (int i = 0; i &amp;#x3C; n; i++) {
long x1 = P[i][0], y1 = P[i][1];
long x2 = P[(i+1)%n][0], y2 = P[(i+1)%n][1];
if (onSeg(x1, y1, x2, y2, qx, qy)) return “ON”;
if ((y1 &amp;#x3C;= qy &amp;#x26;&amp;#x26; qy &amp;#x3C; y2) || (y2 &amp;#x3C;= qy &amp;#x26;&amp;#x26; qy &amp;#x3C; y1)) {
double xInt = x1 + (double)(qy - y1) * (x2 - x1) / (y2 - y1);
if (qx &amp;#x3C; xInt) cnt++;
}
}
return cnt % 2 == 1 ? “IN” : “OUT”;
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
int n = Integer.parseInt(st.nextToken());
int q = Integer.parseInt(st.nextToken());
long[][] P = new long[n][2];
for (int i = 0; i &amp;#x3C; n; i++) {
st = new StringTokenizer(br.readLine());
P[i][0] = Long.parseLong(st.nextToken());
P[i][1] = Long.parseLong(st.nextToken());
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i &amp;#x3C; q; i++) {
st = new StringTokenizer(br.readLine());
long qx = Long.parseLong(st.nextToken());
long qy = Long.parseLong(st.nextToken());
sb.append(pip(P, qx, qy)).append(‘\n’);
}
System.out.print(sb);
}
}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;기본 예제&quot;,       input: &lt;/code&gt;6 4
0 0
4 0
4 2
2 2
2 4
0 4
1 1
5 1
2 2
0 0&lt;code&gt;,       output: &lt;/code&gt;IN
OUT
ON
ON`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;점 1개 판정&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(N)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Q 개 점&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(N Q)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(N)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;볼록 다각형&lt;/strong&gt;: O(log N) 이분 탐색 ([[Point in Convex Polygon|볼록 다각형 내부 점 판정]]).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Winding number&lt;/strong&gt;: crossing number 와 달리 방향 고려, self-intersecting 다각형에서도 올바름. CCW 부호를 누적해 wn != 0 이면 내부.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Grid point location&lt;/strong&gt;: 쿼리가 많으면 plane sweep 으로 O(N log N) 전처리 후 O(log N) 쿼리.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GIS / 그래픽스&lt;/strong&gt;: 구역 판별, hit testing, 영역 필터링.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;꼭짓점 중복 교차&lt;/strong&gt;: 반직선이 꼭짓점 통과 시 두 번 세는 문제. upward/downward edge 끝점을 한쪽만 inclusive.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;수평 변&lt;/strong&gt;: 반직선과 평행하면 무시. &lt;code&gt;y&lt;/code&gt; 비교 부등호로 자연히 걸러짐.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;부동소수점 오차&lt;/strong&gt;: 교차점 x 계산에 double. 정수 좌표면 유리수 유지 또는 CCW 만으로 &lt;code&gt;onSegment&lt;/code&gt; 검사.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;경계 점&lt;/strong&gt;: 점이 변 위일 때 ON. &lt;code&gt;onSegment&lt;/code&gt; 함수로 먼저 검사.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 1688&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;지민이의 테러&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;23.417%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1600-1699/1688&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2254&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;감옥 건설&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;33.149%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2200-2299/2254&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 3679&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;단순 다각형&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;26.233%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/3600-3699/3679&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Convex Hull|볼록 껍질]]&lt;/li&gt;
&lt;li&gt;[[Point in Convex Polygon|볼록 다각형 내부 점 판정]]&lt;/li&gt;
&lt;li&gt;[[CCW|CCW (시계/반시계 판정)]]&lt;/li&gt;
&lt;li&gt;[[Angle Sorting|각도 정렬]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>geometry</category><category>point-in-non-convex-polygon</category><author>koa (김신건)</author></item><item><title>다각형 넓이 (Polygon Area)</title><link>https://shinkeonkim.com/wiki/algorithm/geometry/polygon-area/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/geometry/polygon-area/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;다각형 넓이 (Polygon Area)&lt;/strong&gt; 는 2D 평면 위 &lt;strong&gt;단순다각형 (non-self-intersecting polygon)&lt;/strong&gt; 의 면적을 &lt;strong&gt;신발끈 공식 (Shoelace Formula)&lt;/strong&gt; 으로 O(N) 에 계산하는 알고리즘.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;다각형의 넓이는 기하 문제에서 가장 빈번하게 등장하는 연산.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Naive&lt;/strong&gt;: 삼각형 분할 후 각각 넓이 합산. O(N) 이지만 구현이 번거롭고 분할 방식을 결정해야 함.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shoelace&lt;/strong&gt;: 각 변의 기여도를 외적 하나로 계산. O(N) 에 통일된 공식.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;다각형의 부호 있는 넓이는 각 변 (i, i+1) 의 외적의 합의 절반&lt;/em&gt;. 즉 &lt;code&gt;0.5 * | sum (x_i * y_{i+1} - x_{i+1} * y_i) |&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;이 공식은 다각형이 볼록인지 오목인지, 시계/반시계 방향인지 관계없이 항상 성립.&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;p&gt;다각형의 꼭짓점을 &lt;code&gt;P[0], P[1], ..., P[N-1]&lt;/code&gt; (반시계 또는 시계). 각 변 &lt;code&gt;P[i] -&gt; P[i+1]&lt;/code&gt; 에 대해, &lt;strong&gt;사다리꼴의 부호 있는 넓이&lt;/strong&gt; 를 더한다.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;S = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;for i in 0..N-1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    j = (i + 1) % N&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    S += P[i].x * P[j].y&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    S -= P[j].x * P[i].y&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;area = abs(S) / 2&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이론적 근거: 그린 정리 (Green’s theorem) 의 이산 버전. 각 변의 사다리꼴 넓이가 모두 더해지면 내부는 중첩되고 외부는 상쇄됨.&lt;/p&gt;
&lt;p&gt;부호: &lt;code&gt;S &gt; 0&lt;/code&gt; 이면 반시계 방향, &lt;code&gt;S &amp;#x3C; 0&lt;/code&gt; 이면 시계 방향.&lt;/p&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;polygon_area(P[0..N-1]):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    sum = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for i = 0 to N-1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        j = (i + 1) % N&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        sum += P[i].x * P[j].y&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        sum -= P[j].x * P[i].y&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return abs(sum) / 2.0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: `// Polygon area (shoelace), O(N)
#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
using ll = long long;&lt;/p&gt;
&lt;p&gt;struct Point { ll x, y; };&lt;/p&gt;
&lt;p&gt;double polygon_area(vector&lt;point&gt;&amp;#x26; p) {
ll sum = 0;
int n = p.size();
for (int i = 0; i &amp;#x3C; n; i++) {
int j = (i + 1) % n;
sum += p[i].x * p[j].y;
sum -= p[j].x * p[i].y;
}
return abs(sum) / 2.0;
}&lt;/point&gt;&lt;/p&gt;
&lt;p&gt;int main() {
vector&lt;point&gt; poly = {{0,0}, {4,0}, {5,2}, {2,5}, {-1,2}};
cout &amp;#x3C;&amp;#x3C; fixed &amp;#x3C;&amp;#x3C; setprecision(1) &amp;#x3C;&amp;#x3C; polygon_area(poly) &amp;#x3C;&amp;#x3C; “\n”;
vector&lt;point&gt; tri = {{0,0}, {3,0}, {0,4}};
cout &amp;#x3C;&amp;#x3C; fixed &amp;#x3C;&amp;#x3C; setprecision(1) &amp;#x3C;&amp;#x3C; polygon_area(tri) &amp;#x3C;&amp;#x3C; “\n”;
}&lt;code&gt;,     },     {       language: &quot;python&quot;,       label: &quot;Python&quot;,       code: &lt;/code&gt;# Shoelace formula, O(N)
def polygon_area(poly):
n = len(poly)
s = 0
for i in range(n):
j = (i + 1) % n
s += poly[i][0] * poly[j][1]
s -= poly[j][0] * poly[i][1]
return abs(s) / 2.0&lt;/point&gt;&lt;/point&gt;&lt;/p&gt;
&lt;p&gt;poly = [(0,0), (4,0), (5,2), (2,5), (-1,2)]
print(f”{polygon_area(poly):.1f}”)
tri = [(0,0), (3,0), (0,4)]
print(f”{polygon_area(tri):.1f}“)&lt;code&gt;,     },     {       language: &quot;java&quot;,       label: &quot;Java&quot;,       code: &lt;/code&gt;import java.util.*;
public class Main {
static double polygonArea(long[][] p) {
long sum = 0;
int n = p.length;
for (int i = 0; i &amp;#x3C; n; i++) {
int j = (i + 1) % n;
sum += p[i][0] * p[j][1];
sum -= p[j][0] * p[i][1];
}
return Math.abs(sum) / 2.0;
}&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;public static void main(String[] args) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    long[][] poly = {{0,0}, {4,0}, {5,2}, {2,5}, {-1,2}};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    System.out.printf(&quot;%.1f%n&quot;, polygonArea(poly));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    long[][] tri = {{0,0}, {3,0}, {0,4}};&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    System.out.printf(&quot;%.1f%n&quot;, polygonArea(tri));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;오각형 + 삼각형&quot;,       input: ``,       output: &lt;/code&gt;19.0
6.0`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;

















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(N) (N = 꼭짓점 수)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;O(1) 추가 (입력 배열 제외)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;h3 id=&quot;1-볼록다각형-여부-방향-일관성&quot;&gt;1. 볼록다각형 여부 (방향 일관성)&lt;/h3&gt;
&lt;p&gt;Shoelace 의 &lt;code&gt;sum&lt;/code&gt; 부호로 다각형이 반시계(양수)인지 시계(음수)인지 판별. 모든 연속 세 점의 CCW 가 방향과 일치하는지 확인.&lt;/p&gt;
&lt;h3 id=&quot;2-다각형의-무게-중심&quot;&gt;2. 다각형의 무게 중심&lt;/h3&gt;
&lt;p&gt;면적으로 가중치를 준 꼭짓점 평균으로 centroid 계산 가능.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;cx = sum((x_i + x_{i+1}) * (x_i*y_{i+1} - x_{i+1}*y_i)) / (6 * area)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;cy = sum((y_i + y_{i+1}) * (x_i*y_{i+1} - x_{i+1}*y_i)) / (6 * area)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;3-오목-다각형에서도-동작&quot;&gt;3. 오목 다각형에서도 동작&lt;/h3&gt;
&lt;p&gt;Shoelace 는 내부 영역의 중첩과 상쇄가 자동으로 처리되므로 오목 다각형에서도 올바른 값.&lt;/p&gt;
&lt;h3 id=&quot;4-정수-좌표--long-long&quot;&gt;4. 정수 좌표 + long long&lt;/h3&gt;
&lt;p&gt;좌표가 정수이면 넓이는 &lt;code&gt;.0&lt;/code&gt; 또는 &lt;code&gt;.5&lt;/code&gt;. 정수 배율 결과가 필요하면 &lt;code&gt;abs(sum)&lt;/code&gt; 반환 후 따로 2 로 나누기.&lt;/p&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-오버플로우&quot;&gt;1. 오버플로우&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;x_i * y_j&lt;/code&gt; 가 &lt;code&gt;10^6 * 10^6 = 10^12&lt;/code&gt;. N=10^5 면 sum 은 &lt;code&gt;10^17&lt;/code&gt; 까지 가능. C++ &lt;code&gt;long long&lt;/code&gt; (9e18) 은 OK, &lt;code&gt;int&lt;/code&gt; 는 무조건 overflow.&lt;/p&gt;
&lt;h3 id=&quot;2-다각형-방향&quot;&gt;2. 다각형 방향&lt;/h3&gt;
&lt;p&gt;입력이 시계(cw)인지 반시계(ccw)인지 모를 수 있음. &lt;code&gt;abs(sum)&lt;/code&gt; 으로 절대값 처리.&lt;/p&gt;
&lt;h3 id=&quot;3-마지막-변&quot;&gt;3. 마지막 변&lt;/h3&gt;
&lt;p&gt;i=N-1 일 때 j=0 으로 돌아오는 것 (&lt;code&gt;(i+1) % N&lt;/code&gt;) 이 중요. 열린 폴리라인이 아닌 닫힌 다각형이어야 함.&lt;/p&gt;
&lt;h3 id=&quot;4-정수-나눗셈&quot;&gt;4. 정수 나눗셈&lt;/h3&gt;
&lt;p&gt;C++ 에서 &lt;code&gt;abs(sum) / 2&lt;/code&gt; 는 정수 나눗셈. &lt;code&gt;2.0&lt;/code&gt; 으로 나누거나 &lt;code&gt;(double)&lt;/code&gt; 캐스팅 필요.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2166&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;다각형의 면적&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;39.5%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2100-2199/2166&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2477&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;참외밭&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;44.5%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2400-2499/2477&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 1004&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;어린 왕자&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;33.2%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1000-1099/1004&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 1709&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;(관련: 원의 넓이)&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;(수집 안 됨)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1700-1799/1709&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[CCW]] (외적의 기초)&lt;/li&gt;
&lt;li&gt;[[Convex Hull]] (볼록 껍질에서도 같은 공식)&lt;/li&gt;
&lt;li&gt;[[Geometry]] (기하 기본)&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>geometry</category><category>polygon-area</category><author>koa (김신건)</author></item><item><title>피타고라스 정리 (Pythagorean Theorem)</title><link>https://shinkeonkim.com/wiki/algorithm/geometry/pythagoras/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/geometry/pythagoras/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;피타고라스 정리&lt;/strong&gt;는 직각삼각형에서 빗변 c 의 제곱이 두 직각변 a, b 의 제곱의 합과 같다는 정리: c^2 = a^2 + b^2. 기원전 6세기 그리스 수학자 피타고라스가 증명. &lt;strong&gt;피타고라스 수 (Pythagorean triple)&lt;/strong&gt; 는 이 등식을 만족하는 세 자연수 (a, b, c) 쌍으로 (3, 4, 5) 가 가장 잘 알려짐.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;2D 평면에서 두 점 사이의 거리, 직각 판별, 원의 방정식 등 기하의 거의 모든 곳에서 피타고라스 정리가 쓰임.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Naive 접근&lt;/strong&gt;: 세 변이 주어졌을 때 a^2 + b^2 = c^2 를 확인하려면 세 변을 정렬한 후 O(1).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;핵심 통찰&lt;/strong&gt;: c 가 항상 가장 크므로 정렬 후 한 번의 검사로 직각 판별 가능.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PS 위치&lt;/strong&gt;: 두 점 거리의 제곱을 그대로 비교해 실수 연산 회피. 좌표 기하 문제의 가장 기초.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;c^2 = a^2 + b^2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;직각삼각형:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- a, b: 직각변 (legs)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;- c: 빗변 (hypotenuse), 항상 가장 긴 변&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Euclid 공식 (primitive triple 생성):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;a = m^2 - n^2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;b = 2mn&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;c = m^2 + n^2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;조건: m &gt; n, gcd(m,n)=1, m-n 은 홀수&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;is_right_triangle(a, b, c):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    sides = [a, b, c] 정렬&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return sides[0]^2 + sides[1]^2 == sides[2]^2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;generate_triples(limit):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for m = 2..sqrt(limit):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        for n = 1..m-1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            if (m-n) is even: continue&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            if gcd(m, n) != 1: continue&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            a = m^2 - n^2, b = 2mn, c = m^2 + n^2&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            if c &gt; limit: continue&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            for k = 1; k*c &amp;#x3C;= limit; k++:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                output (k*a, k*b, k*c)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// 피타고라스 수 생성 + 직각 판별 #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; int main() {     int limit; cin &gt;&gt; limit;     vector&amp;#x3C;tuple&amp;#x3C;int,int,int&gt;&gt; triples;     for (int m = 2; m*m &amp;#x3C;= limit; m++) {         for (int n = 1; n &amp;#x3C; m; n++) {             if ((m-n) % 2 == 0) continue;             if (gcd(m, n) != 1) continue;             int a = m*m - n*n, b = 2*m*n, c = m*m + n*n;             if (c &gt; limit) continue;             for (int k = 1; k*c &amp;#x3C;= limit; k++)                 triples.push_back({k*a, k*b, k*c});         }     }     cout &amp;#x3C;&amp;#x3C; &quot;Found &quot; &amp;#x3C;&amp;#x3C; triples.size() &amp;#x3C;&amp;#x3C; &quot; triples:\\n&quot;;     for (auto&amp;#x26; [a,b,c] : triples)         cout &amp;#x3C;&amp;#x3C; a &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; b &amp;#x3C;&amp;#x3C; &quot; &quot; &amp;#x3C;&amp;#x3C; c &amp;#x3C;&amp;#x3C; &quot;\\n&quot;;     int x, y, z; cin &gt;&gt; x &gt;&gt; y &gt;&gt; z;     vector&amp;#x3C;int&gt; v = {x, y, z};     sort(v.begin(), v.end());     bool right = v[0]*v[0] + v[1]*v[1] == v[2]*v[2];     cout &amp;#x3C;&amp;#x3C; (right ? &quot;RIGHT&quot; : &quot;NOT RIGHT&quot;) &amp;#x3C;&amp;#x3C; &quot;\\n&quot;; }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: &lt;code&gt;from math import gcd def generate_triples(limit):     triples = []     for m in range(2, int(limit**0.5)+1):         for n in range(1, m):             if (m-n) % 2 == 0: continue             if gcd(m, n) != 1: continue             a, b, c = m*m - n*n, 2*m*n, m*m + n*n             if c &gt; limit: continue             k = 1             while k*c &amp;#x3C;= limit:                 triples.append((k*a, k*b, k*c))                 k += 1     return triples limit = int(input()) triples = generate_triples(limit) print(f&quot;Found {len(triples)} triples&quot;) for t in triples: print(*t) x, y, z = sorted(map(int, input().split())) print(&quot;RIGHT&quot; if x*x + y*y == z*z else &quot;NOT RIGHT&quot;)&lt;/code&gt;,
},
{
language: “java”,
label: “Java”,
code: `import java.util.*;&lt;/p&gt;
&lt;p&gt;public class Main {
static int gcd(int a, int b) { return b==0 ? a : gcd(b, a%b); }
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int limit = Integer.parseInt(br.readLine());
List&lt;string&gt; out = new ArrayList&amp;#x3C;&gt;();
int cnt = 0;
for (int m = 2; m&lt;em&gt;m &amp;#x3C;= limit; m++) {
for (int n = 1; n &amp;#x3C; m; n++) {
if ((m-n)%2==0) continue;
if (gcd(m,n)!=1) continue;
int a = m&lt;/em&gt;m-n&lt;em&gt;n, b = 2&lt;/em&gt;m&lt;em&gt;n, c = m&lt;/em&gt;m+n&lt;em&gt;n;
if (c &gt; limit) continue;
for (int k = 1; k&lt;/em&gt;c &amp;#x3C;= limit; k++) {
out.add((k&lt;em&gt;a)+” ”+(k&lt;/em&gt;b)+” ”+(k*c));
cnt++;
}
}
}
System.out.println(“Found ” + cnt + ” triples”);
out.forEach(System.out::println);
StringTokenizer st = new StringTokenizer(br.readLine());
int[] v = {Integer.parseInt(st.nextToken()),
Integer.parseInt(st.nextToken()),
Integer.parseInt(st.nextToken())};
Arrays.sort(v);
boolean right = v[0]*v[0]+v[1]*v[1]==v[2]*v[2];
System.out.println(right ? “RIGHT” : “NOT RIGHT”);
}
}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;기본&quot;,       input: &lt;/code&gt;50
3 4 5&lt;code&gt;,       output: &lt;/code&gt;Found 5 triples
3 4 5
5 12 13
8 15 17
7 24 25
20 21 29
RIGHT`,
},
]}
/&gt;&lt;/string&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;직각 판별 (3 변)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(1)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Triple 생성 (limit L)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(L log L)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Triple 생성 공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(L)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;응용&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;설명&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;유클리드 거리&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;sqrt((x1-x2)^2 + (y1-y2)^2)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;제곱 거리 비교&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;sqrt 없이 제곱 상태로 비교 (실수 오차 회피)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;내적과 직각&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;두 벡터의 내적이 0 이면 직각. 좌표 기하에서 활용&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-빗변-식별-실수&quot;&gt;1. 빗변 식별 실수&lt;/h3&gt;
&lt;p&gt;가장 큰 변을 빗변으로 지정해야 함. 정렬 없이 a^2 + b^2 == c^2 만 검사하면 c 가 빗변이 아닐 때 오답.&lt;/p&gt;
&lt;h3 id=&quot;2-오버플로우&quot;&gt;2. 오버플로우&lt;/h3&gt;
&lt;p&gt;N=10^5 에서 좌표 제곱은 10^10 -&gt; 32-bit int 초과. 반드시 &lt;code&gt;long long&lt;/code&gt; 사용.&lt;/p&gt;
&lt;h3 id=&quot;3-실수-sqrt-비교&quot;&gt;3. 실수 sqrt 비교&lt;/h3&gt;
&lt;p&gt;sqrt(a^2 + b^2) == c 는 부동소수점 오차 유발. 반드시 정수 제곱으로 비교.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 4153&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;직각삼각형&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/4100-4199/4153&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 3000&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;직각삼각형&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/3000-3099/3000&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 1711&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;직각삼각형&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1700-1799/1711&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 1485&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;정사각형&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1400-1499/1485&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Geometry Basic|기하 기본]]&lt;/li&gt;
&lt;li&gt;[[Distance|거리 (유클리드)]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>geometry</category><category>pythagoras</category><author>koa (김신건)</author></item><item><title>회전 캘리퍼스 (Rotating Calipers)</title><link>https://shinkeonkim.com/wiki/algorithm/geometry/rotating-calipers/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/geometry/rotating-calipers/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;회전 캘리퍼스 (Rotating Calipers)&lt;/strong&gt; 는 볼록 다각형 위에서 &lt;strong&gt;서로 마주보는 두 평행선 (겹지름, antipodal pair)&lt;/strong&gt; 을 회전시키며 &lt;strong&gt;다각형의 최장 거리, 최소 폭, 최대 삼각형 면적&lt;/strong&gt; 등을 O(N) 에 구하는 기법.&lt;/p&gt;
&lt;p&gt;미하일 슐로스만 (M. Shamos) 의 박사 논문 (1978) 에서 처음 제안.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;N 개 점 중 가장 먼 두 점 (farthest pair) 을 구한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Naive&lt;/strong&gt;: 모든 쌍 거리 계산 O(N^2). N=10^5 면 10^10, 불가능.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Convex Hull + Rotating Calipers&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;Convex hull 구축 O(N log N)&lt;/li&gt;
&lt;li&gt;Hull 위에서 rotating calipers O(N)&lt;/li&gt;
&lt;li&gt;총 O(N log N)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;가장 먼 두 점은 항상 볼록 껍질 위에 있다. 캘리퍼스를 360도 회전시키면서 antipodal pair 의 거리를 갱신하면 O(N).&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;h3 id=&quot;antipodal-pair-겹지름&quot;&gt;Antipodal pair (겹지름)&lt;/h3&gt;
&lt;p&gt;볼록 다각형의 두 꼭짓점에 &lt;strong&gt;평행한 두 접선&lt;/strong&gt; 을 그을 수 있을 때, 그 쌍을 antipodal (마주보는) pair 라고 함.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;한 360도 회전 동안 antipodal pair 의 개수 = O(N)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;각 pair 의 거리 = max distance candidate&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;캘리퍼스-회전-two-pointer&quot;&gt;캘리퍼스 회전 (Two-pointer)&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;1. hull 구축 (반시계 방향)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;2. hull[0] 에 대해 가장 먼 hull[j] 찾기 (j = 1)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;3. for i = 0..N-1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;       while distance(hull[i], hull[j+1]) &gt; distance(hull[i], hull[j]):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;           j = (j + 1) % N&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;       ans = max(ans, dist(hull[i], hull[j]))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;   // i 가 한 바퀴 도는 동안 j 도 많아야 2N 번 움직임 -&gt; O(N)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;rotating_calipers(hull):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    N = |hull|&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    j = 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    ans = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for i = 0..N-1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        while True:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            nxt = (j + 1) % N&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            if area(hull[i], hull[i+1], hull[nxt]) &amp;#x3C;= area(hull[i], hull[i+1], hull[j]):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                break&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            j = nxt&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        ans = max(ans, dist2(hull[i], hull[j]))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return sqrt(ans)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// area(a, b, c) = |ccw(a, b, c)| (삼각형 넓이 2배)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;// hull[i] - hull[i+1] 변에 대해 가장 먼 점 hull[j]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// Rotating Calipers: farthest pair on convex hull #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; using ll = long long; struct Point { ll x, y; }; ll ccw(Point a, Point b, Point c) {     return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x); } ll dist2(Point a, Point b) {     ll dx = a.x - b.x, dy = a.y - b.y;     return dx*dx + dy*dy; } double rotating_calipers(vector&amp;#x3C;Point&gt;&amp;#x26; hull) {     int n = hull.size();     if (n == 2) return sqrt(dist2(hull[0], hull[1]));     int j = 1;     ll ans2 = 0;     for (int i = 0; i &amp;#x3C; n; i++) {         int ni = (i + 1) % n;         while (true) {             int nj = (j + 1) % n;             if (abs(ccw(hull[i], hull[ni], hull[nj])) &amp;#x3C;=                 abs(ccw(hull[i], hull[ni], hull[j]))) break;             j = nj;         }         ans2 = max(ans2, dist2(hull[i], hull[j]));         ans2 = max(ans2, dist2(hull[ni], hull[j]));     }     return sqrt(ans2); } int main() {     vector&amp;#x3C;Point&gt; hull = {{0,0},{2,0},{3,1},{1,3},{-1,2}};     cout.precision(6);     cout &amp;#x3C;&amp;#x3C; fixed &amp;#x3C;&amp;#x3C; &quot;Max distance: &quot; &amp;#x3C;&amp;#x3C; rotating_calipers(hull) &amp;#x3C;&amp;#x3C; &quot;\\n&quot;; }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: `# Rotating Calipers: farthest pair on convex hull
import math
def ccw(ax, ay, bx, by, cx, cy):
return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax)&lt;/p&gt;
&lt;p&gt;def dist2(a, b):
return (a[0]-b[0])**2 + (a[1]-b[1])**2&lt;/p&gt;
&lt;p&gt;def rotating_calipers(hull):
n = len(hull)
if n == 2: return math.sqrt(dist2(hull[0], hull[1]))
j = 1
ans2 = 0
for i in range(n):
ni = (i + 1) % n
while True:
nj = (j + 1) % n
if abs(ccw(*hull[i], *hull[ni], *hull[nj])) &amp;#x3C;= &lt;br&gt;
abs(ccw(*hull[i], *hull[ni], *hull[j])):
break
j = nj
ans2 = max(ans2, dist2(hull[i], hull[j]))
ans2 = max(ans2, dist2(hull[ni], hull[j]))
return math.sqrt(ans2)&lt;/p&gt;
&lt;p&gt;hull = [(0,0), (2,0), (3,1), (1,3), (-1,2)]
print(f”Max distance: {rotating_calipers(hull):.6f}“)&lt;code&gt;,     },     {       language: &quot;java&quot;,       label: &quot;Java&quot;,       code: &lt;/code&gt;// Rotating Calipers: farthest pair on convex hull&lt;/p&gt;
&lt;p&gt;public class Main {
static class Point { long x, y; Point(long x, long y) { this.x = x; this.y = y; } }
static long ccw(Point a, Point b, Point c) {
return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
}
static long dist2(Point a, Point b) {
long dx = a.x - b.x, dy = a.y - b.y;
return dx&lt;em&gt;dx + dy&lt;/em&gt;dy;
}
static double rotatingCalipers(List&lt;point&gt; hull) {
int n = hull.size();
if (n == 2) return Math.sqrt(dist2(hull.get(0), hull.get(1)));
int j = 1;
long ans2 = 0;
for (int i = 0; i &amp;#x3C; n; i++) {
int ni = (i + 1) % n;
while (true) {
int nj = (j + 1) % n;
if (Math.abs(ccw(hull.get(i), hull.get(ni), hull.get(nj))) &amp;#x3C;=
Math.abs(ccw(hull.get(i), hull.get(ni), hull.get(j)))) break;
j = nj;
}
ans2 = Math.max(ans2, dist2(hull.get(i), hull.get(j)));
ans2 = Math.max(ans2, dist2(hull.get(ni), hull.get(j)));
}
return Math.sqrt(ans2);
}
public static void main(String[] args) {
List&lt;point&gt; hull = Arrays.asList(
new Point(0,0), new Point(2,0), new Point(3,1),
new Point(1,3), new Point(-1,2));
System.out.printf(“Max distance: %.6f%n”, rotatingCalipers(hull));
}
}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;기본 예제&quot;,       input: ``,       output: &lt;/code&gt;Max distance: 4.472136`,
},
]}
/&gt;&lt;/point&gt;&lt;/point&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;





















&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (Convex Hull 포함)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N log N) (정렬)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;시간 (Hull 위 calipers)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N) (two-pointer)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;j 포인터는 전체 루프에서 최대 2N 번만 전진 -&gt; amortized O(N).&lt;/p&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;h3 id=&quot;1-최장-거리-farthest-pair&quot;&gt;1. 최장 거리 (Farthest pair)&lt;/h3&gt;
&lt;p&gt;가장 먼 두 점. hull + rotating calipers.&lt;/p&gt;
&lt;h3 id=&quot;2-최소-폭-minimum-width&quot;&gt;2. 최소 폭 (Minimum width)&lt;/h3&gt;
&lt;p&gt;평행한 두 직선 사이에 hull 을 끼울 때 최소 거리.&lt;/p&gt;
&lt;h3 id=&quot;3-최대-면적-삼각형-largest-triangle-in-convex-polygon&quot;&gt;3. 최대 면적 삼각형 (Largest triangle in convex polygon)&lt;/h3&gt;
&lt;p&gt;Antipodal pair 개념 확장: hull[i], hull[j], hull[k] 세 점. O(N^2) (또는 O(N) with rotating calipers 3 개).&lt;/p&gt;
&lt;h3 id=&quot;4-최대-내접-사각형&quot;&gt;4. 최대 내접 사각형&lt;/h3&gt;
&lt;p&gt;회전 캘리퍼스로 hull 에 내접하는 최대 면적 사각형.&lt;/p&gt;
&lt;h3 id=&quot;5-minimum-bounding-box-최소-면적-외접-직사각형&quot;&gt;5. Minimum bounding box (최소 면적 외접 직사각형)&lt;/h3&gt;
&lt;p&gt;hull 의 각 변을 기준으로 캘리퍼스로 나머지 세 변의 위치 O(N) 결정.&lt;/p&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-정수-vs-실수&quot;&gt;1. 정수 vs 실수&lt;/h3&gt;
&lt;p&gt;거리 제곱을 정수로 유지하다가 최종 sqrt 만 실수. 부동소수점 오차 최소화.&lt;/p&gt;
&lt;h3 id=&quot;2-hull-이-점-2개&quot;&gt;2. Hull 이 점 2개&lt;/h3&gt;
&lt;p&gt;N=2 면 거리만 출력. calipers 불필요.&lt;/p&gt;
&lt;h3 id=&quot;3-j-가-i-를-추월&quot;&gt;3. j 가 i 를 추월&lt;/h3&gt;
&lt;p&gt;i 와 j 가 동기화되지 않게 주의. while 조건으로 j 가 i 를 넘어가는 것 방지.&lt;/p&gt;
&lt;h3 id=&quot;4-ccw-부호-일관성&quot;&gt;4. CCW 부호 일관성&lt;/h3&gt;
&lt;p&gt;hull 이 반시계(ccw)로 주어져야 area 부호가 일정. Building hull 시 ccw convention 통일.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;









































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 9240&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;로버트 후드&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;37.2%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/9200-9299/9240&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 10254&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;고속도로&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;23.2%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/10200-10299/10254&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 1310&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;달리기 코스&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;54.1%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1300-1399/1310&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2626&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;헬기착륙장&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;23.9%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2600-2699/2626&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 10903&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Wall construction&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;61.4%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/10900-10999/10903&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Convex Hull|볼록 껍질]]&lt;/li&gt;
&lt;li&gt;[[Geometry|2D 기하 기본]]&lt;/li&gt;
&lt;li&gt;[[CCW|CCW (시계/반시계 판정)]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>geometry</category><category>rotating-calipers</category><author>koa (김신건)</author></item><item><title>선인장 그래프 (Cactus Graph)</title><link>https://shinkeonkim.com/wiki/algorithm/graph/cactus/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/graph/cactus/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;선인장 그래프 (Cactus Graph)&lt;/strong&gt; 는 &lt;strong&gt;모든 간선이 최대 한 개의 단순 사이클에만 포함&lt;/strong&gt;되는 무방향 연결 그래프. 트리 (tree) 의 일반화로, 사이클은 허용하되 “사이클끼리 간선을 공유하지 않음”.&lt;/p&gt;
&lt;p&gt;형태: 여러 사이클이 점 (정점) 으로만 연결된 구조. 간선 수 E ≤ V + (사이클 수 - 1), 트리면 E = V - 1.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;일반 그래프는 사이클이 복잡하게 엉켜 DP / 최단 경로 / 매칭이 NP-hard. 트리는 선형 시간 가능하지만 사이클 없음. 선인장은 &lt;strong&gt;사이클이 있으면서도 트리와 비슷한 구조적 성질&lt;/strong&gt;을 보존.&lt;/p&gt;
&lt;p&gt;naive: 일반 그래프 알고리즘 O(V^2) 또는 O(V E). 핵심 통찰: 각 사이클을 “super-node” 로 축약하면 트리 → DFS / DP 가능. 사이클 내부는 별도 처리.&lt;/p&gt;
&lt;p&gt;PS 에서: “도로망에 사이클 하나”, “회로 네트워크 (공유 안 함)”, BOJ 지름 / 최단 경로 변형.&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;h3 id=&quot;invariant&quot;&gt;invariant&lt;/h3&gt;
&lt;p&gt;간선 e 는 최대 하나의 사이클 C 에만 속함. 두 사이클이 간선을 공유하지 않음 → &lt;strong&gt;간선 분할 (edge partition)&lt;/strong&gt; 이 깔끔.&lt;/p&gt;
&lt;h3 id=&quot;dfs-기반-분해&quot;&gt;DFS 기반 분해&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;DFS tree 를 만든다.&lt;/li&gt;
&lt;li&gt;back edge (u, v) 가 나오면 u → v 경로 + (u, v) 가 사이클 C.&lt;/li&gt;
&lt;li&gt;C 의 간선들을 “이미 사이클에 포함됨” 마킹. 다른 back edge 가 C 의 간선을 다시 쓰면 선인장 아님.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;시간 O(V + E), 공간 O(V).&lt;/p&gt;
&lt;h3 id=&quot;dp-on-cactus&quot;&gt;DP on cactus&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;각 사이클을 “구간” 으로 보고 사이클 내 DP.&lt;/li&gt;
&lt;li&gt;사이클을 하나의 노드로 축약한 트리에서 tree DP.&lt;/li&gt;
&lt;li&gt;둘 합치기: 사이클 DP → 트리 DP.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;예: 선인장 지름 = max(트리 지름, 각 사이클 내 최대 거리).&lt;/p&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;is_cactus(G):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    visited = [false] * V&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    edge_used = set()&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for root in V:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if not visited[root]:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            if not dfs_cactus(root, -1, visited, edge_used):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                return false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;dfs_cactus(u, parent, visited, edge_used):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    visited[u] = true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for v in adj[u]:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if v == parent: continue&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        edge = (min(u,v), max(u,v))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if visited[v]:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            # back edge&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            if edge in edge_used:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                return false   # 이미 다른 사이클에 속함&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            path = trace_path(u, v)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            for e in path:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                if e in edge_used:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                    return false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                edge_used.add(e)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        else:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            if not dfs_cactus(v, u, visited, edge_used):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                return false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return true&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: `// 선인장 그래프 검증: DFS + back edge 중복 체크
#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;&lt;/p&gt;
&lt;p&gt;vector&amp;#x3C;vector&lt;int&gt;&gt; adj;
vector&lt;int&gt; parent, depth;
set&amp;#x3C;pair&amp;#x3C;int,int&gt;&gt; cycleEdges;
bool isCactus;&lt;/int&gt;&lt;/int&gt;&lt;/p&gt;
&lt;p&gt;void dfs(int u, int p, int d) {
parent[u] = p;
depth[u] = d;
for (int v : adj[u]) {
if (v == p) continue;
auto edge = minmax(u, v);
if (depth[v] == -1) {
dfs(v, u, d + 1);
} else if (depth[v] &amp;#x3C; depth[u]) {
// back edge u -&gt; v
// trace cycle
for (int x = u; x != v; x = parent[x]) {
auto e = minmax(x, parent[x]);
if (cycleEdges.count(e)) { isCactus = false; return; }
cycleEdges.insert(e);
}
if (cycleEdges.count(edge)) { isCactus = false; return; }
cycleEdges.insert(edge);
}
}
}&lt;/p&gt;
&lt;p&gt;int main() {
int n, m; cin &gt;&gt; n &gt;&gt; m;
adj.resize(n); parent.resize(n, -1); depth.resize(n, -1);
isCactus = true;
for (int i = 0; i &amp;#x3C; m; i++) {
int u, v; cin &gt;&gt; u &gt;&gt; v;
adj[u].push_back(v);
adj[v].push_back(u);
}
for (int i = 0; i &amp;#x3C; n; i++)
if (depth[i] == -1) dfs(i, -1, 0);
cout &amp;#x3C;&amp;#x3C; (isCactus ? “cactus” : “not cactus”) &amp;#x3C;&amp;#x3C; “\n”;
}&lt;code&gt;,     },     {       language: &quot;python&quot;,       label: &quot;Python&quot;,       code: &lt;/code&gt;# 선인장 그래프 판별
import sys
sys.setrecursionlimit(10**6)
input = sys.stdin.readline&lt;/p&gt;
&lt;p&gt;def is_cactus(n, edges):
adj = [[] for _ in range(n)]
for u, v in edges:
adj[u].append(v)
adj[v].append(u)
parent = [-1] * n
depth = [-1] * n
cycle_edges = set()
result = [True]&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;def dfs(u, p, d):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    parent[u], depth[u] = p, d&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for v in adj[u]:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if v == p:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            continue&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        edge = (min(u, v), max(u, v))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if depth[v] == -1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            dfs(v, u, d + 1)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        elif depth[v] &amp;#x3C; depth[u]:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            # back edge&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            x = u&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            while x != v:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                e = (min(x, parent[x]), max(x, parent[x]))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                if e in cycle_edges:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                    result[0] = False&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                    return&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                cycle_edges.add(e)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                x = parent[x]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            if edge in cycle_edges:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                result[0] = False&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                return&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            cycle_edges.add(edge)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;for i in range(n):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if depth[i] == -1:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        dfs(i, -1, 0)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;return result[0]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;n, m = map(int, input().split())
edges = [tuple(map(int, input().split())) for _ in range(m)]
print(“cactus” if is_cactus(n, edges) else “not cactus”)&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;선인장&quot;,       input: &lt;/code&gt;5 6
0 1
1 2
2 0
2 3
3 4
4 2&lt;code&gt;,       output: &lt;/code&gt;cactus&lt;code&gt;,     },     {       label: &quot;비선인장 (간선 공유)&quot;,       input: &lt;/code&gt;4 5
0 1
1 2
2 0
0 2
2 3&lt;code&gt;,       output: &lt;/code&gt;not cactus`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;선인장 판별&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(V + E) DFS&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;선인장 DP&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(V + E) (사이클 + 트리 DP)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;지름&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(V + E)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;최단 경로&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O((V + E) log V) (Dijkstra)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(V + E)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;h3 id=&quot;1-선인장-지름&quot;&gt;1. 선인장 지름&lt;/h3&gt;
&lt;p&gt;각 사이클 내 최대 거리 + 트리 지름. 사이클을 “무게 중심” 으로 축약.&lt;/p&gt;
&lt;h3 id=&quot;2-선인장-최단-경로&quot;&gt;2. 선인장 최단 경로&lt;/h3&gt;
&lt;p&gt;Dijkstra / BFS. 사이클 구조가 트리와 비슷해 일반 그래프보다 빠름.&lt;/p&gt;
&lt;h3 id=&quot;3-선인장-dp&quot;&gt;3. 선인장 DP&lt;/h3&gt;
&lt;p&gt;부분합 DP, 매칭, 독립 집합 모두 O(V + E). 일반 그래프는 NP.&lt;/p&gt;
&lt;h3 id=&quot;4-방향-선인장&quot;&gt;4. 방향 선인장&lt;/h3&gt;
&lt;p&gt;각 간선이 최대 한 사이클. functional graph 의 변형.&lt;/p&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-간선-공유-놓침&quot;&gt;1. 간선 공유 놓침&lt;/h3&gt;
&lt;p&gt;두 사이클이 정점을 공유해도 OK. 간선을 공유하면 NO.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  O---O&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  |\ /|&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  | X |  ← 간선 (중앙 교차) 공유 → 선인장 아님&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  |/ \|&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;  O---O&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;2-back-edge-중복-처리&quot;&gt;2. back edge 중복 처리&lt;/h3&gt;
&lt;p&gt;같은 back edge 를 두 번 방문하면 잘못된 판단. set 으로 관리.&lt;/p&gt;
&lt;h3 id=&quot;3-비연결-그래프&quot;&gt;3. 비연결 그래프&lt;/h3&gt;
&lt;p&gt;선인장 정의는 “연결 그래프”. 비연결이면 각 컴포넌트 따로 판별.&lt;/p&gt;
&lt;h3 id=&quot;4-dp-순서&quot;&gt;4. DP 순서&lt;/h3&gt;
&lt;p&gt;사이클 내부 DP → 트리 DP 순서. 역순이면 값 꼬임.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 10891&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Cactus? Not Cactus?&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/10800-10899/10891&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 13926&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;gahui and sousenkyo 2&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/13900-13999/13926&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 17409&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;트리의 외심&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/17400-17499/17409&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2406&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;전략적 배치&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2400-2499/2406&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Tree|트리]]&lt;/li&gt;
&lt;li&gt;[[DFS|깊이 우선 탐색]]&lt;/li&gt;
&lt;li&gt;[[Cycle Detection|사이클 탐지]]&lt;/li&gt;
&lt;li&gt;[[Graph DP|그래프 동적 계획법]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>graph</category><category>cactus</category><category>tree</category><category>cycle</category><author>koa (김신건)</author></item><item><title>서큘레이션 (Circulation)</title><link>https://shinkeonkim.com/wiki/algorithm/graph/circulation/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/graph/circulation/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;서큘레이션 (Circulation)&lt;/strong&gt; 은 방향 그래프 G = (V, E) 에서 모든 간선에 &lt;strong&gt;하한 l(u,v)&lt;/strong&gt; 과 &lt;strong&gt;상한 c(u,v)&lt;/strong&gt; 이 주어질 때, &lt;strong&gt;모든 정점에서 유량 보존 (in-flow = out-flow)&lt;/strong&gt; 을 만족하는 유량 f: E -&gt; R 을 찾는 문제.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;l(u,v) ≤ f(u,v) ≤ c(u,v) (용량 제약)&lt;/li&gt;
&lt;li&gt;모든 v in V: sum_{(u,v) in E} f(u,v) = sum_{(v,w) in E} f(v,w) (유량 보존)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;최대 유량의 일반화: 하한이 0 이면 일반 max-flow 와 동일. source/sink 대신 &lt;strong&gt;모든 정점이 보존&lt;/strong&gt;되어야 함.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;실제 흐름 문제는 “이 파이프에 최소 X 만큼은 흘러야 한다” 는 하한 조건이 자주 붙음.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;naive: 하한을 무시하고 max-flow 를 돌린 뒤 보정 -&gt; 불가능한 경우가 많음.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;표준 변환&lt;/strong&gt;: 하한을 먼저 보낸 뒤, 각 정점의 surplus/deficit 을 super-source/sink 로 연결 -&gt; 일반 max-flow 로 환원.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;하한을 만족시키는 최소 유량을 가상의 source/sink 로 조정&lt;/em&gt;하면 일반 max-flow 문제가 됨.&lt;/p&gt;
&lt;p&gt;PS 응용: 파이프 최소 유량, 작업 최소 배정량, 교통망 최소 통행량, 화학 공정 흐름.&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;h3 id=&quot;max-flow-로의-환원&quot;&gt;Max-flow 로의 환원&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;모든 간선 (u,v) 에 하한 l(u,v) 만큼 미리 흘려보낸다.&lt;/li&gt;
&lt;li&gt;각 정점 v 의 순 유입량 b(v) = sum l(u,v) - sum l(v,w) 를 계산.&lt;/li&gt;
&lt;li&gt;b(v) &gt; 0 이면 (공급 과잉) super-source S -&gt; v 로 용량 b(v) 의 간선 추가.&lt;/li&gt;
&lt;li&gt;b(v) &amp;#x3C; 0 이면 (공급 부족) v -&gt; super-sink T 로 용량 -b(v) 의 간선 추가.&lt;/li&gt;
&lt;li&gt;원래 간선의 잔여 용량 = c(u,v) - l(u,v).&lt;/li&gt;
&lt;li&gt;S -&gt; T max-flow 실행.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;S 에서 나가는 모든 간선이 포화&lt;/strong&gt; (flow = sum_{b(v)&gt;0} b(v)) -&gt; feasible circulation 존재.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;정당성&quot;&gt;정당성&lt;/h3&gt;
&lt;p&gt;b(v) 의 총합은 항상 0 (모든 l(u,v) 가 b(u) 에서 -1 배, b(v) 에서 +1 배로 정확히 한 번씩 더해지므로). S -&gt; T flow 가 모든 surplus 를 sink 로 전달하면, 남은 간선으로 하한 이상의 유량이 보존 조건을 만족하며 흐를 수 있음.&lt;/p&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;feasible_circulation(G, l, c):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    G&apos; = copy of G, remove all edges&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    add super-source S, super-sink T&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for each (u,v) in E:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        add edge u-&gt;v with cap c[u][v] - l[u][v]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        b[u] -= l[u][v]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        b[v] += l[u][v]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for each node v:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if b[v] &gt; 0: add edge S-&gt;v with cap b[v]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if b[v] &amp;#x3C; 0: add edge v-&gt;T with cap -b[v]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    flow = max_flow(G&apos;, S, T)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if flow == sum(b[v] for b[v] &gt; 0): return &quot;YES&quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    else: return &quot;NO&quot;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: `// Circulation with lower bounds, reduction to Dinic max flow
#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;
using ll = long long;&lt;/p&gt;
&lt;p&gt;struct Edge { int to, rev; ll cap; };&lt;/p&gt;
&lt;p&gt;class Dinic {
public:
vector&amp;#x3C;vector&lt;edge&gt;&gt; g;
vector&lt;int&gt; level, it;
Dinic(int n) : g(n), level(n), it(n) {}
void add_edge(int u, int v, ll cap) {
g[u].push_back({v, (int)g[v].size(), cap});
g[v].push_back({u, (int)g[u].size() - 1, 0});
}
bool bfs(int s, int t) {
fill(level.begin(), level.end(), -1);
queue&lt;int&gt; q; q.push(s); level[s] = 0;
while (!q.empty()) {
int u = q.front(); q.pop();
for (auto&amp;#x26; e : g[u])
if (e.cap &gt; 0 &amp;#x26;&amp;#x26; level[e.to] &amp;#x3C; 0) {
level[e.to] = level[u] + 1; q.push(e.to);
}
}
return level[t] &gt;= 0;
}
ll dfs(int u, int t, ll f) {
if (u == t) return f;
for (int &amp;#x26;i = it[u]; i &amp;#x3C; (int)g[u].size(); i++) {
Edge&amp;#x26; e = g[u][i];
if (e.cap &gt; 0 &amp;#x26;&amp;#x26; level[u] &amp;#x3C; level[e.to]) {
ll ret = dfs(e.to, t, min(f, e.cap));
if (ret &gt; 0) {
e.cap -= ret;
g[e.to][e.rev].cap += ret;
return ret;
}
}
}
return 0;
}
ll max_flow(int s, int t) {
ll flow = 0, INF = 1e18;
while (bfs(s, t)) {
fill(it.begin(), it.end(), 0);
while (ll f = dfs(s, t, INF)) flow += f;
}
return flow;
}
};&lt;/int&gt;&lt;/int&gt;&lt;/edge&gt;&lt;/p&gt;
&lt;p&gt;int main() {
int n, m; cin &gt;&gt; n &gt;&gt; m;
Dinic dinic(n + 2);
int S = n, T = n + 1;
vector&lt;ll&gt; b(n, 0);
for (int i = 0; i &amp;#x3C; m; i++) {
int u, v; ll l, c; cin &gt;&gt; u &gt;&gt; v &gt;&gt; l &gt;&gt; c;
u—; v—;
dinic.add_edge(u, v, c - l);
b[u] -= l;
b[v] += l;
}
ll sum_pos = 0;
for (int i = 0; i &amp;#x3C; n; i++) {
if (b[i] &gt; 0) { dinic.add_edge(S, i, b[i]); sum_pos += b[i]; }
else if (b[i] &amp;#x3C; 0) dinic.add_edge(i, T, -b[i]);
}
ll flow = dinic.max_flow(S, T);
cout &amp;#x3C;&amp;#x3C; (flow == sum_pos ? “YES\n” : “NO\n”);
}&lt;code&gt;,     },     {       language: &quot;python&quot;,       label: &quot;Python&quot;,       code: &lt;/code&gt;# Circulation with lower bounds, using Dinic max flow
import sys
sys.setrecursionlimit(10**6)
input = sys.stdin.readline&lt;/ll&gt;&lt;/p&gt;
&lt;p&gt;class Dinic:
def &lt;strong&gt;init&lt;/strong&gt;(self, n):
self.n = n
self.g = [[] for _ in range(n)]
def add_edge(self, u, v, cap):
self.g[u].append([v, cap, len(self.g[v])])
self.g[v].append([u, 0, len(self.g[u]) - 1])
def bfs(self, s, t):
self.level = [-1] * self.n
q = [s]; self.level[s] = 0
for u in q:
for v, cap, rev in self.g[u]:
if cap &gt; 0 and self.level[v] &amp;#x3C; 0:
self.level[v] = self.level[u] + 1
q.append(v)
return self.level[t] &gt;= 0
def dfs(self, u, t, f):
if u == t: return f
for i in range(self.it[u], len(self.g[u])):
self.it[u] = i
v, cap, rev = self.g[u][i]
if cap &gt; 0 and self.level[u] &amp;#x3C; self.level[v]:
ret = self.dfs(v, t, min(f, cap))
if ret &gt; 0:
self.g[u][i][1] -= ret
self.g[v][rev][1] += ret
return ret
return 0
def max_flow(self, s, t):
flow = 0; INF = 10**18
while self.bfs(s, t):
self.it = [0] * self.n
while True:
f = self.dfs(s, t, INF)
if not f: break
flow += f
return flow&lt;/p&gt;
&lt;p&gt;n, m = map(int, input().split())
dinic = Dinic(n + 2)
S, T = n, n + 1
b = [0] * n
for _ in range(m):
u, v, l, c = map(int, input().split())
u -= 1; v -= 1
dinic.add_edge(u, v, c - l)
b[u] -= l; b[v] += l
sum_pos = 0
for i in range(n):
if b[i] &gt; 0:
dinic.add_edge(S, i, b[i]); sum_pos += b[i]
elif b[i] &amp;#x3C; 0:
dinic.add_edge(i, T, -b[i])
flow = dinic.max_flow(S, T)
print(“YES” if flow == sum_pos else “NO”)&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;순환 그래프&quot;,       input: &lt;/code&gt;4 4
1 2 2 5
2 3 1 4
3 4 1 5
4 1 2 6&lt;code&gt;,       output: &lt;/code&gt;YES&lt;code&gt;,     },     {       label: &quot;불가능&quot;,       input: &lt;/code&gt;3 2
1 2 3 5
2 3 0 2&lt;code&gt;,       output: &lt;/code&gt;NO`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;변환&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(E)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Max-flow (Dinic)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(V^2 E) 일반 그래프&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Max-flow (Push-Relabel)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(V^3)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(V+E)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Minimum cost circulation&lt;/strong&gt;: 각 간선에 비용이 있을 때, 하한을 만족하는 최소 비용 circulation. Successive Shortest Augmenting Path 등으로 해결.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Feasibility with demands&lt;/strong&gt;: source/sink 가 있는 flow 에 하한 추가 -&gt; demand node 개념.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Upper/lower bound flow decomposition&lt;/strong&gt;: circulation = cycle basis 의 합.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dual of max flow&lt;/strong&gt;: min-cut 과 circulation 정리로 연결.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;냉난방 공조, 화학 물질 흐름&lt;/strong&gt; 등 연속 시스템 모델링.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-정수-overflow&quot;&gt;1. 정수 overflow&lt;/h3&gt;
&lt;p&gt;b(v) 의 합이 int 범위를 넘을 수 있음. long long 사용.&lt;/p&gt;
&lt;h3 id=&quot;2-하한이-상한보다-큰-경우&quot;&gt;2. 하한이 상한보다 큰 경우&lt;/h3&gt;
&lt;p&gt;l &gt; c 인 간선이 입력되면 바로 infeasible. 전처리 필요.&lt;/p&gt;
&lt;h3 id=&quot;3-그래프가-연결되지-않은-경우&quot;&gt;3. 그래프가 연결되지 않은 경우&lt;/h3&gt;
&lt;p&gt;각 컴포넌트의 b(v) 합이 0 이어야 함. 컴포넌트 별로 독립적으로 검증.&lt;/p&gt;
&lt;h3 id=&quot;4-max-flow-값--sum_pos-만-체크&quot;&gt;4. MAX FLOW 값 == sum_pos 만 체크&lt;/h3&gt;
&lt;p&gt;flow &gt; sum_pos 는 불가능 (S 의 나가는 간선 총합 = sum_pos). flow = sum_pos 면 모든 surplus 가 T 로 전달됨.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;





























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2316&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;도시 왕복하기 2&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2300-2399/2316&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 12918&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;정리정돈&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/12900-12999/12918&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 1031&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;스타 대결&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/1000-1099/1031&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Maximum Flow|최대 유량]] (max-flow, Dinic, Edmonds-Karp)&lt;/li&gt;
&lt;li&gt;[[Minimum Cost Flow|최소 비용 유량]]&lt;/li&gt;
&lt;li&gt;[[Bipartite Matching|이분 매칭]] (서큘레이션의 특수 case)&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>graph</category><category>circulation</category><category>flow</category><category>network-flow</category><author>koa (김신건)</author></item><item><title>차수열 (Degree Sequence)</title><link>https://shinkeonkim.com/wiki/algorithm/graph/degree-sequence/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/graph/degree-sequence/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;차수열 (Degree Sequence)&lt;/strong&gt; 은 그래프의 각 정점 차수를 내림차순으로 나열한 수열 d[1] &gt;= d[2] &gt;= … &gt;= d[N]. 이 수열을 만족하는 단순 그래프(loop / multigraph 없음)가 존재하면 &lt;strong&gt;graphic sequence&lt;/strong&gt; 라고 한다.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;“이 차수열을 가진 단순 그래프가 실제로 존재하는가?” 를 판별해야 할 때:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;naive&lt;/strong&gt;: 모든 가능한 그래프를 생성해 차수열 비교. 지수적 불가능.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Havel-Hakimi / Erdos-Gallai&lt;/strong&gt;: O(N^2) 에 necessary and sufficient 조건 판별.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심 통찰: &lt;em&gt;가장 큰 차수 정점을 그 다음 큰 정점들에 연결하고 제거하는 과정을 반복하면, 차수열의 실현 가능성이 결정된다.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;h3 id=&quot;havel-hakimi-정리-구성적&quot;&gt;Havel-Hakimi 정리 (구성적)&lt;/h3&gt;
&lt;p&gt;내림차순 차수열 d[1..N] 이 graphic 할 필요충분조건:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;d[1] &amp;#x3C;= N-1 이고&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;d[1] 을 제거한 후, d[1] 개의 다음 정점들의 차수를 1씩 감소시킨 새 수열이 graphic 함.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;erdos-gallai-정리-필요충분&quot;&gt;Erdos-Gallai 정리 (필요충분)&lt;/h3&gt;
&lt;p&gt;내림차순 d[1..N] 이 graphic 할 필요충분조건:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;모든 d[i] ≤ N-1&lt;/li&gt;
&lt;li&gt;sum(d) 가 짝수&lt;/li&gt;
&lt;li&gt;모든 1 ≤ k ≤ N 에 대해:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;sum_{i=1..k} d[i] &amp;#x3C;= k(k-1) + sum_{i=k+1..N} min(d[i], k)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;is_graphic(d):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    d 를 내림차순 정렬&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if any d[i] &amp;#x3C; 0: return false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if sum(d) % 2 == 1: return false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    if all d[i] == 0: return true&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    x = d[0]  # 가장 큰 차수&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    d[0] = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for i = 1..x:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        d[i] -= 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if d[i] &amp;#x3C; 0: return false&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return is_graphic(d)  # 재귀&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: &lt;code&gt;// Havel-Hakimi 로 graphic sequence 판별 #include &amp;#x3C;bits/stdc++.h&gt; using namespace std; bool havel_hakimi(vector&amp;#x3C;int&gt; d) {     while (true) {         sort(d.rbegin(), d.rend());          // 내림차순         while (!d.empty() &amp;#x26;&amp;#x26; d.back() == 0)   // 0 제거             d.pop_back();         if (d.empty()) return true;           // 모두 0         int x = d.front();                    // 최대 차수         d.erase(d.begin());         if (x &gt; (int)d.size()) return false;  // 정점 부족         for (int i = 0; i &amp;#x3C; x; i++) {             d[i]--;             if (d[i] &amp;#x3C; 0) return false;         }     } } int main() {     ios::sync_with_stdio(false);     cin.tie(nullptr);     int n; cin &gt;&gt; n;     vector&amp;#x3C;int&gt; d(n);     for (auto&amp;#x26; v : d) cin &gt;&gt; v;     cout &amp;#x3C;&amp;#x3C; (havel_hakimi(d) ? 1 : -1) &amp;#x3C;&amp;#x3C; &quot;\\n&quot;;     return 0; }&lt;/code&gt;,
},
{
language: “python”,
label: “Python”,
code: `# Havel-Hakimi algorithm
from heapq import nlargest&lt;/p&gt;
&lt;p&gt;def is_graphic(d):
d = sorted(d, reverse=True)
while d and d[-1] == 0:
d.pop()
if not d:
return True
x = d[0]
d = d[1:]
if x &gt; len(d):
return False
for i in range(x):
d[i] -= 1
if d[i] &amp;#x3C; 0:
return False
return is_graphic(d)&lt;/p&gt;
&lt;p&gt;n = int(input())
d = list(map(int, input().split()))
print(1 if is_graphic(d) else -1)&lt;code&gt;,     },     {       language: &quot;java&quot;,       label: &quot;Java&quot;,       code: &lt;/code&gt;// Havel-Hakimi in Java&lt;/p&gt;
&lt;p&gt;public class Main {
static boolean hh(ArrayList&lt;integer&gt; d) {
while (true) {
Collections.sort(d, Collections.reverseOrder());
while (!d.isEmpty() &amp;#x26;&amp;#x26; d.get(d.size()-1) == 0)
d.remove(d.size()-1);
if (d.isEmpty()) return true;
int x = d.remove(0);
if (x &gt; d.size()) return false;
for (int i = 0; i &amp;#x3C; x; i++) {
d.set(i, d.get(i) - 1);
if (d.get(i) &amp;#x3C; 0) return false;
}
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
ArrayList&lt;integer&gt; d = new ArrayList&amp;#x3C;&gt;();
for (int i = 0; i &amp;#x3C; n; i++) d.add(sc.nextInt());
System.out.println(hh(d) ? 1 : -1);
}
}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;Graphic 가능&quot;,       input: &lt;/code&gt;5
3 3 2 2 2&lt;code&gt;,       output: &lt;/code&gt;1&lt;code&gt;,     },     {       label: &quot;Graphic 불가능&quot;,       input: &lt;/code&gt;4
3 3 3 1&lt;code&gt;,       output: &lt;/code&gt;-1`,
},
]}
/&gt;&lt;/integer&gt;&lt;/integer&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Havel-Hakimi 시간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N^2) (매 단계 정렬)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Erdos-Gallai 시간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N^2) (k 마다 합)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;최적화 Erdos-Gallai&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N log N) (prefix sum + binary search)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;공간&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;정리&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;접근&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;특징&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Havel-Hakimi&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;구성적 (constructive)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;실제 그래프 복원 가능&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Erdos-Gallai&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;판별적 (decision)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;부등식만 확인&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Gale-Ryser&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;이분 그래프용&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;bipartite degree sequence&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-정렬-유지&quot;&gt;1. 정렬 유지&lt;/h3&gt;
&lt;p&gt;각 단계마다 내림차순 정렬을 유지해야 함. 차수를 감소시킨 후 정렬하지 않으면 결과가 달라짐.&lt;/p&gt;
&lt;h3 id=&quot;2-차수-합-홀짝&quot;&gt;2. 차수 합 홀짝&lt;/h3&gt;
&lt;p&gt;차수 합이 홀수면 무조건 불가능 (handshaking lemma).&lt;/p&gt;
&lt;h3 id=&quot;3-n1-일-때&quot;&gt;3. N=1 일 때&lt;/h3&gt;
&lt;p&gt;N=1 이면 차수열 [0] 만 유효. d[1] &gt; 0 이면 불가능 (loop 필요).&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2084&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;차수열&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;46.8%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2000-2099/2084&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 13560&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;축구 게임&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;26.8%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/13500-13599/13560&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 31217&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Y&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;59.0%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/31200-31299/31217&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 3386&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;테니스 클럽&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;25.6%&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/3300-3399/3386&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[Graph Traversal|그래프 탐색]]&lt;/li&gt;
&lt;li&gt;[[MST|최소 스패닝 트리]]&lt;/li&gt;
&lt;li&gt;[[Bipartite Graph|이분 그래프]] (Gale-Ryser)&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>graph</category><category>degree-sequence</category><author>koa (김신건)</author></item><item><title>네트워크 유량 (Network Flow)</title><link>https://shinkeonkim.com/wiki/algorithm/graph/flow/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/graph/flow/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;네트워크 유량 (Network Flow)&lt;/strong&gt; 은 방향 그래프 G = (V, E) 에서 각 간선 (u, v) 의 &lt;strong&gt;용량 (capacity)&lt;/strong&gt; c(u, v) 가 주어질 때, 소스 s 에서 싱크 t 로 흐를 수 있는 &lt;strong&gt;최대 유량 (max flow)&lt;/strong&gt; 을 구하는 문제.&lt;/p&gt;
&lt;p&gt;제약:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;용량 제약 (capacity constraint)&lt;/strong&gt;: 각 간선의 유량 f(u, v) ≤ c(u, v).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;유량 보존 (flow conservation)&lt;/strong&gt;: s, t 제외 모든 정점에서 들어오는 유량 = 나가는 유량.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;가장 유명한 알고리즘: Ford-Fulkerson (개념), Edmonds-Karp (O(VE^2)), Dinic (O(V^2 E)).&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;“파이프 네트워크에서 최대로 보낼 수 있는 물의 양”, “교통망 최대 수송량”, “이분 매칭 (bipartite matching)”, “최소 컷 (min-cut)” 모두 유량으로 모델링.&lt;/p&gt;
&lt;p&gt;naive: 모든 경로 나열 O(2^E). 핵심 통찰: &lt;strong&gt;증가 경로 (augmenting path)&lt;/strong&gt; 를 반복. 남은 용량 (residual capacity) 가 있는 s → t 경로를 찾아 유량을 밀어 넣으면, 최종적으로 최대 유량 수렴. &lt;strong&gt;max-flow min-cut theorem&lt;/strong&gt;: 최대 유량 = 최소 컷.&lt;/p&gt;
&lt;p&gt;PS: “매칭 개수”, “분리 집합”, “비용 + 유량 (MCMF)”, BOJ 이분 매칭 대부분.&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;h3 id=&quot;residual-graph&quot;&gt;residual graph&lt;/h3&gt;
&lt;p&gt;원 그래프 G 와 현재 유량 f 에서, &lt;strong&gt;잔여 그래프 (residual graph)&lt;/strong&gt; G_f:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;간선 (u, v) 가 c(u, v) - f(u, v) &gt; 0 이면 (u, v) 간선 추가 (forward edge).&lt;/li&gt;
&lt;li&gt;f(u, v) &gt; 0 이면 (v, u) 간선 추가, 용량 = f(u, v) (backward edge).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;증가 경로: G_f 에서 s → t 경로. 경로 용량 = min(residual capacity).&lt;/p&gt;
&lt;h3 id=&quot;ford-fulkerson-반복&quot;&gt;Ford-Fulkerson 반복&lt;/h3&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;f = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;while exists augmenting path p in G_f:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    push flow along p&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    update f and G_f&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;return f&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;증가 경로를 어떻게 찾느냐에 따라 복잡도 다름.&lt;/p&gt;
&lt;h3 id=&quot;edmonds-karp-bfs&quot;&gt;Edmonds-Karp (BFS)&lt;/h3&gt;
&lt;p&gt;증가 경로를 &lt;strong&gt;BFS&lt;/strong&gt; 로. 가장 짧은 경로 우선 → O(VE) 번 반복, 각 BFS O(E) → O(VE^2).&lt;/p&gt;
&lt;h3 id=&quot;dinic-level-graph--blocking-flow&quot;&gt;Dinic (level graph + blocking flow)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;level graph&lt;/strong&gt;: BFS 로 s 에서 거리 d[v] 계산.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;blocking flow&lt;/strong&gt;: d[u] + 1 = d[v] 인 간선만 DFS. 한 번에 여러 경로 동시 처리.&lt;/li&gt;
&lt;li&gt;반복: level graph 재구성 → blocking flow. 총 O(V) 번 → O(V^2 E).&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Edmonds-Karp(G, s, t):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for each edge (u, v):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        f[u][v] = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    while true:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        path, bottleneck = BFS_augment(G_f, s, t)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if path is null: break&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        for (u, v) in path:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            f[u][v] += bottleneck&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            f[v][u] -= bottleneck&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return sum of f[s][v] for all v&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;BFS_augment(G_f, s, t):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    parent = [-1] * V&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    queue = [s]&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    while queue not empty:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        u = queue.pop(0)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        for v in adj[u]:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            if parent[v] == -1 and residual[u][v] &gt; 0:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                parent[v] = u&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                queue.append(v)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                if v == t:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                    path = trace(parent, s, t)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                    bottle = min(residual[u][v] for (u,v) in path)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                    return path, bottle&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return null, 0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: `// Edmonds-Karp: O(VE^2)
#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;&lt;/p&gt;
&lt;p&gt;const int INF = 1e9;
int n, cap[105][105], flow[105][105];&lt;/p&gt;
&lt;p&gt;int bfs(int s, int t, vector&lt;int&gt;&amp;#x26; parent) {
fill(parent.begin(), parent.end(), -1);
parent[s] = s;
queue&amp;#x3C;pair&amp;#x3C;int,int&gt;&gt; q; q.push({s, INF});
while (!q.empty()) {
auto [u, f] = q.front(); q.pop();
if (u == t) return f;
for (int v = 0; v &amp;#x3C; n; v++) {
int res = cap[u][v] - flow[u][v];
if (parent[v] == -1 &amp;#x26;&amp;#x26; res &gt; 0) {
parent[v] = u;
int nf = min(f, res);
q.push({v, nf});
}
}
}
return 0;
}&lt;/int&gt;&lt;/p&gt;
&lt;p&gt;int maxFlow(int s, int t) {
int total = 0;
vector&lt;int&gt; parent(n);
while (int pushed = bfs(s, t, parent)) {
total += pushed;
int v = t;
while (v != s) {
int u = parent[v];
flow[u][v] += pushed;
flow[v][u] -= pushed;
v = u;
}
}
return total;
}&lt;/int&gt;&lt;/p&gt;
&lt;p&gt;int main() {
int m, s, t; cin &gt;&gt; n &gt;&gt; m &gt;&gt; s &gt;&gt; t;
for (int i = 0; i &amp;#x3C; m; i++) {
int u, v, c; cin &gt;&gt; u &gt;&gt; v &gt;&gt; c;
cap[u][v] += c;
}
cout &amp;#x3C;&amp;#x3C; maxFlow(s, t) &amp;#x3C;&amp;#x3C; “\n”;
}&lt;code&gt;,     },     {       language: &quot;python&quot;,       label: &quot;Python&quot;,       code: &lt;/code&gt;# Edmonds-Karp max flow
from collections import deque
import sys
input = sys.stdin.readline&lt;/p&gt;
&lt;p&gt;def bfs(s, t, parent, cap, flow, n):
parent[:] = [-1] * n
parent[s] = s
q = deque([(s, float(‘inf’))])
while q:
u, f = q.popleft()
if u == t:
return f
for v in range(n):
res = cap[u][v] - flow[u][v]
if parent[v] == -1 and res &gt; 0:
parent[v] = u
nf = min(f, res)
q.append((v, nf))
return 0&lt;/p&gt;
&lt;p&gt;def max_flow(n, s, t, cap):
flow = [[0]*n for _ in range(n)]
parent = [-1] * n
total = 0
while True:
pushed = bfs(s, t, parent, cap, flow, n)
if pushed == 0:
break
total += pushed
v = t
while v != s:
u = parent[v]
flow[u][v] += pushed
flow[v][u] -= pushed
v = u
return total&lt;/p&gt;
&lt;p&gt;n, m, s, t = map(int, input().split())
cap = [[0]*n for _ in range(n)]
for _ in range(m):
u, v, c = map(int, input().split())
cap[u][v] += c
print(max_flow(n, s, t, cap))&lt;code&gt;,     },     {       language: &quot;java&quot;,       label: &quot;Java&quot;,       code: &lt;/code&gt;// Edmonds-Karp in Java&lt;/p&gt;
&lt;p&gt;public class Main {
static int n, cap[][], flow[][];
static final int INF = (int)1e9;&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;static int bfs(int s, int t, int[] parent) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    Arrays.fill(parent, -1);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    parent[s] = s;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    Queue&amp;#x3C;int[]&gt; q = new LinkedList&amp;#x3C;&gt;();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    q.add(new int[]{s, INF});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    while (!q.isEmpty()) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        int[] cur = q.poll();&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        int u = cur[0], f = cur[1];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if (u == t) return f;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        for (int v = 0; v &amp;#x3C; n; v++) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            int res = cap[u][v] - flow[u][v];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            if (parent[v] == -1 &amp;#x26;&amp;#x26; res &gt; 0) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                parent[v] = u;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                int nf = Math.min(f, res);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;                q.add(new int[]{v, nf});&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return 0;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;static int maxFlow(int s, int t) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    int total = 0;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    int[] parent = new int[n];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    while (true) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        int pushed = bfs(s, t, parent);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if (pushed == 0) break;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        total += pushed;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        for (int v = t; v != s; v = parent[v]) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            int u = parent[v];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            flow[u][v] += pushed;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;            flow[v][u] -= pushed;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return total;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;public static void main(String[] args) throws IOException {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    StringTokenizer st = new StringTokenizer(br.readLine());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    n = Integer.parseInt(st.nextToken());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    int m = Integer.parseInt(st.nextToken());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    int s = Integer.parseInt(st.nextToken());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    int t = Integer.parseInt(st.nextToken());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    cap = new int[n][n]; flow = new int[n][n];&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    for (int i = 0; i &amp;#x3C; m; i++) {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        st = new StringTokenizer(br.readLine());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        int u = Integer.parseInt(st.nextToken());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        int v = Integer.parseInt(st.nextToken());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        int c = Integer.parseInt(st.nextToken());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        cap[u][v] += c;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    System.out.println(maxFlow(s, t));&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;}&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;기본&quot;,       input: &lt;/code&gt;6 7 0 5
0 1 3
0 2 2
1 3 3
2 3 2
2 4 2
3 5 4
4 5 3&lt;code&gt;,       output: &lt;/code&gt;5`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;알고리즘&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;시간&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;공간&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Ford-Fulkerson&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(E · max_flow), 정수 용량시 pseudo-polynomial&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(V^2)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Edmonds-Karp&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(V E^2)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(V^2)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Dinic&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(V^2 E), 이분 그래프면 O(E √V)&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(V^2)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Push-Relabel&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(V^3), 실제 빠름&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(V^2)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;h3 id=&quot;1-이분-매칭-bipartite-matching&quot;&gt;1. 이분 매칭 (bipartite matching)&lt;/h3&gt;
&lt;p&gt;왼쪽 그룹 L, 오른쪽 R. s → L (용량 1), R → t (용량 1), L-R 간선 (용량 1). max flow = 최대 매칭.&lt;/p&gt;
&lt;h3 id=&quot;2-최소-컷-min-cut&quot;&gt;2. 최소 컷 (min-cut)&lt;/h3&gt;
&lt;p&gt;max-flow min-cut theorem: 최대 유량 = 최소 컷. BFS 로 s 에서 도달 가능 정점 집합 S, 나머지 T. cut = 간선 (u, v) where u ∈ S, v ∈ T.&lt;/p&gt;
&lt;h3 id=&quot;3-비용-유량-mcmf-min-cost-max-flow&quot;&gt;3. 비용 유량 (MCMF, Min-Cost Max-Flow)&lt;/h3&gt;
&lt;p&gt;각 간선에 비용. 최대 유량 중 최소 비용 경로. SPFA / Dijkstra + 유량.&lt;/p&gt;
&lt;h3 id=&quot;4-다중-소스싱크&quot;&gt;4. 다중 소스/싱크&lt;/h3&gt;
&lt;p&gt;여러 소스 s_i, 싱크 t_j. super-source S → s_i (INF), t_j → super-sink T (INF).&lt;/p&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-양방향-간선&quot;&gt;1. 양방향 간선&lt;/h3&gt;
&lt;p&gt;무방향 간선 (u, v) 는 (u, v), (v, u) 두 방향 각각 cap 추가.&lt;/p&gt;
&lt;h3 id=&quot;2-역방향-간선-초기화&quot;&gt;2. 역방향 간선 초기화&lt;/h3&gt;
&lt;p&gt;flow[v][u] = -flow[u][v]. 초기 0.&lt;/p&gt;
&lt;h3 id=&quot;3-정수-용량-가정&quot;&gt;3. 정수 용량 가정&lt;/h3&gt;
&lt;p&gt;Ford-Fulkerson 은 무리수 용량이면 무한 루프 가능. Edmonds-Karp / Dinic 은 OK.&lt;/p&gt;
&lt;h3 id=&quot;4-overflow&quot;&gt;4. overflow&lt;/h3&gt;
&lt;p&gt;용량 합이 int 범위 넘으면 long long.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 6086&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;최대 유량&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/6000-6099/6086&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2188&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;축사 배정 (이분 매칭)&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2100-2199/2188&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 11375&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;열혈강호&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/11300-11399/11375&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 17412&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;도시 왕복하기 1&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/17400-17499/17412&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[BFS|너비 우선 탐색]]&lt;/li&gt;
&lt;li&gt;[[Bipartite Matching|이분 매칭]]&lt;/li&gt;
&lt;li&gt;[[Min-Cut|최소 컷]]&lt;/li&gt;
&lt;li&gt;[[MCMF|최소 비용 최대 유량]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>graph</category><category>flow</category><category>max-flow</category><category>ford-fulkerson</category><category>edmonds-karp</category><category>dinic</category><author>koa (김신건)</author></item><item><title>함수 그래프 (Functional Graph)</title><link>https://shinkeonkim.com/wiki/algorithm/graph/functional-graph/</link><guid isPermaLink="true">https://shinkeonkim.com/wiki/algorithm/graph/functional-graph/</guid><pubDate>Thu, 25 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;정의&quot;&gt;정의&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;함수 그래프 (Functional Graph)&lt;/strong&gt; 는 정점 집합 V 와 함수 f: V → V 로 정의되는 방향 그래프. 각 정점 v 에서 정확히 하나의 간선 v → f(v) 가 나간다. 형태는 &lt;strong&gt;여러 사이클 (cycle) + 각 사이클로 들어가는 트리 (tree)&lt;/strong&gt; 구조.&lt;/p&gt;
&lt;h2 id=&quot;문제-상황과-동기&quot;&gt;문제 상황과 동기&lt;/h2&gt;
&lt;p&gt;PS 에서 “다음 칸으로 이동”, “연결된 정점으로 한 단계” 문제는 out-degree 가 정확히 1 인 그래프. BFS/DFS 로 naive 하게 x 번 진행하면 O(x · N), 구간이 10^9 이상이면 터진다. 핵심 통찰: 경로는 반드시 “tree part → cycle entry → cycle 회전”, 세 구간으로 분해. &lt;strong&gt;Floyd’s cycle finding&lt;/strong&gt; (토끼와 거북) 으로 사이클 길이 + 시작점 O(N) 에 찾고, 나머지는 prefix jump 또는 modular 계산.&lt;/p&gt;
&lt;p&gt;자주 등장: BOJ 상태 전이 시뮬레이션, “N 번 이동 후 위치”, “언제 사이클 만나는가”, 해시 충돌 분석, 수열 반복 규칙.&lt;/p&gt;
&lt;h2 id=&quot;시각화&quot;&gt;시각화&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;{}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;핵심-아이디어&quot;&gt;핵심 아이디어&lt;/h2&gt;
&lt;h3 id=&quot;invariant&quot;&gt;invariant&lt;/h3&gt;
&lt;p&gt;정점 v 에서 출발하면 경로는 &lt;em&gt;tree tail → cycle&lt;/em&gt; 두 단계.&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;v → f(v) → f²(v) → ... → entry → [cycle 시작] → entry (다시)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;사이클 진입까지 거리 λ (lambda), 사이클 길이 μ (mu).&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;f^k(v) = {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    tree 안,           k &amp;#x3C; λ&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    cycle 회전,        k ≥ λ,  (k - λ) mod μ 위치&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;floyds-cycle-finding&quot;&gt;Floyd’s cycle finding&lt;/h3&gt;
&lt;p&gt;토끼 (2배 속도), 거북 (1배 속도):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;phase 1&lt;/strong&gt;: 토끼가 사이클 한 바퀴 돌 동안 거북을 따라잡으면 사이클 존재 확정.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;phase 2&lt;/strong&gt;: 토끼를 시작점으로, 둘 다 1 step 씩 움직여 만나면 그 점이 entry.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;phase 3&lt;/strong&gt;: entry 에서 한 바퀴 돌면 μ.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;시간: O(λ + μ), 공간 O(1). 비순환 그래프면 null 이나 범위 밖 도달.&lt;/p&gt;
&lt;h3 id=&quot;binary-lifting-optional&quot;&gt;binary lifting (optional)&lt;/h3&gt;
&lt;p&gt;k 가 큰 경우 &lt;code&gt;f^k(v)&lt;/code&gt; 를 O(log k) 에 구하려면:&lt;/p&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;plaintext&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;dp[v][i] = f^(2^i)(v)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;전처리 O(N log N), 쿼리 O(log k).&lt;/p&gt;
&lt;h2 id=&quot;알고리즘&quot;&gt;알고리즘&lt;/h2&gt;
&lt;pre class=&quot;astro-code github-dark&quot; style=&quot;background-color:#24292e;color:#e1e4e8; overflow-x: auto;&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Floyd_cycle(f, start):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    tortoise = start&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    hare = start&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # phase 1: 사이클 감지&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    loop&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        tortoise = f(tortoise)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        hare = f(f(hare))&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if hare == tortoise: break&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        if hare null or f(hare) null: return (λ=∞, μ=0)  # 비순환&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # phase 2: entry 찾기&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    tortoise = start&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    while tortoise ≠ hare:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        tortoise = f(tortoise)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        hare = f(hare)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    entry = tortoise&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # phase 3: μ 측정&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    μ = 1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    hare = f(entry)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    while hare ≠ entry:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        hare = f(hare)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        μ++&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    # λ 측정&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    λ = 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    p = start&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    while p ≠ entry:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        p = f(p)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;        λ++&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;    return (λ, μ, entry)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;구현&quot;&gt;구현&lt;/h2&gt;
&lt;p&gt;&amp;#x3C;CodeWithOutput
variants={[
{
language: “cpp”,
label: “C++”,
code: `// Functional graph: Floyd’s cycle detection
#include &amp;#x3C;bits/stdc++.h&gt;
using namespace std;&lt;/p&gt;
&lt;p&gt;struct Cycle {
int lambda, mu, entry;
};&lt;/p&gt;
&lt;p&gt;Cycle floyd(const vector&lt;int&gt;&amp;#x26; f, int start) {
auto step = [&amp;#x26;](int x) { return (x &gt;= (int)f.size() || x &amp;#x3C; 0) ? -1 : f[x]; };
int t = start, h = start;
// phase 1
do {
t = step(t);
h = step(step(h));
if (h &amp;#x3C; 0 || t &amp;#x3C; 0) return {-1, 0, -1};
} while (t != h);
// phase 2
int entry = start;
while (entry != h) { entry = step(entry); h = step(h); }
// phase 3
int mu = 1;
for (int x = step(entry); x != entry; x = step(x)) mu++;
int lambda = 0;
for (int x = start; x != entry; x = step(x)) lambda++;
return {lambda, mu, entry};
}&lt;/int&gt;&lt;/p&gt;
&lt;p&gt;int main() {
int n, s; cin &gt;&gt; n &gt;&gt; s;
vector&lt;int&gt; f(n);
for (auto&amp;#x26; v : f) cin &gt;&gt; v;
auto [lam, mu, ent] = floyd(f, s);
cout &amp;#x3C;&amp;#x3C; “lambda=” &amp;#x3C;&amp;#x3C; lam &amp;#x3C;&amp;#x3C; ” mu=” &amp;#x3C;&amp;#x3C; mu &amp;#x3C;&amp;#x3C; ” entry=” &amp;#x3C;&amp;#x3C; ent &amp;#x3C;&amp;#x3C; “\n”;
}&lt;code&gt;,     },     {       language: &quot;python&quot;,       label: &quot;Python&quot;,       code: &lt;/code&gt;# Functional graph cycle detection
import sys
input = sys.stdin.readline&lt;/int&gt;&lt;/p&gt;
&lt;p&gt;def floyd(f, start):
def step(x):
return f[x] if 0 &amp;#x3C;= x &amp;#x3C; len(f) else None
t, h = start, start
# phase 1
while True:
t = step(t)
h = step(step(h)) if step(h) is not None else None
if h is None or t is None:
return None, 0, None
if t == h:
break
# phase 2
entry = start
while entry != h:
entry, h = step(entry), step(h)
# phase 3
mu = 1
x = step(entry)
while x != entry:
x = step(x)
mu += 1
# lambda
lam = 0
x = start
while x != entry:
x = step(x)
lam += 1
return lam, mu, entry&lt;/p&gt;
&lt;p&gt;n, s = map(int, input().split())
f = list(map(int, input().split()))
lam, mu, ent = floyd(f, s)
print(f”lambda={lam} mu={mu} entry={ent}“)&lt;code&gt;,     },   ]}   cases={[     {       label: &quot;기본 사이클&quot;,       input: &lt;/code&gt;6 0
1 2 3 4 2 5&lt;code&gt;,       output: &lt;/code&gt;lambda=2 mu=3 entry=2`,
},
]}
/&gt;&lt;/p&gt;
&lt;h2 id=&quot;복잡도&quot;&gt;복잡도&lt;/h2&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;항목&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;값&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;사이클 탐지&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(λ + μ) 시간, O(1) 공간&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;f^k(v) 단일&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(k) naive, O(log k) binary lifting&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;전처리 (lifting)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(N log N) 시간, O(N log N) 공간&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;쿼리 (lifting)&lt;/strong&gt;&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;O(log k)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;변형--활용&quot;&gt;변형 / 활용&lt;/h2&gt;
&lt;h3 id=&quot;1-상태-전이-시뮬레이션&quot;&gt;1. 상태 전이 시뮬레이션&lt;/h3&gt;
&lt;p&gt;“N 번 이동 후 위치” 문제. k &amp;#x3C; λ 면 순회, k ≥ λ 면 (k - λ) mod μ.&lt;/p&gt;
&lt;h3 id=&quot;2-연결-요소-분석&quot;&gt;2. 연결 요소 분석&lt;/h3&gt;
&lt;p&gt;각 정점에서 Floyd 돌려 entry 찾으면, 같은 entry 를 공유하는 정점끼리 한 “weakly connected component”.&lt;/p&gt;
&lt;h3 id=&quot;3-사이클-없는-경우&quot;&gt;3. 사이클 없는 경우&lt;/h3&gt;
&lt;p&gt;hare 가 null 이면 무한히 뻗어나가거나 단절. 예: 해시 체인.&lt;/p&gt;
&lt;h3 id=&quot;4-다중-출발&quot;&gt;4. 다중 출발&lt;/h3&gt;
&lt;p&gt;여러 시작점이 같은 사이클로 모이는가? entry 기준 그룹.&lt;/p&gt;
&lt;h2 id=&quot;함정&quot;&gt;함정&lt;/h2&gt;
&lt;h3 id=&quot;1-out-degree--1&quot;&gt;1. out-degree ≠ 1&lt;/h3&gt;
&lt;p&gt;일반 그래프라면 functional graph 가 아님. 이 기법 안 먹힘.&lt;/p&gt;
&lt;h3 id=&quot;2-범위-체크&quot;&gt;2. 범위 체크&lt;/h3&gt;
&lt;p&gt;f[v] 가 배열 밖이면 비순환. step 함수 null 처리.&lt;/p&gt;
&lt;h3 id=&quot;3-λ-계산-빠뜨림&quot;&gt;3. λ 계산 빠뜨림&lt;/h3&gt;
&lt;p&gt;phase 1~3 은 μ, entry 만 줌. λ 는 start → entry 거리 따로.&lt;/p&gt;
&lt;h3 id=&quot;4-k-매우-큰-경우&quot;&gt;4. k 매우 큰 경우&lt;/h3&gt;
&lt;p&gt;k &gt; 10^9 면 naive loop 불가. binary lifting 또는 mod μ 쓰기.&lt;/p&gt;
&lt;h2 id=&quot;boj-연습-문제&quot;&gt;BOJ 연습 문제&lt;/h2&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;번호&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;제목&lt;/th&gt;&lt;th align=&quot;right&quot;&gt;정답률&lt;/th&gt;&lt;th align=&quot;left&quot;&gt;링크&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 2331&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;반복수열&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/2300-2399/2331&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 9466&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;텀 프로젝트&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/9400-9499/9466&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 15591&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;MooTube (Silver)&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/15500-15599/15591&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;BOJ 16958&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;텔레포트&lt;/td&gt;&lt;td align=&quot;right&quot;&gt;-&lt;/td&gt;&lt;td align=&quot;left&quot;&gt;&lt;a href=&quot;https://github.com/kokoa-lab/boj-problems/tree/main/organize_problems/16900-16999/16958&quot;&gt;kokoa-lab&lt;/a&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;참고&quot;&gt;참고&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[[DFS|깊이 우선 탐색]]&lt;/li&gt;
&lt;li&gt;[[Cycle Detection|사이클 탐지]]&lt;/li&gt;
&lt;li&gt;[[Binary Lifting|이진 점프]]&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>algorithm</category><category>graph</category><category>functional-graph</category><category>cycle-detection</category><author>koa (김신건)</author></item></channel></rss>