알고리즘, 처음부터 차근차근
기호도 용어도 낯선 상태에서 읽기 시작해도 괜찮다. 각 절은 "이게 무엇을 푸는 알고리즘인가"부터 시작해서 작은 예시로 끝난다.
00
시작하기 전에 · 기호와 용어
본문에서 계속 나오는 기호들을 먼저 풀어둔다. 한 번만 보고 지나가도 뒤가 훨씬 편해진다.
수학 기호
- n — 입력 크기. 정렬할 숫자가 100개면 n = 100.
- log n — 밑이 2인 로그. "n을 2로 몇 번 나눠야 1이 되나?" n=8이면 3번(8→4→2→1).
- $\lfloor x \rfloor$ — 버림 (2.7 → 2). $\lceil x \rceil$ — 올림 (2.1 → 3).
- $\sum_{i=1}^{n} f(i)$ — i가 1부터 n까지 가면서 f(i)를 다 더함.
- E[X] — X의 기댓값 (평균적으로 얼마가 나오는지).
- Pr(A) — 사건 A가 일어날 확률.
집합 기호
- ∈ — "속한다." $a \in A$는 "a가 A의 원소다".
- ⊆ — "부분집합이다."
- ∪ / ∩ — 합집합 / 교집합.
- \ — 차집합. $A \setminus \{x\}$는 A에서 x를 뺀 집합.
- ∅ — 빈 집합.
알고리즘 용어
- 배열(array) — 같은 타입의 데이터를 순서대로 나열한 것. A[1], A[2], …
- 재귀(recursion) — 함수가 자기 자신을 호출하는 것.
- 점화식(recurrence) — T(n)을 T(n/2) 같은 더 작은 경우로 표현한 식.
- 피벗(pivot) — 기준이 되는 값.
- 그래프(graph) — 점(노드, 정점)과 점을 잇는 선(간선)으로 이루어진 구조. 지하철 노선도를 떠올리면 된다.
Part I · 기초
1 · 문제와 사례의 구별
문제(problem)는 일반적인 질문이고, 사례(instance)는 거기에 구체적 입력을 넣은 것이다.
- 문제: "정수 n개를 작은 순서로 정렬하라."
- 사례: "[3, 1, 4, 1, 5, 9]를 작은 순서로 정렬하라."
왜 구별해야 하나
같은 알고리즘이라도 어떤 사례가 들어오느냐에 따라 속도가 다르기 때문이다. 예) 삽입 정렬은 이미 정렬된 배열을 주면 빠르지만, 완전히 뒤집힌 배열을 주면 느리다.
풀 수 없는 문제도 있다
"어떤 프로그램이 언젠가 멈출지 판정하라"는 정지 문제(Halting Problem)는 일반적으로 어떤 알고리즘으로도 풀 수 없음이 증명되어 있다. 세상엔 원리적으로 풀 수 없는 문제가 있다는 뜻.
2 · 빅오 표기법
"n이 커질 때 알고리즘이 얼마나 빨리 느려지는가?"를 표현하는 도구.
두 알고리즘의 실행 시간이 각각 $n^2$과 $n^3$이라면, n이 아주 커질 때 $n^2$이 훨씬 덜 커지므로 $n^2$ 쪽이 빠르다. 빅오 표기법은 이 "성장 속도"를 비교할 수 있는 형태로 만든 것이다.
세 가지 표기
| 기호 | 의미 | 말로 하면 |
|---|---|---|
| $f = O(g)$ | 상한 | f는 g보다 빨리 자라지 않는다 |
| $f = \Omega(g)$ | 하한 | f는 g보다 느리게 자라지 않는다 |
| $f = \Theta(g)$ | 양쪽 | f와 g는 같은 속도로 자란다 |
정식 정의: $f(n) = O(g(n))$이란, 어떤 양수 c와 $n_0$이 존재해서 $n \ge n_0$일 때 항상 $f(n) \le c \cdot g(n)$인 것.
예시로 감 잡기
- $3n^2 + 5n = O(n^2)$ — 큰 n에서는 $n^2$ 항이 지배적.
- $n \log n = O(n^2)$ — 상한으로도 맞다 (더 느슨하지만).
- $n^2 \ne O(n)$ — $n^2$은 n보다 훨씬 빨리 자란다.
3 · 점화식과 재귀 트리
재귀 알고리즘의 시간을 계산하는 도구.
점화식이란
자기 자신으로 자기를 정의하는 식. 예) 병합 정렬의 시간:
뜻: n을 반으로 쪼개 두 조각으로 재귀 호출한 뒤(2T(n/2)), 합치는 데 cn 시간이 든다.
재귀 트리로 풀기
각 단계를 트리로 펼쳐보자.
레벨 0: cn (합: cn) 레벨 1: cn/2 cn/2 (합: cn) 레벨 2: cn/4 cn/4 cn/4 cn/4 (합: cn) ... 레벨 log n: n개의 리프, 각 c씩 (합: cn)
각 레벨의 총 비용이 cn으로 일정하고, 레벨 수는 $\log n + 1$이다. 따라서:
Part II · 정렬
4 · 삽입 정렬
무엇을 푸나 — 숫자 배열을 작은 순서(또는 큰 순서)로 재배치한다.
일상 비유
손에 든 카드를 정리할 때 사람이 자연스럽게 쓰는 방식이다. 새 카드를 한 장씩 보면서, 이미 정리된 앞부분에 적절한 자리를 찾아 끼워 넣는다.
알고리즘
INSERTION-SORT(A) for j = 2 to A.length key = A[j] i = j - 1 while i > 0 and A[i] > key A[i+1] = A[i] // 큰 원소를 한 칸 뒤로 i = i - 1 A[i+1] = key // 빈 자리에 key
예시: [5, 2, 4, 6, 1, 3]
| 단계 | 상태 |
|---|---|
| 시작 | [5 | 2, 4, 6, 1, 3] |
| 2 삽입 | [2, 5 | 4, 6, 1, 3] |
| 4 삽입 | [2, 4, 5 | 6, 1, 3] |
| 6 삽입 | [2, 4, 5, 6 | 1, 3] |
| 1 삽입 | [1, 2, 4, 5, 6 | 3] |
| 3 삽입 | [1, 2, 3, 4, 5, 6] |
시간 복잡도
- 최선 (이미 정렬됨): $\Theta(n)$
- 최악 (뒤집힘): $\Theta(n^2)$ — 매번 앞으로 끝까지 밀어야 하므로 $1+2+\cdots+(n-1) = n(n-1)/2$
- 평균: $\Theta(n^2)$
데이터가 작거나 거의 정렬돼 있으면 실제로 빠르다. 하지만 일반적으로는 느린 축에 든다.
5 · 병합 정렬
무엇을 푸나 — 정렬. 단, 최악의 경우에도 $\Theta(n \log n)$을 보장한다.
핵심 아이디어: 쪼개서 각자 정렬 후 합치기
- 분할: 배열을 반으로 나눈다.
- 정복: 두 절반을 재귀적으로 각각 정렬한다.
- 결합(merge): 정렬된 두 절반을 차례로 비교하며 하나로 합친다.
예시: [5, 2, 4, 6, 1, 3, 2, 6]
분할: [5,2,4,6,1,3,2,6]
→ [5,2,4,6] [1,3,2,6]
→ [5,2] [4,6] [1,3] [2,6]
→ [5][2] [4][6] [1][3] [2][6]
결합: [2,5] [4,6] [1,3] [2,6]
→ [2,4,5,6] [1,2,3,6]
→ [1,2,2,3,4,5,6,6]
결합 과정 (핵심)
[2,5]와 [4,6]을 합칠 때: 양쪽 맨 앞을 비교 → 작은 걸 뽑기. "2 vs 4" → 2, "5 vs 4" → 4, "5 vs 6" → 5, 남은 6. 결과 [2,4,5,6].
시간
점화식 $T(n) = 2T(n/2) + \Theta(n)$ → $\Theta(n \log n)$.
어디가 비싼가? 대부분의 일이 합치는(merge) 단계에서 일어난다. 이 점에서 퀵 정렬과 대비된다(퀵은 분할에서 일한다).
6 · 힙 정렬
무엇을 푸나 — 정렬. 힙(heap)이라는 자료구조의 힘을 빌린다.
힙이란
Max-heap: "부모 ≥ 자식"이라는 규칙을 지키는 이진 트리. 맨 위(루트)에 항상 최댓값이 온다. 모양은 "거의 꽉 찬" 이진 트리 — 마지막 줄만 왼쪽부터 일부 채워져 있고 나머지는 다 차 있다.
배열로 저장: 인덱스 i의 부모는 $i/2$, 자식은 2i와 2i+1.
세 가지 함수
① Max-Heapify(A, i) — i와 자식들 중 가장 큰 값을 i로 올린다. 바꾼 자리에서 재귀. 시간 $O(\log n)$.
② Build-Max-Heap(A) — 배열 전체를 힙으로 만든다. 중간부터 거꾸로 Heapify.
BUILD-MAX-HEAP(A) for i = ⌊A.length/2⌋ downto 1 MAX-HEAPIFY(A, i)
시간 $O(n)$ — 겉보기엔 $O(n \log n)$ 같지만, 밑에 있는 노드가 훨씬 많고 그들에겐 할 일이 적어서 총합이 선형으로 떨어진다.
③ Heapsort — 루트(최댓값)를 배열 끝으로 보낸 뒤, 힙 크기를 줄이고 다시 Heapify. n−1번 반복.
비유
스포츠 토너먼트를 떠올리면 된다. 매번 1등을 뽑아 빼고, 나머지로 다시 토너먼트를 돌린다. 차이점은 매번 새로 토너먼트를 짜지 않고 트리 한 줄만 수선한다는 것.
시간
전체: $O(n) + (n-1)\cdot O(\log n) = O(n \log n)$.
7 · 퀵 정렬
무엇을 푸나 — 정렬. 실무에서 가장 많이 쓰이는 정렬.
핵심 아이디어: 기준 세우고 양쪽으로 가르기
- 배열에서 피벗(기준값) 하나 고른다. 보통 맨 뒤.
- 피벗보다 작은 것은 왼쪽, 큰 것은 오른쪽으로 보낸다 → 피벗은 정확히 자기 자리로 간다.
- 왼쪽, 오른쪽 각각에 대해 같은 일을 재귀적으로.
Partition 과정
PARTITION(A, p, r) x = A[r] // 피벗 = 맨 뒤 i = p - 1 for j = p to r-1 if A[j] <= x i = i + 1 swap(A[i], A[j]) swap(A[i+1], A[r]) return i+1 // 피벗의 새 위치
예시: [2, 1, 3, 4, 10, 12, 6, 9, 5] — 피벗 5
왼쪽으로 가는 것: 2, 1, 3, 4. 오른쪽으로 가는 것: 10, 12, 6, 9. 결과: [2,1,3,4, 5, 10,12,6,9]. 5가 자기 자리.
복잡도는 피벗 운에 달렸다
- 최선 (피벗이 중간값): $T(n) = 2T(n/2) + n$ → $\Theta(n \log n)$
- 최악 (피벗이 최대/최소): $T(n) = T(n-1) + n$ → $\Theta(n^2)$
최악을 피하려고 피벗을 무작위로 고르는 게 무작위 퀵 정렬(10절).
Part III · 확률적 분석
8 · 고용 문제
무엇을 푸나 — "평균적으로 몇 번이나 일어나는가?"를 계산하는 연습 문제.
상황
지원자 n명을 한 명씩 면접한다. 지금까지 본 사람 중 가장 뛰어난 사람이 나오면 고용한다 (이전 사람은 해고). 지원자 순서는 완전히 무작위다. 평균 몇 번 고용할까?
직관은 왜 틀리는가
"매번 1등이 바뀌어서 n번쯤 고용할 것 같다"고 생각하기 쉽지만 틀리다. 답은 $O(\log n)$이다.
풀이 아이디어: 0-1 변수로 쪼개기
$X_i = $ "i번째 사람을 고용하면 1, 아니면 0"으로 정의. 그러면 총 고용 수 $X = X_1 + X_2 + \cdots + X_n$.
i번째 사람을 고용하려면 앞 i명 중 1등이어야 한다. 순서가 무작위이니 확률은 $1/i$.
$H_n$은 조화수라고 부르는 유명한 수열이다.
9 · 지시 확률 변수
무엇을 푸나 — 복잡한 기댓값을 쉽게 계산하는 일반적 기법.
정의
어떤 사건 A에 대해 "A가 일어나면 1, 아니면 0"인 변수를 $I\{A\}$로 쓴다.
그 기댓값은 바로 그 사건이 일어날 확률:
기댓값의 선형성
중요: 변수들이 독립이 아니어도 성립. 이게 굉장히 편리하다.
작은 예
공정한 동전을 n번 던졌을 때 앞면이 나오는 횟수의 기댓값은? $X_i = $ "i번째가 앞면이면 1"로 두면 $E[X_i] = 1/2$, 총 $E[X] = n/2$.
10 · 무작위 퀵 정렬의 O(n log n) 증명
무엇을 푸나 — "무작위로 피벗을 고르면 평균 $O(n \log n)$"임을 증명.
전제
- 서로 다른 정수 n개.
- 매번 남은 원소 중 균등 무작위로 피벗 선택.
- 총 비교 횟수를 X라 하자. 실행 시간은 $O(n + X)$.
증명 단계
- 정렬 후 순서대로 원소를 $z_1, z_2, \ldots, z_n$이라 부른다 ($z_i$ = i번째로 작은 값).
- $X_{ij}$ = "$z_i$와 $z_j$가 비교되면 1, 아니면 0"로 정의. 총 비교 $X = \sum_{i
- 핵심 관찰: 퀵 정렬에서 두 원소가 직접 비교되는 건 둘 중 하나가 피벗일 때뿐이다. 집합 $Z_{ij} = \{z_i, z_{i+1}, \ldots, z_j\}$에서 가장 먼저 피벗으로 뽑히는 원소가 $z_i$나 $z_j$여야만 둘이 비교된다. 중간의 원소가 먼저 피벗이 되면 두 원소는 서로 다른 반쪽으로 갈라져 영영 만나지 않는다.
- $|Z_{ij}| = j-i+1$이고 균등 확률이므로:
$E[X_{ij}] = \Pr(z_i \text{ 또는 } z_j\text{가 먼저 피벗}) = \dfrac{2}{j-i+1}$- 선형성과 변수 치환($k = j-i$)으로 합산:
$E[X] = \sum_{i=1}^{n-1}\sum_{k=1}^{n-i}\frac{2}{k+1} < \sum_{i=1}^{n-1} O(\log n) = O(n \log n)$
Part IV · 동적 계획법
11 · DP의 두 가지 특성
무엇을 푸나 — 최적화 문제를 "작은 부분 문제를 풀어 저장해두고 재활용"하는 방식.
DP가 작동하려면 문제가 다음 두 성질을 가져야 한다.
① 최적 부분 구조 (Optimal Substructure)
전체 최적해가 부분 문제들의 최적해로 이루어짐.
② 겹치는 부분 문제 (Overlapping Subproblems)
같은 부분 문제를 반복해서 풀어야 함 → 표에 답을 저장해 재활용.
DP vs 탐욕 비교
| 특성 | 의미 | 필요한 기법 |
|---|---|---|
| Optimal substructure | 최적해가 부분해의 최적해로 | DP, Greedy 둘 다 |
| Overlapping subproblems | 같은 부분 문제 반복 | DP |
| Greedy choice | 순간 최선이 전체 최적 | Greedy |
"잘라 붙이기" 증명
최적 부분 구조를 증명할 때 쓰는 표준 기법: "부분해가 최적이 아니라면 더 좋은 해로 잘라 붙이면 전체가 더 좋아지고, 이건 원래 해가 최적이라는 가정에 모순."
12 · 행렬 연쇄 곱셈
무엇을 푸나 — 여러 행렬을 곱할 때 곱셈 횟수가 최소인 괄호 조합을 찾는다.
왜 순서가 중요한가
행렬 곱은 결합법칙이 성립한다 ($A(BC) = (AB)C$). 그런데 비용은 같지 않다. $p \times q$ 행렬과 $q \times r$ 행렬을 곱하면 스칼라 곱셈은 $p \cdot q \cdot r$번 필요하다. 순서에 따라 비용이 크게 달라진다.
문제 (시험 단골)
- $A_1$: 2 × 3, $A_2$: 3 × 4, $A_3$: 4 × 2, $A_4$: 2 × 3
- 차원 수열: $p_0=2, p_1=3, p_2=4, p_3=2, p_4=3$
점화식
$m[i,j]$ = $A_i$부터 $A_j$까지 곱하는 최소 비용.
해석: 둘로 가르는 모든 방법을 시도해서 제일 싼 걸 고른다.
표를 대각선 순서로 채움
길이 2
- $m[1,2] = 2 \cdot 3 \cdot 4 = 24$
- $m[2,3] = 3 \cdot 4 \cdot 2 = 24$
- $m[3,4] = 4 \cdot 2 \cdot 3 = 24$
길이 3
- $m[1,3]$: k=1이면 $0 + 24 + 2\cdot 3\cdot 2 = 36$, k=2이면 40. → 36
- $m[2,4]$: k=2이면 60, k=3이면 $24 + 0 + 3\cdot 2\cdot 3 = 42$. → 42
길이 4
- $m[1,4]$: k=1이면 60, k=2이면 72, k=3이면 $36 + 0 + 2\cdot 2\cdot 3 = 48$. → 48
최종 표
| m[i,j] | j=1 | j=2 | j=3 | j=4 |
|---|---|---|---|---|
| i=1 | 0 | 24 | 36 | 48 |
| i=2 | — | 0 | 24 | 42 |
| i=3 | — | — | 0 | 24 |
| i=4 | — | — | — | 0 |
최적 괄호화: ((A₁(A₂A₃))A₄). 최소 비용 48.
13 · 최장 공통 부분수열 (LCS)
무엇을 푸나 — 두 문자열 X, Y에서 순서를 지키며 공통으로 뽑을 수 있는 가장 긴 문자열.
어디에 쓰이나
- DNA 비교: 두 유전자 서열의 유사도.
- 파일 diff: git이 두 버전 사이의 공통·차이 부분을 찾을 때.
- 맞춤법/교정 제안.
점화식
$c[i,j]$ = X의 앞 i글자와 Y의 앞 j글자의 LCS 길이.
해석: 마지막 글자가 같으면 "+1", 다르면 한쪽을 포기하고 큰 값 쪽을 선택.
예시: X = GTACCGTCA, Y = TCGA
| ∅ | G | T | A | C | C | G | T | C | A | |
|---|---|---|---|---|---|---|---|---|---|---|
| ∅ | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| T | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| C | 0 | 0 | 1 | 1 | 2 | 2 | 2 | 2 | 2 | 2 |
| G | 0 | 1 | 1 | 1 | 2 | 2 | 3 | 3 | 3 | 3 |
| A | 0 | 1 | 1 | 2 | 2 | 2 | 3 | 3 | 3 | 4 |
LCS 길이 4, 역추적하면 TCGA.
14 · 0/1 배낭 문제
무엇을 푸나 — 용량 W짜리 배낭에 물건을 넣어 총 가치를 최대화. 각 물건은 통째로 넣거나 안 넣거나.
어디에 쓰이나
- 여행 짐 싸기 (한정된 캐리어 용량에 뭘 넣을지).
- 예산 한도 안에서 프로젝트 선택.
- 광고 슬롯에 어떤 광고들을 띄울지.
최적 부분 구조 증명
최적해 S에서 물건 j에 대해 두 경우로 나눈다.
- j ∈ S: $S \setminus \{j\}$는 "물건 집합에서 j 제외, 용량 $W - w_j$"인 부분 문제의 최적해여야 한다. 아니면 더 좋은 해 S'가 존재하고, $S' \cup \{j\}$가 원래 S보다 나아져 모순.
- j ∉ S: S 자체가 "j 제외, 용량 W"인 부분 문제의 최적해여야 한다. 같은 논리.
분할(fractional) 배낭과의 차이
물건을 쪼갤 수 있다면 "가치/무게 비율이 높은 순서로" 채우는 탐욕이 맞다. 그러나 0/1 배낭은 탐욕이 통하지 않는다. 반드시 DP가 필요.
Part V · 탐욕 알고리즘
15 · 활동 선택
무엇을 푸나 — 서로 시간이 겹치지 않는 활동을 최대한 많이 고른다.
실생활 비유
회의실 하나에 예약 신청이 여러 건 들어왔다. 시간이 겹치지 않도록 최대한 많은 예약을 받으려면?
탐욕 전략
종료 시간이 가장 이른 활동부터 고른다. 시작 시간이 이른 순이 아니다.
왜 맞는가 (직관)
빨리 끝나는 걸 먼저 고르면 남은 시간 자원이 최대가 된다. 그래서 뒤에 더 많은 활동을 끼울 수 있다.
증명 (교환 논법)
- 활동을 종료 시간 순 정렬: $f_1 \le f_2 \le \cdots \le f_n$.
- 임의의 최적해 A를 잡고, 그 첫 원소를 k라 하자.
- k = 1이면 끝. 아니면 $A' = (A \setminus \{k\}) \cup \{1\}$을 만든다.
- $f_1 \le f_k$이므로 1은 A의 나머지와 겹치지 않음. |A'| = |A| → A'도 최적해. 활동 1 포함. ∎
작은 예시
| 활동 | a1 | a2 | a3 | a4 | a5 |
|---|---|---|---|---|---|
| 시작 s | 1 | 3 | 0 | 5 | 3 |
| 종료 f | 4 | 5 | 6 | 7 | 9 |
종료순으로 고르면 a1(끝 4) → a4(끝 7). 2개 선택.
16 · 매트로이드
무엇을 푸나 — "어떤 문제가 탐욕으로 풀리나?"에 대한 구조적 답.
정의
쌍 $M = (S, I)$가 매트로이드이려면:
- S는 비어있지 않은 유한 집합.
- I는 S의 부분집합 모음으로 두 조건 만족:
- 상속(Hereditary): $X \in I,\ Y \subseteq X \Rightarrow Y \in I$. (독립 집합의 부분집합도 독립)
- 교환(Exchange): $X, Y \in I,\ |X| > |Y| \Rightarrow$ 어떤 $a \in X \setminus Y$에 대해 $Y \cup \{a\} \in I$. (큰 쪽에서 하나를 빌려 작은 쪽에 넣어도 독립)
핵심 정리
문제 구조가 매트로이드 ⇒ 탐욕으로 풀 수 있다.
역은 성립하지 않는다. 활동 선택은 매트로이드가 아니지만 탐욕으로 풀린다.
예시: Graphic Matroid
무방향 그래프 $G = (V, E)$에서 $S = E$, $I = \{A \subseteq E : A \text{가 사이클 없음}\}$(숲, forest). 이게 매트로이드임을 증명할 수 있다.
- 상속: 사이클 없는 집합의 부분집합에 사이클이 생길 순 없음. ∎
- 교환: 숲에서 간선 수는 (정점 수 − 컴포넌트 수). |A| > |B|면 A가 컴포넌트 수가 적으므로, B의 서로 다른 컴포넌트를 잇는 A의 간선을 하나 빌려오면 된다. ∎
17 · 단위 작업 스케줄링
무엇을 푸나 — 각 작업이 1시간씩 걸리고 마감과 벌점이 있을 때, 총 벌점 최소화.
상황
- 작업 n개, 각각 단위 시간 소요.
- 작업 $a_i$에 마감 $d_i$와 벌점 $w_i$.
- 마감을 넘기면 벌점. 마감 안에 끝낸 작업의 벌점 합을 최대화하면 됨.
매트로이드 정의
- $S$ = 모든 작업 집합.
- $I$ = "모두 마감 안에 끝낼 수 있는 작업들의 부분집합".
상속 증명
$A \in I$이고 $B \subseteq A$면, A의 스케줄에서 B 외 작업을 빼면 된다. B의 작업은 앞당겨져서 오히려 여유로움. ∎
교환 증명 (핵심)
보조 정리: $X \in I$이면 X의 작업을 마감 순으로 정렬해 슬롯 1, 2, ...에 배치하면 유효 스케줄.
A, B가 각각 마감 순으로 정렬 배치됐다고 하자. $|A| > |B|$니까 A에만 있는 작업 중 가장 늦은 시각 t에 배치된 a가 존재한다. $t > |B|$이고 $d_a \ge t > |B|$. 그러면 a를 B의 (|B|+1)번째 슬롯에 넣어도 마감 안전. ∎
Part VI · 무작위 알고리즘
18 · Freivalds 알고리즘
무엇을 푸나 — "$AB = C$인지?"를 실제로 AB를 계산하지 않고 확률적으로 검증.
왜 필요한가
행렬 곱 검증을 직접 하면 $O(n^3)$(Strassen이어도 $O(n^{2.81})$). Freivalds는 $O(n^2)$.
알고리즘
- $x \in \{0, 1\}^n$을 균등 무작위로 고른다 (각 성분이 0 또는 1).
- $ABx$와 $Cx$를 비교. 다르면 NO, 같으면 YES.
$Bx$(벡터-행렬 곱)는 $O(n^2)$, 그 결과에 $A$ 곱도 $O(n^2)$. 총 $O(n^2)$.
오류 분석
$AB = C$면 항상 YES (정확). 문제는 $AB \ne C$인데 YES가 나올 확률.
증명 요약
- $D = AB - C \ne 0$. 적어도 한 성분이 0 아님.
- 일반성을 잃지 않고 첫 행의 앞쪽에 $d_1, d_2, \ldots, d_k$ (모두 0 아님), 뒤는 0이라고 가정.
- $Dx = 0$이려면 특히 $d_1 x_1 + d_2 x_2 + \cdots + d_k x_k = 0$.
- 정리: $x_1 = (-d_2 x_2 - \cdots - d_k x_k)/d_1$.
- $x_2, \ldots, x_k$를 먼저 뽑고 $x_1$을 뽑는다고 하면, 우변은 어떤 고정값 $v$.
- $x_1 \in \{0,1\}$ 균등이니 $x_1 = v$일 확률은 최대 1/2. ∎
k번 반복해 모두 YES일 때만 YES라 하면 오답 확률 $(1/2)^k$. 20번이면 0.0001% 이하.
19 · Tutte 행렬
무엇을 푸나 — 이분 그래프에 완벽 매칭(모두가 짝이 있는 매칭)이 있는지 확률적으로 판정.
어디에 쓰이나
학생-과제 배정, 직원-프로젝트 배정처럼 "양쪽이 같은 수고 모두에게 1:1 매칭이 가능한가?"를 묻는 상황.
Tutte 행렬
이분 그래프 $G = (L \cup R, E)$, $|L|=|R|=n$. $n \times n$ 행렬 T:
- $T[i][j] = x_{ij}$ (변수) — 간선 $(L_i, R_j)$가 있을 때
- $T[i][j] = 0$ — 간선 없을 때
Tutte 정리
완벽 매칭 존재 ⇔ T의 행렬식이 다항식으로서 항등적으로 0이 아님.
알고리즘
- 각 $x_{ij}$에 $\{1, \ldots, m^2\}$에서 무작위 정수 대입 (m = 간선 수).
- 행렬식 계산.
- 0 아니면 YES, 0이면 NO.
False negative 가능(매칭은 있는데 우연히 0이 나옴). 여러 번 반복으로 확률 감소.
20 · 이진 결정 트리 동치 판정
무엇을 푸나 — 두 이진 결정 트리가 같은 불리언 함수인지 확률적으로 판정.
이진 결정 트리
- 내부 노드 = 변수. 왼쪽 자식은 "참일 때", 오른쪽 자식은 "거짓일 때".
- 리프 = T 또는 F (최종 결과).
- 같은 함수도 여러 트리로 표현 가능 → 동치 판정이 쉽지 않음.
아이디어
- 소수 $p > 2n$을 고르고, 각 변수에 $\{0, \ldots, 2n-1\}$에서 무작위 수 $i$ 할당(참일 때 값).
- 거짓일 때 값은 $1 - i \pmod p$.
- T로 끝나는 모든 경로의 변수 값들을 곱한 뒤 모두 더해 "서명"을 만든다(mod p).
- 두 트리의 서명이 같으면 YES.
False positive 가능. 결정론적 다항 시간 알고리즘이 존재하는지 아직 알려지지 않은 문제를 무작위로 현실적으로 푼 사례.
Part VII · 그래프
21 · 연결 컴포넌트
무엇을 푸나 — 그래프에서 서로 연결된 정점 그룹들을 찾아낸다.
어디에 쓰이나
- SNS에서 "친구의 친구의 친구…"로 이어지는 사람들의 묶음.
- 회로에서 서로 연결된 부품 그룹.
Disjoint Set 자료구조
- Make-Set(x): x만 들어있는 새 집합.
- Find-Set(x): x가 속한 집합의 대표.
- Union(x, y): 두 집합을 합치기.
알고리즘
CONNECTED-COMPONENTS(G) for each vertex v ∈ G.V MAKE-SET(v) for each edge (u,v) ∈ G.E if FIND-SET(u) ≠ FIND-SET(v) UNION(u, v)
rank + path compression 최적화로 총 시간 $O((V+E)\alpha(V))$, 사실상 선형.
22 · Warshall 알고리즘
무엇을 푸나 — 방향 그래프에서 모든 쌍 (i, j)에 대해 "i에서 j로 가는 경로가 있나?"를 0/1로.
어디에 쓰이나
"A 공항에서 B 공항까지 비행 노선을 갈아타며 갈 수 있는가?" 같은 도달 가능성 문제.
점화식
$r^{(k)}[i][j]$ = "중간 정점으로 {1, …, k}만 허용했을 때 $i \to j$ 경로 존재".
해석: k를 안 지나도 갈 수 있거나, k를 지나서 갈 수 있거나.
시간 $\Theta(V^3)$. 연산 구조: $\{0,1\}$에 AND/OR.
23 · Floyd 알고리즘
무엇을 푸나 — 가중 방향 그래프에서 모든 쌍 (i, j) 사이의 최단 거리.
어디에 쓰이나
"모든 도시 쌍 사이의 최단 경로"가 필요한 교통망 계산, 경기장 좌석 거리표 등.
점화식
구조가 Warshall과 쌍둥이. 값만 0/1 대신 거리, AND/OR 대신 MIN/PLUS.
시간 $\Theta(V^3)$.
24 · Dijkstra 알고리즘
무엇을 푸나 — 한 출발 정점에서 다른 모든 정점까지의 최단 거리. 모든 간선 가중치가 0 이상일 때만.
어디에 쓰이나
내비게이션, 지도 앱, 네트워크 라우팅.
구조
- $D[s] = 0$, 나머지 정점은 $\infty$.
- min-priority queue에 모든 정점 넣기.
- 큐가 빌 때까지:
- D 값 최소인 $w$ 꺼냄.
- $w$의 이웃들에 대해 "w 경유해 가면 짧아지나?" 검사해 D 갱신.
시간 $O((V+E)\log V)$ (이진 힙 기준).
Part VIII · 과제 문제 복기
25 · 과제 Q3 · X(n) 시간 복잡도
X(n) r = 0 for i = 1 to n-1 do for j = i+1 to n do for k = 1 to j do r = r + 1 return r
풀이
- 가장 안쪽 루프는 j번 실행 → r에 j 추가.
- 중간 루프: $\sum_{j=i+1}^{n} j = \frac{n(n+1)}{2} - \frac{i(i+1)}{2}$.
- 바깥 루프까지 합: $$r = \sum_{i=1}^{n-1}\!\left[\frac{n(n+1)}{2} - \frac{i(i+1)}{2}\right] = \frac{n(n+1)(n-1)}{2} - \frac{1}{2}\!\sum_{i=1}^{n-1}\!i^2 - \frac{1}{2}\!\sum_{i=1}^{n-1}\!i$$
- $\sum i^2$, $\sum i$ 공식 대입 후 $\frac{n(n-1)}{2}$로 묶으면:
26 · 과제 Q4 · 점화식 풀이
$T(1) = 1,\ T(n) = 3T(n-1) + 2$ ($n \ge 2$). 닫힌 형태로 구하라.
반복 대입
- $T(2) = 3 + 2 = 3^1 + 3^0 \cdot 2$
- $T(3) = 3^2 + (3^1 + 3^0) \cdot 2$
- $T(4) = 3^3 + (3^2 + 3^1 + 3^0) \cdot 2$
패턴: $T(n) = 3^{n-1} + (3^{n-2} + \cdots + 3^0) \cdot 2 = 3^{n-1} + (3^{n-1} - 1)$.
27 · 재귀 함수 trace
A = [3, 1, 2], n = 3. Y(1)의 실행 결과는?
Y(i) if i == n then print A else Y(i+1) for j = i+1 to n swap(A[i], A[j]) Y(i+1) swap(A[i], A[j]) // 원복
의도: 배열의 모든 순열을 생성.
Trace
- Y(1): A = [3,1,2]. Y(2) 호출
- Y(3): print [3,1,2]
- j=3: swap → [3,2,1], Y(3) print [3,2,1], 원복 → [3,1,2]
- j=2: swap(A[1],A[2]) → [1,3,2], Y(2)
- Y(3): print [1,3,2]
- j=3: swap → [1,2,3], Y(3) print [1,2,3], 원복 → [1,3,2]
- j=3: swap(A[1],A[3]) → [2,1,3], Y(2)
- Y(3): print [2,1,3]
- j=3: swap → [2,3,1], Y(3) print [2,3,1], 원복 → [2,1,3]
출력: [3,1,2], [3,2,1], [1,3,2], [1,2,3], [2,1,3], [2,3,1] — 총 3! = 6개.
Part IX · 체크리스트
28 · 시험 당일 확인 목록
외워둬야 할 것
- 행렬 연쇄 $m[i,j]$ 표 → 48
- 단위 작업 스케줄링이 매트로이드임의 증명 (상속 + 교환)
- 활동 선택 탐욕 선택 성질 증명 (교환 논법)
- 무작위 퀵 정렬 $O(n \log n)$ 증명 ($X_{ij}$, $2/(j-i+1)$)
- Freivalds 오답 증명 ($d^\top x = 0 \Rightarrow x_1$ 확률 $\le 1/2$)
- $X(n) = n(n-1)(n+1)/3$
- $T(n) = 3T(n-1) + 2 \Rightarrow 2 \cdot 3^{n-1} - 1$
증명에서 잘 빠뜨리는 부분
- 매트로이드 교환: "$|A| > |B|$"와 "$a \in A \setminus B$"를 명시.
- 퀵 정렬 분석: "가장 먼저 피벗이 되는 원소" 문구.
- DP 최적 부분 구조: cut-and-paste로 모순 도출.
- 활동 선택: "종료 시간 순 정렬" 가정과 $f_1 \le f_k$.
답안 작성 요령
- 재귀 trace는 한 단계도 빠뜨리지 말 것.
- 행렬 연쇄는 길이 2 → 3 → 4 순으로, k 값과 계산 과정 모두 표시.
- 증명은 "최적해 가정 → 부분해도 최적 주장 → 반대 가정 시 모순" 3단 유지.
- $\Theta$와 $O$를 구별. 상한만 요구하면 $O$면 충분.
"왜 이 기법이 여기서 작동하는가"를 말로 설명할 수 있으면 비슷한 신규 문제도 풀린다.