From 7e9aa1f8e9b8661db6bad1940c1fd1886492d6dc Mon Sep 17 00:00:00 2001 From: pykido Date: Sat, 6 Jun 2026 13:34:48 +0900 Subject: [PATCH 1/4] =?UTF-8?q?docs=20:=20=EC=95=8C=EA=B3=A0=EB=A6=AC?= =?UTF-8?q?=EC=A6=98=20=EB=8F=84=EB=A9=94=EC=9D=B8=20RAG=20data=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../week3/data/patterns/backtracking.md | 40 ++++++++++++++ .../pykido/week3/data/patterns/bfs-dfs.md | 48 ++++++++++++++++ .../week3/data/patterns/binary-search.md | 50 +++++++++++++++++ .../pykido/week3/data/patterns/dijkstra.md | 43 +++++++++++++++ assignments/pykido/week3/data/patterns/dp.md | 50 +++++++++++++++++ .../pykido/week3/data/patterns/greedy.md | 35 ++++++++++++ .../pykido/week3/data/patterns/hash-map.md | 43 +++++++++++++++ .../pykido/week3/data/patterns/heap.md | 55 +++++++++++++++++++ .../pykido/week3/data/patterns/prefix-sum.md | 49 +++++++++++++++++ .../week3/data/patterns/sliding-window.md | 50 +++++++++++++++++ .../week3/data/patterns/two-pointers.md | 37 +++++++++++++ .../pykido/week3/data/patterns/union-find.md | 46 ++++++++++++++++ .../data/problems/pgs-118667-two-queues.md | 51 +++++++++++++++++ .../problems/pgs-42627-disk-controller.md | 47 ++++++++++++++++ .../problems/pgs-42861-connecting-islands.md | 49 +++++++++++++++++ .../data/problems/pgs-42898-school-path.md | 43 +++++++++++++++ .../data/problems/pgs-43165-target-number.md | 36 ++++++++++++ .../problems/pgs-43236-stepping-stones.md | 49 +++++++++++++++++ .../data/problems/pgs-43238-immigration.md | 38 +++++++++++++ .../data/problems/pgs-67258-gem-shopping.md | 44 +++++++++++++++ 20 files changed, 903 insertions(+) create mode 100644 assignments/pykido/week3/data/patterns/backtracking.md create mode 100644 assignments/pykido/week3/data/patterns/bfs-dfs.md create mode 100644 assignments/pykido/week3/data/patterns/binary-search.md create mode 100644 assignments/pykido/week3/data/patterns/dijkstra.md create mode 100644 assignments/pykido/week3/data/patterns/dp.md create mode 100644 assignments/pykido/week3/data/patterns/greedy.md create mode 100644 assignments/pykido/week3/data/patterns/hash-map.md create mode 100644 assignments/pykido/week3/data/patterns/heap.md create mode 100644 assignments/pykido/week3/data/patterns/prefix-sum.md create mode 100644 assignments/pykido/week3/data/patterns/sliding-window.md create mode 100644 assignments/pykido/week3/data/patterns/two-pointers.md create mode 100644 assignments/pykido/week3/data/patterns/union-find.md create mode 100644 assignments/pykido/week3/data/problems/pgs-118667-two-queues.md create mode 100644 assignments/pykido/week3/data/problems/pgs-42627-disk-controller.md create mode 100644 assignments/pykido/week3/data/problems/pgs-42861-connecting-islands.md create mode 100644 assignments/pykido/week3/data/problems/pgs-42898-school-path.md create mode 100644 assignments/pykido/week3/data/problems/pgs-43165-target-number.md create mode 100644 assignments/pykido/week3/data/problems/pgs-43236-stepping-stones.md create mode 100644 assignments/pykido/week3/data/problems/pgs-43238-immigration.md create mode 100644 assignments/pykido/week3/data/problems/pgs-67258-gem-shopping.md diff --git a/assignments/pykido/week3/data/patterns/backtracking.md b/assignments/pykido/week3/data/patterns/backtracking.md new file mode 100644 index 0000000..5374966 --- /dev/null +++ b/assignments/pykido/week3/data/patterns/backtracking.md @@ -0,0 +1,40 @@ +# 백트래킹 (Backtracking) + +## 개념 +가능한 모든 후보를 트리 형태로 탐색하되, 현재 경로가 답이 될 수 없다고 판단되면 더 내려가지 않고 즉시 되돌아오는(가지치기, pruning) DFS 기반 기법이다. 완전 탐색의 일종이지만 유망하지 않은 분기를 잘라내 실제 탐색량을 크게 줄인다. + +## 언제 쓰나 +- 순열, 조합, 부분집합 생성 +- N-Queen, 스도쿠처럼 제약을 만족하는 배치 찾기 +- 합/경우의 수를 만드는 모든 방법 탐색 + +## 시간 복잡도 +최악의 경우 후보 공간 전체를 보므로 지수 시간 (`O(2^N)`, `O(N!)` 등)이다. 가지치기가 효과적일수록 실제 비용은 줄지만 상한은 변하지 않는다. + +## 기본 템플릿 +```python +def subsets(nums): + result = [] + path = [] + + def backtrack(start): + result.append(path[:]) + for i in range(start, len(nums)): + path.append(nums[i]) + backtrack(i + 1) + path.pop() + + backtrack(0) + return result +``` + +## 흔한 실수 +- 경로에 추가한 뒤 되돌릴 때 `pop` 을 빠뜨려 상태가 오염된다. append 와 pop 은 짝을 이뤄야 한다. +- 결과를 저장할 때 `path` 를 그대로 참조로 넣어 이후 변경이 반영된다. `path[:]` 로 복사해야 한다. +- 가지치기 조건을 너무 늦게 검사해 불필요한 깊은 탐색을 한다. +- 메모이제이션이 가능한 문제인데 순수 백트래킹으로만 풀어 시간 초과가 난다. + +## 연관 문제 +- 타겟 넘버 (각 수에 +/- 를 붙이는 모든 경우) +- N-Queen +- 부분집합의 합 diff --git a/assignments/pykido/week3/data/patterns/bfs-dfs.md b/assignments/pykido/week3/data/patterns/bfs-dfs.md new file mode 100644 index 0000000..5e57c07 --- /dev/null +++ b/assignments/pykido/week3/data/patterns/bfs-dfs.md @@ -0,0 +1,48 @@ +# 너비/깊이 우선 탐색 (BFS / DFS) + +## 개념 +그래프나 격자를 체계적으로 순회하는 두 가지 기본 전략이다. **BFS** 는 큐를 사용해 시작점에서 가까운 노드부터 동심원처럼 퍼져 나간다. **DFS** 는 스택(또는 재귀)을 사용해 한 경로를 끝까지 파고든 뒤 되돌아온다. 둘 다 방문 집합으로 중복 방문을 막는다. + +## 언제 쓰나 +- 도달 가능 여부, 연결 요소 개수: BFS / DFS 모두 가능 +- 가중치가 모두 1인 그래프의 최단 거리: BFS (처음 방문하는 시점이 최단) +- 모든 경로 탐색, 백트래킹, 사이클 검출: DFS +- 격자에서 영역 채우기(flood fill): BFS / DFS 모두 가능 + +## 시간 복잡도 +정점 V, 간선 E 에 대해 `O(V + E)`. 격자에서는 칸 수에 비례한다. + +## 기본 템플릿 +```python +from collections import deque + +def bfs(graph, start): + visited = {start} + q = deque([start]) + while q: + node = q.popleft() + for nxt in graph[node]: + if nxt not in visited: + visited.add(nxt) + q.append(nxt) + return visited +``` + +DFS 재귀 형태: +```python +def dfs(graph, node, visited): + visited.add(node) + for nxt in graph[node]: + if nxt not in visited: + dfs(graph, nxt, visited) +``` + +## 흔한 실수 +- 방문 처리를 큐에서 꺼내는 시점에 해서 같은 노드를 여러 번 큐에 넣는다. push 시점에 방문 표시해야 한다. +- BFS 가 아닌 DFS 로 최단 거리를 구하려 한다. 가중치 1 최단 거리는 BFS 가 맞다. +- DFS 재귀 깊이가 1000 을 넘어 `RecursionError` 가 난다. `sys.setrecursionlimit` 또는 명시적 스택으로 전환한다. + +## 연관 문제 +- 타겟 넘버 (각 원소에 +/- 분기, DFS 백트래킹) +- 격자 영역 개수 세기 (flood fill) +- 미로 최단 거리 (BFS) diff --git a/assignments/pykido/week3/data/patterns/binary-search.md b/assignments/pykido/week3/data/patterns/binary-search.md new file mode 100644 index 0000000..49295ab --- /dev/null +++ b/assignments/pykido/week3/data/patterns/binary-search.md @@ -0,0 +1,50 @@ +# 이분 탐색 (Binary Search) + +## 개념 +정렬된 배열에서 탐색 범위를 절반씩 줄여가며 목표값을 찾는 기법이다. 매 단계에서 후보 구간의 중앙값을 확인하고, 목표와의 대소 관계로 한쪽 절반을 버린다. 핵심 전제는 **단조성(monotonicity)** 으로, 구간이 정렬되어 있거나 어떤 결정 함수가 한 방향으로만 변할 때만 적용할 수 있다. + +## 언제 쓰나 +- 정렬된 배열에서 특정 값 또는 그 삽입 위치를 찾을 때 +- "조건을 만족하는 최소/최대 값"을 구하는 최적화 문제 (parametric search). 답 후보 x에 대해 `가능한가?(x)`가 단조 boolean이면 답 자체를 이분 탐색한다. +- lower bound / upper bound 가 필요할 때는 직접 구현보다 `bisect_left`, `bisect_right` 를 쓰는 것이 안전하다. + +## 시간 복잡도 +탐색 구간이 매 단계 절반이 되므로 `O(log N)`. parametric search 는 결정 함수 비용이 `O(f)` 일 때 `O(f log(범위))` 가 된다. + +## 기본 템플릿 +```python +def binary_search(arr, target): + lo, hi = 0, len(arr) - 1 + while lo <= hi: + mid = (lo + hi) // 2 + if arr[mid] == target: + return mid + elif arr[mid] < target: + lo = mid + 1 + else: + hi = mid - 1 + return -1 +``` + +parametric search 형태: +```python +def min_feasible(lo, hi, feasible): + while lo < hi: + mid = (lo + hi) // 2 + if feasible(mid): + hi = mid + else: + lo = mid + 1 + return lo +``` + +## 흔한 실수 +- `lo <= hi` 와 `lo < hi` 를 혼동한다. 닫힌 구간 인덱스 탐색은 `lo <= hi` 가 안전하다. +- parametric search 에서 단조성 증명을 건너뛴다. 단조가 아니면 이분 탐색은 오답을 낸다. +- 탐색 범위 hi 를 너무 작게 잡아 답을 놓친다. 범위는 답이 존재할 수 있는 최댓값까지 충분히 크게 잡는다. +- lower bound 와 upper bound 가 모호할 때 직접 구현하다 off-by-one 을 낸다. + +## 연관 문제 +- 입국심사 (parametric search, 답이 시간에 대해 단조) +- 징검다리 (최소 점프 거리를 이분 탐색) +- 정렬된 배열에서의 값 존재 여부 / 개수 세기 diff --git a/assignments/pykido/week3/data/patterns/dijkstra.md b/assignments/pykido/week3/data/patterns/dijkstra.md new file mode 100644 index 0000000..d420659 --- /dev/null +++ b/assignments/pykido/week3/data/patterns/dijkstra.md @@ -0,0 +1,43 @@ +# 다익스트라 (Dijkstra) + +## 개념 +음수 가중치가 없는 그래프에서 한 시작점으로부터 모든 정점까지의 최단 거리를 구하는 알고리즘이다. 아직 확정되지 않은 정점 중 거리가 가장 짧은 것을 우선순위 큐로 꺼내 확정하고, 그 정점을 거쳐 가는 경로로 인접 정점의 거리를 갱신(relaxation)한다. 한 번 확정된 정점의 거리는 다시 바뀌지 않는다. + +## 언제 쓰나 +- 가중치가 있는(음수 없음) 그래프의 단일 출발점 최단 경로 +- 지도 길찾기, 네트워크 지연 최소화 +- 가중치가 다양한 격자 이동 비용 최소화 + +## 시간 복잡도 +우선순위 큐 구현 기준 `O((V + E) log V)`. 간선마다 최대 한 번 힙에 들어간다. + +## 기본 템플릿 +```python +import heapq + +def dijkstra(graph, start, n): + dist = [float("inf")] * n + dist[start] = 0 + heap = [(0, start)] + while heap: + d, node = heapq.heappop(heap) + if d > dist[node]: + continue + for nxt, w in graph[node]: + nd = d + w + if nd < dist[nxt]: + dist[nxt] = nd + heapq.heappush(heap, (nd, nxt)) + return dist +``` + +## 흔한 실수 +- 음수 가중치 그래프에 적용한다. 음수 간선이 있으면 벨만-포드를 써야 한다. +- 힙에서 꺼낸 거리가 이미 확정된 값보다 크면 건너뛰어야 하는데(`d > dist[node]`) 이 검사를 빠뜨려 중복 처리로 느려진다. +- 거리 배열 초기화를 0 이 아닌 무한대로 해야 하는데 0 으로 두어 갱신이 안 된다. +- 방문 배열 없이 단순 BFS 로 가중치 최단 경로를 구하려 한다. + +## 연관 문제 +- 가중 그래프 최단 경로 +- 배달 (여러 목적지까지 최단 거리) +- 최소 비용 격자 이동 diff --git a/assignments/pykido/week3/data/patterns/dp.md b/assignments/pykido/week3/data/patterns/dp.md new file mode 100644 index 0000000..202c405 --- /dev/null +++ b/assignments/pykido/week3/data/patterns/dp.md @@ -0,0 +1,50 @@ +# 동적 계획법 (Dynamic Programming) + +## 개념 +큰 문제를 겹치는 작은 부분 문제로 나누고, 부분 문제의 답을 저장해 재사용하는 기법이다. 두 가지 전제가 모두 성립해야 한다. **최적 부분 구조** (큰 문제의 최적해가 부분 문제의 최적해로 구성됨) 와 **중복 부분 문제** (같은 부분 문제가 여러 번 등장함). 구현은 재귀 + 메모이제이션의 top-down 과 반복문의 bottom-up 두 가지가 있다. + +## 언제 쓰나 +- 경우의 수 세기 (경로 수, 조합 수) +- 최소 비용 / 최대 가치 최적화 (배낭, 동전 교환) +- 부분 수열 문제 (최장 증가 부분 수열, 편집 거리) + +## 시간 복잡도 +일반적으로 `상태 개수 × 상태당 전이 비용`. 격자 DP 는 보통 `O(M*N)`, 배낭은 `O(N*W)` 이다. + +## 기본 템플릿 +bottom-up 격자 DP: +```python +def grid_paths(m, n, blocked): + dp = [[0] * (n + 1) for _ in range(m + 1)] + dp[1][1] = 0 if (1, 1) in blocked else 1 + for r in range(1, m + 1): + for c in range(1, n + 1): + if (r, c) in blocked: + dp[r][c] = 0 + continue + if (r, c) != (1, 1): + dp[r][c] = dp[r - 1][c] + dp[r][c - 1] + return dp[m][n] +``` + +top-down 메모이제이션: +```python +from functools import lru_cache + +@lru_cache(maxsize=None) +def fib(n): + if n < 2: + return n + return fib(n - 1) + fib(n - 2) +``` + +## 흔한 실수 +- 상태 정의가 모호해 필요한 변수가 누락된다. "dp[i] 가 무엇을 의미하는가"를 한 문장으로 적을 수 있어야 한다. +- top-down 에서 메모이제이션을 빠뜨려 지수 시간으로 퇴화한다. +- 경우의 수 문제에서 모듈러 연산을 매 갱신마다 적용하지 않아 오버플로 또는 오답이 난다. +- 1-based 와 0-based 인덱싱을 섞어 경계에서 틀린다. + +## 연관 문제 +- 등굣길 (격자 경로 수, 물웅덩이 회피, mod 1e9+7) +- 최장 증가 부분 수열 +- 0/1 배낭 diff --git a/assignments/pykido/week3/data/patterns/greedy.md b/assignments/pykido/week3/data/patterns/greedy.md new file mode 100644 index 0000000..c0ea51a --- /dev/null +++ b/assignments/pykido/week3/data/patterns/greedy.md @@ -0,0 +1,35 @@ +# 그리디 (Greedy) + +## 개념 +매 단계에서 그 순간 가장 좋아 보이는 선택을 하고, 그 선택을 번복하지 않는 기법이다. 핵심은 **국소 최적 선택이 전역 최적해로 이어진다는 보장**이 있어야 한다는 점이다. 이 보장은 교환 논법(exchange argument)이나 매트로이드 성질로 증명한다. 증명 없이 적용하면 반례에서 무너진다. + +## 언제 쓰나 +- 회의실 배정처럼 끝나는 시간이 빠른 것부터 고르는 활동 선택 문제 +- 거스름돈 문제(동전 체계가 정준일 때) +- 최소 신장 트리(Kruskal, Prim) + +## 시간 복잡도 +대개 정렬이 지배해 `O(N log N)`. 정렬 후 한 번의 선형 순회로 답을 만든다. + +## 기본 템플릿 +```python +def max_non_overlapping(intervals): + intervals.sort(key=lambda x: x[1]) + count = 0 + end = float("-inf") + for s, e in intervals: + if s >= end: + count += 1 + end = e + return count +``` + +## 흔한 실수 +- 그리디 선택이 최적임을 증명하지 않고 직관만으로 적용한다. DP 가 정답인 문제에 그리디를 써서 틀린다. +- 정렬 기준을 잘못 잡는다. 활동 선택은 시작 시간이 아니라 끝나는 시간 기준 정렬이다. +- 한 번 한 선택을 되돌려야 최적인 문제(분할 가능 배낭이 아닌 0/1 배낭)에 그리디를 적용한다. + +## 연관 문제 +- 섬 연결하기 (간선 비용 오름차순 그리디 선택 = Kruskal MST) +- 회의실 배정 +- 분할 가능 배낭 diff --git a/assignments/pykido/week3/data/patterns/hash-map.md b/assignments/pykido/week3/data/patterns/hash-map.md new file mode 100644 index 0000000..c56b372 --- /dev/null +++ b/assignments/pykido/week3/data/patterns/hash-map.md @@ -0,0 +1,43 @@ +# 해시 맵 (Hash Map) + +## 개념 +키를 해시 함수로 버킷에 분산시켜 평균 `O(1)` 에 삽입/조회/삭제를 지원하는 자료구조다. 파이썬의 `dict` 와 `set` 이 이에 해당한다. "이미 본 적 있는가", "몇 번 등장했는가"를 빠르게 묻는 거의 모든 문제의 기본 도구다. + +## 언제 쓰나 +- 등장 횟수 세기, 빈도 집계 (`collections.Counter`) +- 두 수의 합처럼 "보수가 존재하는가"를 즉시 확인할 때 +- 중복 제거, 멤버십 검사 +- 그룹화 (애너그램 묶기 등) + +## 시간 복잡도 +평균 삽입/조회 `O(1)`, 최악(해시 충돌 다발) `O(N)`. N 개 원소 처리에 전체 `O(N)`. + +## 기본 템플릿 +```python +def two_sum(nums, target): + seen = {} + for i, x in enumerate(nums): + if target - x in seen: + return (seen[target - x], i) + seen[x] = i + return None +``` + +빈도 집계: +```python +from collections import Counter + +def most_common_char(s): + counter = Counter(s) + return counter.most_common(1)[0][0] +``` + +## 흔한 실수 +- 리스트나 딕셔너리처럼 변경 가능한(unhashable) 객체를 키로 쓴다. 튜플로 변환해야 한다. +- 슬라이딩 윈도우와 함께 쓸 때 빠지는 원소의 카운트를 0 으로 만든 뒤 키 삭제를 빼먹어 메모리/로직이 꼬인다. +- 정렬이 필요한 결과를 dict 순회 순서에 의존한다. 삽입 순서는 보장되지만 값 기준 정렬은 별도로 해야 한다. + +## 연관 문제 +- 보석 쇼핑 (윈도우 내 보석 종류 카운트를 dict 로 추적) +- 두 수의 합 +- 애너그램 그룹화 diff --git a/assignments/pykido/week3/data/patterns/heap.md b/assignments/pykido/week3/data/patterns/heap.md new file mode 100644 index 0000000..ff4eb08 --- /dev/null +++ b/assignments/pykido/week3/data/patterns/heap.md @@ -0,0 +1,55 @@ +# 힙 / 우선순위 큐 (Heap / Priority Queue) + +## 개념 +삽입과 최솟값(또는 최댓값) 추출을 모두 `O(log N)` 에 처리하는 완전 이진 트리 기반 자료구조다. 파이썬 `heapq` 는 최소 힙만 제공하므로 최대 힙이 필요하면 값에 음수를 취해 넣는다. 항상 "다음으로 처리할 가장 우선순위 높은 원소"를 빠르게 꺼내야 하는 상황에서 쓴다. + +## 언제 쓰나 +- 매 순간 최소/최대 원소를 꺼내야 할 때 (작업 스케줄링) +- 다익스트라 최단 경로의 우선순위 큐 +- 상위 K 개 원소 유지 (크기 K 힙) +- 여러 정렬된 리스트의 병합 + +## 시간 복잡도 +삽입/추출 각각 `O(log N)`, 최소값 확인은 `O(1)`. 리스트로부터 힙 생성(`heapify`)은 `O(N)`. + +## 기본 템플릿 +```python +import heapq + +def k_smallest(nums, k): + heap = [] + for x in nums: + heapq.heappush(heap, x) + return [heapq.heappop(heap) for _ in range(k)] +``` + +작업 스케줄링 골격: +```python +import heapq + +def schedule(jobs): + jobs.sort() + heap = [] + time = idx = total = 0 + while idx < len(jobs) or heap: + while idx < len(jobs) and jobs[idx][0] <= time: + heapq.heappush(heap, jobs[idx][1]) + idx += 1 + if heap: + duration = heapq.heappop(heap) + time += duration + total += time # 단순화한 형태 + else: + time = jobs[idx][0] + return total +``` + +## 흔한 실수 +- 최대 힙을 직접 구현하려다 실수한다. 음수 부호 트릭이 간단하다. +- 힙의 임의 위치 원소를 직접 수정하면 힙 불변식이 깨진다. 갱신이 필요하면 lazy deletion 을 쓴다. +- 튜플을 넣을 때 첫 원소가 동률이면 두 번째 원소로 비교가 넘어간다. 비교 불가능한 객체를 두 번째에 두면 오류가 난다. + +## 연관 문제 +- 디스크 컨트롤러 (요청을 도착 시간순으로 보며 작업 시간이 짧은 것을 힙에서 우선 처리) +- 다익스트라 +- 상위 K 빈도 원소 diff --git a/assignments/pykido/week3/data/patterns/prefix-sum.md b/assignments/pykido/week3/data/patterns/prefix-sum.md new file mode 100644 index 0000000..c27fbe3 --- /dev/null +++ b/assignments/pykido/week3/data/patterns/prefix-sum.md @@ -0,0 +1,49 @@ +# 누적 합 (Prefix Sum) + +## 개념 +배열의 앞에서부터 누적한 합을 미리 계산해 두면, 임의 구간 `[i, j]` 의 합을 `prefix[j+1] - prefix[i]` 로 `O(1)` 에 구할 수 있다. 구간 합을 여러 번 질의해야 하는 상황에서 매번 다시 더하는 `O(N)` 작업을 `O(1)` 로 줄인다. 2차원으로 확장하면 부분 직사각형 합도 `O(1)` 에 얻는다. + +## 언제 쓰나 +- 구간 합을 여러 번 질의할 때 +- "합이 K 인 부분 배열의 개수" (누적 합 + 해시맵) +- 2차원 격자에서 부분 직사각형 합 + +## 시간 복잡도 +누적 합 전처리 `O(N)`, 이후 각 구간 질의 `O(1)`. 2차원은 전처리 `O(M*N)`, 질의 `O(1)`. + +## 기본 템플릿 +```python +def build_prefix(nums): + prefix = [0] * (len(nums) + 1) + for i, x in enumerate(nums): + prefix[i + 1] = prefix[i] + x + return prefix + +def range_sum(prefix, i, j): + return prefix[j + 1] - prefix[i] +``` + +합이 K 인 부분 배열 개수: +```python +from collections import defaultdict + +def subarray_sum_k(nums, k): + count = total = 0 + seen = defaultdict(int) + seen[0] = 1 + for x in nums: + total += x + count += seen[total - k] + seen[total] += 1 + return count +``` + +## 흔한 실수 +- prefix 배열 크기를 N 으로 잡아 경계 인덱스에서 틀린다. 보통 `N+1` 로 두고 prefix[0]=0 으로 시작하면 깔끔하다. +- 구간 합 공식에서 `prefix[j] - prefix[i]` 처럼 off-by-one 을 낸다. +- 음수가 섞인 배열에서 "합이 K" 문제를 슬라이딩 윈도우로 풀려 한다. 음수가 있으면 누적 합 + 해시맵이 맞다. + +## 연관 문제 +- 합이 K 인 부분 배열의 개수 +- 구간 합 질의 +- 2차원 부분 행렬 합 diff --git a/assignments/pykido/week3/data/patterns/sliding-window.md b/assignments/pykido/week3/data/patterns/sliding-window.md new file mode 100644 index 0000000..fdfbedd --- /dev/null +++ b/assignments/pykido/week3/data/patterns/sliding-window.md @@ -0,0 +1,50 @@ +# 슬라이딩 윈도우 (Sliding Window) + +## 개념 +배열이나 문자열의 **연속 구간**을 두 포인터(left, right)로 표현하고, right 를 늘려 구간을 확장하거나 left 를 늘려 구간을 축소하면서 구간 통계를 유지하는 기법이다. 매번 구간을 새로 계산하지 않고 들어오고 나가는 원소만 갱신하므로 중첩 반복을 한 번의 선형 순회로 바꾼다. + +## 언제 쓰나 +- "길이 K 인 연속 구간의 합/최댓값" 같은 고정 크기 윈도우 +- "조건을 만족하는 가장 긴/짧은 연속 구간" 같은 가변 크기 윈도우 +- 부분 문자열 문제에서 문자 빈도를 해시맵으로 추적할 때 + +## 시간 복잡도 +left 와 right 가 각각 배열을 한 번씩만 지나가므로 전체 `O(N)`. 윈도우 내부 통계를 `O(1)` 로 갱신하는 것이 관건이다. + +## 기본 템플릿 +```python +def longest_unique_substring(s): + seen = {} + left = best = 0 + for right, c in enumerate(s): + if c in seen and seen[c] >= left: + left = seen[c] + 1 + seen[c] = right + best = max(best, right - left + 1) + return best +``` + +가변 윈도우 축소 형태: +```python +def shortest_subarray_at_least(nums, target): + left = total = 0 + best = float("inf") + for right, x in enumerate(nums): + total += x + while total >= target: + best = min(best, right - left + 1) + total -= nums[left] + left += 1 + return best if best != float("inf") else 0 +``` + +## 흔한 실수 +- left 갱신을 빠뜨려 윈도우가 좁혀지지 않는다. +- 윈도우에서 빠지는 원소의 통계를 반영하지 않아 해시맵이 누수된다. +- 가변 윈도우에서 축소 조건을 `if` 로 써서 한 칸만 줄인다. 최소 구간을 찾으려면 `while` 로 끝까지 좁혀야 한다. +- 매 step 마다 `sum()` 을 다시 호출해 `O(N^2)` 로 만든다. + +## 연관 문제 +- 보석 쇼핑 (모든 종류를 포함하는 최소 구간, 가변 윈도우 + 해시맵) +- 고정 길이 부분합의 최댓값 +- 중복 없는 가장 긴 부분 문자열 diff --git a/assignments/pykido/week3/data/patterns/two-pointers.md b/assignments/pykido/week3/data/patterns/two-pointers.md new file mode 100644 index 0000000..f13c499 --- /dev/null +++ b/assignments/pykido/week3/data/patterns/two-pointers.md @@ -0,0 +1,37 @@ +# 투 포인터 (Two Pointers) + +## 개념 +두 개의 인덱스를 이동시키며 배열을 한 번의 순회로 처리하는 기법이다. 대표적으로 정렬된 배열의 양 끝에서 안쪽으로 좁혀가는 **대향 포인터**와, 같은 방향으로 서로 다른 속도로 이동하는 **동방향 포인터**가 있다. 슬라이딩 윈도우는 동방향 투 포인터의 특수한 형태로 볼 수 있다. + +## 언제 쓰나 +- 정렬된 배열에서 합이 특정 값이 되는 쌍을 찾을 때 +- 두 개의 정렬된 리스트를 병합하거나 교집합을 구할 때 +- 양쪽 끝에서 좁혀가며 최적 구간을 찾는 문제 + +## 시간 복잡도 +각 포인터가 배열을 한 번씩만 지나가므로 `O(N)`. 단, 입력이 정렬되어 있어야 하는 경우 정렬 비용 `O(N log N)` 이 추가된다. + +## 기본 템플릿 +```python +def two_sum_sorted(arr, target): + lo, hi = 0, len(arr) - 1 + while lo < hi: + s = arr[lo] + arr[hi] + if s == target: + return (lo, hi) + elif s < target: + lo += 1 + else: + hi -= 1 + return None +``` + +## 흔한 실수 +- 정렬되지 않은 배열에 대향 포인터를 적용한다. 단조성이 깨져 오답이 나온다. +- 포인터 이동 조건을 반대로 작성해 무한 루프에 빠진다. +- `lo < hi` 와 `lo <= hi` 를 혼동해 같은 원소를 두 번 쓰거나 누락한다. + +## 연관 문제 +- 두 큐 합 같게 만들기 (두 큐를 한 덱으로 합치고 양쪽 포인터처럼 pop/push) +- 정렬된 두 배열의 병합 +- 세 수의 합 (한 원소를 고정하고 나머지를 투 포인터로) diff --git a/assignments/pykido/week3/data/patterns/union-find.md b/assignments/pykido/week3/data/patterns/union-find.md new file mode 100644 index 0000000..79525f1 --- /dev/null +++ b/assignments/pykido/week3/data/patterns/union-find.md @@ -0,0 +1,46 @@ +# 유니온 파인드 (Union-Find / Disjoint Set) + +## 개념 +원소들을 서로소 집합으로 관리하며 "두 원소가 같은 집합에 속하는가"(find)와 "두 집합을 합치기"(union)를 거의 상수 시간에 처리하는 자료구조다. 각 원소가 자기 집합의 대표(루트)를 가리키는 트리로 표현하고, **경로 압축**과 **랭크/크기 기반 합치기** 두 최적화를 함께 적용한다. + +## 언제 쓰나 +- 그래프의 연결 요소 개수 세기 +- 사이클 존재 여부 판정 +- 크루스칼 최소 신장 트리에서 간선 추가 시 사이클 검사 + +## 시간 복잡도 +경로 압축 + union by rank 를 적용하면 연산당 거의 상수, 정확히는 역 애커만 함수 `O(α(N))`. 사실상 `O(1)` 로 봐도 된다. + +## 기본 템플릿 +```python +class DisjointSet: + def __init__(self, n): + self.parent = list(range(n)) + self.rank = [0] * n + + def find(self, x): + if self.parent[x] != x: + self.parent[x] = self.find(self.parent[x]) + return self.parent[x] + + def union(self, a, b): + ra, rb = self.find(a), self.find(b) + if ra == rb: + return False + if self.rank[ra] < self.rank[rb]: + ra, rb = rb, ra + self.parent[rb] = ra + if self.rank[ra] == self.rank[rb]: + self.rank[ra] += 1 + return True +``` + +## 흔한 실수 +- 경로 압축을 빼먹어 트리가 한쪽으로 길어지고 find 가 느려진다. +- union 전에 양쪽의 루트를 비교하지 않고 `parent[a]=b` 처럼 직접 연결해 잘못된 집합을 만든다. +- 사이클 검사에서 이미 같은 루트인 경우(union 이 False) 처리를 빠뜨린다. + +## 연관 문제 +- 섬 연결하기 (크루스칼에서 사이클 방지용으로 union-find 사용) +- 친구 관계 그룹 수 +- 그래프 연결성 판정 diff --git a/assignments/pykido/week3/data/problems/pgs-118667-two-queues.md b/assignments/pykido/week3/data/problems/pgs-118667-two-queues.md new file mode 100644 index 0000000..bc925f2 --- /dev/null +++ b/assignments/pykido/week3/data/problems/pgs-118667-two-queues.md @@ -0,0 +1,51 @@ +# 두 큐 합 같게 만들기 (Programmers Lv.2, pgs-118667) + +- 플랫폼: Programmers +- 레벨: Lv.2 +- 토픽: two-pointers, queue +- 링크: https://school.programmers.co.kr/learn/courses/30/lessons/118667 + +## 문제 요약 +길이가 같은 두 큐 `queue1`, `queue2` 가 주어진다. 한쪽에서 원소를 빼 다른 쪽에 넣는 작업을 반복해 두 큐의 합을 같게 만드는 **최소 작업 횟수**를 구한다. 불가능하면 -1. + +## 접근 +두 큐를 덱으로 두고, 한 큐의 합이 더 크면 그 큐의 앞 원소를 빼서 다른 큐의 뒤에 넣으며 두 합을 맞춰 간다(투 포인터처럼 한 방향으로만 옮긴다). 합이 같아지면 종료한다. 전체 합이 홀수면 절대 같아질 수 없으므로 -1. 합 갱신을 `O(1)` 로 하는 것이 핵심이다. + +## 복잡도 +`O(N)`. 작업 횟수 상한이 `4 * N` 이내라 그 안에 못 맞추면 -1 로 판정한다. + +## 핵심 체크포인트 +- 전체 합이 홀수면 즉시 -1. +- 두 큐의 합을 매번 재계산하지 말고, 옮기는 원소만큼 `O(1)` 로 갱신한다. +- 작업 횟수가 `4 * N` 을 넘으면 -1. + +## 흔한 실수 +- 매 스텝마다 `sum()` 을 호출해 `O(N^2)` 로 만들어 TLE. +- `list.pop(0)` 으로 앞에서 빼서 `O(N)` 비용 → 전체 `O(N^2)`. `collections.deque` 를 쓴다. +- 종료 상한 조건을 빼먹어 같아지지 않는 입력에서 무한 루프. + +## 핵심 코드 +```python +from collections import deque + +def solution(queue1, queue2): + q1, q2 = deque(queue1), deque(queue2) + s1, s2 = sum(q1), sum(q2) + if (s1 + s2) % 2: + return -1 + + for count in range(4 * len(queue1)): + if s1 == s2: + return count + if s1 > s2: + x = q1.popleft() + s1 -= x + s2 += x + q2.append(x) + else: + x = q2.popleft() + s2 -= x + s1 += x + q1.append(x) + return -1 +``` diff --git a/assignments/pykido/week3/data/problems/pgs-42627-disk-controller.md b/assignments/pykido/week3/data/problems/pgs-42627-disk-controller.md new file mode 100644 index 0000000..87ad0fa --- /dev/null +++ b/assignments/pykido/week3/data/problems/pgs-42627-disk-controller.md @@ -0,0 +1,47 @@ +# 디스크 컨트롤러 (Programmers Lv.3, pgs-42627) + +- 플랫폼: Programmers +- 레벨: Lv.3 +- 토픽: heap, greedy +- 링크: https://school.programmers.co.kr/learn/courses/30/lessons/42627 + +## 문제 요약 +각 작업이 `[요청 시각, 소요 시간]` 으로 주어진다. 한 번에 하나의 작업만 처리할 수 있고(비선점은 아님, 매 시점 재선택 가능), 각 작업의 **반환 시간(완료 시각 − 요청 시각)의 평균**을 최소화하도록 처리 순서를 정한다. 정수 부분만 반환한다. SJF(Shortest Job First) 스케줄링 문제다. + +## 접근 +요청 시각 기준으로 작업을 정렬해 두고, 현재 시각까지 도착한 작업들을 최소 힙(소요 시간 기준)에 넣는다. 매 처리 시점에 힙에서 **소요 시간이 가장 짧은** 작업을 꺼내 실행한다. 힙이 비어 있으면 다음 작업의 요청 시각으로 시간을 점프한다. 짧은 작업을 먼저 처리할수록 뒤에 밀리는 작업들의 대기 시간 합이 줄어든다(그리디). + +## 복잡도 +`O(N log N)`. 정렬과 힙 연산이 지배한다. + +## 핵심 체크포인트 +- 도착한(요청 시각 <= 현재 시각) 작업만 힙에 넣는다. +- 힙이 비면 일을 만들지 말고 다음 작업 요청 시각으로 시간을 이동한다. +- 반환 시간은 `완료 시각 − 요청 시각` 이고, 마지막에 작업 수로 나눠 정수화한다. + +## 흔한 실수 +- 단순히 요청 시각 순서대로만 처리해(FCFS) 평균을 최소화하지 못한다. 핵심은 소요 시간 기준 힙이다. +- 힙이 비었을 때 시간 점프를 빠뜨려 인덱스/시각이 어긋난다. +- 도착하지 않은 작업까지 힙에 넣어 미래 작업을 미리 실행한다. + +## 핵심 코드 +```python +import heapq + +def solution(jobs): + jobs.sort() + n = len(jobs) + time = idx = total = 0 + heap = [] + while idx < n or heap: + while idx < n and jobs[idx][0] <= time: + heapq.heappush(heap, (jobs[idx][1], jobs[idx][0])) + idx += 1 + if heap: + duration, request = heapq.heappop(heap) + time += duration + total += time - request + else: + time = jobs[idx][0] + return total // n +``` diff --git a/assignments/pykido/week3/data/problems/pgs-42861-connecting-islands.md b/assignments/pykido/week3/data/problems/pgs-42861-connecting-islands.md new file mode 100644 index 0000000..c7a08d2 --- /dev/null +++ b/assignments/pykido/week3/data/problems/pgs-42861-connecting-islands.md @@ -0,0 +1,49 @@ +# 섬 연결하기 (Programmers Lv.3, pgs-42861) + +- 플랫폼: Programmers +- 레벨: Lv.3 +- 토픽: greedy, union-find +- 링크: https://school.programmers.co.kr/learn/courses/30/lessons/42861 + +## 문제 요약 +n 개의 섬과 다리 후보들이 `costs = [[섬A, 섬B, 비용], ...]` 로 주어진다. 모든 섬을 연결하는 데 드는 **최소 비용**을 구한다. 전형적인 최소 신장 트리(MST) 문제다. + +## 접근 +크루스칼 알고리즘. 모든 간선을 비용 오름차순으로 정렬한 뒤, 가장 싼 간선부터 차례로 본다. 두 섬이 이미 같은 집합(연결됨)이면 추가 시 사이클이 생기므로 건너뛰고, 다른 집합이면 다리를 놓고 두 집합을 합친다(union-find). 간선을 `n-1` 개 채우면 종료한다. "지금 가장 싼 것을 고른다"는 그리디 선택이 MST 에서 최적임이 증명되어 있다. + +## 복잡도 +`O(E log E)`. 간선 정렬이 지배하고, union-find 연산은 거의 상수다. + +## 핵심 체크포인트 +- 간선을 비용 기준 오름차순 정렬한다. +- union-find 로 사이클을 검사해 같은 집합이면 건너뛴다. +- 선택한 간선이 `n-1` 개가 되면 모든 섬이 연결된 것이므로 멈춰도 된다. + +## 흔한 실수 +- 사이클 검사를 빼먹고 싼 간선을 무조건 더해 비용이 과다해진다. +- 프림과 크루스칼을 섞어 구현하다 우선순위 큐와 union-find 를 둘 다 어설프게 쓴다. +- union-find 에서 경로 압축을 빼 성능이 떨어진다. + +## 핵심 코드 +```python +def solution(n, costs): + parent = list(range(n)) + + def find(x): + while parent[x] != x: + parent[x] = parent[parent[x]] + x = parent[x] + return x + + total = edges = 0 + for a, b, cost in sorted(costs, key=lambda c: c[2]): + ra, rb = find(a), find(b) + if ra == rb: + continue + parent[ra] = rb + total += cost + edges += 1 + if edges == n - 1: + break + return total +``` diff --git a/assignments/pykido/week3/data/problems/pgs-42898-school-path.md b/assignments/pykido/week3/data/problems/pgs-42898-school-path.md new file mode 100644 index 0000000..a736e6a --- /dev/null +++ b/assignments/pykido/week3/data/problems/pgs-42898-school-path.md @@ -0,0 +1,43 @@ +# 등굣길 (Programmers Lv.3, pgs-42898) + +- 플랫폼: Programmers +- 레벨: Lv.3 +- 토픽: dp +- 링크: https://school.programmers.co.kr/learn/courses/30/lessons/42898 + +## 문제 요약 +m x n 격자에서 (1,1) 에서 출발해 (m,n) 까지 **오른쪽 또는 아래로만** 이동한다. 물에 잠긴 칸 `puddles` 는 지날 수 없다. 가능한 경로의 수를 `1,000,000,007` 로 나눈 나머지로 구한다. + +## 접근 +격자 DP. `dp[r][c]` 를 (1,1) 에서 (r,c) 까지의 경로 수로 정의한다. 각 칸으로 오는 방법은 위에서 내려오거나 왼쪽에서 오는 두 가지뿐이므로 `dp[r][c] = dp[r-1][c] + dp[r][c-1]`. 물웅덩이는 0 으로 고정한다. + +## 복잡도 +모든 칸을 한 번씩 채우므로 `O(m * n)`. + +## 핵심 체크포인트 +- 인덱싱을 1-based 로 통일하면 경계 처리가 간단하다 (`dp` 크기를 `(m+1) x (n+1)`). +- 물웅덩이를 갱신하기 전에 0 으로 막는다. +- 매 갱신마다 `% 1_000_000_007` 을 적용한다. + +## 흔한 실수 +- DFS 재귀로 풀면서 메모이제이션을 안 해 경로 수가 지수적으로 폭발한다. +- 모듈러 연산을 마지막에 한 번만 해서 중간 오버플로(다른 언어)나 누적 오류가 난다. +- 시작 칸 `dp[1][1]` 초기화를 빠뜨린다. + +## 핵심 코드 +```python +def solution(m, n, puddles): + MOD = 1_000_000_007 + blocked = {(c, r) for c, r in puddles} + dp = [[0] * (m + 1) for _ in range(n + 1)] + dp[1][1] = 1 + for r in range(1, n + 1): + for c in range(1, m + 1): + if (c, r) == (1, 1): + continue + if (c, r) in blocked: + dp[r][c] = 0 + continue + dp[r][c] = (dp[r - 1][c] + dp[r][c - 1]) % MOD + return dp[n][m] +``` diff --git a/assignments/pykido/week3/data/problems/pgs-43165-target-number.md b/assignments/pykido/week3/data/problems/pgs-43165-target-number.md new file mode 100644 index 0000000..2930d0d --- /dev/null +++ b/assignments/pykido/week3/data/problems/pgs-43165-target-number.md @@ -0,0 +1,36 @@ +# 타겟 넘버 (Programmers Lv.2, pgs-43165) + +- 플랫폼: Programmers +- 레벨: Lv.2 +- 토픽: bfs-dfs, backtracking +- 링크: https://school.programmers.co.kr/learn/courses/30/lessons/43165 + +## 문제 요약 +음이 아닌 정수 배열 `numbers` 의 각 원소 앞에 `+` 또는 `-` 를 붙여 순서대로 더한다. 그 결과가 `target` 이 되는 경우의 수를 구한다. DFS/백트래킹 입문에 적합한 문제다. + +## 접근 +각 인덱스 i 에서 현재 누적합에 `+numbers[i]` 와 `-numbers[i]` 두 갈래로 분기하는 DFS 를 돈다. 인덱스가 배열 끝(`i == len(numbers)`)에 도달하면 누적합이 target 인지 검사해 경우의 수를 센다. 상태가 `(i, sum)` 으로 정의되므로 메모이제이션 DP 로도 풀 수 있다. + +## 복잡도 +완전 탐색은 `O(2^N)`. `(i, sum)` 상태로 메모이제이션하면 상태 수에 비례하도록 줄일 수 있다. + +## 핵심 체크포인트 +- base case 를 명확히: `i == len(numbers)` 일 때 누적합 검사. +- `+` 와 `-` 두 분기를 모두 재귀 호출한다. +- 누적합은 인자로 전달해 분기마다 독립적으로 유지한다. + +## 흔한 실수 +- DFS 로 충분한데 BFS 로 모든 부분합을 큐에 쌓아 메모리가 폭발한다. +- 누적합을 리스트 append/pop 의 부수효과로 다뤄 분기 간 상태가 오염된다. +- base case 에서 인덱스 경계를 잘못 잡아 한 칸 더 들어가거나 덜 들어간다. + +## 핵심 코드 +```python +def solution(numbers, target): + def dfs(i, total): + if i == len(numbers): + return 1 if total == target else 0 + return dfs(i + 1, total + numbers[i]) + dfs(i + 1, total - numbers[i]) + + return dfs(0, 0) +``` diff --git a/assignments/pykido/week3/data/problems/pgs-43236-stepping-stones.md b/assignments/pykido/week3/data/problems/pgs-43236-stepping-stones.md new file mode 100644 index 0000000..1081399 --- /dev/null +++ b/assignments/pykido/week3/data/problems/pgs-43236-stepping-stones.md @@ -0,0 +1,49 @@ +# 징검다리 (Programmers Lv.4, pgs-43236) + +- 플랫폼: Programmers +- 레벨: Lv.4 +- 토픽: binary-search +- 링크: https://school.programmers.co.kr/learn/courses/30/lessons/43236 + +## 문제 요약 +시작점과 도착점 사이에 바위들의 좌표 `rocks` 가 주어진다. 바위를 최대 n 개 제거할 수 있을 때, 인접한 지점 사이 거리 중 **가장 짧은 거리의 최댓값**을 구한다. + +## 접근 +"최소 점프 거리"가 d 이상이 되도록 만들 수 있는지를 묻는 결정 문제로 바꾼다. 목표 거리 d 가 주어지면, 정렬된 바위를 앞에서부터 보며 직전 유지 지점과의 간격이 d 미만이면 그 바위를 제거한다. 제거 횟수가 n 이하면 d 는 달성 가능하다. d 가 커질수록 제거가 더 많이 필요하므로 단조성이 성립한다. 달성 가능한 **최대 d** 를 이분 탐색한다. + +## 복잡도 +`O(R log(distance))`, R = len(rocks). 결정 함수가 `O(R)`, 탐색 범위가 전체 거리다. + +## 핵심 체크포인트 +- 바위를 정렬하고 시작점(0)과 도착점(distance)을 경계로 함께 고려한다. +- 제거 시뮬레이션은 그리디로, 마지막으로 유지한 위치만 추적한다. +- 최댓값을 찾는 upper bound 형태이므로 `feasible(mid)` 이면 `lo = mid`, 종료 시 lo 처리에 주의한다. + +## 흔한 실수 +- 인접 거리 배열을 정렬하는 잘못된 접근. 정렬하면 위치 정보가 사라져 단조성이 깨진다. +- d 의 탐색 범위를 좌표값이 아니라 인덱스로 잡는다. +- 시작점/도착점을 빼먹어 첫/마지막 간격을 놓친다. + +## 핵심 코드 +```python +def solution(distance, rocks, n): + rocks = sorted(rocks) + [distance] + + def removable(gap): + removed = prev = 0 + for r in rocks: + if r - prev < gap: + removed += 1 + else: + prev = r + return removed <= n + + lo, hi = 1, distance + while lo < hi: + mid = (lo + hi + 1) // 2 + if removable(mid): + lo = mid + else: + hi = mid - 1 + return lo +``` diff --git a/assignments/pykido/week3/data/problems/pgs-43238-immigration.md b/assignments/pykido/week3/data/problems/pgs-43238-immigration.md new file mode 100644 index 0000000..9fc2404 --- /dev/null +++ b/assignments/pykido/week3/data/problems/pgs-43238-immigration.md @@ -0,0 +1,38 @@ +# 입국심사 (Programmers Lv.3, pgs-43238) + +- 플랫폼: Programmers +- 레벨: Lv.3 +- 토픽: binary-search +- 링크: https://school.programmers.co.kr/learn/courses/30/lessons/43238 + +## 문제 요약 +n 명이 입국 심사를 받아야 하고, 각 심사대의 1명당 처리 시간이 배열 `times` 로 주어진다. 모든 사람이 심사를 마치는 데 걸리는 최소 시간을 구한다. n 이 최대 10억 규모라 선형 접근은 불가능하다. + +## 접근 +시간 t 가 주어지면 그 안에 처리 가능한 인원은 `f(t) = sum(t // x for x in times)` 이다. t 가 커질수록 처리 인원도 단조 증가하므로, `f(t) >= n` 을 만족하는 **최소 t** 를 이분 탐색한다. 답 자체를 탐색하는 parametric search 의 전형이다. + +## 복잡도 +`O(M log(max(times) * n))`, M = len(times). 결정 함수 `f(t)` 가 `O(M)`, 탐색 범위가 `max(times) * n` 이다. + +## 핵심 체크포인트 +- 탐색 범위는 `lo = 1`, `hi = max(times) * n` 으로 충분히 크게 잡는다. +- 단조성을 명시한다: 시간이 늘면 처리 인원도 늘어난다. +- `f(mid) >= n` 이면 답 후보를 줄이는 방향(`hi = mid`)으로 lower bound 를 찾는다. + +## 흔한 실수 +- `lo = 0` 으로 시작하면 `0 // x == 0` 이라 무한 루프나 잘못된 경계가 생긴다. +- `hi = max(times)` 로만 두면 사람이 많을 때 답을 못 찾는다. 범위에 n 을 곱해야 한다. +- 선형 탐색은 n 이 10억 스케일이라 시간 초과(TLE). + +## 핵심 코드 +```python +def solution(n, times): + lo, hi = 1, max(times) * n + while lo < hi: + mid = (lo + hi) // 2 + if sum(mid // x for x in times) >= n: + hi = mid + else: + lo = mid + 1 + return lo +``` diff --git a/assignments/pykido/week3/data/problems/pgs-67258-gem-shopping.md b/assignments/pykido/week3/data/problems/pgs-67258-gem-shopping.md new file mode 100644 index 0000000..445acdf --- /dev/null +++ b/assignments/pykido/week3/data/problems/pgs-67258-gem-shopping.md @@ -0,0 +1,44 @@ +# 보석 쇼핑 (Programmers Lv.3, pgs-67258) + +- 플랫폼: Programmers +- 레벨: Lv.3 +- 토픽: sliding-window, hash-map +- 링크: https://school.programmers.co.kr/learn/courses/30/lessons/67258 + +## 문제 요약 +진열대에 보석들이 일렬로 놓여 있고 각 칸의 보석 종류가 `gems` 로 주어진다. **모든 종류의 보석을 하나 이상 포함하는 가장 짧은 연속 구간** [start, end] 를 1-index 로 구한다. 답이 여러 개면 start 가 작은 것. + +## 접근 +가변 슬라이딩 윈도우 + 해시맵. right 를 늘리며 윈도우 안 보석 종류별 개수를 dict 로 센다. 윈도우가 전체 종류를 모두 포함하면, left 를 늘려 윈도우를 최대한 좁히며 최소 길이를 갱신한다. left 를 옮길 때 카운트가 0 이 된 종류는 dict 에서 삭제한다. + +## 복잡도 +`O(N)`. left, right 가 각각 배열을 한 번씩만 지난다. + +## 핵심 체크포인트 +- 전체 보석 종류 수를 미리 `set` 으로 계산해 둔다. +- left 이동 시 카운트가 0 이 되면 키를 삭제해야 "모든 종류 포함" 판정이 정확하다. +- 답은 1-index 이므로 `[left + 1, right + 1]` 로 반환한다. + +## 흔한 실수 +- left 축소를 `if` 로 한 칸만 줄여 최소 윈도우를 놓친다. `while` 로 끝까지 좁혀야 한다. +- 종류 개수 비교를 dict 길이 대신 잘못된 변수로 한다. +- 동일 최소 길이일 때 start 가 더 작은 답으로 갱신해버린다. 더 짧을 때만 갱신한다. + +## 핵심 코드 +```python +def solution(gems): + kinds = len(set(gems)) + window = {} + left = 0 + best = (0, len(gems) - 1) + for right, g in enumerate(gems): + window[g] = window.get(g, 0) + 1 + while len(window) == kinds: + if right - left < best[1] - best[0]: + best = (left, right) + window[gems[left]] -= 1 + if window[gems[left]] == 0: + del window[gems[left]] + left += 1 + return [best[0] + 1, best[1] + 1] +``` From 223c93a8f360ad8c644e0d135377a92fa824f38b Mon Sep 17 00:00:00 2001 From: pykido Date: Sat, 6 Jun 2026 13:35:17 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat=20:=20RAG=20=ED=8C=8C=EC=9D=B4?= =?UTF-8?q?=ED=94=84=EB=9D=BC=EC=9D=B8=20=EA=B5=AC=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assignments/pykido/week3/rag/__init__.py | 7 +++ assignments/pykido/week3/rag/indexing.py | 31 ++++++++++++ assignments/pykido/week3/rag/loader.py | 33 +++++++++++++ assignments/pykido/week3/rag/retriever.py | 21 ++++++++ assignments/pykido/week3/rag/splitter.py | 53 +++++++++++++++++++++ assignments/pykido/week3/rag/vectorstore.py | 36 ++++++++++++++ 6 files changed, 181 insertions(+) create mode 100644 assignments/pykido/week3/rag/__init__.py create mode 100644 assignments/pykido/week3/rag/indexing.py create mode 100644 assignments/pykido/week3/rag/loader.py create mode 100644 assignments/pykido/week3/rag/retriever.py create mode 100644 assignments/pykido/week3/rag/splitter.py create mode 100644 assignments/pykido/week3/rag/vectorstore.py diff --git a/assignments/pykido/week3/rag/__init__.py b/assignments/pykido/week3/rag/__init__.py new file mode 100644 index 0000000..49f00d1 --- /dev/null +++ b/assignments/pykido/week3/rag/__init__.py @@ -0,0 +1,7 @@ +from pathlib import Path + +WEEK3_DIR = Path(__file__).resolve().parent.parent +DATA_DIR = WEEK3_DIR / "data" +INDEX_DIR = WEEK3_DIR / ".cache" + +STRATEGIES = ("recursive", "markdown") diff --git a/assignments/pykido/week3/rag/indexing.py b/assignments/pykido/week3/rag/indexing.py new file mode 100644 index 0000000..5b19874 --- /dev/null +++ b/assignments/pykido/week3/rag/indexing.py @@ -0,0 +1,31 @@ +import os + +from dotenv import find_dotenv, load_dotenv + +from . import STRATEGIES +from .loader import load_documents + +load_dotenv(find_dotenv(), override=True) +os.environ["LANGSMITH_TRACING"] = "false" +from .splitter import split_documents +from .vectorstore import build_vectorstore, save_vectorstore + + +def build_index(strategy: str) -> int: + docs = load_documents() + chunks = split_documents(docs, strategy) + store = build_vectorstore(chunks) + save_vectorstore(store, strategy) + return len(chunks) + + +def build_all() -> dict[str, int]: + counts = {} + for strategy in STRATEGIES: + counts[strategy] = build_index(strategy) + print(f"[{strategy}] indexed {counts[strategy]} chunks") + return counts + + +if __name__ == "__main__": + build_all() diff --git a/assignments/pykido/week3/rag/loader.py b/assignments/pykido/week3/rag/loader.py new file mode 100644 index 0000000..7c387dd --- /dev/null +++ b/assignments/pykido/week3/rag/loader.py @@ -0,0 +1,33 @@ +from pathlib import Path + +from langchain_community.document_loaders import TextLoader +from langchain_core.documents import Document + +from . import DATA_DIR + + +def load_documents(data_dir: Path = DATA_DIR) -> list[Document]: + docs: list[Document] = [] + for path in sorted(data_dir.rglob("*.md")): + loaded = TextLoader(str(path), encoding="utf-8").load() + category = path.parent.name + for doc in loaded: + doc.metadata.update( + { + "source": str(path.relative_to(data_dir)), + "filename": path.name, + "stem": path.stem, + "category": category, + } + ) + docs.append(doc) + return docs + + +if __name__ == "__main__": + documents = load_documents() + print(f"loaded {len(documents)} documents") + for doc in documents[:3]: + print("-" * 60) + print(doc.metadata) + print(doc.page_content[:200]) diff --git a/assignments/pykido/week3/rag/retriever.py b/assignments/pykido/week3/rag/retriever.py new file mode 100644 index 0000000..acb70da --- /dev/null +++ b/assignments/pykido/week3/rag/retriever.py @@ -0,0 +1,21 @@ +from functools import lru_cache + +from langchain_core.documents import Document + +from .vectorstore import load_vectorstore + + +@lru_cache(maxsize=None) +def _store(strategy: str): + return load_vectorstore(strategy) + + +def retrieve(query: str, strategy: str = "markdown", k: int = 4) -> list[Document]: + return _store(strategy).as_retriever(search_kwargs={"k": k}).invoke(query) + + +if __name__ == "__main__": + for strategy in ("recursive", "markdown"): + print(f"\n[{strategy}]") + for doc in retrieve("이분 탐색으로 답을 찾는 최적화 문제", strategy=strategy, k=3): + print(f"- {doc.metadata['chunk_id']} ({doc.metadata['source']})") diff --git a/assignments/pykido/week3/rag/splitter.py b/assignments/pykido/week3/rag/splitter.py new file mode 100644 index 0000000..7ed96e7 --- /dev/null +++ b/assignments/pykido/week3/rag/splitter.py @@ -0,0 +1,53 @@ +from langchain_core.documents import Document +from langchain_text_splitters import ( + MarkdownHeaderTextSplitter, + RecursiveCharacterTextSplitter, +) + +HEADERS = [("#", "h1"), ("##", "h2"), ("###", "h3")] + + +def split_recursive( + docs: list[Document], + chunk_size: int = 700, + chunk_overlap: int = 100, +) -> list[Document]: + splitter = RecursiveCharacterTextSplitter( + chunk_size=chunk_size, + chunk_overlap=chunk_overlap, + separators=["\n\n", "\n", " ", ""], + ) + chunks = splitter.split_documents(docs) + for i, chunk in enumerate(chunks): + chunk.metadata["chunk_strategy"] = "recursive" + chunk.metadata["chunk_id"] = f"{chunk.metadata.get('stem', 'doc')}-rec-{i}" + return chunks + + +def split_markdown(docs: list[Document]) -> list[Document]: + splitter = MarkdownHeaderTextSplitter(headers_to_split_on=HEADERS, strip_headers=False) + chunks: list[Document] = [] + for doc in docs: + for i, section in enumerate(splitter.split_text(doc.page_content)): + section.metadata = {**doc.metadata, **section.metadata} + section.metadata["chunk_strategy"] = "markdown" + section.metadata["chunk_id"] = f"{doc.metadata.get('stem', 'doc')}-md-{i}" + chunks.append(section) + return chunks + + +def split_documents(docs: list[Document], strategy: str) -> list[Document]: + if strategy == "recursive": + return split_recursive(docs) + if strategy == "markdown": + return split_markdown(docs) + raise ValueError(f"unknown strategy: {strategy}") + + +if __name__ == "__main__": + from .loader import load_documents + + docs = load_documents() + for strategy in ("recursive", "markdown"): + chunks = split_documents(docs, strategy) + print(f"[{strategy}] {len(docs)} docs -> {len(chunks)} chunks") diff --git a/assignments/pykido/week3/rag/vectorstore.py b/assignments/pykido/week3/rag/vectorstore.py new file mode 100644 index 0000000..fedea4b --- /dev/null +++ b/assignments/pykido/week3/rag/vectorstore.py @@ -0,0 +1,36 @@ +import shutil + +from langchain_community.vectorstores import FAISS +from langchain_core.documents import Document +from langchain_openai import OpenAIEmbeddings + +from . import INDEX_DIR + +EMBEDDING_MODEL = "text-embedding-3-small" + + +def get_embeddings() -> OpenAIEmbeddings: + return OpenAIEmbeddings(model=EMBEDDING_MODEL) + + +def build_vectorstore(chunks: list[Document]) -> FAISS: + return FAISS.from_documents(chunks, get_embeddings()) + + +def save_vectorstore(store: FAISS, strategy: str) -> None: + path = INDEX_DIR / strategy + if path.exists(): + shutil.rmtree(path) + path.mkdir(parents=True, exist_ok=True) + store.save_local(str(path)) + + +def load_vectorstore(strategy: str) -> FAISS: + path = INDEX_DIR / strategy + if not path.exists(): + raise FileNotFoundError(f"index not found: {path}. run indexing.py first.") + return FAISS.load_local( + str(path), + get_embeddings(), + allow_dangerous_deserialization=True, + ) From bfa3405ac740ac598198eff73d0513abc234ea44 Mon Sep 17 00:00:00 2001 From: pykido Date: Sat, 6 Jun 2026 13:35:36 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat=20:=202-step=20RAG=20Graph=20=EA=B5=AC?= =?UTF-8?q?=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assignments/pykido/week3/graph.py | 86 ++++++++++++++++++++++++++++++ assignments/pykido/week3/schema.py | 12 +++++ assignments/pykido/week3/state.py | 11 ++++ 3 files changed, 109 insertions(+) create mode 100644 assignments/pykido/week3/graph.py create mode 100644 assignments/pykido/week3/schema.py create mode 100644 assignments/pykido/week3/state.py diff --git a/assignments/pykido/week3/graph.py b/assignments/pykido/week3/graph.py new file mode 100644 index 0000000..0f840be --- /dev/null +++ b/assignments/pykido/week3/graph.py @@ -0,0 +1,86 @@ +import os + +from dotenv import find_dotenv, load_dotenv +from langchain_core.documents import Document +from langchain_core.messages import HumanMessage, SystemMessage +from langchain_openai import ChatOpenAI +from langgraph.graph import END, START, StateGraph + +from rag.retriever import retrieve +from schema import RAGAnswer +from state import RAGState + +load_dotenv(find_dotenv(), override=True) +os.environ["LANGSMITH_TRACING"] = "false" +os.environ["LANGSMITH_TRACING_V2"] = "false" + +SYSTEM_PROMPT = """당신은 알고리즘 코딩 테스트를 돕는 학습 코치입니다. +아래 검색된 문서(context)만 근거로 한국어 마크다운으로 답하세요. + +원칙: +1. context 에 있는 내용만 사용하고, 없으면 모른다고 말할 것 +2. 패턴 이름·복잡도·코드는 context 에 적힌 표현을 따를 것 +3. 답변에 어떤 패턴/문제 근거를 썼는지 자연스럽게 녹일 것 +4. 패턴 키와 코드 식별자는 영어 원형 유지 +""" + +DEFAULT_STRATEGY = "markdown" +TOP_K = 4 + +model = ChatOpenAI(model="gpt-4o-mini", temperature=0) +structured_model = model.with_structured_output(RAGAnswer) + + +def format_docs(docs: list[Document]) -> str: + blocks = [] + for i, doc in enumerate(docs, start=1): + blocks.append( + f"[문서 {i}] source={doc.metadata.get('source')} " + f"chunk={doc.metadata.get('chunk_id')}\n{doc.page_content}" + ) + return "\n\n".join(blocks) + + +def collect_sources(docs: list[Document]) -> list[str]: + seen, sources = set(), [] + for doc in docs: + source = doc.metadata.get("source") + if source and source not in seen: + seen.add(source) + sources.append(source) + return sources + + +def retrieve_node(state: RAGState) -> dict: + strategy = state.get("strategy") or DEFAULT_STRATEGY + docs = retrieve(state["question"], strategy=strategy, k=TOP_K) + return {"strategy": strategy, "documents": docs, "context": format_docs(docs)} + + +def generate_node(state: RAGState) -> dict: + messages = [ + SystemMessage(content=SYSTEM_PROMPT), + HumanMessage(content=f"질문: {state['question']}\n\ncontext:\n{state['context']}"), + ] + answer = structured_model.invoke(messages) + final = answer.model_dump() + final["sources"] = collect_sources(state["documents"]) + return {"final_answer": final} + + +def build_graph(): + builder = StateGraph(RAGState) + builder.add_node("retrieve", retrieve_node) + builder.add_node("generate", generate_node) + builder.add_edge(START, "retrieve") + builder.add_edge("retrieve", "generate") + builder.add_edge("generate", END) + return builder.compile() + + +graph = build_graph() + + +def ask(question: str, strategy: str = DEFAULT_STRATEGY) -> dict: + result = graph.invoke({"question": question, "strategy": strategy}) + return result["final_answer"] diff --git a/assignments/pykido/week3/schema.py b/assignments/pykido/week3/schema.py new file mode 100644 index 0000000..bb3ee62 --- /dev/null +++ b/assignments/pykido/week3/schema.py @@ -0,0 +1,12 @@ +from pydantic import BaseModel, Field + + +class RAGAnswer(BaseModel): + """검색된 문서에 근거한 RAG 답변 형식.""" + + answer: str = Field(description="한국어 마크다운 답변. context 근거만 사용하고 모르면 모른다고 한다.") + confidence: float = Field( + ge=0.0, + le=1.0, + description="context 가 질문을 직접 뒷받침하면 0.85+, 부분적 근거면 ≤0.7.", + ) diff --git a/assignments/pykido/week3/state.py b/assignments/pykido/week3/state.py new file mode 100644 index 0000000..f6e8803 --- /dev/null +++ b/assignments/pykido/week3/state.py @@ -0,0 +1,11 @@ +from typing import Optional, TypedDict + +from langchain_core.documents import Document + + +class RAGState(TypedDict): + question: str + strategy: str + documents: list[Document] + context: str + final_answer: Optional[dict] From af20712278179c713d4385873c568ca3508fedc2 Mon Sep 17 00:00:00 2001 From: pykido Date: Sat, 6 Jun 2026 13:35:49 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat=20:=20=EC=B2=AD=ED=82=B9=20=EB=B9=84?= =?UTF-8?q?=EA=B5=90=20=EB=B0=8F=20=EB=8D=B0=EB=AA=A8=20=EB=85=B8=ED=8A=B8?= =?UTF-8?q?=EB=B6=81=20=ED=8C=8C=EC=9D=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assignments/pykido/week3/compare_chunking.py | 32 ++ assignments/pykido/week3/run.ipynb | 476 +++++++++++++++++++ 2 files changed, 508 insertions(+) create mode 100644 assignments/pykido/week3/compare_chunking.py create mode 100644 assignments/pykido/week3/run.ipynb diff --git a/assignments/pykido/week3/compare_chunking.py b/assignments/pykido/week3/compare_chunking.py new file mode 100644 index 0000000..ff57796 --- /dev/null +++ b/assignments/pykido/week3/compare_chunking.py @@ -0,0 +1,32 @@ +import os + +from dotenv import find_dotenv, load_dotenv + +from rag import STRATEGIES +from rag.retriever import retrieve + +load_dotenv(find_dotenv(), override=True) +os.environ["LANGSMITH_TRACING"] = "false" + +COMPARE_QUERIES = [ + "답이 단조성을 가질 때 답 자체를 이분 탐색하는 최적화 문제", + "연속 구간에서 모든 종류를 포함하는 가장 짧은 구간 찾기", + "작업을 소요 시간이 짧은 것부터 처리해 평균 대기를 줄이는 스케줄링", + "최소 비용으로 모든 노드를 연결하는 최소 신장 트리", +] + + +def compare(query: str, k: int = 3) -> None: + print("=" * 90) + print(f"QUERY: {query}") + for strategy in STRATEGIES: + print(f"\n[{strategy}]") + for i, doc in enumerate(retrieve(query, strategy=strategy, k=k), start=1): + preview = doc.page_content[:80].replace("\n", " ") + print(f" {i}. {doc.metadata['chunk_id']:<28} | {preview}") + + +if __name__ == "__main__": + for query in COMPARE_QUERIES: + compare(query) + print() diff --git a/assignments/pykido/week3/run.ipynb b/assignments/pykido/week3/run.ipynb new file mode 100644 index 0000000..e840da1 --- /dev/null +++ b/assignments/pykido/week3/run.ipynb @@ -0,0 +1,476 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f9c39b0e", + "metadata": {}, + "source": [ + "# Week3 - Domain RAG (알고리즘 코칭)\n", + "\n", + "week1~2의 알고리즘 코치 도메인을 그대로 이어, 패턴/문제 풀이 문서를 코퍼스로 한 RAG 파이프라인을 만든다.\n", + "\n", + "- 코퍼스: `data/patterns/*.md` (12) + `data/problems/*.md` (8) = 20개\n", + "- 파이프라인: loader → splitter → embedding → FAISS → retriever → 2-step RAG Q&A\n", + "- 청킹 전략 2개 비교: recursive vs markdown-header\n", + "- 그래프: `retrieve` → `generate` 2-step StateGraph" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "480bdb86", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-06T04:31:12.737459Z", + "iopub.status.busy": "2026-06-06T04:31:12.737271Z", + "iopub.status.idle": "2026-06-06T04:31:12.745406Z", + "shell.execute_reply": "2026-06-06T04:31:12.744990Z" + } + }, + "outputs": [], + "source": [ + "import os\n", + "from dotenv import find_dotenv, load_dotenv\n", + "\n", + "load_dotenv(find_dotenv(), override=True)\n", + "os.environ[\"LANGSMITH_TRACING\"] = \"false\"" + ] + }, + { + "cell_type": "markdown", + "id": "d4b56d78", + "metadata": {}, + "source": [ + "## 1. 코퍼스 로드\n", + "\n", + "`data/` 아래 마크다운 20개를 로드하고 카테고리(patterns/problems) 메타데이터를 붙인다." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "3dd09633", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-06T04:31:12.746985Z", + "iopub.status.busy": "2026-06-06T04:31:12.746882Z", + "iopub.status.idle": "2026-06-06T04:31:13.071861Z", + "shell.execute_reply": "2026-06-06T04:31:13.071451Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "문서 수: 20\n", + "카테고리: {'patterns': 12, 'problems': 8}\n", + "예시 메타: {'source': 'patterns/backtracking.md', 'filename': 'backtracking.md', 'stem': 'backtracking', 'category': 'patterns'}\n" + ] + } + ], + "source": [ + "from collections import Counter\n", + "from rag.loader import load_documents\n", + "\n", + "docs = load_documents()\n", + "print(f\"문서 수: {len(docs)}\")\n", + "print(\"카테고리:\", dict(Counter(d.metadata[\"category\"] for d in docs)))\n", + "print(\"예시 메타:\", docs[0].metadata)" + ] + }, + { + "cell_type": "markdown", + "id": "56f27765", + "metadata": {}, + "source": [ + "## 2. 청킹 전략 비교 (recursive vs markdown-header)\n", + "\n", + "- **recursive**: 700자 기준 문자 분할. 길이로 자르므로 한 섹션이 쪼개지거나 코드 블록이 본문과 섞인다.\n", + "- **markdown**: `#/##/###` 헤더 기준 분할. 개념/복잡도/코드/실수 같은 의미 단위가 보존된다." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "670b94d1", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-06T04:31:13.073679Z", + "iopub.status.busy": "2026-06-06T04:31:13.073575Z", + "iopub.status.idle": "2026-06-06T04:31:13.079546Z", + "shell.execute_reply": "2026-06-06T04:31:13.078988Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "recursive: 47 chunks\n", + " markdown: 128 chunks\n" + ] + } + ], + "source": [ + "from rag.splitter import split_documents\n", + "\n", + "for strategy in (\"recursive\", \"markdown\"):\n", + " chunks = split_documents(docs, strategy)\n", + " print(f\"{strategy:>9}: {len(chunks)} chunks\")" + ] + }, + { + "cell_type": "markdown", + "id": "7c90c63b", + "metadata": {}, + "source": [ + "## 3. 인덱싱 (OpenAI 임베딩 + FAISS)\n", + "\n", + "두 전략 각각 FAISS 인덱스를 만들어 `.cache/` 에 저장한다. (`text-embedding-3-small`)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "e3eb3475", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-06T04:31:13.081205Z", + "iopub.status.busy": "2026-06-06T04:31:13.081104Z", + "iopub.status.idle": "2026-06-06T04:31:15.769613Z", + "shell.execute_reply": "2026-06-06T04:31:15.769013Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[recursive] indexed 47 chunks\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[markdown] indexed 128 chunks\n" + ] + }, + { + "data": { + "text/plain": [ + "{'recursive': 47, 'markdown': 128}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from rag.indexing import build_all\n", + "\n", + "build_all()" + ] + }, + { + "cell_type": "markdown", + "id": "34d0a4b1", + "metadata": {}, + "source": [ + "## 4. 청킹 전략별 검색 결과 비교\n", + "\n", + "같은 쿼리를 두 전략으로 검색해 상위 3개 청크를 비교한다." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "740edf09", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-06T04:31:15.771153Z", + "iopub.status.busy": "2026-06-06T04:31:15.771046Z", + "iopub.status.idle": "2026-06-06T04:31:17.420156Z", + "shell.execute_reply": "2026-06-06T04:31:17.419251Z" + } + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
queryrecursive top-3markdown top-3
0답이 단조성을 가질 때 답 자체를 이분 탐색하는 최적화 문제binary-search-rec-5, binary-search-rec-7, greedy-rec-15binary-search-md-0, pgs-67258-gem-shopping-md-5, greedy-md-4
1연속 구간에서 모든 종류를 포함하는 가장 짧은 구간 찾기sliding-window-rec-24, binary-search-rec-5, binary-search-rec-7pgs-67258-gem-shopping-md-1, heap-md-1, sliding-window-md-5
2작업을 소요 시간이 짧은 것부터 처리해 평균 대기를 줄이는 스케줄링heap-rec-19, pgs-42627-disk-controller-rec-31, pgs-42627-disk-controller-rec-32pgs-42627-disk-controller-md-1, pgs-42627-disk-controller-md-2, pgs-42627-disk-controller-md-5
3최소 비용으로 모든 노드를 연결하는 최소 신장 트리pgs-42861-connecting-islands-rec-34, pgs-42861-connecting-islands-rec-35, pgs-118667-two-queues-rec-29pgs-42861-connecting-islands-md-1, pgs-42861-connecting-islands-md-4, union-find-md-1
\n", + "
" + ], + "text/plain": [ + " query \\\n", + "0 답이 단조성을 가질 때 답 자체를 이분 탐색하는 최적화 문제 \n", + "1 연속 구간에서 모든 종류를 포함하는 가장 짧은 구간 찾기 \n", + "2 작업을 소요 시간이 짧은 것부터 처리해 평균 대기를 줄이는 스케줄링 \n", + "3 최소 비용으로 모든 노드를 연결하는 최소 신장 트리 \n", + "\n", + " recursive top-3 \\\n", + "0 binary-search-rec-5, binary-search-rec-7, greedy-rec-15 \n", + "1 sliding-window-rec-24, binary-search-rec-5, binary-search-rec-7 \n", + "2 heap-rec-19, pgs-42627-disk-controller-rec-31, pgs-42627-disk-controller-rec-32 \n", + "3 pgs-42861-connecting-islands-rec-34, pgs-42861-connecting-islands-rec-35, pgs-118667-two-queues-rec-29 \n", + "\n", + " markdown top-3 \n", + "0 binary-search-md-0, pgs-67258-gem-shopping-md-5, greedy-md-4 \n", + "1 pgs-67258-gem-shopping-md-1, heap-md-1, sliding-window-md-5 \n", + "2 pgs-42627-disk-controller-md-1, pgs-42627-disk-controller-md-2, pgs-42627-disk-controller-md-5 \n", + "3 pgs-42861-connecting-islands-md-1, pgs-42861-connecting-islands-md-4, union-find-md-1 " + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import pandas as pd\n", + "from rag.retriever import retrieve\n", + "\n", + "COMPARE_QUERIES = [\n", + " \"답이 단조성을 가질 때 답 자체를 이분 탐색하는 최적화 문제\",\n", + " \"연속 구간에서 모든 종류를 포함하는 가장 짧은 구간 찾기\",\n", + " \"작업을 소요 시간이 짧은 것부터 처리해 평균 대기를 줄이는 스케줄링\",\n", + " \"최소 비용으로 모든 노드를 연결하는 최소 신장 트리\",\n", + "]\n", + "\n", + "rows = []\n", + "for q in COMPARE_QUERIES:\n", + " rec = [d.metadata[\"chunk_id\"] for d in retrieve(q, strategy=\"recursive\", k=3)]\n", + " mdh = [d.metadata[\"chunk_id\"] for d in retrieve(q, strategy=\"markdown\", k=3)]\n", + " rows.append({\"query\": q, \"recursive top-3\": \", \".join(rec), \"markdown top-3\": \", \".join(mdh)})\n", + "\n", + "pd.set_option(\"display.max_colwidth\", None)\n", + "pd.DataFrame(rows)" + ] + }, + { + "cell_type": "markdown", + "id": "a7ecdf5c", + "metadata": {}, + "source": [ + "## 5. 2-step RAG 그래프\n", + "\n", + "`retrieve` (검색 + context 포맷) → `generate` (structured output 답변 + 근거 문서)." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "484ed34f", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-06T04:31:17.427322Z", + "iopub.status.busy": "2026-06-06T04:31:17.427112Z", + "iopub.status.idle": "2026-06-06T04:31:19.431318Z", + "shell.execute_reply": "2026-06-06T04:31:19.429972Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAG0AAAFNCAIAAACFQXaDAAAQAElEQVR4nOydB3xTVfvHz83ubunepYUyC6VQBARkWkGUUWX5MkRFGSrrRYaAAsqquBB4QV6UjSxB/gi8CCggIHuW0Umhe4808+b/JGnTtLlJbtoTTZvzhU8+ueeee+7Nr2c896yHp1KpEKHB8BABB0RHPBAd8UB0xAPREQ9ERzxg0FFSIbtxtjjniUxaoVTSSC4FQ4pCSG1OURQFdhUHvnEpWqnSHsIRh0PRNByqo0AIxaFUtPqTolV09Vl1IpQKkuFQiNbYZur4FFInoD3UXlWdJqU9pY2Gqr9r4iBdWrqfLeJwkIov5PgE86N6unl4O6CGQTXEftz/dUb+M6lCgfgCJHDgCgTwoyiFTD959W9WUSqQCBSi4Nlp9S/k8tSyIo5GkWrZ4SwNmqoodTitCUQ1Z7WpUXo6VoVTOlnViSONjtq09QPVT1GVohqekIK/uVxKy6S0QoooLvIO4L84wd/dU4DqRT113LU6vTBLLnLitIpx7jXCBzVyLh/Pf3StvCRf4ezOnbikObIci3U8fzj31h+l7t7812cFCgRNrXrd9+UTqKBC2ji8OjnQogst03FPQnpJvnzIZP/AcCfUdNk0P4nH505aakHGtEDHkzuyMlMkExfXJ9s3OvauTZdL0b/mh7KMz1bHnSvSpFJ60ifhyG7Y+8WT0kLFO5+x+skcNpF+Xp8hszMRgVGzQ9y9eNs+T2UT2byOaffLnyVL37QzEbW8PjOkspQ+uy/HbEzzOh7/Mbt9Dxdkr7w00ffexTKz0czoeHpfDtSfL8T7InsltLWzkxt3/1cZpqOZ0fHRlbLWsU3ZxGHDC6955jyRmo5jSse0e2UKOer7uj+yb5q3c4X3yN8PZJuIY0rHK/8rdnJj1aBj5KefflqyZAmynHnz5h0+fBhZh2Z+gpQ7lSYimJKpOE/mFyZCfy/3799H9aLeF7KhdWdHiVhpIoIpO3z9v5P6vObZ9jkPZAXS0tI2btx47do1eIAOHTqMHz8+Ojp68uTJ169f10bYsWNH69at9+7de+7cubt37wqFwpiYmGnTpgUFBcHZuXPncrlcf3//bdu2rV69Gg61Vzk7O589exZZgXWzkqavbWHsrKn8CD1O4e2s0sjIZDKQDIT49ttvN2zYwOPxZs6cKZFINm3a1L59+5dffvnq1asg4s2bN9esWdOxY8eEhIRPP/20sLDw448/1qbA5/OTNKxdu7ZTp04XLlyAwEWLFllJRIDLRY9vGjWAjHbYlBWqs7HIuZ79caZJT08HUcaMGQNiweHKlSshGyqgI7M2UVFRUF2GhISA0HAol8tB7pKSEjc3N+jozMzM3L59u0ikrnmkUimyMtAlXJIrN3bWqI5KpRJZbYYASOPh4fHJJ58MHjy4c+fOkOO6dOliGA0y7NOnT7/44gso1xUVFdpA+AOAjvClefPmWhH/JqArmKaMnTRart29BSCjUmGqcq03UNlt3ry5Z8+eu3bteuutt4YNG3bs2DHDaL///vusWbPatm0Lka9cubJu3bo6iaC/EejAd/Y0Kpep+hEGRlLuiZF1CAsLmzFjxtGjR6GCa9GixeLFix88eFAnzqFDh6DxgbYlMjISCnJZmfn3M+tB0yisldFhHFM6cvlU6t0KZAWgsT5y5Ah8gYLZu3fvVatWQQ2YmJhYJxpUhT4+NYMWp0+fRv8QD64Uw6eDq9HWwpSOrp68rFQJsgIg0NKlS7/66quMjAxoc7Zu3QqNDNSScCo4OBhqQyjFUA9CNrx06RK03XB2586d2muzsrIME4QyDorrIiPcJF4pEzqaimBKx/bPu5UV4n8mACRbsGDBr7/+Onz48Pj4+Bs3boAtGR6u7pobMWIEFGEoy48fP546dWqPHj2giuzevXt2djaYPlBXfvDBB8ePHzdMc9KkSaD+7NmzKysrEW6yUqUBEabGZs30h383Kyk2zqNrnCeyY+C9bsfnT6Z/2cJEHDOvz6HtHO5cKEX2zZGNmTAeazqOmYHTIW8FQpa8fa6wQ69mjBGmT58O1RnjKaintPazIWA59unTB1kHYymDRQyFz9gjnTp1ivGUQqaAURrTmRGxGee6+H/5N88WT1nDnJBYLFZb7EyY0NHBwcHYqYZjwjwy8UguLsx9/t9/nOwVIBw2NQiZhNV44fbP0rg8auxHbAchmwzHfsh69kj8zucRZmOy6l4ctzCsolR5cF0GsicuHstNv1fBRkRk0TyAHStSBSJq5MwwZAec3Z/18Kr43ZWsRESWzkvZsjgFCniTn1Kxe3V6aaH83ZUt2F9i8Typg99kZKZJI6IcBr1p2UyiRsGZ/dmJl8pdPHhQlVlyXb3m7WUli49uyZRKkG8ov+dQT/8wZ9TIKS+RndqZ+zRJAp1jzw/z6PSCxe8d9Z9Hevdi0dVTReWFNIeLRI5c52ZcRyeuwIGjUOh10lHaG9SEqCeCouqZn3ozaDWPUat3TzMNtyqw9lxbhmdWh9PqqaJ14HDU/TSGcDlgGNLicmV5iaKyVAlxRE6cNt2cnx9Sz7mcDZqPq+XqqYL0RHF5sUIhg8RUcv2eaUo7wVhfWUoz05iqPqsNY9CRqg7V/iF0s2k185jrCkZp5j4b9rLq5jXXgSdQp8/hUS7uvIAI4fOvNHQqLAYdrQ309UIfD3RAIBumEUyoNfESYjsQHfFAdMTD3z3tpB7AcCuMViPbhuRHPBAd8UB0xEMj0JHUj3gg+REPREc8EB3xQHTEA2ln8EDyIx6IjnggOuKB6IgHoiMeiI54IDrigeiIB2KH44HkRzz4+flxOLY+jtQIdMzNzbXGUg68NAIdoVATHTFAdMQD0REPREc8EB3xQHTEA9ERD0RHPBAd8UB0xAPREQ9ERzwQHfFAdMQD0REPjUJH213PFRcXl5eXp14DR1HQH07TNHwPCws7dOgQsj1st79+4MCBSLNVnHZQAT4FAsGYMWOQTWK7Oo4bNy44OFg/JCQkZOjQocgmsV0dfX19Bw0apDuE0g2Hf/Mee+yx6XE4KMW6LBkUFBQfH49sFZvW0c3NbfDgwVBFIk11qd0+0zaxuL1+dLMkPbFSzrSNqnbVPqSIOJR+qrrV/JoDpNvmtFa4fgp6Dragmb58+TJNKzt37iwSiSjNOcNHNryw5pTaZRWlUtW9o364PnyByitY0KmXZVsrWKCjUqncuiRVLgODjiOXMVxV43Ks9ip8nZOs6mh1nZDp0Hg6UxnIodkRgKreE4CqlVr1hZr9ABh1pDR7CNAq/SfUPpX65xvsFsAXUQo5DQm+MjkgINzkZoX6d2GpI4i4aV5qWHuHnsOa4DYphtw+n3/rTPHw6QH+YaykZKvjho+SOr/o0aaLHW1gKJPJ9qx6Mi2hBZvIrNqZE9szeXzKrkQEwOx39uDsSUhjE5mVjnkZMtdmtj5zzhr4hjiVF7HasZqVjtLKupvC2AkiJ55MxqreY9XfQysRbesdLlZBpUAqdhuoEz+5eCA64oGVjmDKcuyxerQAVjqCjUkju4Sn4vBZ5SCW5Zqynu8Pm0ZB0XJ87TWHS3G5pGCbgqXdo1Iq7TNDsoW013hg215Tdlus2f1wdjpCLLusH6GPkqLwtde0HN6Q7LF+BHOPcVtYQ9iOz/xTuXHo8P7btn+PbB5WOqqzt9UGxFJTk0ePHWLs7KiR4zpEdUI2D7v3GZp592gsPHxkyp/o2DETUWOAVTbj8CguF1kElMcDB3Z/OPOdvv27lJapPQEdP/HL1OkTB73cEz73H9ilHc/Y+sPGVas/zcnJhmj79u88cHBP/Otx5y+c7T+w67ffJaDa5frevdtzP5r+6tC+4yaMWL/hS61Hw++3fPfyK73l8hoPjXv2bhsY100sFhu7KXtUPIrDYycRm0hQ11r6gs3n848eO9SiRas1q79zdHA89dtx0CuyZetdO468/dY0+Enr1n8B0d6c+N7oUeN9ff3O/Hb19dfegK58sbjiyJH98+ctHT50pH6CT59lzJk7VSKVrPt267JPE1JSHs+cNVmhUPTt8yJI9tdff+pinjt/pnu3Xo6ORm/KHkqhohWsfjlLHRFtYXMN5oKrq9v70+Z06fwcj8c7duznDh06zfhwnodHs5hOsW9OeO/nn38qKio0vEoikYwePWFA/5eCgkL0T5069SufxwcFQ0LCwsLC58xe9DjpIeTciIiWAQFBoJ02WkFB/v37d/r1i4PvjDctKSlGVoBlO0PVYyF5q8i22i8wln/33q3YLt11pzp1ioXA23duMF7YulU7w8B79261bt3Ozc1de+jn5w/yaVMYOGDQufOntW6Z/jh32sHBoefzfYzdNDGR2QtWA2HZzqhoy81HKKTaLzCACfXXlv+uh//6EQzzY50L9SkvL3vw8D5Uo7VSKCyAzwH9B/24bfP1G1diu3Q7f/5Mr179oARAvma8aXFJEbIC7N5nOA16LxSJRFBbvTjw5d69++uHB/gHsU+kmadXVFQ01Kf6gW6u6uwJNQCU7gsXzkZGtrl569rKFd+YuGlwUChiD0fd18UmItt+CqphL9gREZFl5WWdoqtyE+SUrKxnPj6+FqQQ3vLk//6vY4cY3V4VaWkpujoUWpujRw+GhoZDpQxVoYmbenp6IfbQ6jE+NrBuZxrWb/bOW9Mhvxz79TDUUHfu3Fy6bP6sOe9BeUea3ASNw/nzZzMy0k2k8Nprb8C10OBCgYWY/9n0zaS3R6WkJmnP9ukzMDsn6/jxI337vsitttEYb6pvIbED33shB9qZhvVTQJHctHHn7ds3hscPBPOloqJ8+bK12kmh3Z7rGdU+etGSOb+dPmEiBVcX1y3f73UQObw75V/jJ8ZD+f33nEVg02jPBgYEtYps8+jxg/5940zflLHybTis5vdsXpDq7M4b8m4wsjOunii4f6lo2lrzU3zYtjPcRuCIwQpQapuPTUSW44WIts9hBRXbfjOW/eGU/faHs8OKdrhdwW7clY+4dllBqlgPTLEdV1Aq7HFGBYejYjliwEpHLs9O5wGolBTOeXtKBZkHYAZ29SPkRx5psE3Brn602/wI9RmfVQXJej4Fsksg/8hZNbDsyjWHosh8M5Owa2eUMNxD2hlTsNJRIEQ8oV12VHBonhBfP4XQiZKUy5D9UZQj4fHx9eN26udWUcLOHm1aFGTKQts4sYnJSsdWMR4uXtw9q5OQPXFoXQpYzQPG+LOJbMH66992ZybdEge2dAxo6Shg2qhf/VZvuMIcqQwdpTMF1lp6p9JaWtWp6daoG0bVP1U7Wk0svbXz1YH6q94pSj+2UqbIeiJ+9ljs7M4fNavWXAQTWLYfwNkD2cm3xNJKmv0yOcNF/xguo0yNPhleWiNatVg1GuuErQ7h8ikuXxUU4TB4kgUrzRuBX/vdu3c/e/Zszpw5yIYhfirwQHTEA9ERD8SvPR4agY6kXOOB6IgHoiMeiJ8zPJD8iAeiIx6IjnggOuKBtDN4IPkRKS+ArQAAEABJREFUD0RHPBAd8UDqRzyQ/IgHoiMeiI54IDrigeiIB6IjHlq2bEl0xMDjx4+Jfy4MED9neCA64oHoiAeiIx6IjnggOuKB6IgHoiMeiI54IDrigeiIB6IjHoiOeCA64oHoiAfi175B9OvXr7S0VKlU6vYEg0cNDAw8evQosj1sd71C9+7daZrW+rXXAt/j4uKQTWK7Ok6YMMHfv9aa3aCgoFGjRiGbxHZ1jIyM7NKl1u7Mzz//vI+PD7JJbHod0qRJk3R+7X19fUeOHIlsFZvWMTQ0tEePHtrvXbt2hUNkq7Cye1ITS2l5XUcVuoXfunXjKkr9r040gyXn5l3u6sfo99zYB9eKlEq673Njkm9XGImvomot/a99C0pzvvpJ9CMbe0j97Qq4lCosyhmZw4zds2dNamEOWB5IWT8DzrxoLNJQGWzCWCdZ04cmLtXfOsDIVZQm/7i4c8Z/HI6MY0rHHatTZBWqXsN9/Jq7IDumpKTyj91ZZcX0uytaGItjVMcfPk3hCtGwKab+CHbFuZ8znySK31vJLCVzO3PvYpGkgiYi6tNrWACPR53cmcV4lrmdSfyrVORsnzuGm8LNi5eZLGY8xSyWVEJxbX6K19+PyEmolDErxiyWQkaraLJRYV1oBS2TMu9PRjIdHoiOFqAyvjMr0dECNFY7s5lIdLQAyI0ciuTHBqPxe0LyY4Mx4caZ6GgBajfORvIjs1VJ6W9iSWCB0fxot1uvm8aYKMw6Gm3e7RuKQ5H6EQMav1CkvbYmTbxz7NOl8479ehhZnyau48OH9xFWrP5+XVRUuGLl4nv3b4cEhw0d+vrTp0/OnT/z49b9SLPwd8t/11+6fD43N7t9++jhQ0d269YTwlNTkye9PWr9dz/u2rX1/IWz3t4+ffu8OPmd97UOWgsLC9ZvWHv33i2JRBIb2338v94ODlaPux44uGfX7q0zZ8xf8sncYcNGvj9tDqRz5Jf9129cyc7ODAsNHzx42NBXX4OYWifPaxKWbdj45S+HzyKNm/sjvxxITU1q3rxFv74vxo8YY5FDQXVcI9Gx5cfVCUufZKStWb1++bK1ly9fgP86x8DffLt6/4Fdw4eN2rXzlxd691/y6dzf//gNaXzfw+cXa5f37//SyeMXF85f/tO+HWfO/g+pPYwoZ85+9+atazNnLPjv93s93JtNnTbhWeZTpPGOXcf3/Xfrv7hy5eKHH3y0csU3IOLX36y6dPkChB8/pv7895xFWhEb7uZebcZYZIdbSklJ8aVL50e+Pq5tm/aenl6zZ30MWUN7SiqVnjh5dOyYia++Eu/m6jZ40ND+/V7atn2z7toXeg/o88IA0LRjx5gA/8BHjxIh8M6dm0+epC2Yv+y5rj2aNfOc8t4MVzf3Awd2ISbf94sWrVizZn1Mp9hO0V0gJ7aKbPPXlT8NH5LRzb0xX+aWwqwjl8exyHFPcspj+GzfvqP20NnZOSamq/Y76CKTyfT9y0d37JySklRSWqI9jIxsozvl7OxSXl4GX+7cvQnK6jxZg3Zw1a3b13Uxa/m+V6kOHtwzfmI8FGT4/+Dh/WIDdYy5ub995wZijbr30SL7EW6ALBlXKCsrhU8np5p5B66ubtovWl3e//CtOpcUFRZoV/nrir8+cJVcLq/jxd7d3UP3Xed+GR513oIP5XLZO29Pj47u4uLsYngvAP6WjG7uLcyPRjUx8j5DW+YUQSgUwadcVuOjpqi46vk8vbzhc/ashYGBtdw++/j4FRbmG0sQKgcHB4fPln+pH8jlcA1jPnr84MGDewlr1neuLgHwN/D2qjstzZib+wD/IGQBRkXB015rW9LUtOSwMPWQd3l5+fXrf/n6qmcvBgWGaP2F6/zLQxaA2hp+VaHxrBAREVlZWQlaBwZU/c7MrGfubh6GMaFqhk+dcGlpKfC/eVgEY5qGbu59fHwRa6zezsCvDQ1t/uO2TdCkgohffb3C37/KWQboNXHCu9CwQNMBhQta6jlzp3719UrTCULm6tq1R0LCspycbFDq58P73psy7vjxI4YxwdCB+mHvT9tLy0qhafp23ZrYLt2yc9Sj9fD3A1vq6tVLN25eBduL0c29TIbHzxM2+3HunMUJa5ePGz88IrzlwIGDoa5MTLyrPTV61HjIC7v2/ACZFMLbte0we/bHZhNc8dlXYOstXT7//v07kN8HDBg0YsRow2i+vn4LFyyHP+HQYf2g6lg4f1lBYf6ixXMmvPkaWK9vjJ209YeN0Hzv3nVU6+Z+566t/9n0jURSCY8BJpq2rDQc5vk9Py5Lg/Hr+BkWzDeEXAPmCPwq7eH8hTN4XN6ypQmoCXF6V2ZminjKGoYpPtjscHiTnTlrMrzDgKDbd2y5du3yq5qXiqYE9JtxOFZ+L1yyZNWahKWbv1+Xl5cTGtJ8yaKVUE+hpoW634y2cr8ZvKssX2rZa1ZTgrlcG8u9BGMw66ieRE2UNMTS90JL32fsBeN2OBlXsACKg6zeXtsDUEyt3l7bOURHC4BGhsw3wwA0MmT82roQHfHArKOATynIegUDKC7iGnH0wPw+I3SmaIU9OmA3jUSsFDpyGU8x69ixt4u4jOhYl+JcaXBL5n5fZh0jOng4e/AOfJ2CCNX8+mMajGz2GxXAeNbUuuFD3z0tyJR07OPZuqsHsmPSE0uvniqgaDRhcXNjccysYz+0PiMnXaZUQP8lqjeMS/AtuNxwHbslVG8HUE+4HBUY3x5+/NGzQ03ehUXHTmVRZXklQ/2qv5zeRLh61wKmqIZhjCH/O3kyNzdv7BtvMCVdc8hw0+oviEOpqt+L9bdQoDQiM4TrJSUQIbdmAmQOVvajg4eDwz9XspXcYppX7B1g/sf8gxB/H3ggOuKB+IvDA/FrjwdSrvFAdMQD0REPREc8kPYaDyQ/4oHoiAeiIx5I/YgHkh/xQHTEA9ERD6R+xAPJj3ggOuKB6IiHxqEjqR8xQPIjHiIjI4mOGHj48CHxz4UB4ucMD0RHPBAd8UB0xAPREQ9ERzwQHfEAOiqVtj7pvxHoyOVySX7EACnXeCA64oHoiAeiIx6IjniAznAYMkS2DcmPeLBdv/ZDhgxRaCgvL0ea7V9lMpm7u/upU6eQ7WG76xWCg4Pz8vKKi4u1aoKINE33798f2SS2q+OkSZO8vLz0QwICAohfe4uJjY1t27atfkhMTEx4uI26nLV1v/Z+flUbnHp7e9tsZkQ2rmNUVFR0dLT2e5s2bdq1a4dsFVtfFzd+/HhfX1+oKMeOHYtsGDx2T/KtklvnSovz5BIxrVKqF4TXXY5eZwtzgzX+dVbtqx/LcBMApp0BGJf7M1+ugUNpt3FEfCHH1ZPXOtalYy8Ma8sbquPxHzNT74mhm5XH5widBU7uIpGrkCvkcmpvpKC/8l4XVLNg3+AR1CvyUe1wlSaN2oBYFK1d8q9iTNswfZWKVsgVsgpFeWGltEwulyohgn9zYfz0YNQA6q/jxWP5N04XU1zKLcAlINITNVpy04oL0oqVClWLDo4vTQioXyL11HH752llRUqfFu5eIe6oSVCaX/70dr5ASL29vD6mVX103DgvmSfktehmkYeHxkHa9ayKYsk0pp3WTWOxjluWpFBcXnhsIGqi5KYV5ieVTP3CMikts3s2fpTEFwmasIiAT1gzn9bu62YlWXSVBTruWJHG5fFCov1RU8cryMPJU7R5YTL7S9jqeO23wpJ8RcueDTIOGhHNY/yVcnR0yzOW8dnqeOVEUbNQV2RPhHT2TbtbyTIyKx3P7M+hVSr/lo3YSKwHjq4OPAfu/q8z2ERmpePj6+XO3o7IVjnwy+o1345BVsA7zD03Q8ompnkdC/MlskpVSJQFfqyaDJ7BrmAW/nUy32xM8zpeOlLI5dvvXrl8B27yrQqz0cyPFxZkyXgiKw4rXrl+9OKVQ1k5Sf6+LaKjBvTqPlrrU2P73gXwmhDT8aW9B5dKpeLQ4KiX46aHBrdHah+d4p37FyelXIVLuseOQNZE6MwvzJaYjWY+P5YVKYSO1toz8PqtE3sPLQsKaLVg1qFBA6f88eeew8eqfBZyOLz0jDvXbv764Xs/fL74dx5fsOfgUu2pn37+LL8g492J6yaMWZWdm/Lg0QVkNVy8nFQs9hA1ryOthL+JtfLjX9cOh4d2GvHKXBfnZi3Du8T1n3zh8r6y8irHhpDvRg3/2LNZIJfLi+kQl5efDiElpXm37p7q23Mc5E1XF88hcdP5PBGyGtATyCYaq/aaw+UiKwDjqKlPbke2fE4XAlJC/2Bq2k3toY93mFBYZSeIRC7wKa4sLSxS28a+PjVb1QYHtkFWgy8UmHCjqcN8RuNAlyxtlbkCMCitVMqPn9oI//XDyyqq8iNFMfyZK8RqB7tCQY0dJhA4IKtBQ3lksbmveR0pLpJJrDItRCAQgRydowd3aNdPPxwKsomrnBzVHnhl8pq6XyI1357WG3GphGJRaM3rKHTgSirwOO80JMA/slJS1iK8s/ZQoZAXFD1zdzNlq3q4q7us057c1hZnuORx8l9OTtbav7c8r5LLwoW1eandvXkysbWmew0eOOVu4u+Xrx1R15XpN3f8tPA/W6dBeTdxibubT1hIxxOnN+XmpcPwys59ixq0LbY5KkulDi7mmwfzOrZ5zk0pb8Du4SZpHho9c8o2aFg+WfXSf354v1JS/uYba/h8Mz5Xx8QvCQlq99WG8QuX93V0cO0a86r13LJJxfLACPM+YFn1h2+Ym+wV5ubd3O52tZfJZI9/fzZtrfm+cVZ2j1+ooDCjFNkf6VdznDxYScTKwB4+LXjd7CRxicTRjdkovXz18C8nvmE8BVWYsXI6esTi9m1eQJiA6nXLjtmMp6DC5UIfAVM1+tqr86KjBiIjSCsUceP8EAvYjnMd3vg0O13WqjezjwGJpEJcWcJ4qkJc6uTI3AHs7NQMTB+Ej8KiTMZwiaRcJHJmPOXk6K4z9euQfOkpX6AavzAMscCC8cJN85MdPETBUaz+Po2doqyyrPsFUxMiWMa3YJxr8oqIkqzKynLznR9NgMx7+S+96c0+vmXjrmM+Ckz+Mws1de6eTO0a5xHezoLxKIvnAchkys3zUn1bunuFNUEzqLKkMuVKdvwHQX6hllXc9ZmXIhfLv1+SzhfxW/RoUlNTUq9mioulL7zu1b6bxZOW6j/fbMfnacX5CicPYfMu9ZyjZTtk3M4tzasQOnDeXlbP+ecNmv/46GbJHwcKJBU0T8BxbObQLNjF2d2KXVh4qRRLCtPLy/PEcqmSL6Bi+rvFDvRC9QXDfNy8zMoz+/ILs2QKqQq6mCjNhFeVsrbRWzWZU1XVl6c3t7Ou9y7tV5UKGQSqJ9nWDtRMu6X0HEjpxdHcguJSKqWq6nbaT+hzqO5OhY4cF09+t0HuER0aOsUB83quJw9K854pKisUtNx0H0yNkCqkU6I6QG1FqKq+Uro4WtVqJjer5YGhk4DX5coAAAA7SURBVFqvKTUTmquSrbpPze0oDiV0Unn6CRuuXa3fY7Pr4hoXxE8uHoiOeCA64oHoiAeiIx6Ijnj4fwAAAP//hcEbhQAAAAZJREFUAwAmcGLYuAmgHwAAAABJRU5ErkJggg==", + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from IPython.display import Image\n", + "from graph import graph\n", + "\n", + "Image(graph.get_graph().draw_mermaid_png())" + ] + }, + { + "cell_type": "markdown", + "id": "c15500de", + "metadata": {}, + "source": [ + "## 6. 최종 테스트 질문 (5개+)\n", + "\n", + "기본 전략은 markdown-header. 답변과 함께 근거 문서(sources), confidence 를 출력한다." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "3ddb76a9", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-06T04:31:19.433325Z", + "iopub.status.busy": "2026-06-06T04:31:19.433212Z", + "iopub.status.idle": "2026-06-06T04:31:36.103533Z", + "shell.execute_reply": "2026-06-06T04:31:36.102818Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "==========================================================================================\n", + "Q: 입국심사 문제는 왜 이분 탐색으로 푸나요?\n", + "A: 입국심사 문제는 이분 탐색을 사용하는 이유는 주어진 심사대의 처리 시간이 단조 증가하는 성질을 가지기 때문입니다. 즉, 특정 시간 내에 모든 사람이 심사를 마칠 수 있는지 여부가 시간에 대해 단조롭게 변하기 때문에, 이분 탐색을 통해 효율적으로 최소 시간을 찾을 수 있습니다. 이 문제는 n이 최대 10억 규모로 매우 크기 때문에 선형 접근은 불가능하며, 이분 탐색을 통해 가능한 최소 시간을 빠르게 찾는 것이 적합합니다.\n", + "confidence: 0.9\n", + "sources: ['problems/pgs-43238-immigration.md', 'patterns/binary-search.md', 'patterns/hash-map.md']\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "==========================================================================================\n", + "Q: 보석 쇼핑에서 슬라이딩 윈도우를 좁힐 때 while 을 써야 하는 이유는?\n", + "A: 보석 쇼핑 문제에서 슬라이딩 윈도우를 좁힐 때 `while`을 사용하는 이유는, `left` 포인터를 한 칸씩 줄이는 `if` 문으로는 최소 윈도우를 놓칠 수 있기 때문입니다. `while` 문을 사용하면 `left` 포인터를 가능한 한 끝까지 이동시켜 윈도우를 최대한 좁힐 수 있습니다. 이렇게 함으로써 모든 보석 종류를 포함하는 최소 길이를 정확히 갱신할 수 있습니다. 이는 슬라이딩 윈도우 기법의 핵심으로, 구간 통계를 유지하면서 효율적으로 문제를 해결하는 데 도움을 줍니다.\n", + "confidence: 0.9\n", + "sources: ['patterns/hash-map.md', 'problems/pgs-67258-gem-shopping.md', 'patterns/sliding-window.md']\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "==========================================================================================\n", + "Q: 디스크 컨트롤러는 어떤 자료구조로 풀고 핵심 아이디어가 뭔가요?\n", + "A: 디스크 컨트롤러 문제는 주로 **힙(Heap)** 자료구조를 사용하여 해결합니다. 이 문제의 핵심 아이디어는 요청을 도착 시간순으로 정렬한 후, 작업 시간이 짧은 요청을 우선적으로 처리하는 것입니다. 이를 통해 전체 작업의 평균 대기 시간을 최소화할 수 있습니다. 따라서, 요청을 처리할 때는 힙을 활용하여 가장 짧은 작업 시간을 가진 요청을 효율적으로 선택하는 방식으로 접근합니다.\n", + "confidence: 0.9\n", + "sources: ['patterns/heap.md', 'problems/pgs-42627-disk-controller.md', 'patterns/two-pointers.md']\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "==========================================================================================\n", + "Q: 섬 연결하기에서 union-find 는 어떤 역할을 하나요?\n", + "A: 섬 연결하기 문제에서 union-find는 사이클을 검사하는 역할을 합니다. 이 알고리즘은 간선을 비용 기준으로 오름차순 정렬한 후, 각 간선을 선택할 때 union-find를 사용하여 두 정점이 같은 집합에 속하는지를 확인합니다. 만약 같은 집합이라면 사이클이 발생하므로 그 간선을 건너뛰고, 그렇지 않다면 두 정점을 연결합니다. 이 과정을 반복하여 선택한 간선의 수가 `n-1`이 되면 모든 섬이 연결된 것으로 판단하고 알고리즘을 종료합니다. 따라서 union-find는 크루스칼 알고리즘에서 사이클 방지를 위한 중요한 역할을 수행합니다.\n", + "confidence: 0.9\n", + "sources: ['patterns/union-find.md', 'problems/pgs-42861-connecting-islands.md', 'patterns/greedy.md']\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "==========================================================================================\n", + "Q: DFS 로 최단 거리를 구하면 안 되는 이유는?\n", + "A: DFS(깊이 우선 탐색)는 최단 거리를 구하는 데 적합하지 않습니다. 그 이유는 DFS가 경로를 탐색할 때, 모든 가능한 경로를 깊이 있게 탐색하기 때문에 최단 경로를 보장하지 않기 때문입니다. 특히, 가중치가 있는 그래프에서 DFS는 최단 경로를 찾는 데 비효율적이며, 가중치가 1인 경우에는 BFS(너비 우선 탐색)가 최단 거리를 구하는 데 적합하다고 알려져 있습니다. 따라서, DFS로 최단 거리를 구하려고 하면 잘못된 결과를 초래할 수 있습니다.\n", + "confidence: 0.9\n", + "sources: ['patterns/dijkstra.md', 'patterns/bfs-dfs.md', 'problems/pgs-43236-stepping-stones.md', 'problems/pgs-43165-target-number.md']\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "==========================================================================================\n", + "Q: 등굣길 문제에서 DP 상태는 어떻게 정의하나요?\n", + "A: 등굣길 문제에서 DP 상태는 `dp[r][c]`로 정의되며, 이는 (1,1)에서 (r,c)까지의 경로 수를 나타냅니다. 각 칸으로 오는 방법은 위에서 내려오거나 왼쪽에서 오는 두 가지뿐이므로, 상태 전이는 `dp[r][c] = dp[r-1][c] + dp[r][c-1]`로 표현됩니다. 물웅덩이는 0으로 고정하여 경로 수를 계산할 때 이를 고려해야 합니다.\n", + "confidence: 0.9\n", + "sources: ['problems/pgs-42898-school-path.md', 'patterns/dp.md', 'patterns/greedy.md']\n" + ] + } + ], + "source": [ + "from graph import ask\n", + "\n", + "TEST_QUESTIONS = [\n", + " \"입국심사 문제는 왜 이분 탐색으로 푸나요?\",\n", + " \"보석 쇼핑에서 슬라이딩 윈도우를 좁힐 때 while 을 써야 하는 이유는?\",\n", + " \"디스크 컨트롤러는 어떤 자료구조로 풀고 핵심 아이디어가 뭔가요?\",\n", + " \"섬 연결하기에서 union-find 는 어떤 역할을 하나요?\",\n", + " \"DFS 로 최단 거리를 구하면 안 되는 이유는?\",\n", + " \"등굣길 문제에서 DP 상태는 어떻게 정의하나요?\",\n", + "]\n", + "\n", + "for q in TEST_QUESTIONS:\n", + " result = ask(q)\n", + " print(\"=\" * 90)\n", + " print(\"Q:\", q)\n", + " print(\"A:\", result[\"answer\"])\n", + " print(\"confidence:\", result[\"confidence\"])\n", + " print(\"sources:\", result[\"sources\"])" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}