Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions assignments/pykido/week3/compare_chunking.py
Original file line number Diff line number Diff line change
@@ -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()
40 changes: 40 additions & 0 deletions assignments/pykido/week3/data/patterns/backtracking.md
Original file line number Diff line number Diff line change
@@ -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
- 부분집합의 합
48 changes: 48 additions & 0 deletions assignments/pykido/week3/data/patterns/bfs-dfs.md
Original file line number Diff line number Diff line change
@@ -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)
50 changes: 50 additions & 0 deletions assignments/pykido/week3/data/patterns/binary-search.md
Original file line number Diff line number Diff line change
@@ -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, 답이 시간에 대해 단조)
- 징검다리 (최소 점프 거리를 이분 탐색)
- 정렬된 배열에서의 값 존재 여부 / 개수 세기
43 changes: 43 additions & 0 deletions assignments/pykido/week3/data/patterns/dijkstra.md
Original file line number Diff line number Diff line change
@@ -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 로 가중치 최단 경로를 구하려 한다.

## 연관 문제
- 가중 그래프 최단 경로
- 배달 (여러 목적지까지 최단 거리)
- 최소 비용 격자 이동
50 changes: 50 additions & 0 deletions assignments/pykido/week3/data/patterns/dp.md
Original file line number Diff line number Diff line change
@@ -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 배낭
35 changes: 35 additions & 0 deletions assignments/pykido/week3/data/patterns/greedy.md
Original file line number Diff line number Diff line change
@@ -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)
- 회의실 배정
- 분할 가능 배낭
43 changes: 43 additions & 0 deletions assignments/pykido/week3/data/patterns/hash-map.md
Original file line number Diff line number Diff line change
@@ -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 로 추적)
- 두 수의 합
- 애너그램 그룹화
Loading