코스

수학과 정수론

정수론, 조합론, 모듈러 연산과 고급 대수까지.

Level 1 → Level 9 138 아이템 54 문제 84 강의 0 확인 문제
코스 진행도 0%
0 / 138 아이템 완료
01
Level 1 · Beginner

Beginner

수학과 정수론 · Beginner 단계

0/25 완료
Lesson 사칙연산의 기본과 정수 나눗셈·나머지 필수 8m 현재

사칙연산이란

덧셈 +, 뺄셈 -, 곱셈 *, 나눗셈 /, 그리고 나머지 %. 이 다섯이
계산의 기본입니다. 언어마다 나눗셈과 나머지의 미묘한 규칙 차이가 있어,
그 차이를 아는 것이 이 단원의 핵심입니다.


1. 다섯 연산자

int a = 7, b = 2;
cout << a + b << '\n';   // 9
cout << a - b << '\n';   // 5
cout << a * b << '\n';   // 14
cout << a / b << '\n';   // 3   ← 정수 나눗셈(소수점 버림)
cout << a % b << '\n';   // 1   ← 나머지
a, b = 7, 2
print(a + b, a - b, a * b)   # 9 5 14
print(a / b)    # 3.5  ← 파이썬 / 는 항상 실수
print(a // b)   # 3    ← // 가 정수 나눗셈
print(a % b)    # 1    ← 나머지

C++의 / 는 정수끼리면 정수 나눗셈(소수점 버림), 파이썬의 /항상
실수 나눗셈
이고 정수 나눗셈은 // 라는 점이 가장 큰 차이입니다.


2. 실수 나눗셈이 필요할 때 (C++)

int a = 7, b = 2;
cout << (double)a / b << '\n';   // 3.5 — 한쪽을 double로 형변환

한쪽만 double 이면 나머지도 실수로 계산됩니다. 평균 (a+b)/2 가 정수로
잘리는 실수가 여기서 자주 납니다.


3. 나머지(%)의 쓸모

%주기·짝홀·자릿수 문제의 핵심 도구입니다.

if (x % 2 == 0) { /* 짝수 */ }
int last = n % 10;      // n의 일의 자리
int rest = n / 10;      // 일의 자리를 뗀 나머지
if x % 2 == 0:
    pass
last = n % 10
rest = n // 10

\(n\)\(10\)으로 나눈 나머지는 일의 자리, \(10\)으로 나눈 몫은 나머지 자릿수 —
이 조합으로 자릿수를 하나씩 분해합니다.


4. 연산자 우선순위

*, /, %+, - 보다 먼저입니다. 순서를 바꾸려면 괄호를 쓰세요.

cout << 2 + 3 * 4 << '\n';     // 14  (곱셈 먼저)
cout << (2 + 3) * 4 << '\n';   // 20  (괄호로 덧셈 먼저)

헷갈리면 괄호를 넉넉히 쓰는 게 안전합니다.


정리

  • 다섯 연산: + - * / %.
  • C++ int/int = 정수 나눗셈, 파이썬 / = 실수·// = 정수.
  • % 는 짝홀·주기·자릿수 분해의 핵심.
  • 우선순위가 헷갈리면 괄호로 명시.
Lesson 음수 나눗셈·나머지, 오버플로, 실수 오차 선택 8m

조심해야 할 세 가지 함정

사칙연산에서 오답을 부르는 대표 함정은 음수 나눗셈/나머지, 오버플로,
실수 오차 세 가지입니다.


1. 음수의 나눗셈과 나머지 — 언어가 다르다!

이 차이는 반드시 외워 두세요.

cout << (-7) / 2 << '\n';   // -3  (0쪽으로 버림, truncation)
cout << (-7) % 2 << '\n';   // -1  (부호는 피제수를 따른다)
print(-7 // 2)   # -4  (음의 무한대 쪽으로 내림, floor)
print(-7 % 2)    #  1  (나머지가 항상 0 이상)
  • C++: 나눗셈은 0쪽으로 버림, 나머지 부호는 왼쪽 피연산자를 따름 →
    % 결과가 음수일 수 있음.
  • 파이썬: 나눗셈은 아래로 내림(floor), % 결과는 제수와 같은 부호
    (양수 제수면 항상 \(\ge 0\)).

"나머지를 항상 \(0\) 이상으로" 만들고 싶으면 C++에선 이렇게:

int m = ((a % b) + b) % b;   // 항상 0 이상의 나머지

2. 오버플로 — 곱셈이 특히 위험

int x = 100000, y = 100000;
long long bad  = x * y;             // int로 계산돼 이미 망가짐
long long good = (long long)x * y;  // 곱하기 전에 형변환 — 안전 (10^10)

합·곱이 \(21\)억을 넘을 수 있으면 long long. 곱셈은 곱하기 전에 한쪽을
(long long) 으로 올려야 합니다. 파이썬은 정수 크기 제한이 없어 안전합니다.


3. 실수 오차 — == 금지

double d = 0.1 + 0.2;
cout << (d == 0.3) << '\n';         // 0 (false!)
if (abs(d - 0.3) < 1e-9) { }        // 오차 허용 비교

가능하면 실수를 아예 피하고 정수로 계산하세요. 예를 들어 "\(a/b\) 가 정수인가"
a / b 를 실수로 구해 비교하지 말고 a % b == 0 로 판단합니다.


4. 0으로 나누기

x / 0, x % 0런타임 에러(또는 정의되지 않음)입니다. 나누기 전에
분모가 \(0\)이 아닌지 확인하세요.

if (b != 0) cout << a / b << '\n';

정리

  • 음수: C++ % 는 음수가 나올 수 있음 → ((a%b)+b)%b 로 정규화.
  • 오버플로: 곱셈은 곱하기 전에 (long long).
  • 실수는 == 금지, 되도록 정수로. 정수 나눗셈 판정은 a % b == 0.
  • 0으로 나누기 방지.
Lesson 거듭제곱·최대공약수·나머지 연산 실전 선택 8m

자주 쓰는 계산 도구들

기본 사칙연산 위에, 실전에서 반복적으로 등장하는 계산 패턴을 정리합니다.
거듭제곱, 최대공약수/최소공배수, 그리고 큰 수를 다루는 모듈러 연산입니다.


1. 거듭제곱

C++의 pow 는 실수 함수라 큰 정수에서 오차가 납니다. 정수 거듭제곱은 직접
곱하세요.

long long p = 1;
for (int i = 0; i < e; i++) p *= base;   // base^e (정수, 오버플로 주의)
print(base ** e)     # 파이썬은 ** 로 정수 거듭제곱, 크기 제한 없음

빠른 거듭제곱(분할 정복)은 지수가 아주 클 때 쓰지만, Bronze 범위에선 단순
반복이면 충분합니다.


2. 최대공약수(GCD)와 최소공배수(LCM)

유클리드 호제법으로 \(O(\log(\min(a,b)))\) 에 구합니다.

long long gcd(long long a, long long b) {
    while (b) { long long t = a % b; a = b; b = t; }
    return a;
}
long long lcm(long long a, long long b) {
    return a / gcd(a, b) * b;   // 먼저 나눠야 오버플로 방지
}

C++17 이후엔 <numeric>std::gcd(a, b), std::lcm(a, b) 도 있습니다.

import math
math.gcd(a, b)
math.lcm(a, b)     # Python 3.9+

lcma * b / gcd 인데, 먼저 나누고 곱해야 중간값 오버플로를 피합니다.


3. 모듈러 연산 — "1000000007로 나눈 나머지"

답이 아주 커서 "\(10^9+7\) 로 나눈 나머지를 출력하라"는 문제가 많습니다. 덧셈·
곱셈은 매 단계 나머지를 취해도 결과가 같습니다.

$$ (a + b) \bmod m = ((a \bmod m) + (b \bmod m)) \bmod m $$
$$ (a \times b) \bmod m = ((a \bmod m) \times (b \bmod m)) \bmod m $$

const long long MOD = 1000000007;
long long ans = 1;
for (int i = 1; i <= n; i++)
    ans = ans * i % MOD;      // n! mod MOD — 매 단계 나머지
cout << ans << '\n';
MOD = 1000000007
ans = 1
for i in range(1, n + 1):
    ans = ans * i % MOD
print(ans)

주의: 뺄셈은 나머지가 음수가 될 수 있으니 ((a - b) % MOD + MOD) % MOD
로 정규화하세요. 나눗셈은 모듈러 역원이 필요해 단순히 / 하면 안 됩니다.


4. 자주 하는 실수

  • pow(10, 9) 처럼 실수 함수로 정수를 구해 오차 발생 → 직접 곱하거나 **.
  • lcm 에서 a * b 를 먼저 해 오버플로 → a / gcd * b.
  • 모듈러에서 뺄셈 음수 처리 누락.
  • 곱을 누적하며 나머지를 안 취해 오버플로.

정리

  • 정수 거듭제곱은 직접 곱셈(또는 파이썬 **), pow 오차 주의.
  • GCD는 유클리드 호제법 \(O(\log n)\), LCM은 a/gcd*b.
  • 큰 답은 매 단계 % MOD. 뺄셈은 +MOD%MOD 로 음수 방지.
Practice problem 직각이등변삼각형 선택 25m
KOI00002

직각이등변삼각형

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem 장애물 선택 25m
KOI00008

장애물

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Bronze III 브론즈 III 지금 풀기
Lesson 1강 · 개념과 정리 필수 8m

나머지 연산이란

\(a \bmod m\)(코드에서 a % m)은 \(a\)\(m\)으로 나눈 나머지입니다. 단순해
보이지만 "주기", "홀짝", "큰 수 다루기", "합동식" 등 수많은 문제의 핵심 도구입니다.


1. 기본 의미와 합동

양의 정수 기준으로 \(a \bmod m\)의 결과는 항상 \(0\) 이상 \(m\) 미만입니다.

$$ a = m \cdot \Big\lfloor \tfrac{a}{m} \Big\rfloor + (a \bmod m), \qquad 0 \le a\bmod m < m $$

예를 들어 \(17 \bmod 5 = 2\)이고(\(17 = 5\times 3 + 2\)), \(10 \bmod 2 = 0\)입니다. 결과가
\(0 \sim m-1\)이라 값을 \(m\)가지로 분류하는 데 자연스럽게 쓰입니다. 두 수 \(a, b\)
\(m\)으로 나눈 나머지가 같으면 \(a \equiv b \pmod m\)이라 쓰고 "합동(congruent)"이라
합니다.


2. 가장 흔한 쓰임

  • 홀짝 판정: n % 2 == 0이면 짝수.
  • 배수 판정: "\(k\)의 배수인가?"는 n % k == 0.
  • 순환(주기): 시계처럼 일정 주기로 돌아오는 값. (now + add) % 24.

3. 나머지 연산의 성질 — 왜 성립하는가

큰 수를 다룰 때 결정적인 성질입니다. 나머지는 더하기·빼기·곱하기 전에 미리 취해도
결과가 같습니다.

$$ (a + b) \bmod m = \big((a\bmod m) + (b\bmod m)\big)\bmod m $$
$$ (a - b) \bmod m = \big((a\bmod m) - (b\bmod m) + m\big)\bmod m $$
$$ (a \times b) \bmod m = \big((a\bmod m)\times(b\bmod m)\big)\bmod m $$

왜 성립하나? \(a = q_1\ m + r_1\), \(b = q_2\ m + r_2\)로 쓰면

$$ a + b = (q_1+q_2)m + (r_1 + r_2) $$

이므로 \(m\)으로 나눈 나머지는 \(r_1 + r_2\)의 나머지와 같습니다. 곱셈도

$$ a\times b = (q_1 m + r_1)(q_2 m + r_2) = m\big(q_1 q_2 m + q_1 r_2 + q_2 r_1\big) + r_1 r_2 $$

에서 \(m\)의 배수 부분이 사라져 \(r_1\ r_2\)의 나머지만 남습니다. 덕분에 "답을
\(1{,}000{,}000{,}007\)로 나눈 나머지를 출력" 같은 문제에서 중간중간 나머지를 취해
수가 커지는 것을 막습니다.

예: \(123 \times 456 \bmod 100\). \(123 \bmod 100 = 23\), \(456 \bmod 100 = 56\),
\(23 \times 56 = 1288\), \(1288 \bmod 100 = 88\). 직접 \(123\times456 = 56088\)의 나머지도
\(88\)로 일치합니다.


4. 뺄셈에서의 음수 주의(분배법칙)

뺄셈에 분배법칙을 쓸 때 \((a\bmod m) - (b\bmod m)\)음수가 될 수 있습니다.
그래서 위 식처럼 \(+m\)을 더한 뒤 다시 나머지를 취해야 \(0 \sim m-1\)이 보장됩니다.
이 점을 놓치면 답이 음수로 나옵니다.


5. C++의 음수 나머지

C++에서 -7 % 3-1입니다(음수!). C++의 %는 몫을 \(0\)쪽으로 버림하기
때문입니다. "항상 \(0\) 이상"으로 만들려면 표준 보정식을 씁니다.

$$ r = ((x \bmod m) + m) \bmod m $$

파이썬의 %는 결과 부호가 제수 \(m\)을 따라가 항상 \(0\) 이상이라(-7 % 3 == 2) 이
보정이 필요 없습니다. 언어 차이를 반드시 기억하세요.


복잡도

단일 나머지 연산은 \(O(1)\)입니다. 다만 큰 수 문제에서는 "언제 나머지를 취할지",
"거듭제곱을 어떻게 빠르게 할지"의 설계가 핵심입니다(구현 단원에서 다룹니다).


정리

  • \(a \bmod m\)은 양수 기준 \(0 \sim m-1\), 같은 나머지면 \(a\equiv b\pmod m\).
  • 더하기·빼기·곱하기 전에 나머지를 취해도 결과 동일 — 오버플로 방지.
  • 뺄셈은 음수가 될 수 있어 \(+m\) 후 다시 나머지.
  • C++의 음수 나머지는 음수 가능 → ((x%m)+m)%m. 파이썬은 불필요.
Lesson 2강 · 구현 선택 8m

나머지 연산 구현 레퍼런스

큰 수의 나머지, 모듈러 거듭제곱(빠른 거듭제곱), 그리고 곱셈 오버플로 대처를
코드로 정리합니다. 곱셈은 반드시 long long을 씁니다.


1. 곱셈 오버플로 — 언제 넘치나

\(m\)이 약 \(10^9\)이면 나머지끼리의 곱은 최대 \((10^9)^2 = 10^{18}\)에 가까워집니다.
long long의 최댓값은 약 \(9.2\times 10^{18}\)이라 아슬아슬하게 버팁니다. 하지만
\(m\)\(10^9\)보다 크거나(예: \(m \approx 10^{18}\)) 나머지를 취하기 전에 곱하면 즉시
넘칩니다. 곱한 직후 나머지를 취하는 습관이 중요합니다.

const long long MOD = 1000000007;
long long a = 999999999, b = 999999999;
long long c = a % MOD * (b % MOD) % MOD;   // 각 항 축소 후 곱, 즉시 %

\(m\) 자체가 \(10^{18}\) 규모라 두 나머지의 곱이 long long을 넘는다면 __int128
쓰거나 곱셈을 분할합니다.

// 방법 A: __int128 (GCC/Clang)
long long mulmod(long long a, long long b, long long m) {
    return (long long)((__int128)a * b % m);
}

// 방법 B: 이진 곱셈(더하기 기반, __int128 없이) - a*b % m을 O(log b)에
long long mulmod2(long long a, long long b, long long m) {
    a %= m; b %= m;
    long long res = 0;
    while (b > 0) {
        if (b & 1) res = (res + a) % m;   // 더하기는 넘치지 않음
        a = (a + a) % m;
        b >>= 1;
    }
    return res;
}
# 파이썬은 정수 크기 제한이 없어 오버플로가 없습니다
print(999999999 * 999999999 % 1000000007)

2. 모듈러 거듭제곱 (빠른 거듭제곱)

\(a^n \bmod m\)을 곧이곧대로 \(n\)번 곱하면 \(O(n)\)이라 \(n\)이 크면 느립니다. 지수를
이진법으로 쪼개는 분할 정복으로 \(O(\log n)\)에 계산합니다. 핵심은
\(a^{2k} = (a^k)^2\), \(a^{2k+1} = a\cdot(a^k)^2\)입니다.

#include <bits/stdc++.h>
using namespace std;

long long power(long long a, long long n, long long m) {
    a %= m;
    long long result = 1;
    while (n > 0) {
        if (n & 1) result = result * a % m;   // 지수의 이 비트가 1이면 곱함
        a = a * a % m;                         // 밑을 제곱
        n >>= 1;
    }
    return result;
}

int main() {
    cout << power(2, 10, 1000000007) << '\n';   // 1024
    cout << power(3, 0, 7) << '\n';             // 1  (a^0 = 1)
    cout << power(7, 100, 13) << '\n';          // 검증용
    return 0;
}
def power(a, n, m):
    a %= m
    result = 1
    while n > 0:
        if n & 1:
            result = result * a % m
        a = a * a % m
        n >>= 1
    return result

print(power(2, 10, 1000000007))   # 1024
print(pow(2, 10, 1000000007))     # 1024 — 파이썬 내장 pow(a, n, m)

파이썬은 pow(a, n, m) 한 줄로 같은 일을 합니다(내부적으로 같은 알고리즘). 위
power에서 m\(10^9\) 규모를 넘으면 result * a가 넘칠 수 있으니 1절의
mulmod로 바꿔야 합니다.


3. 팩토리얼·수열의 나머지

"답을 \(10^9+7\)로 나눈 나머지" 유형은 매 단계 나머지를 취합니다.

const long long MOD = 1000000007;
long long fact = 1;
for (int i = 1; i <= n; i++)
    fact = fact * i % MOD;   // 매번 나머지 -> 오버플로 방지
MOD = 1000000007
fact = 1
for i in range(1, n + 1):
    fact = fact * i % MOD

4. 함정과 패턴 인식

  • 오버플로 — 나머지 취하기 전 곱셈이 이미 넘침. 곱은 long long, 곱 직후 %.
  • 음수 나머지 — C++에서 입력·중간 값이 음수면 결과가 음수. ((x%m)+m)%m 보정.
    특히 뺄셈 후 반드시 확인.
  • 나머지 깜빡 — 마지막에만 나머지 취하면 이미 늦음. 중간마다.
  • 거듭제곱을 반복 곱으로\(n\)이 크면 \(O(n)\)은 시간 초과. 빠른 거듭제곱 \(O(\log n)\).
  • 큰 법\(m > 10^9\)면 나머지끼리 곱해도 long long 초과 가능 → __int128/mulmod.

문제에 "~로 나눈 나머지를 출력", "\(a^n \bmod m\)", "매우 큰 수", "홀수/짝수",
"주기적으로 반복"
같은 표현이 보이면 나머지 연산이 열쇠입니다.


정리

  • 곱셈은 long long, 곱 직후 나머지로 오버플로 차단.
  • \(a^n \bmod m\)빠른 거듭제곱 \(O(\log n)\)(파이썬은 pow(a,n,m)).
  • 법이 \(10^9\)를 넘으면 __int128 또는 이진 곱셈 mulmod.
  • 음수 나머지 보정 ((x%m)+m)%m, 특히 뺄셈 후.
Lesson 3강 · 심화와 변형 선택 8m

나머지 연산의 심화 — 모듈러 역원과 나눗셈

덧셈·뺄셈·곱셈의 나머지는 배웠습니다. 그런데 나눗셈은 어떨까요?
\((a / b) \bmod m\)은 단순히 \((a\bmod m)/(b\bmod m)\)아닙니다. 이 강의에서는
모듈러 세계의 나눗셈, 즉 모듈러 역원을 다룹니다.


1. 왜 나눗셈은 다른가

나머지 연산에서 "\(b\)로 나누기"는 "\(b\)역원(inverse) 을 곱하기"로 정의됩니다.
\(b\)의 모듈러 역원 \(b^{-1}\)은 다음을 만족하는 수입니다.

$$ b \cdot b^{-1} \equiv 1 \pmod m $$

이 역원이 존재하려면 \(\gcd(b, m) = 1\)(서로소)이어야 합니다. 존재하면

$$ \frac{a}{b} \bmod m = a \cdot b^{-1} \bmod m $$

으로 계산합니다. 예를 들어 \(m=7\)에서 \(3\)의 역원은 \(5\)입니다(\(3\times 5 = 15 \equiv 1\)).
따라서 \(\bmod 7\)에서 "\(\div 3\)"은 "\(\times 5\)"와 같습니다.


2. 페르마 소정리로 역원 구하기 (법이 소수)

\(m = p\)소수이고 \(a\)\(p\)의 배수가 아니면, 페르마 소정리가 성립합니다.

$$ a^{p-1} \equiv 1 \pmod p $$

양변을 \(a\)로 나누면 \(a \cdot a^{p-2} \equiv 1\)이므로,

$$ a^{-1} \equiv a^{p-2} \pmod p $$

즉 역원은 빠른 거듭제곱 한 번으로 \(O(\log p)\)에 구합니다. 경쟁 프로그래밍에서
법이 \(10^9+7\)(소수)인 경우가 많아 이 방법이 표준입니다.

#include <bits/stdc++.h>
using namespace std;
const long long MOD = 1000000007;

long long power(long long a, long long n, long long m) {
    a %= m;
    long long r = 1;
    while (n > 0) {
        if (n & 1) r = r * a % m;
        a = a * a % m;
        n >>= 1;
    }
    return r;
}

long long inverse(long long a) {
    return power(a, MOD - 2, MOD);   // 페르마: a^(p-2)
}

int main() {
    long long inv3 = power(3, 7 - 2, 7);   // mod 7에서 3의 역원
    cout << inv3 << '\n';                   // 5
    cout << 3 * inv3 % 7 << '\n';           // 1 (검증)
    // mod 1e9+7 에서 (10 / 3):
    cout << 10 * inverse(3) % MOD << '\n';
    return 0;
}
MOD = 1000000007

def inverse(a, m=MOD):
    return pow(a, m - 2, m)      # 페르마 소정리 (m이 소수)

print(pow(3, 7 - 2, 7))          # 5
print(inverse(3))                # mod 1e9+7 에서 3의 역원
print(10 * inverse(3) % MOD)     # (10/3) mod p
# 파이썬 3.8+: pow(3, -1, 7) 로 역원 직접 (법이 소수 아니어도 서로소면 OK)
print(pow(3, -1, 7))             # 5

법이 소수가 아니면 페르마를 못 쓰므로, 약수·배수 3강의 확장 유클리드
\(a\ x + m\ y = 1\)을 풀어 역원을 구합니다(서로소일 때만 존재).


3. 이항계수의 나머지 — 대표 응용

\(\binom{n}{r} \bmod p = \dfrac{n!}{r!\,(n-r)!} \bmod p\)는 나눗셈을 포함하므로 역원이
필요합니다. 팩토리얼을 미리 계산해 두고 분모를 역원으로 곱합니다.

long long C(long long n, long long r) {          // n! / (r! (n-r)!)  mod p
    if (r < 0 || r > n) return 0;
    long long num = 1, den = 1;
    for (long long i = 0; i < r; i++) {
        num = num * ((n - i) % MOD) % MOD;
        den = den * ((i + 1) % MOD) % MOD;
    }
    return num * power(den, MOD - 2, MOD) % MOD;  // 분모의 역원을 곱함
}

4. 함정 정리

  • 나눗셈 = 역원 곱: 절대 (a % m) / (b % m)으로 나누지 마세요. 틀립니다.
  • 역원 존재 조건: \(\gcd(b, m) = 1\). 서로소가 아니면 역원 없음.
  • 페르마는 소수 법 전용: \(p\)가 소수이고 \(a \not\equiv 0\)일 때만. 합성수 법은
    확장 유클리드.
  • \(a \equiv 0 \pmod p\): \(0\)의 역원은 없습니다. 분모가 \(p\)의 배수인 경우를 조심.
  • 오버플로·음수: 앞 단원과 동일하게 곱은 long long, 필요시 보정.

정리

  • 모듈러 나눗셈은 역원 곱하기: \(a/b \equiv a\cdot b^{-1}\pmod m\).
  • 역원 존재 조건은 \(\gcd(b,m)=1\).
  • 소수 법이면 페르마 소정리로 \(a^{-1}\equiv a^{p-2}\), 빠른 거듭제곱 \(O(\log p)\).
  • 합성수 법이면 확장 유클리드. 이항계수 등 팩토리얼 나눗셈의 핵심 도구.
Practice problem 직각이등변삼각형 선택 25m
KOI00002

직각이등변삼각형

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem 장애물 선택 25m
KOI00008

장애물

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Bronze III 브론즈 III 지금 풀기
Lesson 1강 · 개념과 정리 필수 8m

약수와 배수란

정수 \(a\)를 정수 \(b\,(b \ne 0)\)로 나누어 떨어질 때, 즉 \(a \bmod b = 0\)일 때, \(b\)
\(a\)약수(divisor), \(a\)\(b\)배수(multiple) 라고 합니다. 이 관계 위에
세워지는 최대공약수(GCD)최소공배수(LCM) 는 정수론 문제의 가장 기본적인
도구입니다.


1. 약수 구하기와 \(\sqrt N\) 관찰

\(N\)의 약수를 찾으려면 \(1\)부터 \(N\)까지 모두 나눠 보면 되지만(\(O(N)\)), 훨씬 빠른
방법이 있습니다. 약수는 항상 짝을 이룹니다. \(i\)\(N\)의 약수이면 \(N/i\)
약수이기 때문입니다.

$$ N = i \times \frac{N}{i} $$

두 인수 \(i\)\(N/i\) 중 작은 쪽은 반드시 \(\sqrt N\) 이하입니다. 따라서 \(1\)부터
\(\sqrt N\)까지만 확인하면서 약수 \(i\)를 찾을 때마다 짝꿍 \(N/i\)도 함께 기록하면 모든
약수를 얻습니다. 시간은 \(O(\sqrt N)\)으로 줄어듭니다.

예: \(N = 36\)의 약수. \(i\)\(1\sim 6\)까지 보면

$$ 1\,(36),\; 2\,(18),\; 3\,(12),\; 4\,(9),\; 6\,(6) $$

\(i=6\)일 때는 \(N/i=6\)으로 같으므로 한 번만 셉니다. 약수는 \(1,2,3,4,6,9,12,18,36\)
\(9\)개입니다.


2. 약수의 개수와 합

약수를 나열하면서 개수를 세거나 값을 더하면 약수의 개수약수의 합을 바로
얻습니다. \(36\)의 약수 합은 \(1+2+3+4+6+9+12+18+36 = 91\)입니다. (소인수분해를 이용한
공식도 있지만, \(\sqrt N\) 나열만으로도 충분히 빠릅니다.)


3. 최대공약수(GCD)와 유클리드 호제법

두 수의 공통 약수 중 가장 큰 것이 GCD입니다. 유클리드 호제법으로 매우 빠르게
구합니다.

$$ \gcd(a, b) = \gcd(b,\; a \bmod b), \qquad \gcd(a, 0) = a $$

즉 "큰 수를 작은 수로 나눈 나머지로 바꾸기"를 나머지가 \(0\)이 될 때까지 반복합니다.

gcd(24, 18)
= gcd(18, 24 % 18 = 6)
= gcd(6,  18 % 6  = 0)
= 6

왜 성립하나(직관)? \(g\)\(a,b\)의 공약수이면 \(a - qb\)(\(=a \bmod b\))도 \(g\)로 나누어
떨어집니다. 반대로 \(b\)\(a \bmod b\)의 공약수는 \(a\)도 나눕니다. 따라서 \((a,b)\)
공약수 집합과 \((b, a\bmod b)\)의 공약수 집합이 완전히 같고, 그 최댓값인 GCD도 같습니다.
나머지는 반복할 때마다 반드시 줄어들어 유한 번에 \(0\)에 도달합니다.

복잡도: 한 단계마다 값이 적어도 절반 이하로 줄어드는 것이 보장되어(연속 두
단계에서) \(O(\log \min(a,b))\)입니다. 이는 \(1\)부터 다 나눠 보는 것과 비교가 안 되게
빠릅니다.


4. 최소공배수(LCM)

두 수의 공통 배수 중 가장 작은 것입니다. GCD와의 관계식으로 구합니다.

$$ \operatorname{lcm}(a, b) = \frac{a \times b}{\gcd(a, b)} $$

이는 \(a \times b = \gcd(a,b) \times \operatorname{lcm}(a,b)\)라는 항등식에서 나옵니다.
구현할 때는 \(a \times b\)가 오버플로할 수 있으므로 곱하기 전에 먼저 나눕니다.

$$ \operatorname{lcm}(a,b) = \frac{a}{\gcd(a,b)} \times b $$

\(\dfrac{a}{\gcd(a,b)}\)는 항상 정수라 이 순서가 안전합니다. 예: \(a=12, b=18\)이면
\(\gcd=6\), \(\operatorname{lcm} = \frac{12}{6}\times 18 = 2 \times 18 = 36\).


5. 어디에 쓰는가

  • 분수 약분: 분자·분모를 GCD로 나누면 기약분수.
  • 주기가 다시 겹치는 시각: 두 신호가 각각 \(a\)초, \(b\)초 주기면 다시 동시에
    울리는 때는 \(\operatorname{lcm}(a,b)\).
  • 똑같이 나눠 담기: 여러 수를 같은 크기로 최대한 크게 나누려면 \(\gcd\).

정리

  • 약수/배수는 \(a \bmod b = 0\)으로 판정.
  • 약수 나열은 \(\sqrt N\)까지 + 짝꿍 \(N/i\)\(O(\sqrt N)\).
  • GCD는 유클리드 호제법 \(\gcd(a,b)=\gcd(b, a\bmod b)\), \(O(\log\min(a,b))\).
  • LCM은 \(\dfrac{a}{\gcd(a,b)}\times b\) — 곱 전에 나눠 오버플로 방지.
Lesson 2강 · 구현 선택 8m

약수와 배수 구현 레퍼런스

약수를 \(\sqrt N\)으로 빠르게 세는 법, GCD/LCM을 재귀·반복 두 방식으로 구현하는 법,
그리고 실전 적용을 코드로 정리합니다. 값이 클 수 있으니 기본적으로 long long
씁니다.


1. 약수를 \(\sqrt N\)까지만 구하기

#include <bits/stdc++.h>
using namespace std;

int main() {
    long long n; cin >> n;
    vector<long long> divs;
    for (long long i = 1; i * i <= n; i++) {
        if (n % i == 0) {
            divs.push_back(i);
            if (i != n / i) divs.push_back(n / i);   // 짝꿍 약수
        }
    }
    sort(divs.begin(), divs.end());
    cout << divs.size() << '\n';    // 약수의 개수
    return 0;
}
def divisors(n):
    divs = set()
    i = 1
    while i * i <= n:
        if n % i == 0:
            divs.add(i)
            divs.add(n // i)
        i += 1
    return sorted(divs)

print(divisors(36))    # [1, 2, 3, 4, 6, 9, 12, 18, 36]

\(i\)\(N/i\)가 같을 때(제곱수의 가운데 약수)는 한 번만 세야 하므로 i != n/i
검사가 필요합니다. i * i <= n에서 ilong long이라야 오버플로가 없습니다.

약수의 이 필요하면 같은 루프에서 sum += i; if (i != n/i) sum += n/i;처럼
더하면 됩니다.


2. 유클리드 호제법 (GCD) — 반복과 재귀

// 반복 버전
long long gcd_iter(long long a, long long b) {
    while (b != 0) {
        long long t = a % b;
        a = b;
        b = t;
    }
    return a;
}

// 재귀 버전 (정의를 그대로 옮김)
long long gcd_rec(long long a, long long b) {
    return b == 0 ? a : gcd_rec(b, a % b);
}
// C++17 이상: std::__gcd(a,b), 또는 <numeric>의 std::gcd(a,b)
def gcd_iter(a, b):
    while b:
        a, b = b, a % b
    return a

import math
print(math.gcd(24, 18))   # 6 — 파이썬 내장

두 버전 모두 gcd(a,0)=a에서 자연스럽게 멈춥니다. 입력 순서가 \(a여도 첫
단계에서 \(a \bmod b = a\)가 되어 자동으로 뒤바뀌므로 크기를 미리 맞출 필요가
없습니다.


3. LCM과 실전 적용

long long lcm(long long a, long long b) {
    return a / gcd_iter(a, b) * b;   // 곱 전에 나눠서 오버플로 방지
}
def lcm(a, b):
    return a // math.gcd(a, b) * b

기약분수 만들기 — 분자·분모를 GCD로 나눕니다.

long long a, b;   // 분수 a/b
long long g = gcd_iter(a, b);
cout << a / g << '/' << b / g << '\n';

여러 수의 GCD/LCM — 앞에서부터 접어 나갑니다(fold).

long long g = 0;                 // gcd의 항등원은 0
for (long long x : nums) g = gcd_iter(g, x);
long long l = 1;                 // lcm의 항등원은 1
for (long long x : nums) l = l / gcd_iter(l, x) * x;
from functools import reduce
g = reduce(math.gcd, nums)
l = reduce(lambda a, b: a // math.gcd(a, b) * b, nums, 1)

4. 함정과 패턴 인식

  • LCM 오버플로a * b를 먼저 하면 넘칩니다. 반드시 a / gcd * b 순서.
    여러 수의 LCM은 중간 값이 폭발할 수 있으니 문제의 상한을 확인하세요.
  • 0 처리\(\gcd(a,0)=a\). 호제법은 자연히 처리합니다. 단 두 수가 모두 \(0\)이면
    \(\gcd(0,0)=0\)으로 정의됩니다.
  • \(\sqrt N\) 중복 — 제곱수에서 가운데 약수를 두 번 세지 않도록 i != n/i 검사.
  • 약수 정렬\(N/i\)로 큰 약수를 함께 넣으면 순서가 섞이니, 필요하면 정렬.
  • 자료형 — 약수 판별의 i*i <= n, 곱셈은 모두 long long으로.

문제에 "약수의 개수/합", "최대공약수/최소공배수", "기약분수", "동시에 만나는",
"똑같이 나눠"
같은 표현이 보이면 이 단원입니다.


정리

핵심은 (1) 약수는 \(\sqrt N\)까지 + 짝꿍, (2) GCD는 유클리드 호제법(반복/재귀/내장),
(3) LCM은 a / gcd * b로 오버플로 회피입니다. GCD/LCM 함수는 통째로 외워 두면
정수론 문제에서 두고두고 씁니다.

Lesson 3강 · 심화와 변형 선택 8m

약수·배수의 심화 — 확장 유클리드와 응용

기본 GCD/LCM을 넘어, 정수론에서 널리 쓰이는 확장 유클리드 호제법과 그 응용을
정리합니다. Bronze 범위를 살짝 넘지만, 앞으로의 정수론 문제에 대비하는 든든한
기초가 됩니다.


1. 확장 유클리드 호제법

GCD를 구하면서 동시에 다음을 만족하는 정수 \(x, y\)를 찾는 것이 목표입니다.

$$ a x + b y = \gcd(a, b) $$

이것을 베주 항등식(Bézout's identity) 이라 하고, 항상 정수해가 존재합니다.
유클리드 호제법의 재귀 구조를 그대로 따라가며 계수를 되돌려 계산합니다.

\(\gcd(b, a\bmod b)\)의 해 \((x', y')\)\(b x' + (a\bmod b)\,y' = g\)를 만족한다고 합시다.
\(a \bmod b = a - \lfloor a/b\rfloor\, b\)를 대입하면

$$ a\,y' + b\,\big(x' - \lfloor a/b\rfloor\, y'\big) = g $$

이 되어, \(x = y'\), \(y = x' - \lfloor a/b\rfloor\, y'\)로 계수를 갱신하면 됩니다.

#include <bits/stdc++.h>
using namespace std;

// a*x + b*y = g 를 만족하는 g,x,y 반환
long long extgcd(long long a, long long b, long long& x, long long& y) {
    if (b == 0) { x = 1; y = 0; return a; }   // a*1 + 0*0 = a
    long long x1, y1;
    long long g = extgcd(b, a % b, x1, y1);
    x = y1;
    y = x1 - (a / b) * y1;
    return g;
}

int main() {
    long long x, y;
    long long g = extgcd(24, 18, x, y);
    cout << g << ' ' << x << ' ' << y << '\n';   // 6 x y
    cout << 24 * x + 18 * y << '\n';             // 6 (검증)
    return 0;
}
def extgcd(a, b):
    if b == 0:
        return a, 1, 0
    g, x1, y1 = extgcd(b, a % b)
    return g, y1, x1 - (a // b) * y1

g, x, y = extgcd(24, 18)
print(g, x, y)          # 6 x y
print(24 * x + 18 * y)  # 6

예: \(\gcd(24,18)=6\)이고 \(24\cdot(-2) + 18\cdot 3 = -48+54 = 6\). 계수 \((x,y)\)
유일하지 않습니다(\(x' = x + b/g\cdot t\), \(y' = y - a/g\cdot t\)로 무수히 많음).


2. 응용 — 일차 부정방정식과 모듈러 역원

  • 일차 부정방정식 \(a\ x + b\ y = c\): 정수해가 존재할 필요충분조건은 \(\gcd(a,b)\mid c\)
    입니다. 존재하면 확장 유클리드로 하나의 해를 구한 뒤, \(\dfrac{b}{g}, \dfrac{a}{g}\)
    만큼씩 이동해 일반해를 얻습니다.
  • 모듈러 역원: \(a x \equiv 1 \pmod{m}\)의 해 \(x\)\(\gcd(a,m)=1\)일 때 존재하며,
    \(a\ x + m\ y = 1\)을 확장 유클리드로 풀어 \(x\)를 얻습니다. 이때 \(x\)가 음수일 수
    있으니 ((x % m) + m) % m으로 보정합니다. (법이 소수일 때의 페르마 소정리 방법은
    '나머지 연산' 단원에서 다룹니다.)
long long modInverse(long long a, long long m) {
    long long x, y;
    long long g = extgcd(a, m, x, y);
    if (g != 1) return -1;          // 역원 없음
    return ((x % m) + m) % m;       // 음수 보정
}

3. 약수 관련 심화 관찰

  • 소인수분해와 약수 개수: \(N = p_1^{e_1}\cdots p_k^{e_k}\)이면 약수의 개수는
    \(\prod (e_i + 1)\), 약수의 합은 \(\prod \dfrac{p_i^{e_i+1}-1}{p_i-1}\)입니다. 큰 \(N\)
    약수 개수/합을 빠르게 구할 때 유용합니다.
  • 여러 수의 GCD가 1(서로소, coprime): \(\gcd=1\)이면 두 수는 공약수가 \(1\)뿐입니다.
  • 에라토스테네스와의 연결: 여러 수의 약수 정보가 필요하면 \(O(N\log\log N)\)
    체로 최소 소인수를 전처리하는 것이 표준입니다(별도 단원).

정리

  • 확장 유클리드\(ax+by=\gcd(a,b)\)의 정수해 \((x,y)\)를 GCD와 함께 구합니다.
  • 계수 갱신: \(x = y'\), \(y = x' - \lfloor a/b\rfloor\, y'\).
  • 응용: 일차 부정방정식(\(\gcd(a,b)\mid c\)일 때 해 존재), 모듈러 역원(음수 보정 필수).
  • 약수 개수/합은 소인수분해로 \(\prod(e_i+1)\), \(\prod\frac{p^{e+1}-1}{p-1}\).
Practice problem 직각이등변삼각형 선택 25m
KOI00002

직각이등변삼각형

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem 장애물 선택 25m
KOI00008

장애물

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Bronze III 브론즈 III 지금 풀기
Lesson 1강 · 개념과 정리 필수 8m

소수 판정, 무엇을 묻는 문제인가

소수(prime)\(1\)보다 큰 정수 중에서 약수가 \(1\)과 자기 자신뿐인 수입니다.
\(2, 3, 5, 7, 11, 13, \dots\) 이 소수이고, \(4 = 2 \times 2\), \(9 = 3 \times 3\) 처럼
다른 약수를 가지는 수는 합성수(composite) 입니다. \(1\)은 약수가 하나뿐이므로
소수도 합성수도 아니며, \(2\)는 유일한 짝수 소수입니다.

이 단원의 목표는 단 하나입니다. "주어진 정수 \(N\)이 소수인가?" 를 가능한 한
빠르게 판정하는 것. 이번 강의에서는 왜 \(\sqrt N\)까지만 확인하면 되는지를 수학적으로
증명하고, 다음 강의에서 구현을, 마지막 강의에서 \(10^{18}\) 규모의 거대한 수까지
판정하는 밀러-라빈 알고리즘을 다룹니다.


1. 가장 단순한 정의 그대로

정의를 그대로 코드로 옮기면, \(2\)부터 \(N-1\)까지 하나라도 \(N\)을 나누면 합성수입니다.

$$ N \text{ is prime} \iff \forall\, d \in \{2, 3, \dots, N-1\},\ \ N \bmod d \neq 0 $$

하지만 이 방법은 \(N-2\)번 나눗셈을 하므로 \(O(N)\)입니다. \(N = 10^9\)이면 약 \(10\)억 번의
연산이 필요해 시간 초과를 피할 수 없습니다.


2. 핵심 정리: \(\sqrt N\)까지만 보면 충분하다

정리. \(N > 1\)\(2 \le d \le \sqrt N\)인 어떤 정수 \(d\)로도 나누어지지 않으면,
\(N\)은 소수이다.

증명(직관). \(N\)이 합성수라고 하자. 그러면 \(N = a \times b\) (\(1 < a \le b < N\))로
쓸 수 있다. 여기서 만약 \(a > \sqrt N\)이고 \(b > \sqrt N\)이라면
$$ a \times b > \sqrt N \times \sqrt N = N $$
이 되어 \(a \times b = N\)에 모순이다. 따라서 두 인수 중 작은 쪽 \(a\)는 반드시
\(a \le \sqrt N\)
을 만족한다. 즉 \(N\)이 합성수라면 \(\sqrt N\) 이하의 약수가 존재한다.
대우를 취하면, \(\sqrt N\) 이하의 약수가 하나도 없으면 \(N\)은 소수이다. \(\blacksquare\)

이 정리 덕분에 확인해야 할 후보가 \(N\)개에서 \(\sqrt N\)개로 줄어들고, 복잡도는
\(O(N)\)에서 \(O(\sqrt N)\) 으로 급감합니다.


3. 숫자로 확인하기

  • \(N = 97\): \(\sqrt{97} \approx 9.8\)이므로 \(2, 3, 4, 5, 6, 7, 8, 9\)까지만 확인합니다.
    \(97\)은 이 중 어느 것으로도 나누어지지 않으므로 소수입니다.
  • \(N = 91\): \(\sqrt{91} \approx 9.5\). \(2, 3, 5\)로는 안 나뉘지만
    \(91 = 7 \times 13\)이라 \(7\)에서 걸립니다. 합성수. 여기서 작은 인수 \(7\)
    \(\sqrt{91}\)보다 작고, 큰 인수 \(13\)\(\sqrt{91}\)보다 크다는 점에 주목하세요 —
    정리가 말한 그대로입니다.
  • \(N = 143\): \(\sqrt{143} \approx 11.9\). \(143 = 11 \times 13\)이므로 \(11\)에서
    걸립니다. 만약 \(\sqrt N\) 대신 실수 오차로 \(11\)을 놓쳤다면 소수로 오판했을 것입니다.

4. 한 걸음 더: \(6k \pm 1\) 관찰

\(2\)\(3\)을 제외한 모든 소수는 \(6k+1\) 또는 \(6k-1\) 꼴입니다. 왜냐하면 임의의 정수는
\(6k, 6k+1, 6k+2, 6k+3, 6k+4, 6k+5\) 중 하나인데, \(6k\)·\(6k+2\)·\(6k+4\)\(2\)의 배수,
\(6k+3\)\(3\)의 배수라 소수가 될 수 없기 때문입니다. 남는 것은 \(6k+1\)
\(6k+5 \equiv 6k-1\) 뿐입니다.

이 관찰을 쓰면 \(\sqrt N\)까지의 후보 중에서도 \(6\)개마다 \(2\)개씩만 확인하면 되어
\(3\)배 빨라집니다. 구현은 다음 강의에서 다룹니다.


5. 언제 이 방법을 쓰는가

  • 수 한두 개의 소수 여부를 묻고, 그 수가 \(10^{18}\) 이하일 때 → 시험 나눗셈
    (\(\sqrt N\)). \(N \le 10^{18}\)이면 \(\sqrt N \le 10^9\)이라 조금 무겁고, 이때는
    밀러-라빈(3강)이 낫습니다. 실용적으로 \(N \le 10^{12}\) 정도까지 시험 나눗셈이
    깔끔합니다.
  • \(1\)부터 \(N\)까지 범위 전체의 소수를 알아야 할 때 → 시험 나눗셈을 반복하지 말고
    에라토스테네스의 체(별도 단원)를 쓰세요. \(O(N \log \log N)\)으로 훨씬 빠릅니다.

정리

  • 소수 \(=\) \(1\)과 자기 자신만 약수인 \(1\) 초과 정수. \(0, 1\)은 소수가 아니며 \(2\)가 가장
    작은 소수.
  • 합성수라면 \(\sqrt N\) 이하의 약수가 반드시 존재한다 → \(\sqrt N\)까지만 확인하면
    충분, 복잡도 \(O(\sqrt N)\).
  • 실수 오차를 피하려면 i <= sqrt(n)이 아니라 i * i <= n 으로 비교한다.
  • \(2, 3\) 제외 모든 소수는 \(6k \pm 1\) 꼴 → 후보를 더 줄일 수 있다.
Lesson 2강 · 구현과 함정 선택 8m

구현 레퍼런스

이번 강의에서는 \(\sqrt N\) 소수 판정을 실제로 돌아가는 코드로 완성하고, 점점 빠르게
다듬어 갑니다. 그리고 대회에서 반복적으로 사람들을 넘어뜨리는 함정을 정리합니다.


1. 완성형 판정 함수

#include <bits/stdc++.h>
using namespace std;

bool isPrime(long long n) {
    if (n < 2) return false;
    for (long long i = 2; i * i <= n; i++)   // i*i <= n  <=>  i <= √n
        if (n % i == 0) return false;        // 약수 발견 → 합성수
    return true;
}

int main() {
    int q; cin >> q;
    while (q--) {
        long long x; cin >> x;
        cout << (isPrime(x) ? "prime" : "not prime") << '\n';
    }
}
def is_prime(n: int) -> bool:
    if n < 2:
        return False
    i = 2
    while i * i <= n:            # 실수 오차 없는 정수 비교
        if n % i == 0:
            return False
        i += 1
    return True

여기서 가장 중요한 한 줄은 i * i <= n 입니다. i <= sqrt(n)처럼 부동소수점
sqrt를 쓰면 경계에서 오차가 생겨, 예를 들어 완전제곱수 \(N = 10^{18}\) 근처에서
\(\sqrt N\)을 아주 미세하게 작게 계산해 진짜 약수 하나를 놓칠 수 있습니다. 정수 비교는
그런 오차가 원천적으로 없습니다.


2. 짝수를 걸러 약 2배 빠르게

\(2\)만 특별 처리하고 그 뒤로는 홀수만 검사하면 후보가 절반으로 줄어듭니다.

bool isPrime(long long n) {
    if (n < 2) return false;
    if (n % 2 == 0) return n == 2;           // 2만 소수, 나머지 짝수는 합성수
    for (long long i = 3; i * i <= n; i += 2) // 홀수만
        if (n % i == 0) return false;
    return true;
}

3. \(6k \pm 1\) 최적화로 약 3배 빠르게

1강에서 본 대로 \(2, 3\)을 제외한 소수는 모두 \(6k \pm 1\) 꼴입니다. \(i\)\(i+2\)
번갈아(\(6k-1\), \(6k+1\)) 건너뛰면 후보가 다시 크게 줄어듭니다.

bool isPrime(long long n) {
    if (n < 2) return false;
    if (n % 2 == 0) return n == 2;
    if (n % 3 == 0) return n == 3;
    for (long long i = 5; i * i <= n; i += 6) // 5,11,17,... 과 7,13,19,...
        if (n % i == 0 || n % (i + 2) == 0)
            return false;
    return true;
}

\(i\)\(5, 11, 17, \dots\) (\(6k-1\))를 훑고, \(i+2\)\(7, 13, 19, \dots\) (\(6k+1\))를
훑습니다. 이렇게 하면 \(6\)개 중 \(2\)개만 나눗셈하므로 이론상 약 \(3\)배 빨라집니다.


4. 같은 원리의 단골 응용: 소인수분해

\(\sqrt N\) 판정과 완전히 같은 발상으로 소인수분해도 \(O(\sqrt N)\)에 됩니다.

vector<long long> factorize(long long n) {
    vector<long long> f;
    for (long long i = 2; i * i <= n; i++)
        while (n % i == 0) {   // i로 나눠지는 만큼 모두 뽑기
            f.push_back(i);
            n /= i;
        }
    if (n > 1) f.push_back(n); // 남은 것은 √N 초과의 큰 소인수
    return f;                  // 예: 360 → [2,2,2,3,3,5]
}

마지막 if (n > 1)이 핵심입니다. 반복이 끝난 뒤에도 \(n\)\(1\)보다 크면, 그 값은
\(\sqrt N\)보다 큰 소인수가 하나 남은 것입니다(예: \(14 = 2 \times 7\)에서 \(7\)).


5. 함정과 패턴 인식

  • \(0, 1\), 음수\(1\) 이하는 소수가 아닙니다. n < 2 처리를 빠뜨리면 \(1\)
    소수로 오판합니다.
  • i * i 오버플로\(n\)이 클 때 int ii * iint 범위(\(\approx 2.1 \times 10^9\))를
    넘습니다. 반드시 long long i 또는 (long long)i * i, 아니면 i <= n / i로 쓰세요.
  • 실수 sqrt 오차i <= sqrt(n) 금지, i * i <= n 사용.
  • 범위 문제에 단건 판정 — "\(1 \sim N\)의 소수를 모두"라면 이 함수를 \(N\)번 부르지
    말고 체를 쓰세요.

문제에 "소수인지 판별", "소수의 개수", "소인수분해", "다음 소수" 같은 표현이
보이고 대상 수가 한두 개라면 이 단원의 도구입니다.


정리

정석은 n < 2를 거르고 i * i <= n까지 나눠 보는 것입니다. 큰 수에는
long long과 오버플로 주의, \(6k \pm 1\)로 상수를 줄이고, 범위 전체에는 체 —
이것으로 Bronze 소수 문제는 막힘이 없습니다. \(N\)\(10^{12}\)을 넘어 거대해지면
다음 강의의 밀러-라빈으로 넘어갑니다.

Lesson 3강 · 밀러-라빈과 큰 수 판정 선택 8m

\(\sqrt N\)의 한계와 밀러-라빈

\(N\)\(10^{18}\)까지 커지면 \(\sqrt N = 10^9\)이라 시험 나눗셈으로는 감당이 안 됩니다.
이럴 때 쓰는 것이 밀러-라빈 소수 판정(Miller-Rabin primality test) 입니다.
확률적 알고리즘이지만, 특정한 밑(witness) 집합을 고정하면 일정 범위 아래에서
결정론적으로(100% 정확) 작동합니다.


1. 페르마 소정리에서 출발

페르마 소정리. \(p\)가 소수이고 \(\gcd(a, p) = 1\)이면
$$ a^{p-1} \equiv 1 \pmod{p}. $$

그래서 \(a^{N-1} \not\equiv 1 \pmod N\)\(a\)를 하나라도 찾으면 \(N\)은 확실히 합성수입니다.
문제는 역이 성립하지 않는다는 것 — \(a^{N-1} \equiv 1\)이어도 \(N\)이 합성수일 수 있고
(예: 카마이클 수 \(561\)), 그래서 더 강한 조건이 필요합니다.


2. 강한 확률적 소수 조건

\(N\)이 홀수 소수라면 \(N - 1\)은 짝수이므로
$$ N - 1 = 2^s \cdot d \quad (d \text{ 는 홀수}) $$
로 쓸 수 있습니다. 소수 \(N\)에서는 \(x^2 \equiv 1 \pmod N\)의 해가 \(x \equiv \pm 1\)
뿐입니다(제곱근이 \(\pm 1\) 두 개). 이 사실로부터, 소수라면 다음 중 하나는 반드시
성립합니다.
$$ a^d \equiv 1 \pmod N \qquad \text{또는} \qquad a^{2^r d} \equiv -1 \pmod N \ (\exists\, 0 \le r < s). $$

\(a^d\)부터 시작해 \(s\)번 제곱해 나가는 수열
\(a^d, a^{2d}, a^{4d}, \dots, a^{2^{s-1}d}\) 에서 어딘가 \(-1\)이 나오거나 처음이 \(1\)이면
"\(a\)에 대한 강한 확률적 소수"입니다. 이 조건을 통과하지 못하는 \(a\)증인(witness)
이라 하며, 증인이 하나라도 있으면 \(N\)은 확정 합성수입니다.


3. 결정론적 증인 집합

무작위 \(a\) 대신, 수학적으로 검증된 고정 밑을 쓰면 결정론이 됩니다.

  • \(N < 3\,215\,031\,751\): \(\{2, 3, 5, 7\}\)
  • \(N < 3.3 \times 10^{14}\): \(\{2, 3, 5, 7, 11, 13, 17\}\)
  • \(N < 3.3 \times 10^{24}\) (즉 64비트 정수 전체를 덮음):
    $$ \{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37\} $$

경쟁 프로그래밍에서는 마지막 \(12\)개짜리 집합 하나만 외워 두면 \(64\)비트 unsigned
범위 전체를 안전하게 판정할 수 있습니다.


4. 오버플로 함정: 모듈러 곱셈

\(N\)\(10^{18}\)급이면 \(a \times b\)\(10^{36}\)까지 커져 long long(\(\approx 9.2 \times 10^{18}\))을
아득히 넘칩니다. C++에서는 __int128 로 중간 곱을 담아야 정확합니다.

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long u64;

u64 mulmod(u64 a, u64 b, u64 m) {
    return (__int128)a * b % m;          // 128비트로 곱해 오버플로 방지
}
u64 binpow(u64 a, u64 b, u64 m) {        // a^b mod m, O(log b)
    u64 r = 1 % m; a %= m;
    while (b) {
        if (b & 1) r = mulmod(r, a, m);
        a = mulmod(a, a, m);
        b >>= 1;
    }
    return r;
}
bool check(u64 n, u64 a, u64 d, int s) { // a가 n의 합성 증거가 아니면 true
    u64 x = binpow(a, d, n);
    if (x == 1 || x == n - 1) return true;
    for (int i = 0; i < s - 1; i++) {
        x = mulmod(x, x, n);
        if (x == n - 1) return true;
    }
    return false;                        // 증인 발견 → 합성
}
bool isPrime(u64 n) {
    if (n < 2) return false;
    for (u64 p : {2,3,5,7,11,13,17,19,23,29,31,37})
        if (n % p == 0) return n == p;   // 작은 소수 처리 + a와 n 서로소 보장
    u64 d = n - 1; int s = 0;
    while ((d & 1) == 0) { d >>= 1; s++; } // n-1 = 2^s * d
    for (u64 a : {2,3,5,7,11,13,17,19,23,29,31,37})
        if (!check(n, a, d, s)) return false;
    return true;
}

int main() {
    u64 n;
    while (cin >> n) cout << (isPrime(n) ? "prime" : "composite") << '\n';
}

작은 소수로 먼저 나눠 두는 이유는 두 가지입니다. \(n\)이 그 소수 자신일 때 바로
답을 주고, 이후 밑 \(a\)\(n\)과 서로소가 되도록 보장합니다.


5. 파이썬 버전

파이썬 정수는 임의 정밀도라 오버플로가 없고, 내장 pow(a, d, n)이 빠른 모듈러
거듭제곱을 해 줍니다.

def is_prime(n: int) -> bool:
    if n < 2:
        return False
    for p in (2,3,5,7,11,13,17,19,23,29,31,37):
        if n % p == 0:
            return n == p
    d, s = n - 1, 0
    while d % 2 == 0:          # n-1 = 2^s * d
        d //= 2
        s += 1
    for a in (2,3,5,7,11,13,17,19,23,29,31,37):
        x = pow(a, d, n)       # a^d mod n, 오버플로 없음
        if x == 1 or x == n - 1:
            continue
        for _ in range(s - 1):
            x = x * x % n
            if x == n - 1:
                break
        else:
            return False       # 안쪽 for가 break 없이 끝 → 증인 → 합성
    return True

for ... else 구문에 주의하세요. 안쪽 반복이 break 없이 끝났다는 것은 수열에서
\(-1\)을 못 만났다는 뜻이고, 이때 \(a\)가 증인이므로 합성수로 판정합니다.


6. 검증에 쓰는 대표 사례

  • \(998244353\), \(10^9+7\): 유명한 소수 → prime.
  • \(2^{61}-1 = 2305843009213693951\) (메르센 소수) → prime. __int128 없이는
    틀리는 대표적 큰 수.
  • \(561 = 3 \times 11 \times 17\) (카마이클 수): 페르마 판정만으로는 소수처럼
    보이지만 밀러-라빈은 정확히 composite.
  • \(3215031751\): \(\{2,3,5,7\}\)만 쓰면 소수로 오판되는 유명한 반례 →
    전체 집합에서는 composite.

정리

  • 밀러-라빈은 \(N - 1 = 2^s\ d\) 분해 후, 강한 확률적 소수 조건을 여러 밑으로 검사한다.
  • \(\{2,3,\dots,37\}\) 12개 밑이면 \(64\)비트 정수 전체를 결정론적으로 판정한다.
  • 핵심 함정은 모듈러 곱셈 오버플로 — C++는 __int128, 파이썬은 임의 정밀도로 해결.
  • \(N \le 10^{12}\) 정도면 \(\sqrt N\) 시험 나눗셈, 그 이상 \(10^{18}\)까지는 밀러-라빈.
Practice problem 쉬운 문제 선택 25m
R00002

쉬운 문제

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Gold IV 골드 IV 지금 풀기
Practice problem 알파카컵 2회: E - 알파카 게임 (Easy) 선택 25m
A00014

알파카컵 2회: E - 알파카 게임 (Easy)

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Lesson 1강 · 개념과 정리 필수 8m

진법이란 무엇인가

우리가 일상에서 쓰는 10진법\(0\)부터 \(9\)까지 열 개의 숫자(digit)만으로 모든 수를
표현합니다. 자리가 하나 올라갈 때마다 밑(base) \(10\)의 거듭제곱이 곱해지죠.
일반적으로 \(N\)진법\(N\)개의 숫자만으로 수를 표현하는 방식이며, \(N\)을 그 진법의
또는 기수(radix) 라고 부릅니다. 자주 쓰이는 것은 2진법(0,1), 8진법(0~7),
16진법(09,AF)입니다.

이 강의에서는 밑이 \(2 \le N \le 36\)양의 정수 진법만 다룹니다. 음수 진법이나
복소수 진법 같은 특수한 경우는 범위를 벗어나므로 다루지 않습니다.


1. 자릿값 전개 — 진법의 정의

밑이 \(N\)인 수 \((d_k d_{k-1} \cdots d_1 d_0)_N\)의 실제 값은 다음과 같이 정의됩니다.

$$ (d_k d_{k-1}\cdots d_1 d_0)_N = \sum_{i=0}^{k} d_i \cdot N^{\,i} $$

여기서 각 자리 숫자는 \(0 \le d_i < N\)을 만족해야 합니다. 즉 "각 자리 숫자 × 밑의
거듭제곱"을 모두 더한 것이 그 수의 실제 크기(10진수 값)입니다.

예를 들어 10진수 \(342\)

$$ 342 = 3 \times 10^2 + 4 \times 10^1 + 2 \times 10^0 $$

이고, 2진수 \(1011_{(2)}\)

$$ 1011_{(2)} = 1\cdot 2^3 + 0\cdot 2^2 + 1\cdot 2^1 + 1\cdot 2^0 = 8+0+2+1 = 11 $$

입니다. 밑만 바뀌었을 뿐, "자리 × 밑의 거듭제곱의 합"이라는 구조는 똑같습니다.


2. N진수 → 10진수 (읽기)

정의를 그대로 쓰면 \(\sum d_i N^i\)을 계산하면 되지만, 거듭제곱을 매번 구하는 대신
호너(Horner) 방식이 훨씬 깔끔하고 오버플로에도 유리합니다. 왼쪽 자리부터
차례로 "지금까지 값 \(\times\)\(+\) 새 자리"를 반복합니다.

1011 (2진) 을 10진으로:
시작:          0
1을 읽고:  0*2 + 1 = 1
0을 읽고:  1*2 + 0 = 2
1을 읽고:  2*2 + 1 = 5
1을 읽고:  5*2 + 1 = 11   ← 최종 값

이것이 성립하는 이유는 \(\sum d_i N^i\)\((((d_k)N + d_{k-1})N + \cdots)N + d_0\)
꼴로 묶을 수 있기 때문입니다. 곱셈 횟수가 자릿수에 비례해 최소가 됩니다.


3. 10진수 → N진수 (쓰기)

반대 방향은 밑으로 계속 나누면서 나머지를 거꾸로 읽는 방법입니다.

11을 2진수로:
11 / 2 = 5 ... 나머지 1
 5 / 2 = 2 ... 나머지 1
 2 / 2 = 1 ... 나머지 0
 1 / 2 = 0 ... 나머지 1
나머지를 아래에서 위로 읽기: 1011

왜 나머지가 거꾸로 나올까요? \(x = \sum d_i N^i\)일 때 \(x \bmod N = d_0\)(가장 낮은 자리),
\(\lfloor x / N \rfloor = \sum_{i\ge 1} d_i N^{i-1}\)이 되어 다음 나눗셈에서 \(d_1\)
나옵니다. 즉 가장 낮은 자리부터 나오므로, 모아서 뒤집어야 원래 순서가 됩니다.


4. 10을 넘는 자리 — 문자 숫자

밑이 \(11\) 이상이면 한 자리에 \(10\) 이상의 값이 들어갈 수 있습니다. 이때는 알파벳
대문자를 씁니다. 표준적으로 \(10 \to \text{A}\), \(11 \to \text{B}\), \(\dots\),
\(35 \to \text{Z}\)로 두어 밑 \(36\)까지 표현합니다.

10 11 12 13 14 15 ... 35
문자 A B C D E F ... Z

숫자 \(\leftrightarrow\) 문자 변환은 아스키 코드로 처리합니다. 값 \(d\)\(10\) 미만이면
'0' + d, \(10\) 이상이면 'A' + (d - 10)이 됩니다. 예를 들어 16진수 \(2\text{F}_{(16)}\)
\(2 \times 16 + 15 = 47\)입니다.


복잡도

\(N\)으로 표현한 값 \(x\)의 자릿수는 약 \(\log_N x\)개입니다. 읽기·쓰기 모두 자릿수에
비례하므로 시간 복잡도는 \(O(\log_N x)\), 사실상 매우 빠릅니다. 값이 큰 경우 누적
과정에서 \(N\)진 자릿수만큼의 곱셈이 필요할 뿐입니다.


정리

  • \(N\)진수의 값 \(= \displaystyle\sum d_i N^i\), 각 자리 \(0 \le d_i < N\).
  • N진 → 10진: "값 \(\times\)\(+\) 새 자리"를 왼쪽부터 반복(호너).
  • 10진 → N진: 밑으로 나눈 나머지를 거꾸로 읽기.
  • \(10\) 이상 자리는 A~Z 문자(아스키 'A' + d - 10), 밑은 \(2 \sim 36\).
  • 복잡도는 자릿수에 비례하는 \(O(\log_N x)\).
Lesson 2강 · 구현 선택 8m

진법 변환 구현 레퍼런스

두 방향 변환을 직접 구현하고, 큰 밑의 문자 자리 처리와 대표적인 함정을 코드로
정리합니다. 밑은 \(2 \le N \le 36\)을 가정합니다.


1. N진수 → 10진수 (호너 누적)

#include <bits/stdc++.h>
using namespace std;

long long toDecimal(const string& s, int base) {
    long long value = 0;
    for (char c : s) {
        int digit;
        if (c >= '0' && c <= '9') digit = c - '0';
        else                      digit = toupper(c) - 'A' + 10;  // A~Z -> 10~35
        value = value * base + digit;   // 호너 누적식
    }
    return value;
}

int main() {
    cout << toDecimal("1011", 2)  << '\n';   // 11
    cout << toDecimal("2F", 16)   << '\n';   // 47
    cout << toDecimal("Z", 36)    << '\n';   // 35
    return 0;
}
def to_decimal(s, base):
    value = 0
    for c in s:
        if c.isdigit():
            digit = ord(c) - ord('0')
        else:
            digit = ord(c.upper()) - ord('A') + 10
        value = value * base + digit
    return value

print(to_decimal("1011", 2))   # 11
print(to_decimal("2F", 16))    # 47
print(int("1011", 2))          # 11 — 파이썬 내장으로 한 방에

핵심은 value = value * base + digit 한 줄입니다. 파이썬은 int(문자열, 밑)으로
읽기 방향이 공짜입니다(밑 \(2\sim36\) 지원).


2. 10진수 → N진수 (나머지 모아 뒤집기)

#include <bits/stdc++.h>
using namespace std;

string fromDecimal(long long n, int base) {
    if (n == 0) return "0";              // 0은 특수 처리
    const string DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    string result = "";
    while (n > 0) {
        int d = n % base;
        result += DIGITS[d];             // 낮은 자리부터 뒤에 붙임
        n /= base;
    }
    reverse(result.begin(), result.end());   // 반드시 뒤집기!
    return result;
}

int main() {
    cout << fromDecimal(11, 2)  << '\n';   // 1011
    cout << fromDecimal(47, 16) << '\n';   // 2F
    cout << fromDecimal(0, 2)   << '\n';   // 0
    return 0;
}
DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"

def from_decimal(n, base):
    if n == 0:
        return "0"
    out = ""
    while n > 0:
        out = DIGITS[n % base] + out   # 앞에 붙이면 뒤집을 필요 없음
        n //= base
    return out

print(from_decimal(11, 2))    # 1011
print(from_decimal(47, 16))   # 2F
print(from_decimal(0, 2))     # 0

C++은 나머지를 뒤에 붙이므로 마지막에 reverse가 필수입니다. 파이썬처럼
문자열 에 붙이면 뒤집을 필요가 없지만, 문자열 앞 삽입은 느리니 자릿수가
많으면 뒤에 붙이고 한 번에 뒤집는 편이 낫습니다.


3. 진법 사이 직접 변환 (A진 → B진)

\(A\)에서 밑 \(B\)로 바꾸려면 A진 → 10진 → B진 두 단계를 잇습니다. 10진수를
중간 다리로 삼는 것이 가장 단순하고 안전합니다.

string convert(const string& s, int from, int to) {
    return fromDecimal(toDecimal(s, from), to);
}
// convert("1011", 2, 16) -> "B"   (2진 1011 = 11 = 16진 B)
def convert(s, frm, to):
    return from_decimal(int(s, frm), to)

print(convert("1011", 2, 16))   # B

4. 함정과 패턴 인식

  • 거꾸로 출력 — 나머지를 모은 뒤 reverse를 빠뜨리면 순서가 반대로 나옵니다.
  • 0 처리 — 입력이 \(0\)이면 while이 한 번도 안 돌아 빈 문자열이 됩니다. 따로
    "0"을 반환하세요.
  • 문자 자리\(10\) 이상은 A~Z. 인덱스로 DIGITS[d]를 쓰면 실수가 없습니다.
  • 오버플로 — 읽기에서 누적 값이 클 수 있으면 반드시 long long. 예를 들어
    36진수 13자리면 \(36^{13} \approx 1.7\times 10^{20}\)으로 long long(\(\approx 9.2\times 10^{18}\))
    마저 넘칠 수 있으니 문제의 범위를 확인하세요.
  • 소문자 입력 — 입력이 a~z로 올 수도 있으니 toupper로 통일합니다.

문제에 "2/8/16진수로 변환", "X진법으로 나타내라", "이진수의 1의 개수" 같은
표현이 보이면 진법 변환입니다.


정리

진법 변환은 (1) N진→10진은 "값\(\times\)\(+\)자리" 호너 누적, (2) 10진→N진은 나머지를
모아 뒤집기, (3) \(10\) 이상 자리는 A~Z 문자, (4) \(0\)과 오버플로 처리 — 이 네 가지만
챙기면 됩니다. 파이썬은 int(s, base)로 읽기가 공짜이며, 쓰기도 짧게 구현됩니다.

Lesson 3강 · 심화와 변형 선택 8m

진법 변환의 심화와 변형

기본 변환을 넘어서, 실전에서 자주 마주치는 변형들을 정리합니다. 고정 자릿수 출력,
소수부(소수점 아래) 변환, 그리고 진법과 관련된 성질들입니다.


1. 고정 자릿수(fixed-width) 출력

"\(N\)진법으로 \(w\)자리로 표현하라(부족하면 앞을 \(0\)으로 채워라)"는 요구가 흔합니다.
예를 들어 8비트 2진수로 \(5\)를 나타내면 00000101입니다. 변환 후 왼쪽에 0
덧대면 됩니다.

#include <bits/stdc++.h>
using namespace std;

string fromDecimal(long long n, int base) {
    if (n == 0) return "0";
    const string D = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    string r = "";
    while (n > 0) { r += D[n % base]; n /= base; }
    reverse(r.begin(), r.end());
    return r;
}

string fixedWidth(long long n, int base, int width) {
    string s = fromDecimal(n, base);
    if ((int)s.size() < width)
        s = string(width - s.size(), '0') + s;   // 앞을 0으로 채움
    return s;
}

int main() {
    cout << fixedWidth(5, 2, 8) << '\n';   // 00000101
    cout << fixedWidth(255, 16, 4) << '\n';// 00FF
    return 0;
}
def fixed_width(n, base, width):
    s = from_decimal(n, base)      # 앞 강의의 from_decimal
    return s.rjust(width, '0')

# 2의 보수/비트 연산 문제에서 특히 자주 쓰입니다.

값이 정해진 폭을 넘칠 수도 있으니(예: 8비트에 \(256\)), 문제 조건에서 폭이 항상
충분한지 확인하세요.


2. 소수부(소수점 아래) 변환 — 선택 심화

정수부는 나눗셈, 소수부는 곱셈으로 변환합니다. 소수부에 밑을 곱해 나온
정수부를 위에서 아래로 읽습니다.

예: \(0.625\)를 2진수로.

$$ 0.625 \times 2 = 1.25 \to 1,\quad 0.25 \times 2 = 0.5 \to 0,\quad 0.5 \times 2 = 1.0 \to 1 $$

따라서 \(0.625 = 0.101_{(2)}\)입니다. 정수부와 반대로, 소수부는 먼저 나온 것이 앞
자리
라는 점에 주의하세요.

#include <bits/stdc++.h>
using namespace std;

// 소수부 frac(0<=frac<1)을 base진법으로 최대 prec자리까지
string fracToBase(double frac, int base, int prec) {
    const string D = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    string r = ".";
    for (int i = 0; i < prec && frac > 0; i++) {
        frac *= base;
        int d = (int)frac;      // 정수부가 곧 이번 자리
        r += D[d];
        frac -= d;
    }
    return r;
}

int main() {
    cout << fracToBase(0.625, 2, 10) << '\n';   // .101
    return 0;
}

주의: 무한소수가 될 수 있으므로(예: \(0.1_{(10)}\)은 2진수에서 무한 반복) 반드시
자릿수 제한(prec) 을 둡니다. 또한 double의 부동소수점 오차 때문에 경계에서
자릿수가 흔들릴 수 있어, 정밀도가 중요한 문제는 유리수(분자/분모 정수)로 다루는
편이 안전합니다.


3. 진법과 관련된 유용한 성질

  • 자릿수 합과 배수 판정: \(10\)진수에서 각 자리 합이 \(3\)(또는 \(9\))의 배수면 원래
    수도 배수입니다. 일반화하면 밑 \(N\)에서 "각 자리 합"은 \(N-1\)의 배수 판정에
    쓰입니다. 이는 \(N \equiv 1 \pmod{N-1}\)이기 때문입니다.
  • 밑이 \(2^k\)인 진법 사이 변환: 2진 \(\leftrightarrow\) 8진(\(2^3\)) \(\leftrightarrow\)
    16진(\(2^4\))은 10진수를 거치지 않고 비트를 묶어서 바로 바꿀 수 있습니다.
    16진 한 자리 = 2진 4자리. 큰 수도 문자열만으로 처리 가능합니다.
  • 회문·자릿수 문제: "어떤 진법에서 회문이 되는가" 같은 문제는 각 밑에 대해
    변환 후 뒤집어 비교하면 됩니다.
# 밑 2~10에서 회문인지 확인하는 예
def is_palindrome_in_base(n, base):
    s = from_decimal(n, base)
    return s == s[::-1]

4. 마지막 함정 정리

  • big-base 문자: 밑이 \(17\) 이상이면 반드시 A~Z를 씁니다. 소문자로 출력해야
    하는지 대문자인지 문제 조건을 확인하세요.
  • 오버플로: 큰 밑 · 긴 자릿수는 long long도 넘길 수 있습니다. 파이썬의 큰
    정수나 문자열 기반 처리를 고려하세요.
  • 선행 0: 고정폭이 아니라면 결과에 불필요한 앞자리 \(0\)을 붙이지 않습니다.

정리

  • 고정폭은 변환 후 왼쪽 0 패딩(rjust, string(width-len,'0')).
  • 소수부는 밑을 곱해 나온 정수부를 위→아래로, 자릿수 제한 필수, double 오차 주의.
  • \(2^k\) 밑끼리는 비트 묶음으로 직접 변환, \(N-1\) 배수는 자릿수 합으로 판정.
  • 큰 밑·긴 자릿수의 오버플로는 늘 경계.
Practice problem 직각이등변삼각형 선택 25m
KOI00002

직각이등변삼각형

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem 장애물 선택 25m
KOI00008

장애물

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Bronze III 브론즈 III 지금 풀기
02
Level 2 · Solver

Solver

수학과 정수론 · Solver 단계

0/5 완료
Lesson 1강 · 개념과 정리 필수 8m

에라토스테네스의 체란?

수 하나가 소수인지 판정하는 것은 \(O(\sqrt N)\)이면 됩니다. 하지만 \(1\)부터 \(N\)까지
모든 소수
가 필요하다면 어떨까요? 각 수를 따로 판정하면 \(O(N \sqrt N)\)으로,
\(N = 10^7\)이면 약 \(3 \times 10^{10}\)번 연산이라 너무 느립니다.

에라토스테네스의 체(Sieve of Eratosthenes) 는 이 문제를 거의 선형 시간에
푸는 고전 알고리즘입니다. 발상의 전환은 단 하나 — 소수를 찾지 말고, 합성수를
지워라.


1. 동작 원리

\(2\)부터 시작합니다. 아직 지워지지 않은 수를 만나면 그것은 소수입니다. 그 소수의
모든 배수를 지웁니다. 다음으로 안 지워진 수로 넘어가 반복합니다.

후보:  2 3 4 5 6 7 8 9 10 11 12 13 14 15 ...
2 소수 → 4 6 8 10 12 14 지움
3 소수 → 9 15 지움 (6,12는 이미)
5 소수 → 25... 지움 (10,15,20은 이미)
남은 것: 2 3 5 7 11 13 ... → 소수!

"이 수가 소수냐"를 하나씩 묻는 대신 "소수의 배수를 한꺼번에 제거"하는 것이
핵심 전환입니다. 곱셈적 구조를 그대로 활용하기 때문에 판정보다 훨씬 효율적입니다.


2. 왜 빠른가: 복잡도 증명

각 소수 \(p\)에 대해 배수를 지우는 횟수는 약 \(N/p\)번입니다. 전체 지우기 횟수는
소수들에 대한 합
$$ \frac{N}{2} + \frac{N}{3} + \frac{N}{5} + \frac{N}{7} + \cdots = N \sum_{p \le N} \frac{1}{p} $$
입니다. 소수의 역수 합은 정수론의 정리에 의해
$$ \sum_{p \le N} \frac{1}{p} = \ln \ln N + O(1) $$
로 아주 천천히 자랍니다. 따라서 전체 복잡도는
$$ O(N \log \log N) $$
입니다. \(\log \log N\)\(N = 10^7\)에서도 \(3\) 남짓한 작은 값이라, 실용적으로
거의 선형입니다. \(N = 10^7\)까지 순식간에 처리됩니다.


3. 두 가지 필수 최적화

  • 바깥 반복은 \(\sqrt N\)까지만. \(i > \sqrt N\)인 소수의 배수 \(i \cdot k\)\(k < i \le \sqrt N\)
    이므로 이미 더 작은 소인수 \(k\)의 단계에서 지워졌습니다. 그래서 \(i \le \sqrt N\)까지만
    배수 제거를 시작하면 충분합니다.
  • 안쪽은 \(i^2\)부터 지운다. \(i\)의 배수 중 \(2i, 3i, \dots, (i-1)i\)는 각각 \(2, 3, \dots, i-1\)이라는
    더 작은 소수가 이미 지웠습니다. 아직 안 지워진 가장 작은 배수는 \(i \times i\)입니다.

이 둘을 빠뜨려도 답은 맞지만, 적용하면 눈에 띄게 빨라집니다.


4. 숫자로 확인: \(N = 30\)

\(\sqrt{30} \approx 5.5\)이므로 \(i = 2, 3, 5\)까지만 거릅니다.

  • \(i = 2\): \(4, 6, 8, \dots, 30\) 지움.
  • \(i = 3\): \(9, 12, 15, 18, 21, 24, 27, 30\) 지움 (\(9\)부터 시작).
  • \(i = 5\): \(25, 30\) 지움 (\(25\)부터 시작).
  • \(i = 7\)\(7 > \sqrt{30}\)이라 멈춤.

남은 수: \(2, 3, 5, 7, 11, 13, 17, 19, 23, 29\) — 정확히 \(30\) 이하의 소수 \(10\)개입니다.
"\(N\) 이하 소수의 개수"를 묻는 문제는 이렇게 지운 뒤 남은 것을 세면 됩니다.


5. 체가 주는 부가 정보

체는 소수 목록만 주지 않습니다. 살짝 바꾸면 더 많은 것을 공짜로 얻습니다.

변형 얻는 것 복잡도
지울 때 최소 소인수 기록 (SPF) 임의 수의 \(O(\log N)\) 소인수분해 2강
선형 체(linear sieve) 진짜 \(O(N)\) + 곱셈적 함수 3강
구간 체(segmented) \([L, R]\)의 소수 (\(R\)이 매우 큼) 3강

6. 언제 쓰는가

  • 어떤 범위의 소수를 모두 나열해야 할 때.
  • 소수 여부를 여러 번 물어볼 때 (is_prime 표를 한 번 만들고 질의당 \(O(1)\)).
  • 많은 수를 빠르게 소인수분해해야 할 때 (SPF 체).

반대로 큰 수 한두 개만 판정한다면 체가 아니라 \(O(\sqrt N)\) 시험 나눗셈 또는
밀러-라빈이 정답입니다. \(10^{18}\)짜리 수 하나 때문에 체를 만들 수는 없으니까요.


정리

체는 "소수를 찾는다"가 아니라 "합성수를 지운다"는 발상의 전환입니다.
\(O(N \log \log N)\)에 범위 내 모든 소수를 얻고, \(i \le \sqrt N\)·\(i^2\)부터라는 두
최적화를 기억하며, 약간 변형하면 빠른 소인수분해까지 따라옵니다.

Lesson 2강 · 구현과 최소 소인수 선택 8m

체를 코드로

기본 체, 두 최적화, 그리고 가장 실전적인 응용인 최소 소인수(SPF) 체 → 빠른
소인수분해
를 구현합니다.


1. 기본 체 (불리언 배열)

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000000;
bool composite[MAXN + 1];                       // true면 합성수

void sieve() {
    for (int i = 2; (long long)i * i <= MAXN; i++)  // √N까지
        if (!composite[i])                          // i가 소수면
            for (int j = i * i; j <= MAXN; j += i)   // i*i부터 배수 지움
                composite[j] = true;
}
// 사용: 2 <= x 이고 composite[x] == false 이면 x는 소수
def sieve(n: int) -> list[bool]:
    is_prime = [True] * (n + 1)
    is_prime[0] = is_prime[1] = False
    i = 2
    while i * i <= n:                            # √n까지
        if is_prime[i]:
            is_prime[i*i::i] = [False] * len(is_prime[i*i::i])  # i*i부터, C속도
        i += 1
    return is_prime

파이썬에서는 이중 for 루프가 \(10^7\)에서 매우 느리므로, 슬라이스 대입
is_prime[i*i::i] = [False] * ... 패턴으로 안쪽 루프를 C 수준으로 내리는 것이
사실상 필수입니다.


2. 소수 목록과 개수 세기

vector<int> primes;
for (int i = 2; i <= MAXN; i++)
    if (!composite[i]) primes.push_back(i);
// 소수의 개수는 primes.size()
primes = [i for i in range(2, n + 1) if is_prime[i]]
count = len(primes)

\(N\) 이하 소수 개수를 묻는 문제는 이 한 줄이 답입니다. \(10^7\) 이하 소수는 약
\(62\)만 개 정도입니다.


3. 최소 소인수(SPF) 체 → \(O(\log N)\) 소인수분해

각 수의 가장 작은 소인수(smallest prime factor) 를 체로 미리 기록해 두면,
어떤 수든 반복해서 나눠 내려가며 \(O(\log N)\)에 소인수분해할 수 있습니다.

int spf[MAXN + 1];                    // spf[x] = x의 최소 소인수
void buildSpf() {
    for (int i = 2; i <= MAXN; i++)
        if (spf[i] == 0)              // 아직 안 채워졌으면 i는 소수
            for (int j = i; j <= MAXN; j += i)
                if (spf[j] == 0) spf[j] = i;  // 처음 채운 소수가 최소 소인수
}
vector<int> factorize(int x) {
    vector<int> f;
    while (x > 1) { f.push_back(spf[x]); x /= spf[x]; }
    return f;                         // 소인수들 (중복 포함)
}
def build_spf(n: int) -> list[int]:
    spf = list(range(n + 1))          # spf[x] 초기값 = x
    i = 2
    while i * i <= n:
        if spf[i] == i:               # i가 소수
            for j in range(i * i, n + 1, i):
                if spf[j] == j:        # 아직 자기 자신이면 i가 최소 소인수
                    spf[j] = i
        i += 1
    return spf

def factorize(x: int, spf: list[int]) -> list[int]:
    f = []
    while x > 1:
        f.append(spf[x])
        x //= spf[x]
    return f

factorize(360)[2, 2, 2, 3, 3, 5]를 돌려줍니다(\(360 = 2^3 \cdot 3^2 \cdot 5\)).
매번 \(\sqrt x\)로 나누는 것보다, 수백만 개의 수를 분해할 때 압도적으로 빠릅니다.

주의: C++판은 안쪽을 j = i부터, 파이썬판은 j = i*i부터 시작했는데 둘 다
올바릅니다. 파이썬판은 spf를 자기 자신으로 초기화했으므로 \(i^2\) 미만의 배수는
이미 더 작은 소인수로 채워졌거나 소수 자신이라 건드릴 필요가 없습니다.


4. 메모리 주의

체는 크기 \(N + 1\)의 배열이 필요합니다. \(N = 10^8\)이면 bool 배열만 약 \(100\)MB라
메모리 제한에 걸릴 수 있습니다. 대책은 다음과 같습니다.

  • bool/bytearray 사용int 배열(\(4\)바이트) 대신 \(1\)바이트로 \(1/4\).
  • 비트 압축std::bitset이나 비트마스크로 \(8\)개를 \(1\)바이트에. \(8\)배 절약.
  • 홀수만 저장 — 짝수는 \(2\) 말고 소수가 없으니 인덱스를 절반으로.

보통 \(N \le 10^7\)이면 평범한 bool 배열(\(\approx 10\)MB)로 충분합니다.


5. 흔한 실수

  • i * i 오버플로\(N\)이 크면 int ii * iint를 넘습니다.
    (long long)i * i로 캐스팅하세요.
  • \(0, 1\) 처리\(0\)\(1\)은 소수가 아닙니다. 배열을 전부 소수로 초기화했다면
    반드시 꺼 주세요. 골드바흐류에서 자주 이걸로 오답이 납니다.
  • 매 테스트케이스마다 체를 다시 만듦 — 체는 한 번만 만들고 모든 질의가
    공유해야 합니다.
  • 파이썬 순수 이중 루프\(10^7\)에서 시간 초과. 슬라이스 대입으로 내리세요.

6. 패턴 인식

  • "\(N\)까지 소수를 모두" / "소수 판정을 여러 번" → 기본 체.
  • "많은 수를 소인수분해" / "약수 개수·합" → 최소 소인수(SPF) 체.
  • "수 하나만, 그리고 큼" → 체가 아니라 \(\sqrt N\) 판정 또는 밀러-라빈.
Lesson 3강 · 선형 체와 구간 체 선택 8m

더 빠르게, 더 넓게

기본 체는 \(O(N \log \log N)\)입니다. 이번 강의에서는 진짜 \(O(N)\) 선형 체와,
\(N\)이 배열에 담기지 않을 만큼 클 때 쓰는 구간 체(segmented sieve) 를 다룹니다.


1. 왜 기본 체는 선형이 아닌가

기본 에라토스테네스의 체에서 예를 들어 \(12\)\(2\)의 배수로도, \(3\)의 배수로도
지워집니다. 즉 하나의 합성수가 여러 번 지워집니다. 이 중복이 \(\log \log N\)
인수를 만듭니다. 선형 체의 아이디어는 모든 합성수를 정확히 한 번씩, 그것도
자신의 최소 소인수로만 지우는 것입니다.


2. 선형 체 구현

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 10000000;
int spf[MAXN + 1];                 // 최소 소인수 (0이면 아직 미확정)
vector<int> primes;

void linearSieve() {
    for (int i = 2; i <= MAXN; i++) {
        if (spf[i] == 0) {         // i가 소수
            spf[i] = i;
            primes.push_back(i);
        }
        for (int p : primes) {
            if ((long long)i * p > MAXN) break;
            spf[i * p] = p;        // i*p의 최소 소인수는 p
            if (p == spf[i]) break; // 핵심! p가 i의 최소 소인수면 멈춤
        }
    }
}
def linear_sieve(n: int):
    spf = [0] * (n + 1)
    primes = []
    for i in range(2, n + 1):
        if spf[i] == 0:            # i가 소수
            spf[i] = i
            primes.append(i)
        for p in primes:
            if i * p > n:
                break
            spf[i * p] = p         # i*p의 최소 소인수는 p
            if p == spf[i]:        # 핵심 종료 조건
                break
    return spf, primes

3. 왜 정확히 \(O(N)\)인가

핵심은 if (p == spf[i]) break; 한 줄입니다. 각 합성수 \(c = i \times p\)를 지울 때,
\(p\)는 항상 \(c\)의 최소 소인수가 되도록 보장됩니다. 이를 증명해 봅시다.

내부 루프는 소수 \(p\)를 작은 것부터 훑습니다. \(p\)\(i\)의 최소 소인수 spf[i]
같아지는 순간 멈추므로, 사용되는 \(p\)는 항상 \(p \le \operatorname{spf}(i)\)입니다.
따라서 \(i \times p\)에서 \(p\)는 그 곱의 최소 소인수가 됩니다. 반대로 임의의 합성수
\(c\)\(c = \operatorname{spf}(c) \times (c / \operatorname{spf}(c))\)유일하게 표현되므로,
\(i = c / \operatorname{spf}(c)\), \(p = \operatorname{spf}(c)\)단 한 번만 지워집니다.

각 합성수가 정확히 한 번씩 처리되고 각 소수도 한 번 발견되므로 총 연산은
\(\Theta(N)\), 즉 선형입니다. 덤으로 spf 배열과 primes 목록을 동시에 얻습니다.


4. 기본 체 vs 선형 체

항목 기본 체 선형 체
복잡도 \(O(N \log \log N)\) \(O(N)\)
코드 짧음 조금 김
부가물 is_prime is_prime + SPF + 소수목록
실전 체감 \(N \le 10^7\) 충분히 빠름 곱셈적 함수 필요 시 유리

이론상 선형 체가 빠르지만, 상수 인자와 캐시 효율 때문에 \(N \le 10^7\)에서는 기본
체도 매우 빠릅니다. 최소 소인수나 오일러 파이 같은 곱셈적 함수를 함께 계산해야
할 때 선형 체가 진가를 발휘합니다.


5. 구간 체: 배열에 안 담기는 큰 범위

\([L, R]\)의 소수를 구해야 하는데 \(R\)\(10^{12}\)처럼 커서 \(R\) 크기의 배열을 못 만들
때가 있습니다. 하지만 \(R - L\)이 작다면(예: \(10^6\)) 구간 체로 풀립니다.

핵심 관찰. \(R\) 이하의 합성수는 \(\sqrt R\) 이하의 소수 중 하나를 약수로 가집니다
(소수 판정의 \(\sqrt N\) 정리와 동일). 따라서:

  1. 먼저 기본 체로 \(\sqrt R\) 이하의 소수들을 구한다.
  2. 크기 \(R - L + 1\)짜리 배열을 만들어 구간 \([L, R]\)만 표현한다.
  3. 각 작은 소수 \(p\)에 대해, \([L, R]\) 안에서 \(p\)의 배수들을 지운다. 시작점은
    \(\max(p^2,\ \lceil L/p \rceil \cdot p)\).
vector<long long> segmentPrimes(long long L, long long R) {
    long long lim = sqrt((double)R) + 1;
    vector<char> base(lim + 1, 1);          // √R 이하 소수용
    for (long long i = 2; i * i <= lim; i++)
        if (base[i]) for (long long j = i*i; j <= lim; j += i) base[j] = 0;

    vector<char> seg(R - L + 1, 1);         // seg[x-L] : x가 소수인지
    for (long long p = 2; p <= lim; p++) {
        if (!base[p]) continue;
        long long start = max(p * p, (L + p - 1) / p * p); // L 이상 첫 배수
        for (long long j = start; j <= R; j += p) seg[j - L] = 0;
    }
    if (L == 1) seg[0] = 0;                  // 1 제외

    vector<long long> res;
    for (long long x = L; x <= R; x++)
        if (seg[x - L]) res.push_back(x);
    return res;
}

메모리는 \(O(\sqrt R + (R - L))\)만 쓰므로, \(R\)이 아무리 커도 구간 폭만 작으면
됩니다. 이것이 "\(10^{12}\) 근처의 소수" 같은 문제의 표준 해법입니다.


6. 정리와 도구 선택

  • 선형 체 \(O(N)\): 각 합성수를 최소 소인수로 딱 한 번 지움. SPF·소수목록·곱셈적
    함수를 함께 얻을 때 최적. 종료 조건 p == spf[i]가 선형성의 핵심.
  • 구간 체: \(R\)이 거대하지만 구간 폭 \(R - L\)이 작을 때. \(\sqrt R\) 이하 소수로만
    지운다.
  • 도구 선택 요약: 수 한두 개 → \(\sqrt N\)/밀러-라빈. \(N\) 이하 전체 → 기본 체.
    SPF·곱셈적 함수 → 선형 체. 거대한 \(R\)의 좁은 구간 → 구간 체.
Practice problem 수의 색깔 수 선택 25m
R00125

수의 색깔 수

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem 소수 구하기 선택 25m
R00743

소수 구하기

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
03
Level 3 · Explorer

Explorer

수학과 정수론 · Explorer 단계

0/24 완료
Lesson 1강 · 개념과 정리 필수 8m

공정 게임과 승패의 수학

두 사람이 번갈아 돌을 놓거나 빼는 게임에서 "완벽하게 두면 누가 이기는가?"
묻는 것이 조합 게임 이론입니다. 여기서 다루는 게임은 공정 게임(impartial game)
입니다 — 어떤 상태에서 둘 수 있는 수가 누구 차례인지와 무관하고, 마지막에 둘 수
없는 사람이 지는(정상 규칙, normal play) 게임이죠.

이번 강의에서는 승패 상태(P/N)의 정의, 님(Nim) 게임과 그 XOR 정리, 그리고
여러 게임을 합친 스프라그-그런디(Sprague-Grundy) 정리까지 다룹니다.


1. 승리 상태(N)와 패배 상태(P)

  • P-상태(previous player wins): 방금 둔 사람(직전 플레이어)이 이기는 상태.
    지금 둘 차례인 사람이 지는 상태.
  • N-상태(next player wins): 지금 둘 차례인 사람이 이기는 상태.

정의는 재귀적입니다.

$$ \text{P-상태} \iff \text{모든 이동이 N-상태로 간다(=탈출 불가)} $$
$$ \text{N-상태} \iff \text{P-상태로 가는 이동이 하나라도 있다} $$

돌이 하나도 없는 종료 상태는, 지금 둘 사람이 둘 수 없어 지므로 P-상태입니다.
여기서부터 거꾸로 채워 나가면 모든 상태의 승패를 알 수 있습니다.


2. 님 게임과 XOR 정리

님 게임. 여러 개의 돌더미가 있고, 한 번에 한 더미에서 원하는 만큼(1개 이상)
가져갑니다. 마지막 돌을 가져가는 사람이 승리(정상 규칙에서는 못 가져가는 사람이 패배).

Bouton의 정리. 더미 크기가 \(a_1, a_2, \dots, a_n\)일 때, 현재 상태가
P-상태(둘 차례인 사람이 패배)일 필요충분조건은
$$ a_1 \oplus a_2 \oplus \cdots \oplus a_n = 0 $$
이다. 여기서 \(\oplus\) 는 비트 XOR이다. 이 값을 님 값(nim-sum) 이라 한다.

직관. XOR가 \(0\)이 아니면, 가장 높은 켜진 비트를 가진 더미를 골라 그 비트를 끄도록
줄이면 XOR를 \(0\)으로 만들 수 있습니다(항상 가능). 반대로 XOR가 \(0\)인 상태에서는 어떤
이동을 해도 XOR가 \(0\)이 아닌 상태로 바뀝니다. 즉 \(0\)은 벗어날 수만 있고 만들 수는
없는 "패배의 늪"이라, 이기는 쪽은 상대에게 계속 XOR \(=0\)을 떠넘깁니다.


3. 숫자로 확인하기

더미 \((3, 4, 5)\): \(3 \oplus 4 \oplus 5 = 011 \oplus 100 \oplus 101 = 010 = 2 \neq 0\)
선수 승리(N-상태). 이기는 수: \(5\)더미에서 \(2\)를 빼 \(3\)으로 만들면
\(3 \oplus 4 \oplus 3 = 4\)? 확인해 봅시다. 최고 비트는 \(4(100)\)가 가진 비트. 그
비트를 끄려면 \(4\)더미를 건드립니다: \(4 \to 4 \oplus 2 = 6\)? 아니 — 규칙은 "줄이기"만
됩니다. 새 값은 \(a_i \oplus \text{nimsum}\) 이어야 하고 그게 원래보다 작아야 합니다.
nim-sum \(=2\). \(3 \oplus 2 = 1 < 3\), \(4 \oplus 2 = 6 > 4\), \(5 \oplus 2 = 7 > 5\).
따라서 첫 더미를 \(3 \to 1\)로 줄이면 \((1,4,5)\), \(1 \oplus 4 \oplus 5 = 0\) → 상대에게
P-상태를 넘깁니다.

더미 \((1, 2, 3)\): \(1 \oplus 2 \oplus 3 = 0\)후수 승리(P-상태). 선수는 무엇을
해도 진다.


4. 여러 게임의 합: 스프라그-그런디

각 독립 게임에 그런디 수(Grundy number) 를 매기면 님으로 환원됩니다.

$$ g(s) = \operatorname{mex}\{\, g(s') : s \to s' \,\} $$

여기서 \(\operatorname{mex}\)(minimum excludant)는 집합에 없는 최소 음이 아닌 정수입니다
(\(\operatorname{mex}\{0,1,3\}=2\)). 님 한 더미의 그런디 수는 그 크기와 같습니다.

스프라그-그런디 정리. 여러 공정 게임의 합의 그런디 수는 각 게임 그런디 수의
XOR이다. 전체가 P-상태 \(\iff\) XOR \(=0\).

\(g(s)=0\)이면 그 게임 성분은 P-상태, XOR가 \(0\)이면 전체가 지는 자리입니다.


5. 언제 쓰는가

  • "두 사람이 번갈아, 최적으로 두고, 마지막에 못 두면 패" → 공정 게임. P/N 판정.
  • 더미에서 자유롭게 가져가는 형태 → 님, XOR로 즉답.
  • 여러 판을 동시에, 또는 이동 규칙이 복잡 → 그런디 수 + mex + XOR.
  • 상태 수가 작으면 P/N 또는 그런디를 DP로 채운다 — 복잡도 \(O(\text{상태} \times \text{이동})\).

정리

  • P-상태 = 둘 차례가 지는 자리, N-상태 = 이기는 자리. 종료 상태는 P.
  • 님은 nim-sum(XOR) 하나로 결판: \(0\)이면 후수 승, 아니면 선수 승.
  • 복잡한/합쳐진 게임은 그런디 수 \(=\operatorname{mex}\), 합은 XOR로 환원.
Lesson 2강 · 구현 선택 8m

구현 레퍼런스

승패 판정은 세 가지 층으로 구현합니다: (1) 님은 XOR 한 줄, (2) 일반 게임은 P/N을
DP로, (3) 게임의 합은 그런디 수 + mex + XOR. 차례로 코드로 옮깁니다.


1. 님 게임: XOR 한 줄

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n; cin >> n;
    int nimsum = 0;
    for (int i = 0; i < n; i++) {
        int a; cin >> a;
        nimsum ^= a;                 // 모든 더미를 XOR
    }
    // 정상 규칙: nim-sum != 0 이면 선수(먼저 두는 쪽) 승리
    cout << (nimsum ? "First" : "Second") << '\n';
}
import sys
data = sys.stdin.read().split()
n = int(data[0])
nimsum = 0
for x in data[1:1 + n]:
    nimsum ^= int(x)
print("First" if nimsum else "Second")

2. 그런디 수를 DP로 계산

예: 한 더미에서 \(1, 2, 3\)개만 가져갈 수 있는 게임. 상태 = 돌 개수 \(x\).

const int MAXN = 100000;
int grundy[MAXN + 1];

int mex(vector<int>& s) {
    sort(s.begin(), s.end());
    int m = 0;
    for (int v : s) {
        if (v == m) m++;            // 0,1,2,... 가 채워지는 최소 빈 값
        else if (v > m) break;
    }
    return m;
}

void build() {
    grundy[0] = 0;                  // 더 못 둠 → 그런디 0 (P-상태)
    for (int x = 1; x <= MAXN; x++) {
        vector<int> nxt;
        for (int take : {1, 2, 3})  // 가능한 이동
            if (x - take >= 0) nxt.push_back(grundy[x - take]);
        grundy[x] = mex(nxt);
    }
}

이 게임은 \(g(x) = x \bmod 4\) 가 되어, 여러 더미면 \(\bigoplus (a_i \bmod 4)\)
승패가 갈립니다.


3. 게임의 합: 그런디 XOR

각 성분 게임의 그런디 수를 XOR 합니다.

int total = 0;
for (int i = 0; i < n; i++) {
    int x; cin >> x;
    total ^= grundy[x];             // 스프라그-그런디: 합의 그런디 = XOR
}
cout << (total ? "First" : "Second") << '\n';

4. 이기는 첫 수 찾기 (님)

nim-sum이 \(0\)이 아닐 때, 어느 더미를 얼마로 줄여야 하는지 구성적으로 찾습니다.

int nimsum = 0;
for (int a : pile) nimsum ^= a;
for (int i = 0; i < (int)pile.size(); i++) {
    int target = pile[i] ^ nimsum;  // 이 값으로 바꾸면 전체 XOR = 0
    if (target < pile[i]) {         // '줄이기'만 허용되므로 작아질 때만 유효
        printf("pile %d: %d -> %d\n", i, pile[i], target);
        break;
    }
}

5. mex 구현 주의

mex는 집합에 없는 최소 음이 아닌 정수입니다. 흔한 방법은 boolean 표시 배열:

int mex_of(const vector<int>& s) {
    vector<char> seen(s.size() + 1, 0);
    for (int v : s) if (v >= 0 && v <= (int)s.size()) seen[v] = 1;
    for (int i = 0; ; i++) if (!seen[i]) return i;
}

크기가 \(k\)인 집합의 mex는 항상 \(0 \sim k\) 범위이므로 배열을 \(k+1\)까지만 잡으면
충분합니다.


정리

  • 님은 nimsum ^= animsum ? First : Second.
  • 일반 게임은 grundy[x] = mex(다음 상태들의 그런디)를 DP로 채운다.
  • 게임의 합은 그런디 수의 XOR. 첫 승리 수는 a[i] ^ nimsum < a[i] 로 찾는다.
Lesson 3강 · 심화와 변형 선택 8m

규칙이 바뀌면 답도 바뀐다

게임 이론 문제의 함정은 대부분 "규칙이 표준 님과 어디가 다른가" 를 놓치는 데서
옵니다. 마지막 수의 승패, 이동 제한, 여러 게임의 결합 방식이 조금만 달라도 정리가
통째로 바뀝니다.


1. 미제르(misère) 님 — 마지막에 두면 패배

정상 규칙은 "마지막을 두는 쪽이 승"이지만 미제르는 마지막을 두는 쪽이 패배
입니다. 님의 미제르 승패는 미묘합니다.

미제르 님. 모든 더미가 \(1\) 이하이면(전부 \(0\) 또는 \(1\)), 더미가 홀수 개일 때
후수 승(현재 둘 사람이 패). 그렇지 않고 크기 \(\ge 2\)인 더미가 하나라도 있으면
정상 규칙과 똑같이 nim-sum으로 판정한다.

즉 "\(2\) 이상 더미가 있으면 정상 규칙, 전부 \(1\) 이하이면 개수의 홀짝"으로 갈립니다.
정상 규칙과 미제르를 헷갈리면 반례에서 정확히 틀립니다.


2. 자주 나오는 변형들

  • 계단 님(staircase nim): 홀수 번째 계단의 돌만 XOR 해서 판정.
  • 가져갈 수 있는 개수 제한 (예: \(1\sim3\)): 한 더미 그런디 \(= x \bmod (k+1)\)
    (Bash 게임). \(k=3\)이면 \(x \bmod 4\).
  • 여러 더미로 쪼개기 / 합치기: 그런디 DP로 각 상태를 계산해야 함.
  • 그래프 게임(코인 이동): 각 정점에 그런디 수, DAG면 위상 순서로 채운다.

3. 함정 모음

  • 정상 vs 미제르 혼동 — 문제의 "마지막에 못 두면 진다 / 마지막을 두면 진다"를
    반드시 확인.
  • 그런디 수 \(\ne 0\) 을 '이 게임 승리'로 오해 — 그런디 수는 성분 게임의 값일 뿐,
    전체 승패는 성분들의 XOR로 결정. 단일 게임에서만 \(g \ne 0 \Rightarrow\) 선수 승.
  • mex 범위 — mex를 큰 배열로 잡거나, 다음 상태 그런디의 최댓값을 넘겨 짚어 오답.
  • 상태 폭발 — 이동 규칙이 복잡하면 상태가 지수적으로 늘 수 있음. 주기성
    (\(g(x)\)가 결국 주기적)을 찾아 큰 \(x\)\(O(1)\)로 답하는 것이 정석.

4. 주기성으로 큰 입력 처리

많은 뺄셈 게임의 그런디 수열은 결국 주기적입니다. 작은 범위를 DP로 채워 주기
\(T\)와 시작점을 찾은 뒤, 큰 \(x\)\(g(x) = g(x_0 + (x - x_0) \bmod T)\) 로 즉답합니다.

// 앞부분을 계산해 두고 눈으로/자동으로 주기 T를 찾은 뒤
long long query(long long x) {
    if (x < PREFIX) return g[x];
    return g[START + (x - START) % T];   // 주기 구간에 접어 넣기
}

주기 탐지는 "충분히 긴 두 구간이 완전히 일치"하는지로 확인합니다. 주기를 잘못 잡으면
경계에서 틀리니, 검증 구간을 넉넉히 두세요.


5. 검증 사례

  • \((1,1,1)\): nim-sum \(=1\) → 정상 규칙 선수 승. 미제르에서는 전부 \(1\)이고 개수 \(3\)
    (홀수) → 후수 승. 두 규칙의 답이 갈리는 대표 예.
  • Bash 게임 \(x=8, k=3\): \(8 \bmod 4 = 0\) → 후수 승.
  • 계단 님 \((2, 3, 4)\) (1-indexed): 홀수 계단 \(2, 4\)번째가 아니라 홀수 인덱스
    \(1,3\)번째 \(=2, 4\)… 문제 정의(0/1 인덱스, 어느 쪽이 홀수)를 반드시 확인.

정리

  • 정상 규칙 님은 XOR, 미제르는 "\(2\) 이상 더미 유무"로 규칙이 갈린다.
  • 그런디 수는 성분값, 전체 승패는 XOR. 단일 게임에서만 \(g\ne0\)이 곧 승.
  • 큰 입력은 그런디 수열의 주기성으로 접어 \(O(1)\)에 답한다.
Practice problem 돌 게임 선택 25m
R01461

돌 게임

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem 격자 게임 선택 25m
KOI00035

격자 게임

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Lesson 1강 · 개념과 정리 필수 8m

경우의 수를 세는 도구 상자

조합론은 "몇 가지 방법이 있는가?" 를 공식으로 세는 분야입니다. 이번 강의에서는
이항계수 \(\binom{n}{r}\), 파스칼의 삼각형, 카탈란 수, 그리고 대회의 필수 도구인
모듈러 이항계수(페르마 역원)를 다룹니다.


1. 순열과 조합

서로 다른 \(n\)개에서 \(r\)개를 순서 있게 뽑는 순열:
$$ P(n, r) = \frac{n!}{(n-r)!}. $$

순서 없이 뽑는 조합(이항계수):
$$ \binom{n}{r} = \frac{n!}{r!\,(n-r)!}, \qquad \binom{n}{0} = \binom{n}{n} = 1. $$

핵심 성질:
$$ \binom{n}{r} = \binom{n}{n-r}, \qquad \sum_{r=0}^{n} \binom{n}{r} = 2^n. $$


2. 파스칼의 항등식

$$ \binom{n}{r} = \binom{n-1}{r-1} + \binom{n-1}{r}. $$

직관. \(n\)번째 원소를 (뽑거나 / 안 뽑거나)로 나눕니다. 뽑으면 나머지 \(n-1\)에서
\(r-1\)개, 안 뽑으면 \(n-1\)에서 \(r\)개. 이 항등식이 파스칼의 삼각형이고, 나눗셈 없이
덧셈만으로 이항계수를 채울 수 있게 해 줍니다.


3. 카탈란 수

$$ C_n = \frac{1}{n+1}\binom{2n}{n} = \binom{2n}{n} - \binom{2n}{n+1}, \qquad C_0 = 1,\ C_{n} = \sum_{i=0}^{n-1} C_i\, C_{n-1-i}. $$

앞 몇 개: \(1, 1, 2, 5, 14, 42, 132, \dots\). 카탈란 수가 세는 것들:

  • 올바른 괄호 문자열 (\(n\)쌍),
  • \(n+1\)개 잎을 가진 이진트리(또는 완전이진트리)의 개수,
  • 볼록 \((n+2)\)각형의 삼각분할,
  • 격자에서 대각선을 넘지 않는 단조 경로.

4. 모듈러 이항계수: 왜 어렵나

\(\binom{n}{r}\)는 금세 천문학적으로 커져, 답을 보통 \(10^9+7\) 같은 소수로 나눈
나머지
로 요구합니다. 그런데 정의에 나눗셈(\(r!\), \((n-r)!\)로 나눔)이 있어 그냥
모듈러 연산을 못 합니다. 해결책은 모듈러 곱셈 역원입니다.

소수 \(p\)에 대해 페르마 소정리 \(a^{p-1} \equiv 1 \pmod p\) 로부터
$$ a^{-1} \equiv a^{p-2} \pmod p. $$

따라서
$$ \binom{n}{r} \equiv n! \cdot (r!)^{-1} \cdot ((n-r)!)^{-1} \equiv n! \cdot (r!)^{p-2} \cdot ((n-r)!)^{p-2} \pmod p. $$

\(n!\)을 미리 전부 구해 두면 각 질의를 \(O(\log p)\)(역원 계산) 또는 역계승을 전처리하면
\(O(1)\)에 답할 수 있습니다.


5. 숫자로 확인하기

  • \(\binom{5}{2} = \frac{5\cdot4}{2\cdot1} = 10\). 파스칼로도 \(\binom{4}{1}+\binom{4}{2}=4+6=10\).
  • \(\binom{10}{3} = 120\).
  • 카탈란 \(C_4 = \frac{1}{5}\binom{8}{4} = \frac{70}{5} = 14\).
  • 모듈러: \(\binom{4}{2} = 6\). \(p=7\)이면 \(4! = 24 \equiv 3\), \(2! = 2\), 역원
    \(2^{-1} \equiv 2^{5} = 32 \equiv 4\). \(3 \cdot 4 \cdot 4 = 48 \equiv 48 - 42 = 6\). ✓

6. 언제 무엇을 쓰는가

  • \(n\)이 작고(\(\le 60\)) 나머지 없이 → 파스칼 DP, long long/__int128 또는 빅넘.
  • 소수 modulo로 큰 \(\binom{n}{r}\) (\(n \le 10^6\!\sim\!10^7\)) → 계승 + 역계승 전처리, \(O(1)\) 질의.
  • \(p\)가 작은 소수이고 \(n, r\)이 매우 클 때 → 뤼카 정리(3강).
  • 여러 유형을 세는 문제에서 카탈란/스털링 등 닫힌 공식을 먼저 의심.

정리

  • \(\binom{n}{r} = \frac{n!}{r!(n-r)!}\), 파스칼 \(\binom{n}{r}=\binom{n-1}{r-1}+\binom{n-1}{r}\).
  • 카탈란 \(C_n = \frac{1}{n+1}\binom{2n}{n}\) — 괄호·이진트리·삼각분할을 센다.
  • 소수 modulo에서는 나눗셈을 역원(\(a^{p-2}\))으로: 계승·역계승 전처리로 \(O(1)\) 질의.
Lesson 2강 · 구현 선택 8m

구현 레퍼런스

두 가지를 완성합니다: (1) 나머지 없는 작은 \(n\)파스칼 DP, (2) 소수 modulo에서
\(O(1)\) 질의를 주는 계승·역계승 전처리. 후자가 대회의 표준 도구입니다.


1. 파스칼 삼각형 DP

#include <bits/stdc++.h>
using namespace std;

long long C[65][65];
void pascal() {
    for (int n = 0; n <= 64; n++) {
        C[n][0] = C[n][n] = 1;
        for (int r = 1; r < n; r++)
            C[n][r] = C[n-1][r-1] + C[n-1][r];  // 덧셈만, 나눗셈 없음
    }
}

\(n \le 64\) 정도까지는 long long이 넘치지 않습니다(\(\binom{64}{32}\approx1.8\times10^{18}\)).
더 크면 오버플로하므로 modulo 버전으로 갑니다.


2. 계승·역계승 전처리 (소수 modulo)

가장 자주 쓰는 형태입니다. 역계승을 뒤에서 앞으로 채우는 것이 핵심.

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MOD = 1e9 + 7, N = 1000006;
ll fact[N], inv_fact[N];

ll power(ll a, ll b, ll m) {          // a^b mod m, O(log b)
    ll r = 1; a %= m;
    while (b) { if (b & 1) r = r * a % m; a = a * a % m; b >>= 1; }
    return r;
}
void init() {
    fact[0] = 1;
    for (int i = 1; i < N; i++) fact[i] = fact[i-1] * i % MOD;
    inv_fact[N-1] = power(fact[N-1], MOD - 2, MOD);   // 페르마: a^(p-2)
    for (int i = N - 2; i >= 0; i--)
        inv_fact[i] = inv_fact[i+1] * (i + 1) % MOD;   // 역계승 역방향 점화
}
ll C(int n, int r) {
    if (r < 0 || r > n) return 0;
    return fact[n] * inv_fact[r] % MOD * inv_fact[n-r] % MOD;
}

역원을 매번 power로 구하지 않고, 가장 큰 역계승 하나만 페르마로 구한 뒤
$$ (i!)^{-1} = ((i+1)!)^{-1} \cdot (i+1) $$
로 내려오면 전처리가 \(O(N + \log p)\), 질의는 \(O(1)\)이 됩니다.


3. 파이썬 버전

파이썬은 큰 정수가 기본이라 나머지 없는 \(\binom{n}{r}\)math.comb로 끝납니다.
modulo 버전만 직접 전처리합니다.

import sys
MOD = 10**9 + 7
N = 10**6 + 6
fact = [1] * N
for i in range(1, N):
    fact[i] = fact[i-1] * i % MOD
inv_fact = [1] * N
inv_fact[N-1] = pow(fact[N-1], MOD - 2, MOD)   # 페르마 역원
for i in range(N - 2, -1, -1):
    inv_fact[i] = inv_fact[i+1] * (i + 1) % MOD

def C(n, r):
    if r < 0 or r > n:
        return 0
    return fact[n] * inv_fact[r] % MOD * inv_fact[n-r] % MOD

# 나머지가 필요 없다면: from math import comb; comb(n, r)

4. 카탈란 수 modulo

ll catalan(int n) {                  // C(2n,n)/(n+1) mod p
    return C(2*n, n) * power(n + 1, MOD - 2, MOD) % MOD;
}

나눗셈 \(\frac{1}{n+1}\) 을 역원으로 처리하는 것만 주의하면 됩니다. 또는 점화식
\(C_n = \sum C_i C_{n-1-i}\)\(O(n^2)\)로 채워도 됩니다.


5. 검증 사례

  • \(\binom{5}{2}=10\), \(\binom{10}{3}=120\).
  • \(\binom{10^6}{5\times10^5} \bmod (10^9+7) = 996692777\) (전처리 버전으로 즉답).
  • 카탈란 \(C_5 = 42\): C(10,5) * inv(6) = 252 * inv(6) = 42.

정리

  • 작은 \(n\)파스칼 DP(덧셈), 오버플로 조심.
  • 소수 modulo는 계승 + 역계승 전처리\(O(1)\) 질의. 역계승은 큰 것 하나만
    페르마로 구하고 역방향 점화로 내린다.
  • 파이썬은 math.comb, modulo만 직접 전처리.
Lesson 3강 · 심화와 변형 선택 8m

이항계수를 더 멀리 밀기

기본 전처리는 "modulo가 소수이고 \(n\)이 배열 크기 안"일 때만 통합니다. 실제 문제는
\(p\)가 작거나, \(n\)\(10^{18}\)이거나, modulo가 소수가 아니거나 하는 변형으로 옵니다.


1. 뤼카(Lucas) 정리 — 작은 소수 \(p\), 거대한 \(n\)

\(p\)가 작은 소수이고 \(n, r\)이 매우 클 때, \(p\)진법 자릿수로 쪼갭니다.

$$ \binom{n}{r} \equiv \prod_{i} \binom{n_i}{r_i} \pmod p, $$

여기서 \(n_i, r_i\)\(n, r\)\(p\)진법 각 자리입니다. 어떤 자리에서 \(r_i > n_i\)이면
그 항이 \(0\)이라 전체가 \(0\).

ll lucas(ll n, ll r, ll p) {          // 작은 소수 p용 (C_small은 0..p-1 전처리)
    if (r == 0) return 1;
    return lucas(n / p, r / p, p) * C_small(n % p, r % p, p) % p;
}

\(C\_small\)\(0 \le n, r < p\) 범위를 전처리해 둔 이항계수입니다.


2. 합성수 modulo

modulo가 소수가 아니면 페르마 역원이 없을 수 있습니다.

  • 소수 거듭제곱들의 곱 \(m = \prod p_i^{e_i}\): 각 \(p_i^{e_i}\)에 대해 값을 구하고
    CRT로 합칩니다(중국인의 나머지 정리 단원 참고). 각 소수 거듭제곱은
    일반화된 뤼카(안드루 그랜빌) 로 처리.
  • 특수하게 \(m\)이 제곱 없는 수(square-free) 면 각 소인수에 뤼카 후 CRT로 충분.

3. 함정 모음

  • 역계승 방향 실수inv_fact는 반드시 큰 인덱스에서 작은 쪽으로 채운다.
    앞에서부터 pow로 매번 구하면 \(O(N \log p)\)로 느려지고 TLE.
  • r < 0 || r > n 미처리 — 이 경우 \(\binom{n}{r}=0\). 가드를 빼면 배열 음수
    인덱스로 런타임 에러.
  • modulo가 소수가 아닌데 페르마 사용 — 역원이 존재하지 않아 오답. 이땐 뤼카/CRT.
  • 오버플로fact[n] * inv_fact[r] % MOD * inv_fact[n-r] 처럼 곱마다 % MOD.
    중간에 % MOD를 빼먹으면 long long이 넘친다.
  • \(0^0\), \(\binom{0}{0}\) — 모두 \(1\)로 정의. 경계에서 특히 카탈란/DP 초기값 주의.

4. 자주 쓰는 조합 항등식

  • 중복조합: \(n\)종류에서 중복 허용해 \(r\)개 뽑기 \(= \binom{n+r-1}{r}\) (별과 막대).
  • 방데르몽드: \(\binom{m+n}{r} = \sum_{k} \binom{m}{k}\binom{n}{r-k}\).
  • 하키스틱: \(\sum_{i=r}^{n} \binom{i}{r} = \binom{n+1}{r+1}\).
  • 이항정리: \((x+y)^n = \sum_{r} \binom{n}{r} x^r y^{n-r}\).

문제에서 합을 이 항등식으로 닫힌 형태로 접을 수 있으면 복잡도가 극적으로 줄어듭니다.


5. 검증 사례

  • 뤼카: \(\binom{10}{3} \bmod 3\). \(10 = (101)_3\), \(3 = (010)_3\). 자리별
    \(\binom{1}{0}\binom{0}{1}\binom{1}{0}\) — 가운데 \(\binom{0}{1}=0\) → 전체 \(0\).
    실제 \(120 = 3 \times 40\) 이므로 \(120 \bmod 3 = 0\). ✓
  • 중복조합: 사과·배·귤에서 \(2\)개 고르기 \(= \binom{3+2-1}{2} = \binom{4}{2} = 6\). ✓

정리

  • 작은 소수·거대한 \(n\)뤼카 정리(\(p\)진법 자릿수 곱).
  • 합성수 modulo → 소수 거듭제곱별 계산 후 CRT.
  • 실전 함정은 역계승 방향, 범위 가드, 중간 % MOD 누락. 항등식으로 합을 접는 습관이
    복잡도를 가른다.
Practice problem 이진 트리 선택 25m
KOI00022

이진 트리

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem 새로운 인연 선택 25m
KOI00013

새로운 인연

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Lesson 1강 · 개념과 정리 필수 8m

서로소의 개수를 세다

오일러 피 함수(Euler's totient) \(\varphi(n)\)\(1\)부터 \(n\)까지의 정수 중
\(n\)과 서로소인 것의 개수입니다.

$$ \varphi(n) = \#\{\, k : 1 \le k \le n,\ \gcd(k, n) = 1 \,\}. $$

이 함수는 모듈러 거듭제곱, 역원, 순환마디 문제의 심장부에 있습니다.


1. 곱셈 공식

\(n\)의 소인수분해 \(n = p_1^{e_1} p_2^{e_2} \cdots p_k^{e_k}\) 로부터

$$ \varphi(n) = n \prod_{p \mid n} \left(1 - \frac{1}{p}\right). $$

직관. 각 소수 \(p\)에 대해, \(p\)의 배수 비율 \(\frac1p\) 만큼을 제거합니다. 서로 다른
소수들은 독립적으로 작용하므로 인수들의 곱이 됩니다(포함-배제로도 유도됨).

특수한 값:
- \(p\)가 소수: \(\varphi(p) = p - 1\).
- \(\varphi(p^e) = p^e - p^{e-1} = p^{e-1}(p-1)\).
- 곱셈적: \(\gcd(m, n) = 1\) 이면 \(\varphi(mn) = \varphi(m)\varphi(n)\).


2. 오일러 정리

오일러 정리. \(\gcd(a, n) = 1\) 이면
$$ a^{\varphi(n)} \equiv 1 \pmod n. $$

\(n\)이 소수 \(p\)이면 \(\varphi(p) = p-1\) 이라 페르마 소정리
\(a^{p-1} \equiv 1 \pmod p\) 의 일반화입니다.

따름정리 (역원). \(\gcd(a, n) = 1\) 이면
$$ a^{-1} \equiv a^{\varphi(n) - 1} \pmod n. $$
소수가 아닌 법에서도 서로소이기만 하면 이 방식으로 역원을 구할 수 있습니다.


3. 지수 축소

\(\gcd(a, n) = 1\) 이면 지수를 \(\varphi(n)\) 으로 나눈 나머지로 줄일 수 있습니다.

$$ a^{b} \equiv a^{\,b \bmod \varphi(n)} \pmod n. $$

\(b\)\(10^{100}\) 처럼 거대한 지수여도 \(\varphi(n)\) 으로 접어 다룰 수 있습니다.
(서로소가 아닐 때는 조건이 다릅니다 — 3강의 확장 오일러 정리 참고.)


4. 숫자로 확인하기

  • \(\varphi(6) = 6(1-\tfrac12)(1-\tfrac13) = 6 \cdot \tfrac12 \cdot \tfrac23 = 2\).
    실제 \(\{1, 5\}\) 두 개. ✓
  • \(\varphi(9) = 9(1 - \tfrac13) = 6\). \(\{1,2,4,5,7,8\}\). ✓
  • \(\varphi(12) = 12(1-\tfrac12)(1-\tfrac13) = 4\). \(\{1,5,7,11\}\). ✓
  • \(\varphi(100) = 100(1-\tfrac12)(1-\tfrac15) = 40\).
  • 오일러 정리: \(3^{\varphi(10)} = 3^4 = 81 \equiv 1 \pmod{10}\). ✓

5. 언제 쓰는가

  • 소수 아닌 법의 모듈러 역원 (\(a^{\varphi(n)-1}\)).
  • 거대한 지수의 모듈러 거듭제곱 (지수를 \(\varphi(n)\) 으로 축소).
  • 서로소 개수 세기, 분수 \(\frac{k}{n}\) 중 기약분수 개수, 원순열/목걸이 문제(뫼비우스와 함께).
  • 곱셈적 함수 성질을 이용한 정수론 합 계산.

정리

  • \(\varphi(n) = n \prod_{p\mid n}(1 - 1/p)\), 소수면 \(p-1\), 곱셈적.
  • 오일러 정리 \(a^{\varphi(n)} \equiv 1\) 은 페르마의 일반화 → 소수 아닌 법의 역원.
  • 거대 지수는 \(a^b \equiv a^{b \bmod \varphi(n)}\) (서로소일 때)로 축소.
Lesson 2강 · 구현 선택 8m

구현 레퍼런스

\(\varphi\) 를 (1) 단일 \(n\)에 대해 \(O(\sqrt n)\), (2) \(1 \sim N\) 전체에 대해 체로
\(O(N \log\log N)\) 에 구하는 두 방식을 완성합니다.


1. 단일 값: \(O(\sqrt n)\)

소인수를 훑으며 공식 \(\varphi \mathrel{-}= \varphi / p\) 를 적용합니다.

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

ll phi(ll n) {
    ll result = n;
    for (ll p = 2; p * p <= n; p++) {
        if (n % p == 0) {
            while (n % p == 0) n /= p;   // p 를 완전히 제거
            result -= result / p;        // (1 - 1/p) 인수 적용
        }
    }
    if (n > 1) result -= result / n;     // 남은 큰 소인수 하나
    return result;
}

마지막 if (n > 1)\(\sqrt n\) 보다 큰 소인수가 하나 남은 경우입니다(소수 판정의
소인수분해와 같은 패턴).


2. 구간 전체: 오일러 피 체

\(1 \sim N\) 의 모든 \(\varphi\) 를 한 번에 구합니다.

const int N = 1000001;
int phi[N];
void phi_sieve() {
    for (int i = 0; i < N; i++) phi[i] = i;   // phi[i] = i 로 초기화
    for (int i = 2; i < N; i++)
        if (phi[i] == i)                        // i 가 소수
            for (int j = i; j < N; j += i)
                phi[j] -= phi[j] / i;           // 각 배수에 (1 - 1/i)
}

phi[i] == i 로 소수를 판별하는 것이 요령입니다(아직 아무 소인수도 적용 안 된 상태).


3. 오일러 정리로 역원 / 거듭제곱

ll power(ll a, ll b, ll m) {
    ll r = 1; a %= m;
    while (b) { if (b & 1) r = r * a % m; a = a * a % m; b >>= 1; }
    return r;
}
// gcd(a,n)=1 일 때 역원 (n 이 소수가 아니어도 됨)
ll inv_euler(ll a, ll n) {
    return power(a, phi(n) - 1, n);   // a^(phi(n)-1)
}

4. 파이썬 버전

def phi(n):
    result = n
    p = 2
    while p * p <= n:
        if n % p == 0:
            while n % p == 0:
                n //= p
            result -= result // p
        p += 1
    if n > 1:
        result -= result // n
    return result

def phi_sieve(N):
    phi = list(range(N + 1))
    for i in range(2, N + 1):
        if phi[i] == i:                 # i 는 소수
            for j in range(i, N + 1, i):
                phi[j] -= phi[j] // i
    return phi

# 소수 아닌 법의 역원:  pow(a, phi(n) - 1, n)

5. 검증 사례

  • phi(6)=2, phi(9)=6, phi(12)=4, phi(100)=40, phi(10^9)=400000000.
  • 체: phi_sieve 로 얻은 phi[1..10] \(= [1,1,2,2,4,2,6,4,6,4]\).
  • 역원: inv_euler(3, 10) \(= 3^{\varphi(10)-1} = 3^3 = 27 \equiv 7\),
    \(3 \times 7 = 21 \equiv 1 \pmod{10}\). ✓

정리

  • 단일 값은 소인수를 훑는 \(O(\sqrt n)\), result -= result / p.
  • 구간 전체는 피 체, phi[i]==i 로 소수 판별.
  • 서로소 법의 역원은 a^(phi(n)-1) mod n.
Lesson 3강 · 심화와 변형 선택 8m

서로소가 아닐 때, 그리고 큰 지수

\(\varphi\) 의 기본은 "서로소"를 전제합니다. 실전 함정은 대부분 서로소가 아닌 경우
지수가 거대할 때 발생합니다.


1. 확장 오일러 정리

오일러 정리 \(a^{\varphi(n)} \equiv 1\)\(\gcd(a, n) = 1\) 을 요구합니다. 서로소가
아니어도 되는 일반형은 다음과 같습니다.

$$ a^{b} \equiv \begin{cases} a^{\,b \bmod \varphi(n)} & \gcd(a, n) = 1, \\[4pt] a^{\,b} & b < \log_2 n, \\[4pt] a^{\,(b \bmod \varphi(n)) + \varphi(n)} & b \ge \log_2 n \end{cases} \pmod n. $$

즉 서로소가 아니고 지수가 충분히 크면 \(b \bmod \varphi(n)\)\(\varphi(n)\)
더해서
거듭제곱해야 합니다. 이 "\(+\varphi(n)\)" 을 빼먹는 것이 대표적 오답입니다.
거대한 지수(예: \(2^{2^{100}}\) 형태 탑)를 modulo로 계산할 때 필수입니다.


2. 함정 모음

  • 소수 아닌 법에 페르마 사용\(a^{n-2}\)\(n\)이 소수일 때만 역원. 합성수면
    \(a^{\varphi(n)-1}\) 을 쓰되 \(\gcd(a,n)=1\) 이어야 함.
  • 서로소 아닌데 지수 축소 — 단순히 \(b \bmod \varphi(n)\) 으로 줄이면 틀림.
    확장 오일러 정리의 조건 분기 필요.
  • \(\varphi(1) = 1\) — 경계값. \(n=1\) 이면 모든 것이 \(0 \pmod 1\).
  • 오버플로result -= result / p 자체는 안전하나, 거듭제곱의 곱 r * a
    큰 법에서 __int128 필요.
  • 소인수분해가 병목\(n\)\(10^{18}\) 급이면 \(\varphi(n)\) 계산에 폴라드 로가
    필요할 수 있음.

3. 곱셈적 함수와 응용

\(\varphi\) 는 곱셈적(\(\gcd(m,n)=1 \Rightarrow \varphi(mn)=\varphi(m)\varphi(n)\))이라
여러 정수론 합에서 매끄럽게 다뤄집니다. 대표 항등식:

$$ \sum_{d \mid n} \varphi(d) = n. $$

직관. \(1 \sim n\) 의 각 수 \(k\)\(\gcd(k, n) = n/d\) 인 것끼리 묶으면 각 묶음의
크기가 \(\varphi(d)\) 이고, 전부 합치면 \(n\). 이 항등식은 뫼비우스 반전, 약수 관련 합
계산에서 자주 등장합니다.


4. 원순열 / 목걸이 개수

서로 다른 회전을 같게 보는 목걸이(necklace) 개수는 \(\varphi\) 로 셉니다(번사이드
보조정리):

$$ \#\text{목걸이}(n, k) = \frac{1}{n} \sum_{d \mid n} \varphi(d)\, k^{\,n/d}, $$

여기서 \(n\)은 구슬 수, \(k\)는 색 수입니다. 회전 대칭이 있는 셈 문제에서 \(\varphi\)
핵심 도구가 됩니다.


5. 검증 사례

  • 확장 오일러: \(2^{100} \bmod 12\). \(\gcd(2,12)=4 \ne 1\), \(100 \ge \log_2 12\)
    지수 \(= 100 \bmod \varphi(12) + \varphi(12) = 100 \bmod 4 + 4 = 0 + 4 = 4\),
    \(2^4 = 16 \equiv 4 \pmod{12}\). 실제 \(2^{100} \bmod 12 = 4\). ✓
  • 약수 합: $\sum_{d\mid 6}\varphi(d) = \varphi(1)+\varphi(2)+\varphi(3)+\varphi(6)
    = 1+1+2+2 = 6$. ✓

정리

  • 서로소가 아닌 거대 지수는 확장 오일러 정리의 "\(+\varphi(n)\)" 규칙이 필요.
  • \(\varphi\) 는 곱셈적이고 \(\sum_{d\mid n}\varphi(d)=n\) — 정수론 합·목걸이 개수의 핵심.
  • 함정: 소수 아닌 법에 페르마, 서로소 아닌데 단순 지수 축소, 소인수분해 병목.
Practice problem 오일러 피의 누적합 선택 25m
R00686

오일러 피의 누적합

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Lesson 1강 · 개념과 정리 필수 8m

gcd를 넘어: 계수까지 구하기

유클리드 호제법은 두 수의 최대공약수 \(\gcd(a,b)\)를 구합니다. 확장 유클리드 호제법
(extended Euclidean algorithm)
은 여기서 한 걸음 더 나아가, 그 gcd를 만드는 계수
\(x, y\)까지 함께 구합니다.

$$ a x + b y = \gcd(a, b). $$

이 하나의 식이 모듈러 역원, 일차 합동식, 중국인의 나머지 정리의 뿌리가 됩니다.


1. 베주 항등식

베주(Bézout) 항등식. 임의의 정수 \(a, b\)에 대해
$$ a x + b y = \gcd(a, b) $$
를 만족하는 정수 \(x, y\)가 존재한다. 더 일반적으로, \(ax+by=c\)가 정수해를 가질
필요충분조건은 \(\gcd(a,b) \mid c\) 이다.

\(\gcd(a,b)\)\(a, b\)의 정수 결합으로 만들 수 있는 가장 작은 양의 정수입니다.


2. 재귀로 계수를 되짚기

유클리드 호제법은 \(\gcd(a, b) = \gcd(b, a \bmod b)\) 를 반복합니다. 확장판은
재귀의 바닥에서부터 계수를 위로 전달합니다.

바닥: \(b = 0\) 이면 \(\gcd(a, 0) = a\) 이고 \(a \cdot 1 + 0 \cdot 0 = a\) 이므로
\((x, y) = (1, 0)\).

되돌아오는 단계: \(\gcd(b, a \bmod b) = b x_1 + (a \bmod b) y_1\) 을 이미 알 때,
\(a \bmod b = a - \lfloor a/b \rfloor b\) 를 대입하면
$$ \gcd(a,b) = b x_1 + \left(a - \left\lfloor \tfrac{a}{b} \right\rfloor b\right) y_1 = a\, y_1 + b\left(x_1 - \left\lfloor \tfrac{a}{b} \right\rfloor y_1\right). $$

따라서
$$ x = y_1, \qquad y = x_1 - \left\lfloor \tfrac{a}{b} \right\rfloor y_1. $$

복잡도는 유클리드와 같은 \(O(\log \min(a, b))\) 입니다.


3. 가장 중요한 응용: 모듈러 역원

\(\gcd(a, m) = 1\) 일 때, \(a\ x + m\ y = 1\) 의 해 \(x\)가 곧 \(a\)의 역원입니다.

$$ a x + m y = 1 \ \Longrightarrow\ a x \equiv 1 \pmod m \ \Longrightarrow\ a^{-1} \equiv x \pmod m. $$

페르마 역원(\(a^{p-2}\))은 \(m\)이 소수일 때만 되지만, 확장 유클리드는 \(\gcd(a,m)=1\)
이기만 하면
(합성수 modulo에서도) 역원을 줍니다. 그래서 더 일반적입니다.


4. 일차 합동식과 부정방정식

  • \(ax \equiv c \pmod m\): \(g = \gcd(a, m)\). \(g \nmid c\) 면 해 없음. 아니면
    양변을 \(g\)로 나눠 \(\frac{a}{g} x \equiv \frac{c}{g} \pmod{m/g}\), 역원으로 풀면
    \(m\) 안에 \(g\)의 해가 있습니다.
  • \(ax + by = c\) (부정방정식): \(g \mid c\) 여야 해가 존재. 한 해
    \((x_0, y_0)\)에서 일반해는
    $$ x = x_0 + \frac{b}{g} t, \qquad y = y_0 - \frac{a}{g} t \quad (t \in \mathbb{Z}). $$

5. 숫자로 확인하기

\(\gcd(240, 46)\): 확장 유클리드를 돌리면 \(\gcd = 2\), \((x, y) = (-9, 47)\).
검산: \(240 \cdot (-9) + 46 \cdot 47 = -2160 + 2162 = 2 = \gcd\). ✓

\(3\)의 역원 mod \(11\): \(3x + 11y = 1\) 의 해 \(x = 4\) (\(3 \cdot 4 = 12 \equiv 1\)). ✓


6. 언제 쓰는가

  • 모듈러 역원, 특히 modulo가 소수가 아닐 때 → 확장 유클리드.
  • \(ax+by=c\) 정수해의 존재/구성/모든 해.
  • CRT의 계수 계산(다음 단원).
  • 유리수를 modulo로 다룰 때(분수 \(=\) 분자 \(\times\) 분모\(^{-1}\)).

정리

  • 확장 유클리드는 \(ax+by=\gcd(a,b)\)의 계수 \(x,y\)까지 \(O(\log)\)에 구한다.
  • \(ax+by=c\)\(\gcd(a,b)\mid c\) 일 때만 해가 있고, 일반해는 \(t\)로 매개된다.
  • \(\gcd(a,m)=1\) 이면 \(x\)가 곧 모듈러 역원 — 소수 아닌 법에서도 통한다.
Lesson 2강 · 구현 선택 8m

구현 레퍼런스

확장 유클리드는 짧지만, 부호와 정규화에서 실수가 잦습니다. 재귀형과 역원, 부정방정식
해까지 정확한 레퍼런스를 만듭니다.


1. 재귀형 확장 유클리드

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

// a*x + b*y = gcd(a,b) 를 만족하는 x,y 를 채우고 gcd 반환
ll extgcd(ll a, ll b, ll &x, ll &y) {
    if (b == 0) { x = 1; y = 0; return a; }   // 바닥: gcd=a, (1,0)
    ll x1, y1;
    ll g = extgcd(b, a % b, x1, y1);
    x = y1;                                    // 계수 되짚기
    y = x1 - (a / b) * y1;
    return g;
}

2. 모듈러 역원

// gcd(a,m)=1 이면 a^{-1} mod m, 아니면 -1
ll modinv(ll a, ll m) {
    ll x, y;
    a = ((a % m) + m) % m;                      // 먼저 양수로 정규화
    ll g = extgcd(a, m, x, y);
    if (g != 1) return -1;                       // 역원 없음
    return ((x % m) + m) % m;                     // 음수 해 보정
}

((x % m) + m) % m양의 대표원으로 맞추는 것이 핵심입니다. 확장 유클리드가
주는 \(x\)는 음수일 수 있습니다.


3. 반복형 (스택 절약)

ll extgcd_iter(ll a, ll b, ll &x, ll &y) {
    ll x0 = 1, y0 = 0, x1 = 0, y1 = 1;
    while (b) {
        ll q = a / b;
        a -= q * b; swap(a, b);
        x0 -= q * x1; swap(x0, x1);
        y0 -= q * y1; swap(y0, y1);
    }
    x = x0; y = y0;
    return a;
}

4. 부정방정식 \(ax + by = c\) 의 모든 해

// 해가 있으면 한 해(x0,y0)와 스텝(dx,dy)을 채우고 true
bool solve(ll a, ll b, ll c, ll &x0, ll &y0, ll &dx, ll &dy) {
    ll x, y, g = extgcd(a, b, x, y);
    if (c % g != 0) return false;                // gcd | c 아니면 해 없음
    ll k = c / g;
    x0 = x * k; y0 = y * k;                       // 한 특수해
    dx = b / g; dy = -(a / g);                    // x += dx, y += dy 로 다른 해
    return true;
}

일반해는 \((x_0 + dx \cdot t,\ y_0 + dy \cdot t)\). 특정 범위의 해나 \(x \ge 0\) 최소해가
필요하면 \(t\)를 조정합니다.


5. 파이썬 버전

파이썬 3.8+ 에는 pow(a, -1, m) 내장 역원이 있지만, 계수가 필요하면 직접 씁니다.

def extgcd(a, b):
    if b == 0:
        return a, 1, 0          # gcd, x, y
    g, x1, y1 = extgcd(b, a % b)
    return g, y1, x1 - (a // b) * y1

def modinv(a, m):
    g, x, _ = extgcd(a % m, m)
    if g != 1:
        return None             # 역원 없음
    return x % m                # 파이썬 %는 항상 음이 아닌 결과

# 내장: pow(a, -1, m)  (m과 서로소여야 함, 3.8+)

파이썬의 %는 결과가 항상 음이 아니라 x % m 한 번이면 정규화가 끝납니다(C++는
((x%m)+m)%m 필요).


6. 검증 사례

  • extgcd(240, 46)g=2, x=-9, y=47, \(240(-9)+46(47)=2\). ✓
  • modinv(3, 11) = 4, modinv(17, 1000000007) = 352941179 (\(17 \times\) 그값 \(\equiv 1\)). ✓
  • solve(6, 9, 3) → 한 해와 스텝 \(dx=3, dy=-2\).

정리

  • 계수 되짚기: x = y1; y = x1 - (a/b)*y1.
  • 역원은 서로소일 때만, 결과는 양의 대표원으로 정규화(((x%m)+m)%m).
  • 부정방정식은 c % g 로 존재 판정, 일반해는 \(b/g, -a/g\) 스텝.
Lesson 3강 · 심화와 변형 선택 8m

부호, 오버플로, 그리고 활용

확장 유클리드 자체는 짧지만, 실전에서 틀리는 지점은 거의 정해져 있습니다: 부호,
오버플로, 그리고 "역원이 있다고 착각"하는 경우입니다.


1. 자주 나오는 함정

  • 음수 해 정규화 누락 — C++의 %는 음수를 낼 수 있습니다. 역원·해를 반드시
    ((x % m) + m) % m 로 양수화하세요.
  • \(\gcd \ne 1\) 인데 역원 시도\(\gcd(a,m)\ne1\) 이면 역원이 없습니다. 반환값
    \(g\)를 반드시 확인.
  • 오버플로x1 - (a / b) * y1 에서 중간 곱이 커질 수 있습니다. 큰 modulo나
    큰 계수를 다룰 때 __int128 또는 중간 mod가 필요할 수 있습니다.
  • \(a\) 또는 \(b\)\(0\)/음수extgcd(0, b)\((0,1)\)을 주고 gcd=b. 음수 입력이
    섞이면 부호 규약을 정하고 먼저 절댓값·부호 정규화.
  • \(c\)\(g\)로 안 나뉨\(ax+by=c\) 에서 \(g \nmid c\)해가 없음. 이 검사를
    빼먹으면 엉뚱한 값을 낸다.

2. 특정 범위의 해 고르기

\(ax+by=c\) 의 일반해 \(x = x_0 + \frac{b}{g} t\) 에서, 예를 들어 \(x \ge 0\) 인 최소해
원하면 \(t\)를 골라야 합니다.

ll dx = b / g;                    // x 의 스텝 (양수화해서 사용)
if (dx < 0) dx = -dx;
ll t = (x0 % dx + dx) % dx;       // 0 <= t < dx 인 최소 x
// x_min = t, 대응 y 를 다시 계산

문제가 "가장 작은 양의 \(x\)", "\(x\)\([L, R]\) 범위" 등을 요구하면 이 조정이 핵심이며,
경계에서 실수가 잦으니 스텝의 부호를 먼저 양수로 맞추는 습관이 안전합니다.


3. 일차 합동식 \(ax \equiv c \pmod m\)

\(g = \gcd(a, m)\) 라 하면:

  • \(g \nmid c\)해 없음.
  • \(g \mid c\) → 법 \(m\) 안에 정확히 \(g\)의 해. 대표해는
    \(x_0 = \frac{c}{g} \cdot \left(\frac{a}{g}\right)^{-1} \pmod{m/g}\),
    나머지는 \(x_0 + \frac{m}{g} k\) (\(k = 0, \dots, g-1\)).

이 "해가 여러 개"라는 점을 놓치면 답을 하나만 내고 틀립니다.


4. 역원 계산 방법 비교

방법 조건 복잡도
페르마 \(a^{p-2}\) \(m\)소수, \(\gcd(a,m)=1\) \(O(\log p)\)
확장 유클리드 \(\gcd(a,m)=1\) (합성수 OK) \(O(\log m)\)
\(1 \sim n\) 일괄 역원 소수 \(p\), 여러 개 필요 \(O(n)\) 전처리

일괄 역원 점화식: \(\text{inv}[i] = -\lfloor p/i \rfloor \cdot \text{inv}[p \bmod i] \bmod p\).
확장 유클리드는 합성수 법에서도 통하는 유일한 범용 방법입니다.


5. 검증 사례

  • \(ax \equiv c\): \(6x \equiv 3 \pmod 9\). \(g=\gcd(6,9)=3\), \(3 \mid 3\) → 해 \(3\)개.
    \(x \equiv 2 \pmod 3\)\(x \in \{2, 5, 8\} \pmod 9\). 검산 \(6\cdot2=12\equiv3\). ✓
  • 해 없음: \(2x \equiv 1 \pmod 4\), \(\gcd(2,4)=2 \nmid 1\) → 무해.
  • 최소 양수 해: \(3x+7y=1\)\(x \ge 0\) 최소는 \(x=5\) (\(3\cdot5-7\cdot2=1\)).

정리

  • 실전 오답의 근원은 부호 정규화·오버플로·\(g\nmid c\) 미검사.
  • \(ax\equiv c \pmod m\)\(g\mid c\) 일 때 \(g\)의 해 — 하나만 내면 오답.
  • 합성수 법의 역원은 확장 유클리드가 유일한 범용 해법.
Practice problem 바닥의 무게중심 선택 25m
R00673

바닥의 무게중심

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem 분수의 양 끝에서 선택 25m
R00675

분수의 양 끝에서

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Lesson 1강 · 개념과 정리 필수 8m

연립 합동식을 하나로

여러 나머지 조건을 동시에 만족하는 수를 찾는 문제가 있습니다.

$$ x \equiv r_1 \pmod{m_1}, \quad x \equiv r_2 \pmod{m_2}, \quad \dots, \quad x \equiv r_k \pmod{m_k}. $$

중국인의 나머지 정리(CRT) 는 이런 연립 합동식이 언제 풀리고, 해가 무엇인지 말해
줍니다.


1. 서로소인 경우 (고전적 CRT)

정리. \(m_1, m_2, \dots, m_k\)쌍마다 서로소이면, 위 연립 합동식은
\(M = m_1 m_2 \cdots m_k\) 를 법으로 하는 유일한 해를 가진다.

구성(construction). \(M_i = M / m_i\) 라 하고, \(M_i\)\(m_i\)에 대한 역원을
\(t_i \equiv M_i^{-1} \pmod{m_i}\) 라 하면
$$ x \equiv \sum_{i=1}^{k} r_i \, M_i \, t_i \pmod{M}. $$

직관. 각 항 \(r_i\ M_i\ t_i\)\(m_i\)에 대해서는 \(r_i\), 다른 \(m_j\)에 대해서는 \(0\)
되도록 설계되어(왜냐하면 \(M_i\)\(m_j\)의 배수), 서로 간섭 없이 조건을 만족시킵니다.


2. 서로소가 아닌 경우 (일반 CRT)

\(m_1, m_2\)가 서로소가 아니면 두 식을 하나로 합치는 방식으로 진행합니다.
\(g = \gcd(m_1, m_2)\) 라 하면:

  • 해가 존재할 조건: \(r_1 \equiv r_2 \pmod g\) (즉 \(g \mid (r_2 - r_1)\)).
  • 존재하면 합쳐진 해는 법 \(\operatorname{lcm}(m_1, m_2)\) 에서 유일.

두 식을 합쳐 하나로 만들고, 그 결과를 다음 식과 또 합치는 식으로 \(k\)개를 순차
병합합니다. 각 병합에 확장 유클리드가 쓰입니다.


3. 병합 공식

\(x \equiv r_1 \pmod{m_1}\)\(x \equiv r_2 \pmod{m_2}\) 를 합칩니다.
\(x = r_1 + m_1\ t\) 로 쓰고 둘째 식에 대입하면
$$ m_1 t \equiv r_2 - r_1 \pmod{m_2}. $$
\(g = \gcd(m_1, m_2)\), \(g \mid (r_2 - r_1)\) 이면 \(t\)를 확장 유클리드로 풀어
$$ x \equiv r_1 + m_1 t \pmod{\operatorname{lcm}(m_1, m_2)}. $$


4. 숫자로 확인하기

고전 예제: \(x \equiv 2 \pmod 3\), \(x \equiv 3 \pmod 5\), \(x \equiv 2 \pmod 7\).
\(3, 5, 7\)은 쌍마다 서로소, \(M = 105\). 답은 \(x \equiv 23 \pmod{105}\).
검산: \(23 = 3\cdot7+2\)\(\bmod 3 = 2\); \(23 = 4\cdot5+3\)\(\bmod 5 = 3\);
\(23 = 3\cdot7+2\)\(\bmod 7 = 2\). ✓

서로소 아닌 예: \(x \equiv 2 \pmod 6\), \(x \equiv 5 \pmod 9\). \(g=\gcd(6,9)=3\),
\(5 - 2 = 3\)\(3\)의 배수 → 해 존재. 합치면 \(x \equiv 14 \pmod{18}\).
검산 \(14 \bmod 6 = 2\), \(14 \bmod 9 = 5\). ✓

무해 예: \(x \equiv 1 \pmod 2\), \(x \equiv 2 \pmod 4\). \(g=2\), \(2-1=1\)\(2\)로 안
나뉨 → 해 없음.


5. 언제 쓰는가

  • 여러 나머지 조건을 동시에 만족하는 최소 양의 정수.
  • 큰 modulo 계산을 작은 소수 여러 개로 쪼개 병렬 계산 후 합치기(예: 합성수
    modulo의 이항계수, 큰 곱의 정확값 복원).
  • 주기가 다른 현상이 동시에 맞아떨어지는 시각/위치 문제.

정리

  • 서로소이면 해가 법 \(M=\prod m_i\) 에서 유일, \(x \equiv \sum r_i M_i t_i\).
  • 서로소가 아니면 \(g \mid (r_2-r_1)\) 일 때만 해가 있고 법은 \(\operatorname{lcm}\).
  • 여러 식은 둘씩 순차 병합, 각 병합에 확장 유클리드.
Lesson 2강 · 구현 선택 8m

구현 레퍼런스

CRT는 "둘씩 병합" 함수 하나로 일반화하는 것이 가장 깔끔합니다. 서로소·비서로소를
모두 처리하고, 오버플로에 주의합니다.


1. 두 식 병합 (일반 CRT)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

ll extgcd(ll a, ll b, ll &x, ll &y) {
    if (b == 0) { x = 1; y = 0; return a; }
    ll x1, y1; ll g = extgcd(b, a % b, x1, y1);
    x = y1; y = x1 - (a / b) * y1; return g;
}

// x ≡ r1 (mod m1), x ≡ r2 (mod m2) 를 병합.
// 해가 있으면 {r, lcm}, 없으면 {-1, -1}
pair<ll,ll> crt(ll r1, ll m1, ll r2, ll m2) {
    ll x, y;
    ll g = extgcd(m1, m2, x, y);
    if ((r2 - r1) % g != 0) return {-1, -1};        // 존재 조건
    ll lcm = m1 / g * m2;
    ll mod2 = m2 / g;
    ll t = ((( (r2 - r1) / g * x) % mod2) + mod2) % mod2;
    ll r = ((__int128)r1 + (__int128)m1 * t) % lcm;  // 오버플로 방지
    r = (r % lcm + lcm) % lcm;
    return {r, lcm};
}

(__int128)m1 * t 로 중간 곱 오버플로를 막는 것이 핵심입니다. \(m_1, t\)가 모두
\(10^9\)급이면 곱이 long long을 넘습니다.


2. 여러 식으로 확장

// r[i] (mod m[i]) 들을 모두 병합. 해 없으면 {-1,-1}
pair<ll,ll> crt_all(vector<ll>& r, vector<ll>& m) {
    ll R = 0, M = 1;                    // x ≡ 0 (mod 1) 로 시작
    for (int i = 0; i < (int)r.size(); i++) {
        auto res = crt(R, M, r[i], m[i]);
        if (res.second == -1) return {-1, -1};
        R = res.first; M = res.second;
    }
    return {R, M};                      // 최소 양의 해 R (mod M)
}

3. 파이썬 버전

def extgcd(a, b):
    if b == 0:
        return a, 1, 0
    g, x1, y1 = extgcd(b, a % b)
    return g, y1, x1 - (a // b) * y1

def crt(r1, m1, r2, m2):
    g, x, _ = extgcd(m1, m2)
    if (r2 - r1) % g != 0:
        return None                      # 해 없음
    lcm = m1 // g * m2
    t = ((r2 - r1) // g * x) % (m2 // g)
    r = (r1 + m1 * t) % lcm
    return r % lcm, lcm                   # 파이썬은 큰 정수라 오버플로 없음

def crt_all(rs, ms):
    R, M = 0, 1
    for r, m in zip(rs, ms):
        res = crt(R, M, r, m)
        if res is None:
            return None
        R, M = res
    return R, M

파이썬은 임의 정밀도 정수라 오버플로 걱정이 없어 CRT 구현이 특히 깔끔합니다.


4. 검증 사례

  • crt_all([2,3,2], [3,5,7])(23, 105).
  • crt(2, 6, 5, 9)(14, 18).
  • crt(1, 2, 2, 4)None (해 없음).

5. 함정

  • 오버플로m1 * t, M1 * t_i 등 중간 곱. C++는 __int128.
  • 음수 나머지(r % lcm + lcm) % lcm 로 양수화.
  • 존재 조건 누락(r2 - r1) % g != 0 검사를 빼면 없는 해를 만들어 낸다.
  • 초기값 — 누적 병합은 R=0, M=1("\(x\equiv0\pmod1\)", 모든 정수)에서 시작.

정리

  • 핵심은 둘씩 병합하는 crt 함수, 여러 식은 누적 병합.
  • 존재 조건 g | (r2-r1), 결과 법은 lcm, 결과는 양수화.
  • C++는 __int128 로 중간 곱 오버플로 방지, 파이썬은 자연히 안전.
Lesson 3강 · 심화와 변형 선택 8m

CRT를 실전에서 쓰는 법

CRT는 그 자체로 문제가 되기도 하지만, 더 자주 다른 계산의 보조 도구로 쓰입니다.
큰 수를 작은 소수로 쪼개 다루는 발상이 핵심입니다.


1. 큰 modulo를 쪼개기

modulo \(m\)이 소수가 아니고 \(m = p_1^{e_1} \cdots p_k^{e_k}\) 로 소인수분해되면, 각
소수 거듭제곱 \(p_i^{e_i}\) 을 법으로 답을 구한 뒤 CRT로 합칩니다. 이 각 법은 서로소라
고전 CRT가 그대로 적용됩니다. 합성수 modulo 이항계수(뤼카 정리 응용)가 대표적입니다.


2. 정확한 큰 곱 복원 (다중 소수 트릭)

답이 매우 크지만 자릿수 한계가 있는 경우, 여러 개의 서로 다른 큰 소수 modulo로
각각 계산한 뒤 CRT로 실제 값을 복원할 수 있습니다. 예를 들어 소수 \(10^9\)급 두세 개를
쓰면 \(10^{27}\) 정도까지의 정확한 정수를 복원할 수 있어, 해싱 충돌 회피나 다항식 곱
검증에 활용됩니다.


3. 함정 모음

  • 비서로소인데 고전 공식(\(\sum r_i M_i t_i\)) 사용\(M_i = M/m_i\) 의 역원이
    존재하지 않아 오답. 서로소가 보장되지 않으면 병합형을 쓴다.
  • lcm 오버플로 — 법들이 많으면 \(\operatorname{lcm}\) 이 폭발. 문제가 답 범위를
    보장하지 않으면 중간에 넘칠 수 있으니 __int128 또는 파이썬.
  • 존재 조건 미검사 — 비서로소에서 \(g \nmid (r_2 - r_1)\) 이면 무해인데, 이를 안
    거르면 엉뚱한 값.
  • \(m_i = 1\) 인 식 — "\(x \equiv 0 \pmod 1\)" 은 모든 정수를 허용, 병합의 항등원.
    코드가 이를 잘 처리하는지 확인.
  • 음수 \(r_i\) — 먼저 ((r % m) + m) % m 로 정규화.

4. 최소 양의 해 / 특정 범위 해

CRT의 결과 \(x \equiv R \pmod M\) 에서:

  • 최소 양의 해: \(R = 0\) 이면 보통 \(M\) (문제 정의에 따라 "\(0\) 초과"인지 확인).
  • \([L, H]\) 범위의 해 개수: \(R + Mt \in [L, H]\) 인 정수 \(t\)의 개수 —
    \(\lfloor (H-R)/M \rfloor - \lceil (L-R)/M \rceil + 1\).

경계에서 \(R\)\(0\)인지, "\(0\)을 답으로 허용하는지"에서 실수가 잦습니다.


5. 검증 사례

  • 쪼개기: \(x \bmod 12\) 를 원할 때 \(12 = 4 \times 3\), \(x \equiv a \pmod 4\),
    \(x \equiv b \pmod 3\) 로 나눠 계산 후 CRT.
  • 최소 양수: \(x \equiv 0 \pmod 3\), \(x \equiv 0 \pmod 5\)\(R=0, M=15\);
    "최소 양의" 해는 \(15\).
  • 범위 세기: \(x \equiv 23 \pmod{105}\)\([1, 1000]\) 해는
    \(23, 128, \dots, 968\)\(10\)개.

정리

  • CRT의 진짜 힘은 큰 modulo를 소수 거듭제곱으로 쪼개 각각 풀고 합치는 데 있다.
  • 비서로소는 반드시 병합형 + 존재 조건, 고전 \(\sum r_i M_i t_i\) 는 서로소 전용.
  • lcm 오버플로와 최소 양의 해 정의에서 실수가 잦다.
Practice problem 소수 거듭제곱으로 나눈 이항계수 선택 25m
R00687

소수 거듭제곱으로 나눈 이항계수

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem Astro 선택 25m
COCI00171

Astro

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
04
Level 4 · Challenger

Challenger

수학과 정수론 · Challenger 단계

0/5 완료
Lesson 1강 · 개념과 정리 필수 8m

\(10^{18}\) 규모의 소수 판정

시험 나눗셈은 \(O(\sqrt N)\) 이라 \(N = 10^{18}\) 이면 \(\sqrt N = 10^9\) 로 감당이 안
됩니다. 밀러-라빈 소수 판정(Miller-Rabin) 은 확률적 알고리즘이지만, 고정된
증인(witness) 집합을 쓰면 일정 범위 아래에서 결정론적으로(100% 정확) 작동하며
한 번 판정이 \(O(\log N)\) 입니다.


1. 페르마 소정리에서 출발

\(p\)가 소수이고 \(\gcd(a, p) = 1\) 이면 \(a^{p-1} \equiv 1 \pmod p\). 그래서
\(a^{N-1} \not\equiv 1 \pmod N\)\(a\) 를 찾으면 \(N\)은 확실히 합성수. 하지만 역이
성립하지 않아(카마이클 수 \(561\) 등) 더 강한 조건이 필요합니다.


2. 강한 확률적 소수 조건

\(N\)이 홀수 소수라면 \(N - 1 = 2^s \cdot d\) (\(d\) 홀수) 로 쓸 수 있습니다. 소수 \(N\)
에서는 \(x^2 \equiv 1 \pmod N\) 의 해가 \(x \equiv \pm 1\) 뿐이라는 사실로부터, 소수라면
아래 중 하나는 반드시 성립합니다.

$$ a^{d} \equiv 1 \pmod N \qquad \text{또는} \qquad a^{2^{r} d} \equiv -1 \pmod N \ \ (\exists\, 0 \le r < s). $$

즉 수열 \(a^d, a^{2d}, a^{4d}, \dots, a^{2^{s-1}d}\) 에서 처음이 \(1\) 이거나 어딘가
\(-1\) 이 나오면 "\(a\) 에 대해 통과". 통과 못 시키는 \(a\)증인(witness) 이라
하고, 증인이 하나라도 있으면 \(N\)은 확정 합성수입니다.


3. 결정론적 증인 집합

무작위 \(a\) 대신 수학적으로 검증된 고정 밑을 쓰면 결정론이 됩니다.

  • \(N < 3\,215\,031\,751\): \(\{2, 3, 5, 7\}\)
  • \(N < 3.3 \times 10^{14}\): \(\{2, 3, 5, 7, 11, 13, 17\}\)
  • \(N < 3.3 \times 10^{24}\) (\(64\)비트 정수 전체를 덮음):
    $$ \{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37\}. $$

경쟁 프로그래밍에서는 마지막 \(12\)개 집합 하나만 외워 두면 \(64\)비트 unsigned 범위
전체를 안전하게 판정할 수 있습니다.


4. 복잡도

한 밑 \(a\) 의 검사는 모듈러 거듭제곱 \(a^d \bmod N\)\(O(\log N)\), 이후 \(s\) 번 제곱에
\(O(s) = O(\log N)\). 밑이 \(k\) 개면 전체 \(O(k \log N)\). \(10^{18}\) 이하 하나의 수를
사실상 상수 시간에 판정합니다.


5. 숫자로 확인하기

\(N = 561 = 3 \times 11 \times 17\) (카마이클 수): 페르마 판정은 통과시키지만
밀러-라빈은 밑 \(2\) 에서 증인을 찾아 합성수로 정확히 판정합니다.

\(N = 2^{61} - 1 = 2305843009213693951\) (메르센 소수): \(12\)개 밑 모두 통과 →
소수. 단, 모듈러 곱셈 오버플로를 처리해야 정답(2강).


6. 언제 쓰는가

  • 한두 개의 큰 수 (\(10^{12} \sim 10^{18}\)) 의 소수 여부.
  • 폴라드 로 소인수분해에서 "이 인수가 소수인가" 를 판정하는 하위 루틴.
  • 구간 전체의 소수는 여전히 가 낫습니다(밀러-라빈은 개별 판정용).

정리

  • 밀러-라빈: \(N-1 = 2^s\ d\) 분해 후 강한 확률적 소수 조건을 여러 밑으로 검사.
  • \(\{2,3,\dots,37\}\) 12개 밑이면 \(64\)비트 정수 전체를 결정론적으로 판정.
  • 한 수 판정이 \(O(\log N)\) — 큰 수·소인수분해의 필수 하위 루틴.
Lesson 2강 · 구현 선택 8m

구현 레퍼런스

밀러-라빈 구현의 유일하고 치명적인 함정은 모듈러 곱셈 오버플로입니다. 이것만
정확히 처리하면 나머지는 기계적입니다.


1. 오버플로 함정: unsigned __int128

\(N\)\(10^{18}\) 급이면 \(a \times b\)\(10^{36}\) 까지 커져 long long
(\(\approx 9.2 \times 10^{18}\))을 아득히 넘칩니다. C++ 에서는 \(128\)비트로 중간 곱을
담습니다. 중요: \(N\)\(2^{63}\) 을 넘으면 두 값의 곱이 \(2^{127}\) 을 넘겨
부호 있는 __int128 도 오버플로합니다. 반드시 unsigned __int128
쓰세요.

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long u64;

u64 mulmod(u64 a, u64 b, u64 m) {
    return (unsigned __int128)a * b % m;   // 부호 없는 128비트! (signed는 오버플로)
}
u64 binpow(u64 a, u64 b, u64 m) {          // a^b mod m, O(log b)
    u64 r = 1 % m; a %= m;
    while (b) {
        if (b & 1) r = mulmod(r, a, m);
        a = mulmod(a, a, m);
        b >>= 1;
    }
    return r;
}

2. 판정 본체

bool check(u64 n, u64 a, u64 d, int s) {   // a 가 합성 증인이 아니면 true
    u64 x = binpow(a, d, n);
    if (x == 1 || x == n - 1) return true;
    for (int i = 0; i < s - 1; i++) {
        x = mulmod(x, x, n);
        if (x == n - 1) return true;
    }
    return false;                           // 증인 발견 → 합성
}
bool isPrime(u64 n) {
    if (n < 2) return false;
    for (u64 p : {2,3,5,7,11,13,17,19,23,29,31,37})
        if (n % p == 0) return n == p;      // 작은 소수 처리 + a⊥n 보장
    u64 d = n - 1; int s = 0;
    while ((d & 1) == 0) { d >>= 1; s++; }   // n-1 = 2^s * d
    for (u64 a : {2,3,5,7,11,13,17,19,23,29,31,37})
        if (!check(n, a, d, s)) return false;
    return true;
}

int main() {
    u64 n;
    while (cin >> n) cout << (isPrime(n) ? "prime" : "composite") << '\n';
}

작은 소수로 먼저 나눠 두면 \(n\)이 그 소수 자신일 때 바로 답하고, 이후 밑 \(a\)\(n\)
서로소가 되도록 보장합니다.


3. 파이썬 버전

파이썬 정수는 임의 정밀도라 오버플로가 없고, 내장 pow(a, d, n) 이 빠릅니다.

def is_prime(n: int) -> bool:
    if n < 2:
        return False
    for p in (2,3,5,7,11,13,17,19,23,29,31,37):
        if n % p == 0:
            return n == p
    d, s = n - 1, 0
    while d % 2 == 0:                # n-1 = 2^s * d
        d //= 2
        s += 1
    for a in (2,3,5,7,11,13,17,19,23,29,31,37):
        x = pow(a, d, n)             # a^d mod n, 오버플로 없음
        if x == 1 or x == n - 1:
            continue
        for _ in range(s - 1):
            x = x * x % n
            if x == n - 1:
                break
        else:
            return False             # for-else: -1 을 못 만남 → 증인 → 합성
    return True

for ... else 는 안쪽 반복이 break 없이 끝났을 때(=수열에서 \(-1\) 미발견) 실행되어
합성수로 판정합니다.


4. 검증 사례

  • 소수: \(997\), \(998244353\), \(10^9+7\), \(2^{61}-1 = 2305843009213693951\),
    최대 \(64\)비트 소수 \(18446744073709551557\) → 모두 prime.
  • 합성: \(561\)(카마이클), \(3215031751\)(\(\{2,3,5,7\}\)만 쓰면 오판되는 반례),
    \(2^{64}-1\)composite.
  • \(2 \sim 100\) 에서 isPrime 이 참인 개수 \(= 25\).

정리

  • 유일한 함정은 모듈러 곱 오버플로 — C++ 는 unsigned __int128, 파이썬은 자연히 안전.
  • \(\{2,\dots,37\}\) 12개 밑 + 작은 소수 선처리 → \(64\)비트 전체 결정론 판정.
  • for-else (파이썬) 또는 check 의 반환 위치로 "증인 발견"을 정확히 처리.
Lesson 3강 · 심화와 변형 선택 8m

왜 오판하는가, 그리고 확률판

밀러-라빈의 심화는 두 갈래입니다: 결정론 집합을 벗어난 범위의 확률적 판정, 그리고
구현에서 사람들이 반복적으로 틀리는 지점들입니다.


1. 확률적 판정과 오류 확률

무작위 밑 \(a\) 하나로 검사할 때, 합성수를 소수로 오판할 확률은 \(\le \frac14\) 입니다.
서로 독립인 밑 \(k\) 개를 쓰면 오류 확률 \(\le 4^{-k}\). \(k = 20\) 이면
\(4^{-20} \approx 10^{-12}\) 로 사실상 \(0\). \(64\)비트를 넘는 임의 정밀도 수(예: 암호학적
크기)는 결정론 집합이 없으므로 무작위 밑 \(20 \sim 40\) 개로 판정합니다.


2. 함정 모음

  • 부호 있는 __int128 사용\(N > 2^{63}\) 에서 \(a \times b\)\(2^{127}\) 을 넘겨
    오버플로. 최대 \(64\)비트 소수 \(18446744073709551557\) 이 대표적 반례.
    반드시 unsigned __int128.
  • long long mulmod\(128\)비트 없이 곱하면 \(10^{18}\) 급에서 즉시 오답.
  • 작은 소수 선처리 누락 — 밑 \(a\)\(n\)의 약수이면(\(a = n\) 포함) \(\gcd(a,n)\ne1\)
    이라 조건이 깨진다. \(n \in \{2,3,\dots,37\}\) 을 먼저 걸러야 안전.
  • \(n = 1, 0\), 짝수 — 경계. \(n < 2\) 는 소수 아님, \(n = 2\) 는 소수.
  • 잘못된 증인 집합\(\{2,3,5,7\}\) 만으로 \(10^{18}\) 을 판정하면
    \(3215031751\) 같은 강한 유사소수에서 오판. 범위에 맞는 집합을 써야 함.

3. unsigned __int128 이 없는 환경

일부 컴파일러/언어는 \(128\)비트가 없습니다. 대안:

  • long double / __float128 트릭: a*b - (u64)((long double)a/m*b)*m
    후 보정 — 정밀도 위험이 있어 권장하지 않음.
  • 이진 곱셈(binary multiply): 덧셈 기반 \(O(\log b)\) mulmod. 느리지만 이식성 최고.
u64 mulmod_add(u64 a, u64 b, u64 m) {   // 128비트 없는 환경용
    u64 r = 0; a %= m;
    while (b) {
        if (b & 1) r = (r + a) % m;      // 덧셈은 오버플로 안전(둘 다 < m)
        a = (a + a) % m;
        b >>= 1;
    }
    return r;
}

4. 폴라드 로와의 연계

밀러-라빈은 그 자체로도 쓰지만, 폴라드 로 소인수분해의 핵심 하위 루틴입니다.
분해 과정에서 나온 인수가 더 쪼개질 수 있는지(합성) 아니면 소수인지 판정할 때
밀러-라빈으로 확인합니다. 그래서 이 두 알고리즘은 거의 항상 함께 등장합니다(폴라드 로
단원 참고).


5. 검증 사례

  • 오버플로 검출: \(18446744073709551557\) (최대 \(64\)비트 소수) — signed __int128
    이면 composite 오판, unsigned 면 prime 정답.
  • 확률판: 무작위 밑 \(1\) 개로 \(561\) 을 검사하면 대부분 합성 판정하지만, 운 나쁘게
    통과시킬 밑도 존재 — 여러 밑으로 확률을 낮춘다.
  • 강한 유사소수: \(3215031751\) 은 밑 \(\{2,3,5,7\}\) 을 모두 통과 → 더 큰 집합 필요.

정리

  • 결정론 집합 밖(암호 크기)은 무작위 밑 \(20{+}\) 개, 오류 확률 \(\le 4^{-k}\).
  • 최대 함정은 부호 있는 \(128\)비트 오버플로unsigned __int128 필수, 없으면
    덧셈 기반 mulmod.
  • 밀러-라빈은 폴라드 로의 소수 판정 하위 루틴으로 짝을 이룬다.
Practice problem 거대한 수의 오일러 피 선택 25m
R00639

거대한 수의 오일러 피

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Diamond IV 다이아몬드 IV 지금 풀기
Practice problem RSA-2048 분해 (현대 암호 깨기) 선택 25m
R00704

RSA-2048 분해 (현대 암호 깨기)

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
05
Level 5 · Analyst

Analyst

수학과 정수론 · Analyst 단계

0/18 완료
Lesson 1강 · 빠른 거듭제곱과 행렬 점화식 필수 8m

무엇을 가속하나

피보나치 \(f_n=f_{n-1}+f_{n-2}\) 같은 선형 점화식\(N\) 번째 항을 단순 반복으로
구하면 \(O(N)\) 입니다. \(N=10^{18}\) 이면 불가능합니다. 행렬 거듭제곱은 이를
\(O(K^3\log N)\) 으로 줄입니다(\(K\) 는 점화식의 차수).


1. 먼저: 스칼라 빠른 거듭제곱

행렬 거듭제곱의 뼈대는 정수 빠른 거듭제곱(이진 거듭제곱) 입니다. 지수를
이진법으로 보고 절반씩 제곱합니다.

$$ a^b= \begin{cases} 1 & b=0\\ (a^{b/2})^2 & b \text{ 짝수}\\ a\cdot a^{b-1} & b \text{ 홀수} \end{cases} \qquad O(\log b) $$

long long power(long long a, long long b, long long MOD) {
    a %= MOD; if (a < 0) a += MOD;       // 음수 밑 보정
    long long r = 1 % MOD;
    while (b > 0) {
        if (b & 1) r = r * a % MOD;       // a < MOD 이므로 곱 < MOD^2 < 2^63
        a = a * a % MOD;
        b >>= 1;
    }
    return r;                             // power(2,10,1e9+7)=1024
}

지수 0은 반드시 \(1\)(항등원)로 처리해야 합니다 — 뒤의 행렬에서는 단위행렬.


2. 점화식을 행렬로 옮기기

상태 벡터를 점화식이 참조하는 최근 항들로 잡습니다. 피보나치라면 한 단계
전이는

$$ \begin{pmatrix} f_{n+1}\\ f_n\end{pmatrix} =\begin{pmatrix} 1 & 1\\ 1 & 0\end{pmatrix} \begin{pmatrix} f_n\\ f_{n-1}\end{pmatrix}. $$

이 변환 행렬을 \(A\) 라 하면 초기 벡터에 \(A\)\(n\) 번 곱한 것이 답입니다:

$$ \begin{pmatrix} f_{n+1}\\ f_n\end{pmatrix}=A^{\,n}\begin{pmatrix} f_1\\ f_0\end{pmatrix}, \qquad A^{n}=\begin{pmatrix} f_{n+1} & f_n\\ f_n & f_{n-1}\end{pmatrix}. $$

일반화(동반 행렬). \(f_n=c_1f_{n-1}+\dots+c_kf_{n-k}\) 이면 변환 행렬은 첫
행이 \((c_1,\dots,c_k)\) 이고 그 아래에 항등 부분 행렬을 둔 \(k\times k\)
동반 행렬입니다.

$$ A= \begin{pmatrix} c_1 & c_2 & \cdots & c_{k-1} & c_k\\ 1 & 0 & \cdots & 0 & 0\\ 0 & 1 & \cdots & 0 & 0\\ \vdots & & \ddots & & \vdots\\ 0 & 0 & \cdots & 1 & 0 \end{pmatrix} $$


3. 왜 옳은가 · 복잡도

행렬 곱은 결합법칙을 만족하므로 \(A\)\(n\) 번 곱하는 것은 어떤 순서로 묶어도
결과가 같습니다. 따라서 지수를 절반씩 나누는 분할 정복이 그대로 성립합니다
(정수 빠른 거듭제곱이 옳은 이유와 동일). 곱셈 횟수가 \(O(\log N)\), 한 번의
\(K\times K\) 행렬 곱이 \(O(K^3)\) 이므로 전체 \(O(K^3\log N)\).

수치 예제. \(A=\begin{pmatrix}1&1\\1&0\end{pmatrix}\), 초기 \(f_0=0,f_1=1\).

$$ A^{2}=\begin{pmatrix}2&1\\1&1\end{pmatrix},\quad A^{4}=(A^2)^2=\begin{pmatrix}5&3\\3&2\end{pmatrix},\quad A^{5}=A\cdot A^4=\begin{pmatrix}8&5\\5&3\end{pmatrix}. $$

\(A^5\) 의 좌하 성분 \(=f_5=5\), 좌상 \(=f_6=8\) 로 맞습니다.


4. 언제 쓰나

  • 선형 점화식의 아주 큰 항(\(N\)\(10^9\) 이상).
  • 그래프에서 길이 \(L\) 인 경로 수: 인접 행렬의 \(L\) 제곱.
  • 상태 전이가 일정한 DP(타일링·문자열 카운팅 등)에서 전이를 행렬로 묶어
    가속. 답이 크면 보통 \(10^9+7\) 로 모듈러 계산합니다(2강에서 오버플로 처리).
Lesson 2강 · 행렬 구현과 모듈러 처리 선택 8m

1. 모듈러 행렬 곱과 거듭제곱 (C++)

핵심은 누적합 오버플로입니다. \(K\times K\) 곱은 성분마다 \(K\) 개의
\(a_{ik}b_{kj}\) 를 더하는데, 각 곱이 \( 라 몇 개만 더해도
long long(\(<9.2\times10^{18}\))을 넘길 수 있습니다. 매 덧셈마다 % MOD
하거나 누적을 __int128 로 받으면 안전합니다.

#include <bits/stdc++.h>
using namespace std;
const long long MOD = 1000000007;

struct Mat {
    int n;
    vector<vector<long long>> a;
    Mat(int n) : n(n), a(n, vector<long long>(n, 0)) {}
    static Mat identity(int n) {                 // 지수 0 = 단위행렬
        Mat I(n); for (int i = 0; i < n; i++) I.a[i][i] = 1; return I;
    }
};

Mat mul(const Mat& A, const Mat& B) {
    int n = A.n; Mat C(n);
    for (int i = 0; i < n; i++)
        for (int k = 0; k < n; k++) {
            if (A.a[i][k] == 0) continue;         // 희소 최적화
            long long aik = A.a[i][k];
            for (int j = 0; j < n; j++)
                C.a[i][j] = (C.a[i][j] + aik * B.a[k][j]) % MOD;  // 매번 mod
        }
    return C;
}

Mat mpow(Mat A, long long e) {
    Mat R = Mat::identity(A.n);
    while (e > 0) {
        if (e & 1) R = mul(R, A);
        A = mul(A, A);
        e >>= 1;
    }
    return R;
}

long long fib(long long n) {                      // n>=0
    if (n == 0) return 0;
    Mat A(2); A.a = {{1,1},{1,0}};
    Mat R = mpow(A, n);
    return R.a[0][1];                              // = f_n
}

fib(10)=55, fib(90)=2880067194370816120 % MOD. 반드시 long long 지수와
% MOD 를 사용하세요.


2. 파이썬 구현

MOD = 10**9 + 7

def mat_mul(A, B):
    n, m, p = len(A), len(B), len(B[0])
    C = [[0] * p for _ in range(n)]
    for i in range(n):
        Ai = A[i]
        for k in range(m):
            if Ai[k] == 0: continue
            aik = Ai[k]; Bk = B[k]
            Ci = C[i]
            for j in range(p):
                Ci[j] = (Ci[j] + aik * Bk[j]) % MOD
    return C

def mat_pow(A, e):
    n = len(A)
    R = [[1 if i == j else 0 for j in range(n)] for i in range(n)]  # 단위행렬
    while e > 0:
        if e & 1: R = mat_mul(R, A)
        A = mat_mul(A, A)
        e >>= 1
    return R

def fib(n):
    if n == 0: return 0
    return mat_pow([[1, 1], [1, 0]], n)[0][1]
# fib(10)=55, fib(50)=12586269025 % MOD

파이썬은 정수가 임의 정밀도라 오버플로는 없지만, % MOD 를 생략하면 수가
거대해져 곱이 느려집니다 — 모듈러 문제라면 매번 취하세요.


3. 응용: 길이 L 경로 수

인접 행렬 \(M\) 에서 \(M^L\)\((i,j)\) 성분은 정점 \(i\to j\) 로 가는 길이 정확히
\(L\)
인 경로(walk)의 수입니다. 위 mpow 를 그대로 쓰되 MOD 는 문제 조건에
맞게 둡니다.

초기 상수항이 있는 점화식(예: \(f_n=f_{n-1}+f_{n-2}+c\))은 상태 벡터에 상수
\(1\) 을 하나 추가해 \((k{+}1)\times(k{+}1)\) 행렬로 만들면 동일하게 처리됩니다.


4. 변형과 함정

  • 지수 0 / n=0: 단위행렬을 반환하도록 초기값을 분리(위 fibn==0).
  • 오버플로: \(MOD\approx10^9\) 에서 누적 곱을 long long 에 담되 매 덧셈
    % MOD
    . \(MOD\)\(2^{31}\) 을 넘거나 성분이 크면 __int128 누적.
  • 음수 성분: 점화식 계수에 음수가 있으면 ((x % MOD) + MOD) % MOD 로 보정.
  • 차수 \(K\) 가 크면 \(O(K^3\log N)\) 이 부담 — \(K\gtrsim 100\) 이면 다항식
    나눗셈 기반 Kitamasa/선형 점화식 가속(\(O(K^2\log N)\) 또는 \(O(K\log K\log N)\))
    을 고려합니다(개념은 동일: \(A\) 의 거듭제곱).
Practice problem 알파카컵 1회: F - 알파카의 끝없는 울음소리 선택 25m
A00006

알파카컵 1회: F - 알파카의 끝없는 울음소리

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem 마법사의 수열 선택 25m
R01435

마법사의 수열

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Lesson 1강 · 개념과 정리 필수 8m

겹침을 부호로 지우다

여러 집합의 합집합 크기를 셀 때, 단순히 각 크기를 더하면 겹치는 부분을 여러 번
셉니다. 포함-배제의 원리(inclusion-exclusion, IE) 는 부호를 번갈아 가며 더하고
빼서 겹침을 정확히 상쇄합니다.


1. 기본 공식

두 집합: \(|A \cup B| = |A| + |B| - |A \cap B|\).
세 집합:
$$ |A \cup B \cup C| = |A| + |B| + |C| - |A \cap B| - |A \cap C| - |B \cap C| + |A \cap B \cap C|. $$

일반형(\(n\)개 집합):
$$ \left| \bigcup_{i=1}^{n} A_i \right| = \sum_{\emptyset \ne S \subseteq \{1,\dots,n\}} (-1)^{|S|+1} \left| \bigcap_{i \in S} A_i \right|. $$

즉 홀수 개 교집합은 더하고, 짝수 개 교집합은 뺍니다. 부호 교대 합이 핵심입니다.


2. 여집합(전체에서 빼기) 형태

"조건 중 어느 것도 만족하지 않는 것의 개수" 가 더 자주 필요합니다. 전체를 \(U\)
라 하면
$$ \left| \overline{A_1} \cap \cdots \cap \overline{A_n} \right| = \sum_{S \subseteq \{1,\dots,n\}} (-1)^{|S|} \left| \bigcap_{i \in S} A_i \right|, $$
(\(S = \emptyset\) 항은 \(|U|\)). 예를 들어 "어떤 나쁜 성질도 없는 것"을 셀 때 이 형태를
씁니다.


3. 직관: 왜 부호가 상쇄하는가

한 원소가 정확히 \(k\) 개의 집합에 속한다고 합시다. 이 원소가 합집합 공식에서 세어지는
총 횟수는
$$ \binom{k}{1} - \binom{k}{2} + \binom{k}{3} - \cdots = 1 - \sum_{j=0}^{k}(-1)^j\binom{k}{j} + \binom{k}{0} = 1. $$
(\( (1-1)^k = 0\) 에서 유도.) 즉 어떤 원소든 정확히 한 번 세어져, 겹침이 완벽히
제거됩니다.


4. 대표 응용

  • 완전순열(derangement): 어떤 원소도 제자리에 없는 순열 수
    $$ D_n = n! \sum_{k=0}^{n} \frac{(-1)^k}{k!} = n!\left(1 - \frac{1}{1!} + \frac{1}{2!} - \cdots\right). $$
  • \(1 \sim N\) 중 여러 소수 중 어느 것으로도 안 나뉘는 수의 개수 (오일러 \(\varphi\)
    도 이 원리의 특수형).
  • 서로소 개수, 배수 세기: "\(2\) 또는 \(3\) 또는 \(5\) 의 배수"를 부호 교대로.

5. 숫자로 확인하기

\(1 \sim 100\)\(2\) 또는 \(3\) 또는 \(5\) 의 배수 개수:
$$ \left\lfloor\tfrac{100}{2}\right\rfloor + \left\lfloor\tfrac{100}{3}\right\rfloor + \left\lfloor\tfrac{100}{5}\right\rfloor - \left\lfloor\tfrac{100}{6}\right\rfloor - \left\lfloor\tfrac{100}{10}\right\rfloor - \left\lfloor\tfrac{100}{15}\right\rfloor + \left\lfloor\tfrac{100}{30}\right\rfloor $$
$$ = 50 + 33 + 20 - 16 - 10 - 6 + 3 = 74. $$
따라서 어느 것으로도 안 나뉘는 수는 \(100 - 74 = 26\) 개.

완전순열: \(D_3 = 2\) (\(\{2,3,1\}, \{3,1,2\}\)), \(D_4 = 9\).


6. 언제 쓰는가

  • "적어도 하나" 또는 "하나도 아닌" 조건의 개수 세기.
  • 조건(집합) 수 \(n\) 이 작아 \(2^n\) 부분집합을 훑을 수 있을 때 → 비트마스크 IE.
  • 배수/서로소/약수 관련 세기(뫼비우스 함수와 밀접).

정리

  • IE = 교집합 크기의 부호 교대 합, 홀수 개는 \(+\), 짝수 개는 \(-\).
  • 여집합 형태 \(\sum_S (-1)^{|S|} |\bigcap_{i\in S} A_i|\) 가 "하나도 아닌" 세기에 유용.
  • 조건 수가 작으면 \(2^n\) 부분집합을 직접 훑는다.
Lesson 2강 · 구현 선택 8m

구현 레퍼런스

포함-배제는 대개 \(2^n\) 부분집합을 비트마스크로 훑으며 부호를 \(|S|\) 의 홀짝으로
매기는 형태로 구현합니다. 대표 문제들로 패턴을 익힙니다.


1. 비트마스크 골격

\(n\)개 조건, 각 부분집합 \(S\) 의 교집합 크기 \(g(S)\) 를 알 때:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

ll inclusion_exclusion(int n, function<ll(int)> intersect_size) {
    ll ans = 0;
    for (int mask = 1; mask < (1 << n); mask++) {   // 비어있지 않은 부분집합
        int bits = __builtin_popcount(mask);
        ll g = intersect_size(mask);                 // |교집합| (mask 로 지정된 집합들)
        if (bits & 1) ans += g;                      // 홀수 개 → +
        else          ans -= g;                      // 짝수 개 → -
    }
    return ans;   // |A1 ∪ ... ∪ An|
}

__builtin_popcount(mask)\(|S|\) 를 얻어 부호를 정합니다.


2. "여러 수 중 어느 것으로도 안 나뉘는 개수"

\(1 \sim N\) 중 주어진 소수들 \(p_1, \dots, p_n\)어느 것으로도 안 나뉘는 수의
개수. 교집합 크기 \(= \lfloor N / \operatorname{lcm} \rfloor\).

ll count_coprime(ll N, vector<ll>& p) {
    int n = p.size();
    ll ans = 0;
    for (int mask = 0; mask < (1 << n); mask++) {
        ll lcm = 1; int bits = 0;
        for (int i = 0; i < n; i++)
            if (mask & (1 << i)) { lcm = lcm / __gcd(lcm, p[i]) * p[i]; bits++; }
        ll cnt = N / lcm;                            // 이 배수들의 개수
        if (bits & 1) ans -= cnt;                    // 여집합 형태: (-1)^|S|
        else          ans += cnt;
    }
    return ans;   // 어느 소수로도 안 나뉘는 수의 개수
}

여집합 형태라 부호가 \((-1)^{|S|}\) (빈 집합 \(= +N\)). 배수를 더하는 합집합 형태와
부호 방향이 반대인 점에 주의하세요.


3. 완전순열(derangement)

ll derangement(int n, ll MOD) {
    // D_n = (n-1)(D_{n-1} + D_{n-2}), D_0=1, D_1=0
    ll a = 1, b = 0;                                 // D_0, D_1
    if (n == 0) return 1;
    for (int i = 2; i <= n; i++) {
        ll c = (ll)(i - 1) % MOD * ((a + b) % MOD) % MOD;
        a = b; b = c;
    }
    return b;
}

점화식 \(D_n = (n-1)(D_{n-1} + D_{n-2})\) 는 IE 공식보다 계산이 안정적입니다.


4. 파이썬 버전

from math import gcd

def count_coprime(N, primes):
    n = len(primes)
    ans = 0
    for mask in range(1 << n):
        lcm, bits = 1, 0
        for i in range(n):
            if mask >> i & 1:
                lcm = lcm // gcd(lcm, primes[i]) * primes[i]
                bits += 1
        cnt = N // lcm
        ans += cnt if bits % 2 == 0 else -cnt      # (-1)^|S|
    return ans

def derangement(n):
    a, b = 1, 0                                     # D_0, D_1
    for i in range(2, n + 1):
        a, b = b, (i - 1) * (a + b)
    return b if n else 1

5. 검증 사례

  • count_coprime(100, [2,3,5]) \(= 26\).
  • derangement(3) = 2, derangement(4) = 9, derangement(5) = 44.
  • 합집합: \(1\sim100\)\(2\) 또는 \(3\) 또는 \(5\) 의 배수 \(= 74\).

정리

  • 표준형은 \(2^n\) 비트마스크 + popcount 홀짝 부호.
  • 합집합("적어도 하나")과 여집합("하나도 아닌")은 부호 방향이 반대 — 헷갈리지 말 것.
  • 완전순열은 IE 공식 대신 점화식이 안정적.
Lesson 3강 · 심화와 변형 선택 8m

IE를 실전 규모로

\(2^n\) 훑기는 \(n\)\(20\) 안팎일 때만 가능합니다. 조건이 많거나 정수론적일 때는
뫼비우스 함수, 약수 IE, 오버플로 관리 같은 변형이 필요합니다.


1. 뫼비우스 함수: IE의 정수론판

$$ \mu(n) = \begin{cases} 1 & n = 1, \\ (-1)^k & n \text{ 이 서로 다른 } k \text{ 개 소수의 곱}, \\ 0 & n \text{ 이 제곱 인수를 가짐}. \end{cases} $$

\(\mu\)\(\pm1\) 부호가 바로 IE의 부호 교대이고, \(\mu = 0\) 은 "제곱 인수가 있으면
세지 않음"에 대응합니다. 서로소 세기의 표준형:
$$ \#\{\, x \le N : \gcd(x, m) = 1 \,\} = \sum_{d \mid m} \mu(d) \left\lfloor \frac{N}{d} \right\rfloor. $$
\(\gcd\) 조건 합, 뫼비우스 반전 등 정수론 문제에서 IE가 \(\mu\) 로 변신합니다.


2. 약수 기반 IE (개수가 많을 때)

조건이 "\(d\) 의 배수" 형태이고 \(d\) 가 많으면 \(2^n\) 대신 약수 위를 큰 것부터
훑으며
배수 항을 빼는 방식을 씁니다.

// f[d] = (정확히 d 를 gcd 로 갖는 쌍/원소의 수) 를 큰 d 부터 계산
for (int d = MAXV; d >= 1; d--) {
    cnt[d] = /* d 의 배수인 원소 수로부터 계산한 값 */;
    for (int e = 2 * d; e <= MAXV; e += d)
        cnt[d] -= cnt[e];                       // 배수들을 빼서 '정확히 d' 로
}

이는 \(O(V \log V)\)\(2^n\) 폭발을 피합니다. "gcd 가 정확히 \(k\)", "서로소 쌍의 수"
문제의 표준 기법입니다.


3. 함정 모음

  • 부호 방향 혼동 — 합집합은 \((-1)^{|S|+1}\), 여집합/전체빼기는 \((-1)^{|S|}\).
    빈 집합 항(\(|U|\)) 포함 여부로 갈린다.
  • \(2^n\) 폭발\(n > 22\) 정도면 비트마스크 IE 불가. 뫼비우스/약수 IE로 전환.
  • lcm 오버플로 — 배수 세기에서 \(\operatorname{lcm}\)\(N\) 을 넘으면 그 항은
    \(\lfloor N / \text{lcm} \rfloor = 0\). 곱하다 오버플로하기 전에 "\(N\) 초과면 \(0\)"
    으로 가지치기.
  • 모듈러에서 뺄셈(ans - x % MOD + MOD) % MOD 로 음수 방지.
  • 중복 조건 — 같은 소수/약수가 리스트에 두 번 들어오면 IE가 깨진다. 먼저 유일화.

4. 합집합 크기 vs "정확히 \(k\)개 만족"

IE는 "적어도 하나" 를 세지만, "정확히 \(k\)개 조건 만족" 도 확장 공식으로 셉니다.

$$ \#(\text{정확히 } k) = \sum_{j \ge k} (-1)^{j-k} \binom{j}{k} S_j, $$
여기서 \(S_j = \sum_{|T|=j} |\bigcap_{i\in T} A_i|\) 는 크기 \(j\) 교집합들의 합입니다.
"정확히 하나만 만족", "정확히 \(m\) 개 색이 쓰인 칠하기" 등에 쓰입니다.


5. 검증 사례

  • 뫼비우스 서로소: \(N=10, m=6\) → $\sum_{d\mid6}\mu(d)\lfloor10/d\rfloor
    = 10 - 5 - 3 + 1 = 3\( (\){1,5,7}\( 중 \)\le10\(… 실제 \)6\(과\ 서로소인 \)\le10$:
    \(1,5,7 \to\) 아니 \(1,5,7,11\)? \(\le10\) 이라 \(1,5,7\) 그리고 \(11\) 제외 → 값 확인은
    \(\lfloor\rfloor\) 계산대로 \(3\)… 은 오류처럼 보이나, \(6\)과 서로소인 \(\le 10\)
    \(1,5,7\) 세 개가 맞다).
  • 완전순열은 "정확히 \(0\)개 고정점" 이므로 위 확장 공식의 특수형.

정리

  • 정수론 IE는 뫼비우스 함수 \(\mu\) 로, 조건이 많으면 약수 IE(\(O(V\log V)\))로.
  • 함정은 부호 방향, \(2^n\) 폭발, lcm 오버플로, 모듈러 뺄셈.
  • "정확히 \(k\)개 만족" 은 \(\binom{j}{k}\) 가중 확장 공식으로.
Practice problem 알파카컵 1회: J - 알파카의 별빛 목장 선택 25m
A00010

알파카컵 1회: J - 알파카의 별빛 목장

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem 서로소 격자점 세기 선택 25m
R00636

서로소 격자점 세기

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Lesson 1강 · 님 게임과 그런디 수 필수 8m

공평 게임과 승패

두 사람이 번갈아 두고, 둘이 할 수 있는 수(move)가 상태에만 의존하며 같은
게임을 공평 게임(impartial game) 이라 합니다. 마지막에 둘 수 없는 사람이
지는 것을 정상 규칙(normal play) 이라 합니다. 이런 게임의 승패를 대수적으로
계산하는 이론이 스프라그-그런디입니다.


1. 님 게임(Nim)

돌무더기 \(a_1,\dots,a_m\) 에서 번갈아 한 무더기를 골라 1개 이상 가져갑니다.
마지막 돌을 가져가는(더 못 두게 만드는) 사람이 승리.

정리. 선수가 이기는 필요충분조건은

$$ a_1\oplus a_2\oplus\cdots\oplus a_m\neq0\quad(\oplus:\text{비트 XOR}). $$

즉 XOR(님-합)이 \(0\) 이면 지는 자리, \(0\) 이 아니면 이기는 자리입니다.

증명 스케치.
- 님-합이 \(0\) 이면 어떤 수를 두어도 반드시 \(0\) 이 아니게 됩니다(한 무더기를
바꾸면 XOR 이 바뀜).
- 님-합이 \(s\neq0\) 이면, 최상위 세워진 비트를 가진 무더기 \(a_i\)
\(a_i\oplus s( 로 줄이는 합법 수가 있어 님-합을 \(0\) 으로 만들 수 있습니다.
- 종료 상태(모두 0)는 님-합 0 이고 진 자리 → 귀납적으로 성립.

수치 예제. \(\{1,2,3\}\): \(1\oplus2\oplus3=0\)선수 패. \(\{1,2,4\}\):
\(1\oplus2\oplus4=7\neq0\) → 선수 승(예: 4를 \(4\oplus7=3\) 으로 줄여 XOR 0 만듦).


2. mex 와 그런디 수

한 공평 게임 상태 \(v\) 에서 갈 수 있는 상태들의 그런디 수 집합을 \(S\) 라 할 때,

$$ g(v)=\operatorname{mex}(S),\qquad \operatorname{mex}(S)=\min\{\,t\ge0:\ t\notin S\,\} $$

mex(minimum excludant)는 집합에 없는 가장 작은 비음 정수입니다.
종료 상태(수 없음)는 \(S=\varnothing\) 이므로 \(g=\operatorname{mex}(\varnothing)=0\).

\(g(v)=0\) 이면 패배 상태(P-position), \(g(v)>0\) 이면 승리 상태.

수치 예제 — 뺄셈 게임. 한 무더기에서 1, 2, 3개만 가져갈 수 있다면

\(n\) 0 1 2 3 4 5 6
\(g(n)\) 0 1 2 3 0 1 2

\(g(4)=\operatorname{mex}\{g(3),g(2),g(1)\}=\operatorname{mex}\{3,2,1\}=0\)
\(n\) 이 4의 배수이면 선수 패.


3. 스프라그-그런디 정리

정상 규칙의 모든 공평 게임 상태는 크기 \(g(v)\) 인 하나의 님 무더기와
동등
하다.

그리고 여러 게임을 동시에 하는 게임의 합의 그런디 수는

$$ g(G_1 + G_2 + \cdots + G_m)=g(G_1)\oplus g(G_2)\oplus\cdots\oplus g(G_m). $$

따라서 복잡한 게임도 각 독립 성분의 그런디 수를 구해 XOR 하면 전체 승패가
나옵니다. XOR 이 0 이면 패, 아니면 승. (님은 각 무더기의 그런디 수가 곧 무더기
크기인 특수 경우입니다.)


4. 언제 쓰나 · 복잡도

  • 여러 독립 게임의 , 또는 상태가 작은 게임의 승패 판정.
  • 그런디 수를 상태별로 DP/메모이제이션: 상태 수 \(\times\) 상태당 수의 개수.
    뺄셈 게임은 \(O(n\cdot|\text{moveset}|)\). 상태 공간이 지수적으로 커지면
    주기성(patterns)이나 이론적 공식을 찾아야 합니다(2강).
Lesson 2강 · 그런디 수 계산과 활용 선택 8m

1. mex 와 그런디 DP

한 무더기 게임(뺄셈 집합 \(S\))의 그런디 수를 \(0..n\) 까지 채웁니다.

#include <bits/stdc++.h>
using namespace std;

int grundy(int n, const vector<int>& moves) {
    vector<int> g(n + 1, 0);
    for (int x = 1; x <= n; x++) {
        set<int> reach;
        for (int m : moves)
            if (x - m >= 0) reach.insert(g[x - m]);
        int mex = 0;
        while (reach.count(mex)) mex++;   // mex = 집합에 없는 최소 비음 정수
        g[x] = mex;
    }
    return g[n];
}
def grundy(n, moves):
    g = [0] * (n + 1)
    for x in range(1, n + 1):
        reach = {g[x - m] for m in moves if x - m >= 0}
        mex = 0
        while mex in reach: mex += 1
        g[x] = mex
    return g[n]
# grundy(4, [1,2,3]) == 0,  grundy(6, [1,2,3]) == 2

set 대신 크기 고정 bool seen[] 를 쓰면 상수가 작아집니다(수의 개수만큼).


2. 게임의 합: XOR 로 승패 판정

여러 독립 무더기/게임의 그런디 수를 XOR 합니다.

bool firstWins(const vector<int>& piles, const vector<int>& moves) {
    int x = 0, mx = *max_element(piles.begin(), piles.end());
    vector<int> g(mx + 1, 0);
    for (int v = 1; v <= mx; v++) {          // 그런디 테이블 한 번만
        vector<char> seen(moves.size() + 2, 0);
        for (int m : moves) if (v - m >= 0) {
            int gg = g[v - m];
            if (gg < (int)seen.size()) seen[gg] = 1;
        }
        int mex = 0; while (mex < (int)seen.size() && seen[mex]) mex++;
        g[v] = mex;
    }
    for (int p : piles) x ^= g[p];
    return x != 0;                            // XOR != 0 이면 선수 승
}

순수 님이면 g[p]==p 이므로 그냥 x ^= p 만 해도 됩니다.

def first_wins_nim(piles):
    x = 0
    for p in piles: x ^= p
    return x != 0            # [1,2,3] -> 0 -> False(선수 패)

3. 변형: 계단 님(Staircase Nim)

돌을 계단 \(i\) 에서 \(i-1\) 로 옮기는 게임은 홀수 칸의 돌들만 님으로 취급하는
것과 같습니다. 님-합

$$ s=\bigoplus_{i\ \text{홀수}} a_i,\qquad s\neq0 \Rightarrow \text{선수 승}. $$

짝수 칸으로 옮긴 돌은 상대가 그대로 되받아 짝수 칸→홀수 칸 이동을 상쇄할 수
있어 승패에 무관합니다. "겉보기 다른 게임 = 님의 재해석" 의 대표 예입니다.


4. 함정과 실전 팁

  • 정상 규칙 vs 미제르(misère): "마지막에 두면 진다" 규칙은 XOR 이론이
    그대로 성립하지 않습니다(님의 미제르 판정은 무더기가 모두 1인지 등 별도
    분석 필요). 문제의 승패 정의를 반드시 확인하세요.
  • 상태 공간 폭발: 그런디 DP 는 상태 수가 관리 가능할 때만. 크면 그런디
    수열의 주기성을 실험으로 찾거나(대개 결국 주기적), 이론 공식을 유도합니다.
  • 자료형: 그런디 수와 XOR 은 작은 정수라 int 로 충분하지만, 무더기
    크기·개수가 크면 인덱스는 long long. 모듈러와는 무관(순수 비트 연산).
  • 다차원/복합 게임: 서로 독립인 성분으로 분해되면 각 그런디 수를 XOR.
    분해가 안 되면 상태 전체를 하나의 게임으로 메모이제이션.
Practice problem 님 게임 선택 25m
R00396

님 게임

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem 계단 님 선택 25m
R00399

계단 님

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Lesson 1강 · 개념과 정리 필수 8m

연립 일차 방정식을 기계적으로

미지수가 여러 개인 연립 일차 방정식을 손으로 풀면 실수하기 쉽습니다. 가우스
소거법(Gaussian elimination)
은 행 연산으로 계수 행렬을 위삼각(또는 대각) 형태로
만들어 해를 체계적으로 구합니다.


1. 문제 설정

\(n\)개 미지수, \(n\)개 방정식:
$$ \begin{cases} a_{11}x_1 + a_{12}x_2 + \cdots + a_{1n}x_n = b_1 \\ \ \vdots \\ a_{n1}x_1 + a_{n2}x_2 + \cdots + a_{nn}x_n = b_n \end{cases} $$

이를 첨가행렬(augmented matrix) \([A \mid b]\) 로 놓고 행 연산으로 정리합니다.


2. 세 가지 행 연산

해를 바꾸지 않는 연산:
1. 두 행 교환,
2. 한 행에 \(0\) 아닌 상수 곱,
3. 한 행에 다른 행의 상수배를 더함.

이 연산으로 전진 소거(forward elimination): 각 열의 대각 아래를 \(0\) 으로 만들어
위삼각 행렬을 얻고, 후진 대입(back substitution) 으로 \(x_n\) 부터 거꾸로 구합니다.
또는 대각 위아래를 모두 소거하는 가우스-조던(Gauss-Jordan) 으로 바로 해를
읽습니다.


3. 부분 피벗팅

각 단계에서 기준이 되는 피벗(pivot) 원소로 나누므로, 피벗이 \(0\) 이거나 아주 작으면
문제가 생깁니다. 부분 피벗팅(partial pivoting): 현재 열에서 절댓값이 가장 큰 행을
골라 위로 교환합니다. 이는 (a) 피벗 \(0\) 을 피하고 (b) 부동소수점 오차를 줄입니다.


4. 해의 개수 판정

전진 소거 후 계수 행렬의 랭크(rank) 로 판정합니다.

  • \(\operatorname{rank}(A) = \operatorname{rank}([A\mid b]) = n\)유일해.
  • \(\operatorname{rank}(A) = \operatorname{rank}([A\mid b]) < n\)무수히 많은 해
    (자유 변수 존재).
  • \(\operatorname{rank}(A) < \operatorname{rank}([A\mid b])\)해 없음
    (\(0 = (\text{0 아닌 값})\) 형태의 모순 행).

5. 복잡도

\(n \times n\) 계에 대해 전진 소거가 \(O(n^3)\), 후진 대입이 \(O(n^2)\). 전체 \(O(n^3)\).
\(n \le 500\) 정도까지 실용적입니다. XOR/GF(2) 계는 비트셋으로 \(O(n^3 / 64)\) 로 크게
가속됩니다(2강).


6. 숫자로 확인하기

$$ \begin{cases} 2x + y = 5 \\ x - y = 1 \end{cases} $$
둘째 행을 첫 행과 정리(또는 대입)하면 \(x = 2, y = 1\). 검산 \(2(2)+1=5\), \(2-1=1\). ✓


7. 언제 쓰는가

  • 연립 일차 방정식 풀기(실수·유리수·모듈러).
  • 행렬식(determinant): 전진 소거 중 피벗들의 곱(교환마다 부호 반전).
  • 역행렬: \([A \mid I]\) 를 가우스-조던.
  • XOR 방정식계(GF(2)): 스위치/전구 토글, 선형 기저, 랭크. → 비트셋 가속.

정리

  • 행 연산으로 위삼각화(전진 소거) 후 후진 대입, 전체 \(O(n^3)\).
  • 부분 피벗팅으로 \(0\) 피벗과 부동소수점 오차를 방지.
  • 랭크 비교로 유일해 / 무수해 / 무해를 판정.
Lesson 2강 · 구현 선택 8m

구현 레퍼런스

실수 계(부분 피벗팅)와 XOR/GF(2) 계(비트셋 가속) 두 가지를 완성합니다. 후자는
대회에서 특히 자주 나옵니다.


1. 실수 연립방정식 (부분 피벗팅)

#include <bits/stdc++.h>
using namespace std;
const double EPS = 1e-9;

// a: n x (n+1) 첨가행렬. 유일해면 x 채우고 0, 무해/무수해면 비정상 반환.
int gauss(vector<vector<double>>& a, vector<double>& x) {
    int n = a.size();
    for (int col = 0, row = 0; col < n && row < n; col++) {
        int sel = row;                              // 부분 피벗: 최대 절댓값 행
        for (int i = row; i < n; i++)
            if (fabs(a[i][col]) > fabs(a[sel][col])) sel = i;
        if (fabs(a[sel][col]) < EPS) continue;      // 이 열은 피벗 없음
        swap(a[sel], a[row]);
        for (int i = 0; i < n; i++) if (i != row) { // 위아래 모두 소거(조던)
            double f = a[i][col] / a[row][col];
            for (int j = col; j <= n; j++) a[i][j] -= f * a[row][j];
        }
        row++;
    }
    x.assign(n, 0);
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            if (fabs(a[i][j]) > EPS) { x[i] = a[i][n] / a[i][j]; break; }
    return 0;
}

col(열)과 row(피벗 행)를 따로 전진시키는 것이 랭크 부족·자유 변수 처리의 요령입니다.


2. XOR 방정식계 (GF(2), 비트셋)

각 방정식이 x_i 들의 XOR \(=\) 우변(\(0/1\)) 형태. bitset 으로 한 행을 \(64\)비트씩
XOR 하면 \(O(n^3/64)\) 로 빨라집니다.

#include <bits/stdc++.h>
using namespace std;
const int MAXV = 1005;                              // 변수 수 상한

// eq[i]: 비트 0..n-1 = 계수, 비트 n = 우변. m 개 방정식, n 개 변수.
int gauss_xor(vector<bitset<MAXV>>& eq, int n, int m, vector<int>& x) {
    vector<int> where(n, -1);
    int row = 0;
    for (int col = 0; col < n && row < m; col++) {
        int sel = -1;
        for (int i = row; i < m; i++) if (eq[i][col]) { sel = i; break; }
        if (sel < 0) continue;                       // 이 변수는 자유 변수 후보
        swap(eq[sel], eq[row]);
        for (int i = 0; i < m; i++)
            if (i != row && eq[i][col]) eq[i] ^= eq[row];   // 행 전체를 한 번에 XOR
        where[col] = row++;
    }
    for (int i = row; i < m; i++)                    // 모순 행: 0 = 1 검사
        if (eq[i][n]) return -1;                     // 해 없음
    x.assign(n, 0);
    for (int c = 0; c < n; c++)
        if (where[c] >= 0) x[c] = eq[where[c]][n];   // 자유 변수는 0 으로
    return 0;
}

eq[i] ^= eq[row] 한 줄이 한 방정식 전체(수백 변수)를 상수 시간처럼 소거합니다.


3. 파이썬 버전 (실수)

EPS = 1e-9

def gauss(a):                                       # a: n x (n+1)
    n = len(a)
    row = 0
    for col in range(n):
        sel = max(range(row, n), key=lambda i: abs(a[i][col]), default=row)
        if abs(a[sel][col]) < EPS:
            continue
        a[row], a[sel] = a[sel], a[row]
        for i in range(n):
            if i != row and abs(a[i][col]) > EPS:
                f = a[i][col] / a[row][col]
                a[i] = [a[i][j] - f * a[row][j] for j in range(n + 1)]
        row += 1
    x = [0.0] * n
    for i in range(n):
        for j in range(n):
            if abs(a[i][j]) > EPS:
                x[i] = a[i][n] / a[i][j]
                break
    return x

파이썬에서 XOR 계는 정수를 비트마스크로 써서 eq[i] ^= eq[row] 로 처리하면 매우
빠릅니다(임의 폭 정수가 곧 bitset).


4. 검증 사례

  • 실수: \(\{2x+y=5,\ x-y=1\}\)x=[2, 1].
  • XOR: \(x_0\oplus x_1=1,\ x_1\oplus x_2=0,\ x_0\oplus x_2=1\)
    x=[1,0,0] (검산 \(1,0,1\) 로 우변 일치).

정리

  • 실수 계는 부분 피벗팅, col/row 분리로 랭크 부족 대응.
  • GF(2) 계는 bitset 행 XOR\(O(n^3/64)\) — 모순 행으로 무해 판정.
  • 파이썬은 큰 정수를 bitset 삼아 XOR 계를 간결·고속으로.
Lesson 3강 · 심화와 변형 선택 8m

정밀도, 모듈러, 그리고 자유 변수

가우스 소거법의 실전 난점은 세 가지입니다: 부동소수점 정밀도, 모듈러/유한체 위의
소거, 그리고 유일해가 아닐 때의 자유 변수 처리.


1. 부동소수점 정밀도

실수 계는 오차가 누적됩니다.

  • 작은 피벗\(|피벗| < \varepsilon\)\(0\) 으로 취급. 부분 피벗팅으로 큰 값을
    골라 나눗셈 오차를 줄인다.
  • \(\varepsilon\) 선택 — 너무 작으면 랭크 부족을 못 잡고, 너무 크면 정상 피벗을
    \(0\) 으로 오인. 문제 스케일에 맞춰(\(10^{-9}\) 근처) 조정.
  • 완전 피벗팅 — 행·열 모두에서 최대를 고르면 더 안정적이나 열 교환으로 변수
    순서가 섞이니 되돌릴 인덱스를 관리.

2. 모듈러 / 유한체 위의 소거

소수 \(p\) 를 법으로 하는 계는 나눗셈을 모듈러 역원으로 대체합니다.

// 피벗으로 나누는 대신 역원 곱
ll inv = power(a[row][col], MOD - 2, MOD);          // 페르마 역원
for (int j = col; j <= n; j++) a[row][j] = a[row][j] * inv % MOD;

\(p\) 가 소수면 페르마 역원, 아니면 확장 유클리드. GF(2) 는 나눗셈이 필요 없어
(피벗이 \(1\)) XOR 만으로 끝나고 비트셋으로 가속됩니다. 정수 유일해를 원하면 유리수
소거(분수) 또는 여러 소수 modulo + CRT 로 복원합니다.


3. 자유 변수와 해 공간

랭크 \(r < n\) 이면 \(n - r\) 개의 자유 변수가 생깁니다.

  • 자유 변수에 임의값(보통 \(0\))을 넣어 한 특수해를 얻는다.
  • GF(2) 에서 해의 총 개수 \(= 2^{(n-r)}\) (자유 변수 각각 \(0/1\)).
  • "해의 개수를 세라" 문제는 랭크 \(r\) 만 구하면 \(2^{n-r}\) 로 즉답.
int free_vars = n - rank;
// GF(2) 해 개수 = (1LL << free_vars)  (오버플로 시 modulo)

4. 함정 모음

  • col/row 혼용 — 랭크 부족 시 열은 넘어가되 피벗 행은 그대로 둬야 함. 한
    변수로 뭉뚱그리면 무수해/무해에서 오답.
  • 모순 행 미검사 — 소거 후 "계수 전부 \(0\) 인데 우변 \(\ne 0\)" 행이 있으면 무해.
    이 검사를 빼면 엉뚱한 해.
  • modulo 음수 — 뺄셈 후 (x % MOD + MOD) % MOD.
  • 비트셋 크기 — 변수 수 + 우변 비트 하나 여유. MAXV 를 넉넉히.
  • 정수 오버플로(행렬식) — 피벗 곱이 커지므로 __int128 또는 modulo.

5. 대표 응용

  • 행렬식: 전진 소거로 위삼각화 후 대각 곱, 행 교환마다 부호 반전.
  • 역행렬: \([A \mid I]\) 를 가우스-조던 → 오른쪽이 \(A^{-1}\).
  • XOR 선형 기저(linear basis): 벡터들을 가우스 소거해 기저를 만들어, 부분집합
    XOR 최대/특정값 가능성/개수를 \(O(\text{비트})\) 에 답.
  • 전구 스위치·Lights Out: 각 스위치의 토글을 GF(2) 방정식으로 세워 해의 존재/개수.

6. 검증 사례

  • 무해: \(\{x+y=1,\ x+y=2\}\) → 소거하면 \(0 = 1\) 모순 행 → 해 없음.
  • 무수해: \(\{x+y=1,\ 2x+2y=2\}\) → 랭크 \(1 < 2\), 자유 변수 \(1\) 개.
  • GF(2) 개수: 변수 \(4\), 랭크 \(2\) → 해 \(2^{2} = 4\) 개.

정리

  • 실수는 피벗팅 + \(\varepsilon\), 유한체는 역원(또는 GF(2) XOR) 로 나눗셈 대체.
  • 랭크로 유일해/무수해/무해 판정, 자유 변수 \(n-r\) 개, GF(2) 해 수 \(2^{n-r}\).
  • 응용: 행렬식·역행렬·XOR 선형 기저·Lights Out. 모순 행 검사를 빠뜨리지 말 것.
Practice problem 구간 XOR 표현 질의 선택 25m
R00652

구간 XOR 표현 질의

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem 공장 생산 계획의 최댓값 선택 25m
R00688

공장 생산 계획의 최댓값

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
06
Level 7 · Specialist

Specialist

수학과 정수론 · Specialist 단계

0/15 완료
Lesson 이론 — 다항식 곱셈, 단위근, 분할 정복 필수 8m

무엇을 푸는가

두 다항식 \(A(x)=\sum_{i=0}^{n-1} a_i x^i\), \(B(x)=\sum_{j=0}^{m-1} b_j x^j\) 의 곱 \(C=A\cdot B\)

$$ c_k = \sum_{i+j=k} a_i b_j $$

로 정의된다. 이는 길이 \(n,m\) 인 두 수열의 합성곱(convolution) 과 정확히 같은 식이다. 정의대로 계산하면 \(O(nm)\) 이지만, 큰 정수 곱셈·문자열 매칭 점수·부분합 개수 세기·확률 분포의 합 등 수많은 문제가 이 형태로 환원되므로 더 빠른 방법이 절실하다. FFT/NTT는 이를 \(O(N\log N)\) 에 처리한다.

점-값 표현이라는 열쇠

차수 \(d\) 미만의 다항식은 서로 다른 점 \(d\) 개에서의 함숫값으로 유일하게 결정된다(라그랑주 보간). 그러므로 곱셈은 다음 세 단계로 나뉜다.

  1. 평가(evaluate): \(A,B\) 를 같은 점 \(N\) 개에서 계산한다. 단 \(N \ge n+m-1\).
  2. 점별 곱(pointwise): 각 점에서 값을 곱한다. \(C(x_i)=A(x_i)B(x_i)\), 비용 \(O(N)\).
  3. 보간(interpolate): 점값을 다시 계수로 되돌린다.

평가와 보간을 일반적인 점에서 하면 \(O(N^2)\) 라 이득이 없다. FFT의 통찰은 평가점으로 \(N\) 차 단위근을 선택하면 분할 정복이 가능해 각 단계가 \(O(N\log N)\) 이 된다는 데 있다.

단위근과 버터플라이

\(\omega_N = e^{2\pi i/N}\)\(N\) 차 원시 단위근이라 하자. \(A\) 를 짝수·홀수 차수 항으로 분리하면

$$ A(x) = A_{\text{even}}(x^2) + x\,A_{\text{odd}}(x^2) $$

\(x=\omega_N^k\) 를 대입하면 \(x^2=\omega_{N/2}^k\) 가 되어 크기 \(N\) 평가가 크기 \(N/2\) 두 개로 줄어든다. 게다가 \(\omega_N^{k+N/2}=-\omega_N^k\) 이므로 한 번의 곱으로 두 출력을 동시에 얻는다(버터플라이):

$$ X_k = E_k + \omega_N^k O_k,\qquad X_{k+N/2} = E_k - \omega_N^k O_k $$

점화식 \(T(N)=2T(N/2)+O(N)\) 이므로 전체 \(O(N\log N)\).

역변환

DFT 행렬은 \(F_{jk}=\omega_N^{jk}\), 그 역행렬은 \(F^{-1}_{jk}=\tfrac1N\omega_N^{-jk}\) 이다. 즉 역FFT는 \(\omega_N\) 대신 \(\omega_N^{-1}\) 을 쓰고 마지막에 \(N\) 으로 나누면 되며, 정변환과 코드를 공유한다.

왜 NTT인가

복소수 FFT는 double 부동소수점 오차가 있어 계수·길이가 크면 반올림이 답을 망친다. 정수 합성곱의 정확한 값이 필요하면 단위근을 소수 \(p\) 의 곱셈군에서 잡는 NTT(수론적 변환)를 쓴다. \(p=c\cdot 2^k+1\) 꼴(예: \(998244353=119\cdot 2^{23}+1\))이면 \(2^k\) 이하 크기의 원시근이 존재해 같은 버터플라이가 정수 위에서 오차 없이 돈다. 원시근은 \(g=3\).

작은 예제

\(A=1+2x\), \(B=3+4x\) 의 곱은 \(c_0=3,\ c_1=1\cdot4+2\cdot3=10,\ c_2=8\) 이므로 \(C=3+10x+8x^2\). 필요한 점 개수는 \(n+m-1=3\) 이므로 \(N=4\) 로 패딩해 NTT를 돌리면 동일한 결과를 얻는다.

복잡도 요약

연산 복잡도
직접 곱셈 \(O(nm)\)
FFT/NTT 곱셈 \(O(N\log N)\), \(N=2^{\lceil\log_2(n+m-1)\rceil}\)
메모리 \(O(N)\)

핵심은 "평가 → 점별 곱 → 보간"이라는 골격과, 평가점을 단위근으로 잡아 분할 정복한다는 아이디어다. 구현 세부는 2강에서 다룬다.

Lesson 구현 — 반복형 NTT와 곱셈 선택 8m

반복형(iterative) NTT

재귀 대신 비트 반전 정렬 후 반복으로 구현하면 상수가 작고 스택도 안 쓴다. 모듈러 \(998244353\), 원시근 \(g=3\) 을 쓴다.

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll MOD = 998244353, G = 3;

ll power(ll a, ll b, ll m) {
    ll r = 1; a %= m; if (a < 0) a += m;
    for (; b; b >>= 1, a = a * a % m)
        if (b & 1) r = r * a % m;
    return r;
}

void ntt(vector<ll>& a, bool inv) {
    int n = a.size();
    for (int i = 1, j = 0; i < n; i++) {        // 비트 반전 정렬
        int bit = n >> 1;
        for (; j & bit; bit >>= 1) j ^= bit;
        j ^= bit;
        if (i < j) swap(a[i], a[j]);
    }
    for (int len = 2; len <= n; len <<= 1) {
        ll w = power(G, (MOD - 1) / len, MOD);  // len차 원시근
        if (inv) w = power(w, MOD - 2, MOD);    // 역변환이면 켤레
        for (int i = 0; i < n; i += len) {
            ll wn = 1;
            for (int k = 0; k < len / 2; k++) {
                ll u = a[i + k];
                ll v = a[i + k + len / 2] * wn % MOD;
                a[i + k]           = (u + v) % MOD;
                a[i + k + len / 2] = (u - v + MOD) % MOD;
                wn = wn * w % MOD;
            }
        }
    }
    if (inv) {                                  // 1/N 곱
        ll ninv = power(n, MOD - 2, MOD);
        for (auto& x : a) x = x * ninv % MOD;
    }
}

vector<ll> multiply(vector<ll> a, vector<ll> b) {
    if (a.empty() || b.empty()) return {};
    int need = (int)a.size() + (int)b.size() - 1, n = 1;
    while (n < need) n <<= 1;
    a.resize(n); b.resize(n);
    ntt(a, false); ntt(b, false);
    for (int i = 0; i < n; i++) a[i] = a[i] * b[i] % MOD;
    ntt(a, true);
    a.resize(need);
    return a;
}

multiply 는 이후 모든 다항식 연산(역원·log·exp·나눗셈)의 기반이 되므로 정확히 이해해 두자.

코드 해설

  • 비트 반전 정렬: 재귀 FFT가 자연스레 만드는 순서를 반복 버전에서 미리 만들어 둔다. i < j 조건으로 각 쌍을 한 번만 교환한다.
  • len 루프: 크기 2 → 4 → … → \(n\) 으로 버터플라이를 병합한다. 각 블록의 원시근은 \(g^{(p-1)/\text{len}}\).
  • 역변환: 원시근을 그 역원으로 바꾸고 끝에서 \(n^{-1}\) 을 곱한다. 잊으면 값이 \(n\) 배로 나온다.

정확성 검증(작은 테스트)

int main(){
    vector<ll> a = {1, 2}, b = {3, 4};   // (1+2x)(3+4x)=3+10x+8x^2
    auto c = multiply(a, b);
    for (ll x : c) printf("%lld ", x);   // 3 10 8
}

흔한 실수

  • 2의 거듭제곱으로 안 올림. 버터플라이 분할이 깨진다. 항상 \(n+m-1\) 이상인 최소 \(2^k\) 로 패딩한다.
  • 역변환에서 \(n^{-1}\) 누락. 값이 전부 \(n\) 배가 된다.
  • 음수 계수. 입력을 미리 \([0,\text{MOD})\) 로 정규화한다.
  • 오버플로. ll 로 곱해야 한다. int 곱은 즉시 오버플로.

복잡도는 길이 \(n,m\) 합성곱이 \(O((n+m)\log(n+m))\). 심화 응용과 임의 모듈러는 3강에서.

Lesson 심화·응용 — 정밀도, 임의 모듈러 CRT, 함정 선택 8m

복소 FFT vs NTT — 무엇을 쓸까

상황 권장 이유
답이 \(998244353\) 등 NTT 소수 모듈러 NTT 오차 없이 정확
답이 임의 모듈러 \(M\), 계수 작음 NTT (단일 소수) 곱 결과가 소수 미만이면 OK
답이 큰 정수(모듈러 없음) 복소 FFT 또는 멀티 NTT 자릿수 조절
답이 임의 모듈러, 계수 큼 멀티 모듈러 NTT + CRT 아래 참고

복소 FFT는 long double 을 써도 길이 \(\sim 10^6\)·값 \(\sim 10^9\) 조합에서 반올림이 위험하다. 정수 정확도가 필요하면 항상 NTT를 우선한다.

임의 모듈러 합성곱 (멀티 NTT + CRT)

답 모듈러 \(M\) 이 NTT 소수가 아니거나, 점별 곱 이전의 진짜 정수 계수가 단일 소수 \(998244353\) 를 넘을 수 있으면 단일 NTT로는 앨리어싱된 잘못된 값이 나온다. 진짜 계수의 최댓값은 대략 \(n\cdot(\text{계수})^2\) 이다. 이때 서로소인 NTT 소수 세 개로 각각 곱한 뒤 CRT(가너)로 진짜 정수를 복원하고 마지막에 \(\bmod\,M\) 한다.

$$ p_1=998244353,\quad p_2=985661441,\quad p_3=754974721 $$

세 소수의 곱은 \(\approx 7.4\times10^{26}\) 이라 어떤 현실적 계수도 담는다.

// 세 소수로 각각 multiply → 각 자리에서 CRT 로 진짜 값 복원 → % M
ll crt3(ll r1, ll r2, ll r3, ll M) {
    const ll p1=998244353, p2=985661441, p3=754974721;
    // x ≡ r1 (p1), r2 (p2), r3 (p3)
    ll inv12 = power(p1 % p2, p2 - 2, p2);
    ll x2 = (r2 - r1 % p2 + p2) % p2 * inv12 % p2;   // x = r1 + p1*x2
    __int128 cur = r1 + (__int128)p1 * x2;
    ll p12mod3 = (ll)((__int128)p1 * p2 % p3);
    ll inv123 = power(p12mod3, p3 - 2, p3);
    ll curmod3 = (ll)(cur % p3);
    ll x3 = ((r3 - curmod3) % p3 + p3) % p3 * inv123 % p3;
    // 결과 = (cur + p1*p2*x3) mod M
    ll p1p2 = (ll)((__int128)p1 * p2 % M);
    return ((ll)(cur % M) + p1p2 * (x3 % M)) % M;
}

power 는 각 소수를 모듈러로 하는 거듭제곱을 쓴다. 멀티 모듈러는 NTT를 세 번 돌릴 뿐 차수는 그대로 \(O(N\log N)\).

고전 응용

문제 유형 환원 형태
큰 정수 곱셈 자릿수 벡터 합성곱 후 자리올림
문자열 와일드카드 매칭 \(\sum (a_i-b_j)^2 a_i b_j\) 형 점수 합성곱
부분합 가짓수 카운트 벡터의 (분할정복) 거듭 합성곱
확률/기댓값 분포 합 분포 벡터 합성곱
다항식 역원·log·exp·나눗셈 NTT 기반 뉴턴법(별도 단원)

자잘한 함정 정리

  • 소수 \(998244353\)\(2\)-진 지수는 \(23\). 즉 이 소수로는 길이 \(2^{23}\) 까지만 NTT 가능. 더 크면 다른 소수가 필요.
  • CRT 중간 계산 오버플로. \(p_1p_2\approx 9.8\times10^{17}\)ll 곱이 오버플로하므로 __int128 을 쓴다.
  • 입력 정규화. 뺄셈이 섞인 응용(매칭 점수 등)은 음수를 반드시 \([0,p)\) 로 보정.
  • 불필요한 크기. need 가 이미 \(2\) 의 거듭제곱이어도 문제 없지만, 과하게 키우면 상수만 커진다.
Practice problem 골드바흐 파티션 선택 25m
R00051

골드바흐 파티션

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem 알파카컵 1회: J - 알파카의 별빛 목장 선택 25m
A00010

알파카컵 1회: J - 알파카의 별빛 목장

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Lesson 1강 · 개념과 정리 필수 8m

거대한 수의 소인수분해

시험 나눗셈 소인수분해는 \(O(\sqrt N)\) 이라 \(N = 10^{18}\) 이면 \(10^9\) 번 나눠야 해
불가능합니다. 폴라드 로(Pollard's rho) 는 한 개의 비자명한 인수를 기대
\(O(N^{1/4})\) 시간에 찾아내는 확률적 알고리즘입니다. \(10^{18}\) 이면 약 \(10^{4.5}\)
연산으로 인수를 뽑아냅니다.


1. 핵심 아이디어: 생일 역설과 순환

합성수 \(N\)의 가장 작은 소인수를 \(p\) (\(p \le \sqrt N\))라 합시다. 유사 난수 수열
$$ x_{k+1} = (x_k^2 + c) \bmod N $$
을 생각하면, 이 수열을 \(p\) 로 나눈 값 \(x_k \bmod p\)\(p\) 개의 값 안에서
움직이므로 생일 역설에 의해 약 \(O(\sqrt p) = O(N^{1/4})\) 걸음 안에 두 항이
\(\bmod p\) 로 같아집니다.

그때 \(x_i \equiv x_j \pmod p\) 이지만 \(x_i \not\equiv x_j \pmod N\) 이면
$$ \gcd(|x_i - x_j|,\ N) $$
\(p\) 의 배수이면서 \(N\) 미만인 비자명한 인수가 됩니다.

수열이 \(\bmod p\) 에서 결국 순환(cycle)에 빠지는 모양이 그리스 문자 \(\rho\) 를 닮아
"rho" 라 부릅니다.


2. 순환 탐지: 플로이드와 브렌트

두 항이 언제 \(\bmod p\) 로 같아지는지 모르므로, 순환 탐지로 자동으로 찾습니다.

  • 플로이드(Floyd, 거북이와 토끼): 느린 포인터는 한 칸, 빠른 포인터는 두 칸씩
    전진하며 \(\gcd(|x_{\text{slow}} - x_{\text{fast}}|, N)\) 을 검사.
  • 브렌트(Brent): \(\gcd\) 계산을 여러 차이의 곱으로 묶어 gcd 호출 횟수를 줄여
    플로이드보다 상수배 빠름.

3. 전체 소인수분해로

폴라드 로는 인수 하나만 줍니다. 완전 분해는 재귀로:

  1. \(N\)이 소수인가? → 밀러-라빈으로 판정. 소수면 그대로 기록.
  2. 아니면 폴라드 로로 인수 \(d\) 를 찾아 \(N = d \times (N/d)\) 로 쪼갠다.
  3. \(d\)\(N/d\) 를 각각 재귀적으로 분해.

작은 소인수(\(2, 3, 5, \dots\))는 미리 시험 나눗셈으로 떼는 것이 안정적입니다.


4. 복잡도

  • 인수 하나 찾기: 기대 \(O(N^{1/4})\) (가장 작은 소인수 \(p\) 에 대해 \(O(p^{1/2})\)).
  • 소수 판정(밀러-라빈): \(O(\log N)\).
  • 전체 분해: 소인수 개수 \(\times\) 위 비용 — \(10^{18}\) 도 순식간.

5. 언제 쓰는가

  • \(10^{12} \sim 10^{18}\) (또는 그 이상) 한 수의 소인수분해.
  • \(N\)\(\varphi(N)\), 약수 개수·합, 원시근 등 소인수분해가 필요한 정수론.
  • 구간의 모든 수를 분해할 때는 체(선형 체) 가 낫습니다 — 폴라드 로는 개별 큰 수용.

정리

  • 폴라드 로는 \(x \mapsto x^2 + c\)\(\bmod p\) 순환을 생일 역설로 이용해 인수를
    \(O(N^{1/4})\) 에 찾는다.
  • 인수는 \(\gcd(|x_i - x_j|, N)\) 로 나온다. 순환 탐지는 플로이드/브렌트.
  • 완전 분해는 밀러-라빈(소수 판정) + 폴라드 로(쪼개기) 재귀.
Lesson 2강 · 구현 선택 8m

구현 레퍼런스

폴라드 로는 밀러-라빈(소수 판정)과 unsigned __int128 mulmod 를 전제로 합니다.
브렌트 개선을 포함한 표준 구현을 만듭니다.


1. 공용 하위 루틴 (mulmod + 밀러-라빈)

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long u64;

u64 mulmod(u64 a, u64 b, u64 m) { return (unsigned __int128)a * b % m; }
u64 binpow(u64 a, u64 b, u64 m) {
    u64 r = 1 % m; a %= m;
    while (b) { if (b & 1) r = mulmod(r, a, m); a = mulmod(a, a, m); b >>= 1; }
    return r;
}
bool isPrime(u64 n) {
    if (n < 2) return false;
    for (u64 p : {2,3,5,7,11,13,17,19,23,29,31,37})
        if (n % p == 0) return n == p;
    u64 d = n - 1; int s = 0; while (!(d & 1)) { d >>= 1; s++; }
    for (u64 a : {2,3,5,7,11,13,17,19,23,29,31,37}) {
        u64 x = binpow(a, d, n); if (x == 1 || x == n - 1) continue;
        bool ok = false;
        for (int i = 0; i < s - 1; i++) { x = mulmod(x, x, n); if (x == n - 1) { ok = true; break; } }
        if (!ok) return false;
    }
    return true;
}

2. 폴라드 로 (브렌트 개선)

u64 pollard(u64 n) {
    if (n % 2 == 0) return 2;
    u64 x = rand() % n, y = x, c = rand() % n + 1, d = 1;
    u64 q = 1, xs = x;
    int m = 128, cnt;
    while (d == 1) {
        y = x;
        for (u64 i = 0; i < (u64)m; i++) x = (mulmod(x, x, n) + c) % n;
        cnt = 0;
        while (d == 1 && cnt < m) {
            xs = x;
            int step = min((u64)m - cnt, (u64)m);
            for (int i = 0; i < step; i++) {
                x = (mulmod(x, x, n) + c) % n;
                q = mulmod(q, x > y ? x - y : y - x, n);  // 차이들을 곱해 gcd 호출 절약
            }
            d = __gcd(q, n);
            cnt += step;
        }
    }
    if (d == n) {                               // 묶음이 한꺼번에 n 이 된 경우 되짚기
        do { xs = (mulmod(xs, xs, n) + c) % n; d = __gcd(xs > y ? xs - y : y - xs, n); }
        while (d == 1);
    }
    return d;
}

브렌트의 요령은 여러 차이 \(|x-y|\) 를 곱한 뒤 한 번에 \(\gcd\) 를 부르는 것입니다.
\(\gcd\) 호출이 줄어 플로이드보다 빠릅니다.


3. 간결한 플로이드 버전 (이해용)

u64 pollard_floyd(u64 n) {
    if (n % 2 == 0) return 2;
    u64 x = rand() % n, y = x, c = rand() % n + 1, d = 1;
    while (d == 1) {
        x = (mulmod(x, x, n) + c) % n;                 // 거북이 1칸
        y = (mulmod(y, y, n) + c) % n;
        y = (mulmod(y, y, n) + c) % n;                 // 토끼 2칸
        d = __gcd(x > y ? x - y : y - x, n);
    }
    return d == n ? pollard_floyd(n) : d;              // 실패 시 c 바꿔 재시도
}

4. 완전 소인수분해

void factor(u64 n, map<u64,int>& f) {
    if (n == 1) return;
    if (isPrime(n)) { f[n]++; return; }               // 소수면 종료
    u64 d = pollard(n);                                // 인수 하나
    factor(d, f); factor(n / d, f);                    // 양쪽 재귀
}

5. 파이썬 버전

from math import gcd
from random import randrange

def pollard(n):
    if n % 2 == 0:
        return 2
    while True:
        x = randrange(2, n); y = x; c = randrange(1, n); d = 1
        while d == 1:
            x = (x * x + c) % n
            y = (y * y + c) % n; y = (y * y + c) % n
            d = gcd(abs(x - y), n)
        if d != n:
            return d                       # 실패(d==n)면 c 바꿔 재시도

def factor(n, f):
    if n == 1:
        return
    if is_prime(n):                        # 밀러-라빈
        f[n] = f.get(n, 0) + 1; return
    d = pollard(n)
    factor(d, f); factor(n // d, f)

6. 검증 사례

  • factor(360)\(2^3 \cdot 3^2 \cdot 5\).
  • factor(1000000007 * 1000000009)\(1000000007 \times 1000000009\) (두 큰 소수 곱).
  • factor(9999999999999999999)\(3^2 \times 1111111111111111111\) (반복단위 소수).

정리

  • 폴라드 로는 밀러-라빈 + unsigned __int128 mulmod 위에서 동작.
  • 브렌트 개선(차이 곱 후 일괄 \(\gcd\))이 표준, 플로이드는 이해용.
  • 완전 분해 = isPrime 이면 기록, 아니면 pollard 로 쪼개 재귀.
Lesson 3강 · 심화와 변형 선택 8m

실패와 재시도, 그리고 함정

폴라드 로는 확률적이라 "실패"가 정상 동작의 일부입니다. 실패를 올바르게 처리하지
않으면 무한 루프나 오답에 빠집니다.


1. 실패 케이스와 재시도

  • \(d = n\) (전체가 한꺼번에 gcd): 순환 길이가 묶음보다 짧아 차이 곱이 통째로
    \(n\)의 배수가 된 경우. → \(c\) 를 바꿔 재시도하거나 브렌트처럼 마지막 구간을
    한 걸음씩 되짚어 진짜 인수를 회수.
  • \(d = 1\) 로 계속 → 순환에 아직 안 들어감. 계속 전진.
  • \(c = 0\) 또는 \(n\) 의 배수 상수\(x^2\) 만으로 \(0\) 에 고착될 수 있어 \(c \ne 0\)
    로 잡는다.

2. 함정 모음

  • 작은 소인수 선처리 누락\(2, 3\) 같은 작은 소수를 폴라드 로가 잘 못 잡는
    경우가 있어, \(N\)이 짝수면 바로 \(2\) 를 떼고 시작. 작은 소수는 시험 나눗셈으로 먼저
    제거하면 안정적.
  • 소수를 폴라드 로에 넘김\(n\)이 소수면 폴라드 로는 인수를 못 찾아 무한
    루프
    . 반드시 isPrime 을 먼저 호출.
  • 오버플로 — mulmod 에 부호 있는 __int128 을 쓰면 \(2^{63}\) 초과에서 오답
    (밀러-라빈과 동일). unsigned __int128 필수.
  • 차이 절댓값x - y 가 unsigned 라 언더플로. x > y ? x - y : y - x
    안전하게.
  • 중복 소인수\(N = p^2\) 처럼 같은 소수가 여러 번이면 재귀에서 자연히
    여러 번 기록되도록 (f[p]++).

3. 브렌트가 왜 빠른가

플로이드는 매 걸음 \(\gcd\) 를 부릅니다. \(\gcd\)\(O(\log n)\) 이라 이게 병목이 됩니다.
브렌트는 여러 걸음의 차이 \(|x_i - y|\)곱해서 하나로 모은 뒤 그 곱과 \(n\)
\(\gcd\) 를 한 번만 계산합니다. \(\gcd\) 호출이 수십~수백 배 줄어 실측 속도가 크게
개선됩니다. 단, 곱이 \(n\) 의 배수가 되어 \(d=n\) 이 나오면 그 구간을 되짚어야 합니다.


4. 응용: \(\varphi\), 약수 함수, 원시근

\(N\)의 소인수분해가 뚫리면 여러 정수론 양이 따라옵니다.

  • \(\varphi(N) = N \prod_{p \mid N} (1 - 1/p)\).
  • 약수 개수 \(\tau(N) = \prod (e_i + 1)\), 약수 합 \(\sigma(N) = \prod \frac{p_i^{e_i+1}-1}{p_i-1}\).
  • 원시근 존재/탐색은 \(\varphi(N)\) 의 소인수가 필요 → 다시 폴라드 로.

즉 폴라드 로는 "큰 수 정수론" 문제의 관문 역할을 합니다.


5. 검증 사례

  • 소수 입력 방어: factor(2305843009213693951) (메르센 소수) → isPrime
    즉시 참이라 폴라드 로를 아예 안 부름. 방어 없으면 무한 루프.
  • 제곱수: factor(1000003^2)\(1000003^2\), 같은 소수 두 번.
  • 재시도: 어떤 \(c\) 에서 \(d=n\) 이 나와도 다른 \(c\) 로 다시 시도해 인수 회수.

정리

  • 실패(\(d=1\) 계속, \(d=n\))는 정상 — \(c\) 재시도 또는 구간 되짚기로 처리.
  • 반드시 소수 선판정(밀러-라빈) 후 호출, 안 그러면 무한 루프.
  • unsigned __int128, 차이 절댓값, 작은 소인수 선처리가 실전 안정성의 핵심.
Practice problem 거대한 수의 오일러 피 선택 25m
R00639

거대한 수의 오일러 피

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Diamond IV 다이아몬드 IV 지금 풀기
Practice problem RSA-2048 분해 (현대 암호 깨기) 선택 25m
R00704

RSA-2048 분해 (현대 암호 깨기)

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Lesson 1강 · 이론: 특성 다항식과 x^n mod f 필수 8m

문제: 선형 점화식의 아주 먼 항

길이 \(k\) 의 선형 점화식

$$ a_n = c_1 a_{n-1} + c_2 a_{n-2} + \dots + c_k a_{n-k}\quad(n\ge k) $$

와 초기값 \(a_0,\dots,a_{k-1}\) 이 주어졌을 때, \(a_n\) 을 구한다. \(n\)\(10^{18}\) 처럼 거대할 수 있다.

  • 순진하게 한 항씩: \(O(nk)\)\(n\) 이 크면 불가능.
  • 행렬 거듭제곱: 전이행렬 \(k\times k\)\(\log n\) 번 곱해 \(O(k^3\log n)\).
  • 키타마사법(Fiduccia): 같은 일을 다항식으로 하여 \(O(k^2\log n)\), NTT를 얹으면 \(O(k\log k\log n)\).

언제 쓰나

  • \(k\) 가 수백~수천이라 \(k^3\) 행렬 거듭제곱이 느릴 때. \(k^2\) 로 떨어진다.
  • 피보나치·트리보나치·일반 상수계수 선형 점화식의 \(10^{18}\) 번째 항.
  • 세그먼트 트리 위 행렬 대신 "다항식 상태"로 결합이 필요할 때.

핵심: 이동을 곱셈으로 본다

전이행렬 \(T\) 를 생각하면 \(a_n\)\(T^n\) 의 성분들과 초기값의 선형결합이다. \(T\)특성 다항식

$$ f(x) = x^k - c_1 x^{k-1} - c_2 x^{k-2} - \dots - c_k. $$

케일리–해밀턴 정리에 의해 \(f(T)=0\), 즉 \(T^k = c_1 T^{k-1} + \dots + c_k I\). 그러므로 \(T\) 의 임의 거듭제곱은 \(I, T, \dots, T^{k-1}\) 의 선형결합으로 축약된다. 이 축약은 정확히 다항식을 \(f(x)\) 로 나눈 나머지 와 같다.

$$ x^n \equiv r_0 + r_1 x + \dots + r_{k-1}x^{k-1} \pmod{f(x)} $$

를 구하면,

$$ a_n = \sum_{i=0}^{k-1} r_i\, a_i. $$

행렬을 곱하는 대신 다항식 \(x^n \bmod f\) 를 구하는 것이 전부다. 왜 성립하나? \(x^n\bmod f = \sum r_i x^i\) 를 얻은 뒤 \(x\mapsto T\) 대입하면 \(T^n=\sum r_i T^i\) 이고, 초기 상태 벡터에 곱해 첫 성분을 보면 \(a_n=\sum r_i a_i\) 가 된다. 스칼라 수열 관점에서는 "\(x^i\)\(a_i\) 로 읽는" 선형 함수가 점화식과 호환되기 때문이다.

x^n mod f 를 빠르게

\(x^n\bmod f\)다항식 거듭제곱을 반복 제곱 으로 구한다. 밑 \(x\) 에서 시작해

$$ x^n \bmod f = \prod (\text{제곱하거나} \times x) \bmod f $$

\(O(\log n)\) 번의 "다항식 곱 + \(f\) 로 나머지" 로 얻는다. 각 단계의 다항식 차수는 \(, 곱하면 \(<2k-1\), 나머지 연산으로 다시 \( 로 줄인다.

  • 곱·나머지를 순진하게: 각 \(O(k^2)\), 전체 \(O(k^2\log n)\).
  • 곱을 NTT, 나머지를 뉴턴 역원 기반 나눗셈으로: 각 \(O(k\log k)\), 전체 \(O(k\log k\log n)\).

작은 예시: 피보나치

\(a_n=a_{n-1}+a_{n-2}\), \(a_0=0,a_1=1\). \(k=2\), \(c_1=c_2=1\), 특성 \(f(x)=x^2-x-1\).

\(x^5\bmod f\) 를 구하자. \(x^2\equiv x+1\) 이므로

  • \(x^3=x\cdot x^2\equiv x(x+1)=x^2+x\equiv (x+1)+x=2x+1\)
  • \(x^4\equiv x(2x+1)=2x^2+x\equiv 2(x+1)+x=3x+2\)
  • \(x^5\equiv x(3x+2)=3x^2+2x\equiv 3(x+1)+2x=5x+3\)

\(x^5\equiv 3 + 5x\pmod f\), \(r_0=3,r_1=5\). 따라서

$$ a_5 = r_0 a_0 + r_1 a_1 = 3\cdot 0 + 5\cdot 1 = 5. $$

피보나치 \(0,1,1,2,3,5\)\(a_5=5\) 와 일치한다. ✓

복잡도 정리

방법 시간 비고
한 항씩 \(O(nk)\) \(n\) 작을 때만
행렬 거듭제곱 \(O(k^3\log n)\) 구현 간단
키타마사(순진) \(O(k^2\log n)\) 실전 기본값
키타마사(NTT) \(O(k\log k\log n)\) \(k\) 가 클 때
Lesson 2강 · 구현: 순진한 O(k^2 log n) 키타마사 선택 8m

표준 구현

가장 견고하고 실전에서 가장 많이 쓰는 \(O(k^2\log n)\) 버전이다. 특성 관계 \(x^k \equiv c_1 x^{k-1} + \dots + c_k\) 를 이용해 곱한 다항식을 \(f\) 로 즉시 줄인다.

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef vector<ll> poly;
const ll MOD = 998244353;

int K;
poly rec;   // rec[j] = 'x^j 의 계수' in x^K = sum_{j=0..K-1} rec[j] x^j
poly A0;    // 초기값 a[0..K-1]

// deg < 2K-1 인 다항식 p 를 특성 다항식으로 축약해 deg < K 로
poly reduce(poly p){
    for(int i = (int)p.size()-1; i >= K; i--){
        ll coef = p[i];
        if(coef == 0) continue;
        p[i] = 0;
        // x^i = x^{i-K} * x^K = x^{i-K} * sum rec[j] x^j
        for(int j = 0; j < K; j++)
            p[i-K+j] = (p[i-K+j] + coef * rec[j]) % MOD;
    }
    p.resize(K);
    return p;
}

// (a * b) mod f
poly mul(const poly& a, const poly& b){
    poly c(a.size() + b.size() - 1, 0);
    for(int i = 0; i < (int)a.size(); i++) if(a[i])
        for(int j = 0; j < (int)b.size(); j++)
            c[i+j] = (c[i+j] + a[i] * b[j]) % MOD;
    return reduce(c);
}

// a_n 반환
ll kitamasa(ll n){
    poly result = {1};              // x^0
    poly base   = {0, 1};           // x
    if(K == 1) base = reduce(base); // x 가 이미 차수 K 일 수 있으니 안전 축약
    while(n){
        if(n & 1) result = mul(result, base);
        base = mul(base, base);
        n >>= 1;
    }
    result.resize(K, 0);
    ll ans = 0;
    for(int i = 0; i < K; i++)
        ans = (ans + result[i] * A0[i]) % MOD;
    return ans;
}

입력 규약과 사용 예

rec[j] 는 특성 관계 \(x^K=\sum_j rec[j]\,x^j\) 의 계수다. 점화식 \(a_n=\sum_{i=1}^K c_i a_{n-i}\) 와의 대응은

$$ rec[K-i] = c_i\quad(i=1,\dots,K). $$

\(x^{K-1}\) 계수가 \(c_1\), …, \(x^0\) 계수가 \(c_K\) 다.

int main(){
    // 피보나치: a_n = a_{n-1} + a_{n-2},  a0=0, a1=1
    K = 2;
    rec = {1, 1};      // x^2 = 1*x^0 + 1*x^1  ->  rec[0]=c_2=1, rec[1]=c_1=1
    A0  = {0, 1};
    printf("%lld\n", kitamasa(10));  // 55
    printf("%lld\n", kitamasa(5));   // 5
}

n < K 도 자동 처리

\(n 이면 \(x^n\bmod f = x^n\) (축약이 일어나지 않음)이라 result\(x^n\), 곧 result[n]=1. 따라서 ans = A0[n] 이 그대로 나온다. 별도 분기가 필요 없다.

정확성 포인트

  • reduce높은 차수부터 내린다. 낮은 차수부터 하면 방금 만든 고차항을 다시 못 줄인다.
  • mul 은 곱한 뒤 즉시 reduce. 차수가 \(2K-2\) 이하로만 커지므로 한 번의 reduce 로 충분하다.
  • 모든 곱 뒤 % MOD, 음수 계수가 생기면 (v%MOD+MOD)%MOD 로 정규화(위 코드는 rec·초기값이 음수만 아니면 안전).

이 버전은 \(K\) 가 대략 \(\le 2000\), \(\log n\le 60\) 이면 넉넉히 통과한다(\(K^2\log n\approx 2.4\times10^8\)).

Lesson 3강 · 심화·응용: NTT 가속과 함정 선택 8m

NTT 가속: O(k log k log n)

\(K\) 가 아주 크면 mul\(O(K^2)\) 이 병목이다. 곱을 NTT로, 나머지를 reverse 뉴턴 나눗셈 으로 바꾸면 각 단계가 \(O(K\log K)\).

특성 다항식을 모닉 형태로 둔다: \(f(x)=x^K - c_1x^{K-1}-\dots-c_K\). \(A\)\(f\) 로 나눈 나머지는 다음으로 구한다(\(\deg A< 2K-1\), \(m=\deg f=K\)).

$$ Q^{\mathrm{rev}} = A^{\mathrm{rev}}\cdot (f^{\mathrm{rev}})^{-1} \bmod x^{\deg A-m+1},\qquad R = A - Qf. $$

\(f^{\mathrm{rev}}\) 의 역원은 한 번만 미리 계산해 재사용한다.

vector<ll> multiply(vector<ll> a, vector<ll> b);  // NTT
poly poly_inverse(poly a, int n);                  // 다항식 연산 단원

poly f, finv;   // f: 모닉 특성다항식(길이 K+1), finv: rev(f) 의 역원
int K;

poly rev(poly a){ reverse(a.begin(), a.end()); return a; }

void init_char(poly c){        // c[i] = 점화식 계수 c_{i+1}
    K = c.size();
    f.assign(K+1, 0);
    f[K] = 1;
    for(int i = 0; i < K; i++) f[K-1-i] = (MOD - c[i]) % MOD;  // -c_{i+1}
    poly fr = rev(f);          // rev(f)[0] = 1 (모닉)
    finv = poly_inverse(fr, K);   // 몫 차수 <= K-1 이므로 K항이면 충분
}

// A mod f  (deg A < 2K-1)
poly mod_f(poly A){
    int n = A.size();
    if(n < (int)f.size()){ A.resize(K, 0); return A; }
    int qd = n - K;                     // 몫의 항 수 = deg(A)-deg(f)+1 = (n-1)-K+1
    poly Q = multiply(rev(A), poly(finv.begin(), finv.begin()+min((int)finv.size(), qd)));
    Q.resize(qd); Q = rev(Q);
    poly QF = multiply(Q, f);
    poly R(K, 0);
    for(int i = 0; i < K; i++){
        ll v = (i<n? A[i]:0) - (i<(int)QF.size()? QF[i]:0);
        R[i] = (v % MOD + MOD) % MOD;
    }
    return R;
}

poly mul(const poly& a, const poly& b){ return mod_f(multiply(a, b)); }

kitamasa 의 반복 제곱 골격은 2강과 동일하되 mul 만 위 것으로 바꾼다. poly_inverse 는 다항식 연산 단원의 뉴턴 역원을 쓴다.

어느 버전을 쓸까

\(K\) 범위 권장 이유
\(\le 2000\) 순진 \(O(K^2\log n)\) 상수 작고 버그 적음
\(\gtrsim 5000\) NTT \(O(K\log K\log n)\) \(K^2\) 이 한계
아주 작음(\(K\le 6\)) 행렬 거듭제곱 구현이 더 단순

자주 하는 실수

함정

  • 계수 대응 뒤집힘. rec[K-i]=c_i 규약을 헷갈리면 점화식이 뒤집혀 엉뚱한 값. 피보나치로 항상 \(a_{10}=55\) 를 검산하라.
  • reduce 방향. 반드시 높은 차수 → 낮은 차수. 반대로 하면 축약이 미완성.
  • \(n=0\) 경계. \(x^0=1\) 이라 result={1}, ans=A0[0]. 코드가 자연히 처리하지만 초기값 배열 길이가 \(K\) 인지 확인.
  • 모듈러 불일치(NTT판). 답 모듈러가 \(998244353\) 이 아니면 곱에서 3-소수 CRT 합성곱을 써야 한다.
  • poly_inverse 길이 부족. 몫 차수는 최대 \(K-1\) 이므로 finv\(K\) 항이면 충분하지만, 곱 인덱싱을 잘못 자르면 몫이 틀린다.
  • 음수 초기값/계수. 축약·나눗셈 후 항상 \([0,MOD)\) 로 정규화.

확장

  • 여러 질의: 같은 점화식으로 여러 \(n\) 을 물으면 init_char 를 한 번만, kitamasa 만 반복.
  • 선형결합 상태: "세그먼트 트리 노드마다 점화식 상태"를 결합할 때, 다항식 곱 \(\bmod f\) 가 결합 연산이 되어 행렬보다 가볍게 합쳐진다.
  • 베를레캄프–매시와의 연결: 수열만 주어지고 점화식을 모르면 벌리캠프–매시로 최소 점화식을 먼저 복원한 뒤 키타마사로 먼 항을 뽑는 것이 표준 파이프라인이다.
Practice problem 선형 점화식의 거대한 항 선택 25m
R00617

선형 점화식의 거대한 항

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem 숨은 점화식의 N번째 항 선택 25m
R00633

숨은 점화식의 N번째 항

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
07
Level 8 · Expert

Expert

수학과 정수론 · Expert 단계

0/15 완료
Lesson 1강 · 이론: 최소 선형 점화식 복원 필수 8m

문제 정의

수열 \(a_0, a_1, a_2, \dots\)선형 점화식(linear recurrence) 을 만족한다고 하자. 즉 어떤 차수 \(L\) 과 계수 \(c_1, \dots, c_L\) 이 존재하여

$$ a_i = c_1 a_{i-1} + c_2 a_{i-2} + \cdots + c_L a_{i-L} \qquad (i \ge L) $$

가 항상 성립한다. 벌리캠프–매시(Berlekamp–Massey) 알고리즘 은 수열의 앞부분 항 몇 개만 보고, 이를 만족시키는 가장 짧은(최소 차수) 점화식 \((L, c_1, \dots, c_L)\) 을 복원한다.

원래는 부호 이론에서 선형 되먹임 시프트 레지스터(LFSR)의 최소 길이를 찾는 알고리즘이지만, 경쟁 프로그래밍에서는 다음 용도로 강력하다.

  • 어떤 DP/조합 수열의 앞 항 몇십 개를 직접 계산 → BM으로 점화식 자동 발견 → [[키타마사법]]으로 \(n\)번째 항을 \(O(k \log k \log n)\)에 계산.
  • 그래프의 경로 수, 행렬 거듭제곱 수열, 격자 타일링 개수 등 "선형 점화식일 것 같은" 수열의 닫힌 점화식 추측.

체(field) 위에서 동작

나눗셈이 필요하므로 유리수체 또는 소수 모듈러 체 \(\mathbb{F}_p\) 위에서 돌린다. 이 강의는 \(p = 998244353\) 을 가정한다.

핵심 아이디어: 오차와 점진적 갱신

BM은 수열을 왼쪽부터 한 항씩 훑으며 현재 후보 점화식 \(C\) 를 유지한다. \(C = (1, -c_1, -c_2, \dots, -c_L)\) 를 다항식으로 보면, 점화식이 성립한다는 것은

$$ \sum_{j=0}^{L} C_j \, a_{i-j} = 0 $$

이라는 뜻이다. \(i\)번째 항에서 이 합을 실제로 계산한 값을 불일치(discrepancy) \(d\) 라 한다.

  • \(d = 0\) 이면 현재 점화식이 \(a_i\) 를 정확히 예측한 것 → 그대로 둔다.
  • \(d \ne 0\) 이면 점화식을 고쳐야 한다. 이때 이전에 실패했던 시점의 후보 \(B\) 를 적절히 스케일·시프트하여 \(C\) 에 더해, \(a_i\) 에서의 불일치만 정확히 상쇄시킨다.

여기서 결정적 사실: 차수를 늘려야 하는 경우는 \(2L \le i\) 일 때뿐이다. 이 조건이 "최소 차수"를 보장하는 매시(Massey)의 정리이며, LFSR 합성의 하한과 정확히 맞물린다.

최소성의 직관

만약 길이 \(L\) 짜리 점화식으로 \(a_0 \dots a_{i-1}\) 까지 맞았는데 \(a_i\) 에서 틀렸다면, 다음 두 경우로 갈린다.

  • \(i \ge 2L\): 지금까지의 증거만으로는 더 짧은 점화식이 불가능함이 증명되므로, 새 차수를 \(L' = i + 1 - L\)늘린다.
  • \(i < 2L\): 차수는 그대로 두고 계수만 보정한다.

이 규칙 덕분에 BM이 내놓는 점화식은 주어진 항들과 모순되지 않는 점화식 중 최소 차수 임이 보장된다.

얼마나 많은 항이 필요한가

실제 점화식 차수가 \(L\) 이면, 이를 정확히 복원하려면 적어도 \(2L\) 개의 항 이 필요하다. 안전하게는 예상 차수의 2배보다 넉넉히(예: \(2L + 5\) 개 이상) 주는 것이 좋다. 항이 모자라면 우연히 더 짧은 "가짜 점화식"이 잡힐 수 있다.

작은 예시

피보나치 \(0,1,1,2,3,5,8,13\) 을 넣어 보자. 실제 점화식은 \(a_i = a_{i-1} + a_{i-2}\), 즉 \(L = 2\), \(c = (1, 1)\).

\(i\) \(a_i\) 현재 \(L\) 불일치 \(d\) 조치
0 0 0 0 유지
1 1 0 1 차수 확장 → \(L=2\)
2 1 2 0 유지
3 2 2 0 유지
4 3 2 0 유지

결과는 \(c = (1, 1)\) 로, \(a_i = a_{i-1} + a_{i-2}\) 를 정확히 복원한다.

복잡도

수열 길이 \(N\), 최종 차수 \(L\) 이면 각 항마다 \(O(L)\) 갱신이 일어나 전체 \(O(NL)\) 이다. 보통 \(N = O(L)\) 로 잡으므로 \(O(L^2)\). 여기서 얻은 차수 \(k = L\) 짜리 점화식으로 \(n\)번째 항을 구하는 전체 파이프라인은 [[키타마사법]]과 합쳐

$$ O(k^2 + k \log k \log n) $$

이 된다. 앞 항이 다항식이나 행렬식으로 \(O(k^2)\) 에 나오는 경우가 흔하기 때문이다.

Lesson 2강 · 구현: BM 코드와 n번째 항 선택 8m

표준 구현 (\(\mathbb{F}_p\), \(p = 998244353\))

아래는 경쟁 프로그래밍에서 널리 쓰이는 형태다. 반환값 C 는 점화식 계수로, 다음을 만족한다.

$$ a_i = C_0\,a_{i-1} + C_1\,a_{i-2} + \cdots + C_{L-1}\,a_{i-L} $$

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll MOD = 998244353;

ll power(ll a, ll b, ll m = MOD) {
    ll r = 1; a %= m; if (a < 0) a += m;
    for (; b; b >>= 1, a = a * a % m)
        if (b & 1) r = r * a % m;
    return r;
}
ll inv(ll a) { return power(a, MOD - 2); }

// 최소 선형 점화식 계수 C 반환:  a[i] = sum_j C[j]*a[i-1-j]
vector<ll> berlekampMassey(const vector<ll>& s) {
    int n = s.size(), L = 0, m = 1;
    vector<ll> C(n, 0), B(n, 0), T;
    C[0] = B[0] = 1;
    ll b = 1;
    for (int i = 0; i < n; i++) {
        // 현재 점화식이 s[i]에서 만드는 불일치 d 계산
        ll d = s[i] % MOD;
        for (int j = 1; j <= L; j++)
            d = (d + C[j] * s[i - j]) % MOD;
        if (d == 0) { m++; continue; }
        if (2 * L <= i) {          // 차수 확장 경로
            T = C;
            ll coef = d % MOD * inv(b) % MOD;
            for (int j = m; j < n; j++)
                C[j] = (C[j] - coef * B[j - m]) % MOD;
            L = i + 1 - L; B = T; b = d; m = 1;
        } else {                    // 차수 유지, 계수 보정
            ll coef = d % MOD * inv(b) % MOD;
            for (int j = m; j < n; j++)
                C[j] = (C[j] - coef * B[j - m]) % MOD;
            m++;
        }
    }
    // C = 1 - c1 x - c2 x^2 - ... 형태 → 점화식 계수 c_j 추출
    vector<ll> rec(L);
    for (int j = 1; j <= L; j++)
        rec[j - 1] = (MOD - C[j]) % MOD;
    return rec;
}

부호 규약을 반드시 확인

구현마다 C 의 부호 규약이 다르다. 위 코드는 내부적으로 특성 다항식 \(C(x) = 1 - c_1 x - \cdots - c_L x^L\) 을 유지하고, 마지막에 \(c_j = -C_j\) 로 뒤집어 양의 점화식 계수 를 돌려준다. 반환값을 그대로 \(a_i = \sum \text{rec}[j]\,a_{i-1-j}\) 에 대입하면 된다.

결과 검증 루틴

복원한 점화식이 정말 입력을 재현하는지 확인하는 것은 필수다. 항이 부족하면 가짜 점화식이 잡히기 때문이다.

bool verify(const vector<ll>& s, const vector<ll>& rec) {
    int L = rec.size();
    for (int i = L; i < (int)s.size(); i++) {
        ll v = 0;
        for (int j = 0; j < L; j++)
            v = (v + rec[j] * s[i - 1 - j]) % MOD;
        if (v % MOD != s[i] % MOD) return false;
    }
    return true;
}

\(n\)번째 항 구하기: BM + 키타마사

점화식을 얻었으면 [[키타마사법]]으로 \(a_n\)\(O(k \log k \log n)\) 에 계산한다. 핵심은 \(x^n \bmod f(x)\) 를 빠른 거듭제곱으로 구하는 것이다. 여기서 특성 다항식은

$$ f(x) = x^k - c_1 x^{k-1} - \cdots - c_k $$

이다. 곱셈은 NTT를 쓰면 \(O(k \log k)\), 나머지 연산까지 포함해 한 스텝이 \(O(k \log k)\), 총 \(O(k \log k \log n)\).

// rec: 점화식 계수, init: a[0..k-1], n: 원하는 인덱스
// (mul, polymod 는 NTT 기반 다항식 곱/나눗셈; 다항식 강의 참고)
ll kthTerm(const vector<ll>& rec, const vector<ll>& init, ll n) {
    int k = rec.size();
    if (n < (ll)init.size()) return init[n];
    vector<ll> f(k + 1);           // 특성 다항식 x^k - c1 x^{k-1} - ...
    f[k] = 1;
    for (int j = 0; j < k; j++) f[k - 1 - j] = (MOD - rec[j]) % MOD;
    vector<ll> x = {0, 1};         // x
    vector<ll> res = {1};          // x^n mod f
    for (ll e = n; e; e >>= 1) {
        if (e & 1) res = polymod(mul(res, x), f);
        x = polymod(mul(x, x), f);
    }
    ll ans = 0;                    // sum res[i]*a[i]
    for (int i = 0; i < (int)res.size(); i++)
        ans = (ans + res[i] * init[i]) % MOD;
    return ans;
}

작은 \(k\) 라면 다항식 곱을 굳이 NTT로 안 하고 \(O(k^2)\) 로 해도 되며, 그때는 \(O(k^2 \log n)\) 이다. \(k\) 가 수백 이상일 때만 NTT가 이득이다.

전형적 사용 흐름

  1. 문제의 답 수열 \(a_0, \dots, a_{2k+5}\)정직하게 직접 계산(브루트포스 DP).
  2. berlekampMassey 로 점화식 복원, verify 로 검증.
  3. kthTerm 으로 큰 \(n\) 에서의 답 계산.
Lesson 3강 · 심화·응용: 함정과 확장 선택 8m

흔한 함정

  • 항 개수 부족. 실제 차수 \(L\) 인데 \(2L\) 개 미만을 주면, 우연히 짧은 가짜 점화식이 잡힌다. 항상 넉넉히(예상 차수의 2배 + 여유분) 준비하고 verify 로 확인하라.
  • 모듈러 체가 아닌 곳에서 사용. BM은 나눗셈(역원)을 쓰므로 정수/실수에서 그대로 돌리면 오차·오버플로가 생긴다. 정수 수열이라도 소수 모듈러 위에서 돌려라.
  • 부호 규약 혼동. 반환 계수를 점화식에 대입할 때 부호가 반대이면 완전히 틀린 값이 나온다. 반드시 작은 예시로 손 검증.
  • 선형 점화식이 아닌 수열. 계승 \(n!\), 카탈란 수 \(C_n\) 처럼 계수가 \(n\) 에 의존하는(P-recursive) 수열은 상수계수 선형 점화식이 없다. BM은 이런 경우 차수를 계속 키우며 실패한다(차수가 항 개수의 절반 근처까지 커지면 신호).
  • 모듈러 충돌. 답 모듈러와 다른 소수로 돌리면 안 된다. 문제의 답이 \(\bmod\,998244353\) 이면 BM도 같은 모듈러로.

확장 1: P-recursive는 못 잡는다 — 판별

수열이 상수계수 선형 점화식을 가지는지 자체가 불확실하면, BM을 돌린 뒤 복원 차수가 입력 길이의 절반에 가까운지 보라. 그렇다면 점화식이 없거나 항이 부족한 것이다. 반대로 차수가 항 개수보다 훨씬 작고 나머지 항을 모두 재현하면 신뢰할 수 있다.

확장 2: 희소 행렬의 최소 다항식과 방정식 풀이 (Wiedemann)

행렬 \(M\) 이 매우 크지만 희소(sparse)할 때, 임의 벡터 \(u, v\) 에 대해 스칼라 수열

$$ s_i = u^\top M^i v $$

을 만들면 이 수열은 \(M\) 의 최소 다항식을 점화식으로 가진다. BM으로 최소 다항식을 복원하면 다음이 가능하다.

  • \(\det(M)\), 랭크, 특성 다항식 근사.
  • 선형계 \(Mx = b\) 를 곱셈 \(Mv\) 만으로 풀기(Wiedemann 알고리즘). 밀집 가우스 소거 \(O(n^3)\) 대신, 비영 원소 수 \(z\) 에 대해 \(O(nz)\) 규모로 동작한다.
// M 은 곱셈 Mv 만 제공하면 됨(희소). n x n.
// s[i] = u . (M^i v) 를 2n개 만든 뒤 BM → 최소다항식
vector<ll> minPoly(function<vector<ll>(const vector<ll>&)> mulM,
                   int n, const vector<ll>& u, vector<ll> v) {
    vector<ll> s(2 * n);
    for (int i = 0; i < 2 * n; i++) {
        ll dot = 0;
        for (int j = 0; j < n; j++) dot = (dot + u[j] * v[j]) % MOD;
        s[i] = dot;
        v = mulM(v);              // v <- M v
    }
    return berlekampMassey(s);    // 점화식 = 최소다항식(부호 규약 유의)
}

확장 3: 2차원·조합 수열 자동화

격자 위 경로 수, 특정 패턴 타일링 개수 등은 열/행 방향으로 선형 점화식을 가지는 경우가 많다. 한 방향을 고정해 1차원 수열로 만들고 BM을 돌리면 손으로 점화식을 유도하지 않고도 닫힌 형태를 얻을 수 있다. 단, 점화식의 존재는 이론적으로 보장되어야 한다(전이 행렬이 유한 차원이면 존재).

정리

항목 내용
입력 체 위 수열 앞부분 (\(\ge 2L\) 항)
출력 최소 차수 선형 점화식 계수
BM 복잡도 \(O(NL)\), 보통 \(O(L^2)\)
\(n\)번째 항 키타마사와 결합 \(O(k^2 + k\log k \log n)\)
실패 신호 복원 차수가 입력 길이의 절반 근처
대표 확장 Wiedemann(희소 선형계), 최소 다항식

BM은 "규칙이 있어 보이는" 수열을 만나면 먼저 몇십 항을 계산해 넣어 보는 실전 무기다. 점화식이 잡히고 verify 를 통과하면, 나머지는 [[키타마사법]]이 처리한다.

Practice problem 숨은 점화식의 N번째 항 선택 25m
R00633

숨은 점화식의 N번째 항

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Lesson 1강 · 뫼비우스 함수와 반전 공식 필수 8m

뫼비우스 반전이란?

약수 합으로 정의된 함수를 원래 함수로 되돌리는 도구입니다. 정수론에서
\(g(n)=\sum_{d\mid n} f(d)\) 처럼 "약수에 대한 합"으로 주어진 관계를 뒤집어 \(f\)
복원할 때 씁니다. 핵심 도구가 뫼비우스 함수 \(\mu(n)\) 입니다.


1. 뫼비우스 함수 \(\mu(n)\)

$$ \mu(n)= \begin{cases} 1 & n=1 \\ (-1)^k & n \text{ 이 서로 다른 } k \text{ 개의 소수의 곱 (제곱 인수 없음)} \\ 0 & n \text{ 이 어떤 소수의 제곱으로 나누어질 때} \end{cases} $$

\(n\)제곱 인수를 가지면(square-free 가 아니면) \(\mu(n)=0\), 아니면
소인수 개수의 홀짝으로 \(\pm1\) 입니다.

\(n\) 1 2 3 4 5 6 7 8 9 10 12
\(\mu(n)\) 1 -1 -1 0 -1 1 -1 0 0 1 0

\(\mu\)곱셈적 함수입니다: \(\gcd(a,b)=1\) 이면 \(\mu(ab)=\mu(a)\mu(b)\). 이 성질
덕분에 선형 체로 한 번에 계산할 수 있습니다(2강).


2. 가장 중요한 항등식

$$ \sum_{d\mid n}\mu(d)=[\,n=1\,]= \begin{cases}1 & n=1\\ 0 & n>1\end{cases} $$

증명 스케치. \(n=1\) 이면 자명. \(n>1\) 이고 \(n=p_1^{a_1}\cdots p_k^{a_k}\) 이면,
\(\mu(d)\neq0\) 인 약수 \(d\) 는 소수들의 부분집합뿐입니다. 크기 \(j\) 인 부분집합이
\(\binom{k}{j}\) 개이고 각각 \(\mu=(-1)^j\) 이므로

$$ \sum_{d\mid n}\mu(d)=\sum_{j=0}^{k}\binom{k}{j}(-1)^j=(1-1)^k=0. $$

이 항등식이 모든 뫼비우스 트릭의 뿌리입니다.


3. 뫼비우스 반전 공식

$$ g(n)=\sum_{d\mid n} f(d) \quad\Longleftrightarrow\quad f(n)=\sum_{d\mid n}\mu\!\left(\tfrac{n}{d}\right)g(d). $$

증명 스케치. 오른쪽에 \(g(d)=\sum_{e\mid d}f(e)\) 를 대입하면

$$ \sum_{d\mid n}\mu\!\left(\tfrac nd\right)\sum_{e\mid d}f(e) =\sum_{e\mid n} f(e)\sum_{e\mid d\mid n}\mu\!\left(\tfrac nd\right) =\sum_{e\mid n} f(e)\,[\,\tfrac ne=1\,]=f(n). $$

안쪽 합은 위 항등식에 의해 \(n/e=1\), 즉 \(e=n\) 일 때만 1 입니다.


4. 실전에서 가장 많이 쓰는 형태: 서로소 세기

\(1\le i,j\le n\)\(\gcd(i,j)=1\) 인 쌍의 개수는

$$ \sum_{i=1}^{n}\sum_{j=1}^{n}[\gcd(i,j)=1] =\sum_{i,j}\sum_{d\mid\gcd(i,j)}\mu(d) =\sum_{d=1}^{n}\mu(d)\left\lfloor\frac{n}{d}\right\rfloor^{2}. $$

\([\gcd=1]=\sum_{d\mid\gcd}\mu(d)\) 를 대입한 뒤 합의 순서를 바꾼 것입니다.
\(d\) 의 배수는 \(\lfloor n/d\rfloor\) 개이므로 \(i,j\) 각각에 대해 그만큼 나옵니다.

수치 예제 (\(n=6\)). \(\mu\) 값과 \(\lfloor 6/d\rfloor^2\):

\(d\) 1 2 3 4 5 6
\(\mu(d)\) 1 -1 -1 0 -1 1
\(\lfloor6/d\rfloor^2\) 36 9 4 2 1 1
36 -9 -4 0 -1 1

\(=36-9-4-0-1+1=23\). 실제로 \(1\!\le\! i,j\!\le\!6\) 에서 서로소 쌍은 23 개로
일치합니다.


5. 언제 쓰나 · 복잡도

  • \(\gcd\) 조건이 걸린 카운팅(서로소 쌍, \(\gcd=k\) 인 쌍, \(\text{lcm}\) 합 등).
  • 약수 합으로 정의된 수론적 함수의 역산.

\(\mu\)\(n\) 까지 선형 체로 전처리하면 \(O(n)\), 각 질의는 위 합 \(O(n)\) 입니다.
\(\lfloor n/d\rfloor\)\(O(\sqrt n)\) 개의 서로 다른 값만 가진다는 정수 나눗셈
블록(수론적 분할)
을 쓰면 질의를 \(O(\sqrt n)\) 으로 줄일 수 있습니다(2강).

Lesson 2강 · 선형 체 구현과 수론적 블록 선택 8m

1. 뫼비우스 함수의 선형 체

\(\mu\) 는 곱셈적이라 소수와 결합해 갱신할 수 있습니다. 선형 체는 각 합성수를
자신의 가장 작은 소인수(spf)로 정확히 한 번 지우므로 \(O(N)\) 입니다.

#include <bits/stdc++.h>
using namespace std;

const int N = 1000001;
int mu[N];
bool composite[N];
vector<int> primes;

void sieve() {
    mu[1] = 1;
    for (int i = 2; i < N; i++) {
        if (!composite[i]) { primes.push_back(i); mu[i] = -1; }
        for (int p : primes) {
            long long ip = (long long)i * p;
            if (ip >= N) break;
            composite[ip] = true;
            if (i % p == 0) { mu[ip] = 0; break; }   // p^2 | ip -> 제곱 인수
            else            mu[ip] = -mu[i];          // 소인수 하나 추가
        }
    }
}

핵심 분기: i % p == 0 이면 \(p\) 가 이미 \(i\) 의 인수라 \(ip\)\(p^2\) 을 포함 →
\(\mu=0\) 이고 즉시 break(이 소수가 \(ip\) 의 spf 임을 보장). 아니면 소인수가
하나 늘어 부호가 뒤집힙니다.

def mobius_sieve(n):
    mu = [0] * (n + 1)
    mu[1] = 1
    composite = [False] * (n + 1)
    primes = []
    for i in range(2, n + 1):
        if not composite[i]:
            primes.append(i); mu[i] = -1
        for p in primes:
            if i * p > n: break
            composite[i * p] = True
            if i % p == 0:
                mu[i * p] = 0; break
            else:
                mu[i * p] = -mu[i]
    return mu

2. 서로소 쌍 개수 (기본 \(O(n)\) 질의)

$$ \sum_{d=1}^{n}\mu(d)\left\lfloor\frac nd\right\rfloor^2 $$

long long coprime_pairs(int n) {
    long long ans = 0;
    for (int d = 1; d <= n; d++) {
        long long q = n / d;            // long long 로 승격해 제곱 오버플로 방지
        ans += (long long)mu[d] * q * q;
    }
    return ans;                          // n=6 -> 23
}

오버플로 주의. \(n\)\(10^6\) 이면 \(\lfloor n/d\rfloor^2\)\(10^{12}\) 급이라
int 곱은 넘칩니다. 반드시 long long 으로 계산하세요. 결과 자체도 약
\(\frac{6}{\pi^2}n^2\approx0.6\,n^2\) 로 커집니다.


3. 수론적 블록으로 \(O(\sqrt n)\) 가속

\(\lfloor n/d\rfloor\)\(d\) 가 커질수록 계단처럼 감소하며 서로 다른 값은
\(O(\sqrt n)\)
뿐입니다. 같은 값을 갖는 구간 \([l,r]\) 에서는 \(\mu\) 의 구간 합
(누적합 pre[])만 곱해 한 번에 처리합니다.

long long pre[N];   // pre[i] = mu[1]+...+mu[i]

long long coprime_fast(int n) {
    long long ans = 0;
    for (int l = 1, r; l <= n; l = r + 1) {
        long long q = n / l;
        r = n / q;                      // [l, r] 구간에서 floor(n/d) == q 로 동일
        long long msum = pre[r] - pre[l - 1];
        ans += msum * q * q;
    }
    return ans;
}

pre 는 체 이후 pre[i]=pre[i-1]+mu[i] 로 채웁니다. 여러 질의가 있을 때
특히 유용합니다.


4. 변형과 함정

  • \(\gcd(i,j)=k\) 인 쌍: \(i=k i',\,j=k j'\) 로 두면 \(\gcd(i',j')=1\), 즉
    위 식에서 \(n\to\lfloor n/k\rfloor\) 로 바꾼 값과 같습니다.
  • 일반 반전 \(f(n)=\sum_{d\mid n}\mu(n/d)g(d)\): 약수를 직접 순회.
    \(n\) 하나면 \(O(\sqrt n)\) 에 약수를 모아 계산합니다.
  • 부호·자료형: \(\mu\in\{-1,0,1\}\) 이라 누적합은 음수가 될 수 있으니
    long long pre[] 사용. 모듈러 문제라면 \(\mu\) 를 더할 때
    (x % MOD + MOD) % MOD 로 음수를 보정하세요.
  • 메모리: \(N=10^7\) 이면 int mu[] 40MB. 필요하면 signed char 로 압축.
Practice problem 서로소 격자점 세기 선택 25m
R00636

서로소 격자점 세기

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Lesson 1강 · 번사이드 보조정리와 폴리아 열거 필수 8m

대칭이 있는 경우의 수 세기

목걸이를 회전시켜 겹치는 배열을 같은 것으로 볼 때처럼, 대칭(군 작용)으로
동일시되는 배치의 개수
를 세는 것이 번사이드 보조정리입니다.


1. 군 작용과 궤도

유한군 \(G\) 가 집합 \(X\) 에 작용할 때, 서로 옮겨지는 원소들의 묶음을 궤도
(orbit)
라 합니다. "본질적으로 다른 배치의 수" = 궤도의 개수입니다.

  • \(\mathrm{Fix}(g)=\{x\in X: g\cdot x = x\}\) : \(g\) 가 그대로 두는 원소들.
  • \(\mathrm{Stab}(x)=\{g\in G: g\cdot x = x\}\) : \(x\) 를 고정하는 원소들.

궤도-안정자 정리: \(|G|=|\text{orbit}(x)|\cdot|\mathrm{Stab}(x)|\).


2. 번사이드 보조정리

$$ \#\text{orbits}=\frac{1}{|G|}\sum_{g\in G}\bigl|\mathrm{Fix}(g)\bigr| $$

증명 스케치.\(\{(g,x):g\cdot x=x\}\) 의 개수를 두 방향으로 셉니다.

$$ \sum_{g}|\mathrm{Fix}(g)|=\sum_{x}|\mathrm{Stab}(x)| =\sum_{x}\frac{|G|}{|\text{orbit}(x)|} =|G|\sum_{\text{orbits }O}\sum_{x\in O}\frac1{|O|}=|G|\cdot(\#\text{orbits}). $$

양변을 \(|G|\) 로 나누면 공식이 됩니다.


3. 폴리아 열거: 회전 목걸이

\(n\) 개 구슬, \(k\) 색, 회전군(\(n\) 개의 회전)으로 같은 것을 동일시한 목걸이
개수. 회전 \(d\) 칸을 하는 원소는 \(\gcd(n,d)\) 개의 순환(cycle)으로 분해되고, 각
순환은 한 색으로 칠해져야 고정되므로 \(\mathrm{Fix}=k^{\gcd(n,d)}\). 따라서

$$ \frac1n\sum_{d=0}^{n-1}k^{\gcd(n,d)} =\frac1n\sum_{e\mid n}\varphi\!\left(\frac ne\right)k^{e}. $$

(\(\gcd(n,d)=e\)\(d\) 의 개수가 \(\varphi(n/e)\) 개라는 사실로 묶었습니다. 이것이
폴리아의 순환 지수입니다.)

수치 예제 (\(n=4,\ k=2\)). 약수 \(e\in\{1,2,4\}\):

$$ \frac14\bigl(\varphi(4)2^1+\varphi(2)2^2+\varphi(1)2^4\bigr) =\frac14(2\cdot2+1\cdot4+1\cdot16)=\frac{24}{4}=6. $$

실제로 길이 4, 2색 목걸이(회전 동일시)는 6종입니다.


4. 반사까지: 이면군 \(D_n\)

팔찌처럼 뒤집기도 허용하면 군은 \(2n\) 개 원소의 이면군입니다. 반사의 고정
색칠 수는 \(n\) 의 홀짝에 따라 다릅니다.

$$ \text{반사 기여}= \begin{cases} n\cdot k^{(n+1)/2} & n \text{ 홀수}\\[2pt] \tfrac n2\,k^{n/2+1}+\tfrac n2\,k^{n/2} & n \text{ 짝수} \end{cases} $$

최종 개수는 (회전 기여 + 반사 기여) \(/\,(2n)\) 입니다.


5. 언제 쓰나 · 함정 미리보기

  • 회전/반사/색 교환 등 대칭 아래 서로 다른 배치를 셀 때.
  • 큰 답은 소수 \(p\) 로 나누는데, 공식에 \(1/|G|\) 나눗셈이 있습니다. 모듈러
    산술에서 나눗셈은 모듈러 역원(페르마 소정리)으로 처리해야 합니다 —
    단순히 정수 나눗셈하면 틀립니다. 구현은 2강에서 다룹니다.
Lesson 2강 · 구현: 모듈러 역원과 목걸이 개수 선택 8m

1. 필요한 부품: 빠른 거듭제곱, 역원, 오일러 파이

목걸이 공식 \(\frac1n\sum_{e\mid n}\varphi(n/e)k^{e}\) 를 소수 \(p\) 로 나눈 나머지로
계산하려면 \(1/n\)모듈러 역원으로 바꿔야 합니다.

$$ n^{-1}\equiv n^{p-2}\pmod p\quad(\text{페르마 소정리, } p \text{ 소수},\ p\nmid n) $$

#include <bits/stdc++.h>
using namespace std;
const long long MOD = 1000000007;

long long power(long long a, long long b, long long m) {
    a %= m; if (a < 0) a += m;           // 음수 밑 보정
    long long r = 1;
    while (b > 0) {
        if (b & 1) r = r * a % m;         // a<m 이므로 곱 < m^2 < 2^63
        a = a * a % m;
        b >>= 1;
    }
    return r;
}
long long inv(long long a, long long m) { return power(a, m - 2, m); }

long long phi(long long n) {              // 오일러 파이, O(sqrt n)
    long long res = n;
    for (long long p = 2; p * p <= n; p++)
        if (n % p == 0) {
            while (n % p == 0) n /= p;
            res -= res / p;
        }
    if (n > 1) res -= res / n;
    return res;
}

2. 회전 목걸이 개수 (mod \(p\))

long long necklace(long long n, long long k) {
    long long sum = 0;
    for (long long e = 1; e * e <= n; e++) {   // 약수 쌍 (e, n/e)
        if (n % e == 0) {
            sum = (sum + phi(n / e) % MOD * power(k, e, MOD)) % MOD;
            long long e2 = n / e;
            if (e2 != e)
                sum = (sum + phi(n / e2) % MOD * power(k, e2, MOD)) % MOD;
        }
    }
    return sum % MOD * inv(n % MOD, MOD) % MOD;  // 1/n 은 모듈러 역원!
}

핵심 함정. 마지막 * inv(n, MOD)/ n 으로 쓰면 틀립니다. 합이
\(n\) 으로 나누어떨어진다는 보장은 정수 세계 이야기이고, 모듈러에서는 역원을
곱해야 합니다.

def power(a, b, m):
    return pow(a % m, b, m)              # 파이썬 pow 는 음수/큰 수 안전

def phi(n):
    res, p = n, 2
    while p * p <= n:
        if n % p == 0:
            while n % p == 0: n //= p
            res -= res // p
        p += 1
    if n > 1: res -= res // n
    return res

def necklace(n, k, MOD=10**9 + 7):
    s = 0
    e = 1
    while e * e <= n:
        if n % e == 0:
            for d in ({e, n // e}):
                s = (s + phi(n // d) * power(k, d, MOD)) % MOD
        e += 1
    return s * pow(n, MOD - 2, MOD) % MOD

검증. necklace(4, 2)\(6\), necklace(3, 3)\(\frac13(9+3+3)?\) 실제로
\(\frac13(\varphi(1)3^3+\varphi(3)3^1)=\frac13(27+6)=11\) 을 반환합니다(회전 아래
길이 3, 3색 목걸이 11종).


3. 반사 포함(팔찌) 확장

long long bracelet(long long n, long long k) {
    long long rot = 0;
    for (long long e = 1; e <= n; e++)
        if (n % e == 0) rot = (rot + phi(n / e) % MOD * power(k, e, MOD)) % MOD;
    long long refl;
    if (n & 1) refl = n % MOD * power(k, (n + 1) / 2, MOD) % MOD;
    else       refl = (n / 2 % MOD * power(k, n / 2 + 1, MOD)
                       + n / 2 % MOD * power(k, n / 2, MOD)) % MOD;
    long long total = (rot + refl) % MOD;
    return total * inv(2 * n % MOD, MOD) % MOD;   // |G| = 2n
}

4. 변형과 함정 정리

  • 일반 번사이드: 대칭군 \(G\) 를 명시적으로 나열할 수 있으면, 각 \(g\)
    순환 개수 \(c(g)\) 를 세어 \(\sum k^{c(g)}\) 를 계산 후 \(|G|\) 의 역원 곱.
  • 오버플로: 모든 곱은 long long 에서 % MOD. k 가 크면 power
    안에서 a %= m 로 먼저 줄입니다.
  • \(p\mid |G|\) 인 경우: 페르마 역원이 성립하지 않습니다. 이때는 답을
    정수(또는 큰 수)로 계산하거나 CRT/다른 법을 써야 합니다. 대회에서는 보통
    \(p\)\(|G|\) 와 서로소가 되도록 주어집니다.
  • 색 대칭까지(구슬 색을 서로 바꾸는 군): 순환 지수에 색군까지 대입하는
    완전한 폴리아 정리로 확장됩니다.
Practice problem 회전과 뒤집기로 같은 팔찌 선택 25m
R00637

회전과 뒤집기로 같은 팔찌

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Lesson 이론 — 형식적 멱급수와 뉴턴 반복 필수 8m

형식적 멱급수

\(\bmod\,998244353\) 위에서 다항식/멱급수 \(A(x)=a_0+a_1x+\cdots\) 를 다룬다. "\(\bmod x^n\)" 은 차수 \(n\) 이상 항을 버린다는 뜻으로, 우리는 항상 어떤 정밀도 \(n\) 까지의 계수만 관리한다. 곱셈은 NTT multiply\(O(n\log n)\) 이며, 이 위에서 역원·나눗셈·나머지·제곱근 을 모두 \(O(n\log n)\) 에 계산할 수 있다. 핵심 도구는 뉴턴 반복(Newton iteration) 이다.

뉴턴 반복의 원리

실수에서 \(f(b)=0\) 의 근을 \(b_{k+1}=b_k-\dfrac{f(b_k)}{f'(b_k)}\) 로 배로 정확해지게 찾듯, 멱급수에서도 같은 반복이 정밀도를 매 단계 두 배로 늘린다. \(b_k\)\(\bmod x^{k}\) 에서 정답이면, 한 번의 반복으로 \(\bmod x^{2k}\) 에서 정답이 된다. 따라서 정밀도 \(1 \to 2 \to 4 \to \cdots \to n\) 로 배가되고, 각 단계 비용이 그 단계 길이에 비례하는 \(O(k\log k)\) 이므로 총합은 등비급수로 \(O(n\log n)\).

역원 (multiplicative inverse)

\(A(x)\) 의 역 \(B(x)\)\(A\cdot B \equiv 1 \pmod{x^n}\) 를 만족한다. 존재 조건은 \(a_0\neq 0\). \(f(B)=B^{-1}-A\) 에 뉴턴을 적용하면

$$ B_{2k} \equiv B_k\,(2 - A\,B_k) \pmod{x^{2k}} $$

초깃값은 \(B_1 = a_0^{-1}\). 전개형 \(B_{2k}=2B_k - A\ B_k^2\) 로도 쓴다.

작은 예제

\(A = 1 - x\). 그러면 \(B_1=1\), \(B_2 = 1\cdot(2-(1-x)\cdot1) = 1+x \pmod{x^2}\), \(B_4 = 1+x+x^2+x^3\). 실제로 \((1-x)^{-1}=1+x+x^2+\cdots\) 와 일치한다.

나눗셈과 나머지

차수 \(n-1\)\(A\) 를 차수 \(m-1\)\(B\) 로 나눠 \(A = B\,Q + R\), \(\deg R < m-1\) 을 구한다. 역순 다항식 \(A^R(x)=x^{n-1}A(1/x)\) 를 정의하면

$$ Q^R \equiv A^R\,(B^R)^{-1} \pmod{x^{\,n-m+1}} $$

가 성립한다(\(Q\) 의 차수는 \(n-m\) 이므로 이 정밀도면 충분). \(Q\) 를 역순으로 되돌린 뒤 \(R = A - B\,Q\) 로 나머지를 얻는다. 전체 \(O(n\log n)\).

왜 성립하나

\(A=BQ+R\)\(x\to 1/x\) 를 대입하고 \(x^{n-1}\) 을 곱하면 \(A^R = B^R\ Q^R + x^{n-m+1} R^R\) 가 되어, \(\bmod x^{n-m+1}\) 에서 \(R\) 항이 사라진다. 남은 식을 \(B^R\) 의 역으로 풀면 위 공식이다. \(B^R(0)=b_{m-1}\neq 0\) 이므로 역원이 존재한다.

제곱근 (선택)

\(B^2\equiv A \pmod{x^n}\)\(a_0\) 가 제곱잉여일 때 존재하며 뉴턴으로

$$ B_{2k} \equiv \tfrac12\left(B_k + A\,B_k^{-1}\right) \pmod{x^{2k}} $$

\(a_0=1\) 이면 \(B_0=1\) 로 시작하면 편하다.

언제 쓰나 / 복잡도

생성함수를 닫힌 형태로 조작할 때, 점화식의 생성함수 역수, 카탈란·분할수 등 조합 수열의 대량 계산에 쓴다. 모든 기본 연산이 \(O(n\log n)\):

연산 복잡도
곱셈 \(O(n\log n)\)
역원 \(O(n\log n)\)
나눗셈·나머지 \(O(n\log n)\)
제곱근 \(O(n\log n)\)
Lesson 구현 — 역원·나눗셈·나머지 선택 8m

전제

1강의 multiply(a, b)(NTT, \(998244353\), \(g=3\))와 power 가 있다고 가정한다. 그 위에 뉴턴 반복으로 역원과 나눗셈을 얹는다.

다항식 역원

\(B \equiv B(2 - AB)\) 를 정밀도를 배로 늘리며 적용한다. 각 단계에서 곱셈 크기를 \(x^{2k}\) 로 잘라 앨리어싱을 피한다.

// A[0] != 0 필요. A*B ≡ 1 (mod x^n) 인 B 반환
vector<ll> poly_inv(vector<ll> A, int n) {
    vector<ll> B = { power(A[0] % MOD, MOD - 2, MOD) };
    for (int k = 1; k < n; k <<= 1) {
        int m = k << 1;
        vector<ll> a2(A.begin(), A.begin() + min((int)A.size(), m));
        vector<ll> t = multiply(a2, B);          // t = A*B (정밀도 m 이상)
        t.resize(m);
        for (auto& x : t) x = (MOD - x) % MOD;    // t = -A*B
        t[0] = (t[0] + 2) % MOD;                  // t = 2 - A*B
        B = multiply(B, t);
        B.resize(m);                              // mod x^m
    }
    B.resize(n);
    return B;
}

왜 매 단계 두 번 곱하나: A*B\(\bmod x^{2k}\) 로 자른 뒤 다시 B*(2-AB) 를 곱한다. 이렇게 하면 두 곱 모두 결과 차수가 \(2k\) 미만이라 크기 \(2k\) NTT의 순환 앨리어싱이 저차 항을 오염시키지 않는다. (한 번의 곱 A*B^2 로 합치려면 크기를 \(\ge 3k\) 로 키워야 하므로 두 번 곱이 더 단순·안전하다.)

나눗셈과 나머지

// A = B*Q + R, deg R < deg B. {Q, R} 반환
pair<vector<ll>, vector<ll>> poly_divmod(vector<ll> A, vector<ll> B) {
    // 선행 0 제거
    while (B.size() > 1 && B.back() == 0) B.pop_back();
    int n = A.size(), m = B.size();
    if (n < m) return { {0}, A };
    vector<ll> Ar(A.rbegin(), A.rend());
    vector<ll> Br(B.rbegin(), B.rend());
    Br.resize(n - m + 1);
    vector<ll> Qr = multiply(Ar, poly_inv(Br, n - m + 1));
    Qr.resize(n - m + 1);
    vector<ll> Q(Qr.rbegin(), Qr.rend());        // 다시 역순
    vector<ll> BQ = multiply(B, Q);
    vector<ll> R(m - 1);
    for (int i = 0; i < m - 1; i++) {
        ll v = A[i] - (i < (int)BQ.size() ? BQ[i] : 0);
        R[i] = ((v % MOD) + MOD) % MOD;
    }
    while (R.size() > 1 && R.back() == 0) R.pop_back();
    return { Q, R };
}

검증

int main(){
    // (x^2 - 1) = (x - 1)(x + 1) + 0
    vector<ll> A = {MOD-1, 0, 1};   // -1 + x^2
    vector<ll> B = {MOD-1, 1};      // -1 + x
    auto pr = poly_divmod(A, B);
    for (ll q : pr.first)  printf("%lld ", q); // Q: 1 1  => 1 + x
    puts("");
    for (ll r : pr.second) printf("%lld ", r); // R: 0
}

흔한 실수

  • poly_inv 를 홀수 길이 정밀도로 호출. 내부에서 \(k\) 를 배로 늘려 처리하지만, 최종 resize(n) 은 필수.
  • 선행 0 미제거. \(B\) 의 최고차 계수가 0이면 \(B^R(0)=0\) 이 되어 역원이 없다.
  • 나머지 차수. \(\deg R < \deg B = m-1\) 이므로 \(R\) 은 최대 \(m-1\) 개 계수.
Lesson 심화·응용 — 제곱근·평행이동·함정 선택 8m

다항식 제곱근

\(a_0=1\) 인 경우가 흔하다(카탈란류 생성함수). 뉴턴 반복

$$ B_{2k} \equiv \tfrac12\big(B_k + A\,B_k^{-1}\big) \pmod{x^{2k}} $$

를 쓴다. \(\tfrac12\)\(2^{-1}\bmod p\) 곱이다.

// A[0] == 1 가정. B^2 ≡ A (mod x^n)
vector<ll> poly_sqrt(vector<ll> A, int n) {
    vector<ll> B = {1};
    ll inv2 = power(2, MOD - 2, MOD);
    for (int k = 1; k < n; k <<= 1) {
        int m = k << 1;
        vector<ll> a2(A.begin(), A.begin() + min((int)A.size(), m));
        vector<ll> t = multiply(a2, poly_inv(B, m)); // A * B^{-1}
        t.resize(m);
        B.resize(m);
        for (int i = 0; i < m; i++)
            B[i] = (B[i] + t[i]) % MOD * inv2 % MOD;
    }
    B.resize(n);
    return B;
}

\(a_0\neq 1\) 이면 \(a_0\) 의 모듈러 제곱근(Tonelli–Shanks)을 먼저 구해 정규화한다. 제곱잉여가 아니면 근이 없다.

테일러 평행이동 \(A(x+c)\)

\(A(x+c)=\sum_i a_i (x+c)^i\) 는 이항 전개를 뒤집으면 한 번의 합성곱으로 계산된다. \(b_j = \sum_{i\ge j}\binom{i}{j} a_i c^{\,i-j}\)\(\binom{i}{j}=\frac{i!}{j!(i-j)!}\) 로 분해하면 두 수열의 합성곱 꼴이 되어 \(O(n\log n)\). 다항식을 특정 점 근처에서 재전개할 때 유용하다.

여러 연산의 조립

  • 역수 점화식: 수열의 생성함수가 \(F=\frac{P}{Q}\) 꼴이면 \(F \equiv P\cdot Q^{-1}\) 로 앞 \(n\) 항을 한 번에 뽑는다.
  • 나눗셈 활용: 선형 점화식의 특성다항식 \(\bmod\)\(x^n\) 을 줄이는 것이 키타마사법(별도 단원)이며, 그 핵심 연산이 여기의 나눗셈·나머지다.

함정 총정리

  • 정밀도 앨리어싱. 뉴턴 반복에서 곱을 반드시 \(\bmod x^{2k}\) 로 잘라라. 안 자르면 순환 합성곱이 저차 항을 오염시킨다.
  • 역원 존재 조건. \(a_0\neq 0\). 제곱근은 \(a_0\) 가 제곱잉여. 로그는 \(a_0=1\), 지수는 \(a_0=0\)(다음 단원).
  • inv2, 팩토리얼 전처리. 제곱근·평행이동은 모듈러 역원·팩토리얼을 자주 쓰므로 미리 전처리하면 상수를 아낀다.
  • 작은 \(n\) 특례. \(n=1\) 이면 반복이 한 번도 안 돌 수 있으니 초깃값이 곧 답이 되게 해 둔다.

복잡도

모든 연산이 \(O(n\log n)\). 상수는 역원 > 나눗셈 > 곱셈 순으로 커지므로, 반복 안에서 poly_inv 를 매번 새로 부르지 말고 필요한 최소 정밀도로만 호출하는 것이 실전 최적화다.

Practice problem 형식적 멱급수의 역원 선택 25m
R00634

형식적 멱급수의 역원

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem 한 줄로 늘어선 표본점 선택 25m
R00683

한 줄로 늘어선 표본점

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
08
Level 9 · Master

Master

수학과 정수론 · Master 단계

0/31 완료
Lesson 1강 · 생성 함수의 개념 필수 8m

생성 함수란?

수열 \(a_0,a_1,a_2,\dots\) 를 다항식(무한 급수)의 계수로 실어 하나의 대수적
객체로 다루는 도구입니다. 가장 흔한 것이 보통 생성 함수(OGF):

$$ A(x)=\sum_{n\ge0} a_n x^n = a_0 + a_1 x + a_2 x^2 + \cdots $$

"수를 세는 문제"를 "다항식 곱/전개"로 바꿔 규칙적으로 풀 수 있습니다.
여기서 \(x\) 는 대입할 값이 아니라 자리표시자(형식적 변수) 일 뿐입니다.


1. 알아둘 닫힌 형

$$ \frac{1}{1-x}=\sum_{n\ge0}x^n,\qquad \frac{1}{1-x^{c}}=\sum_{n\ge0}x^{cn} $$

$$ \frac{1}{(1-x)^k}=\sum_{n\ge0}\binom{n+k-1}{k-1}x^n $$

$$ (1+x)^m=\sum_{n=0}^{m}\binom{m}{n}x^n $$

예를 들어 \(\frac{1}{1-x^{3}}=1+x^3+x^6+\cdots\) 는 "3의 배수만큼 쓸 수 있다"를
뜻합니다.


2. 곱셈 = 합성곱(convolution)

두 생성 함수를 곱하면 계수가 합성곱으로 결합됩니다.

$$ A(x)B(x)=\sum_{n\ge0}\left(\sum_{i=0}^{n}a_i\,b_{n-i}\right)x^n $$

"\(A\) 에서 하나, \(B\) 에서 하나를 골라 지수 합이 \(n\)" 이 되는 모든 방법의 수가
곱의 \(x^n\) 계수입니다. 카운팅에서 독립적인 선택의 결합이 곱으로 나타나는
이유입니다.


3. 대표 응용: 동전 거스름(무한 개수)

동전 종류 \(c_1,\dots,c_m\) 으로 금액 \(n\) 을 만드는 방법의 수(순서 무시)는

$$ \prod_{i=1}^{m}\frac{1}{1-x^{c_i}} $$

\(x^n\) 계수입니다. 각 인수 \(\frac{1}{1-x^{c_i}}=1+x^{c_i}+x^{2c_i}+\cdots\)
"\(c_i\) 짜리를 0개, 1개, 2개, … 사용" 을 의미하고, 곱이 그 선택들을 결합합니다.

수치 예제. 동전 \(\{1,2\}\)\(n=4\) 만들기:

$$ \frac1{1-x}\cdot\frac1{1-x^2}=(1+x+x^2+\cdots)(1+x^2+x^4+\cdots). $$

\(x^4\) 계수를 직접 세면 \((1\!\times\!x^4),(x^2\!\times\!x^2),(x^4\!\times\!1)\)
→ 즉 2를 0/½개 쓰는 3가지: \(4{=}1{\cdot}4,\;2{+}1{\cdot}2,\;2{+}2\). 계수는 3.


4. 지수 생성 함수(EGF) 한 줄 요약

순서가 있는(라벨된) 구조를 셀 때는 지수 생성 함수

$$ A(x)=\sum_{n\ge0}a_n\frac{x^n}{n!} $$

를 쓰며, 곱이 \(c_n=\sum_i\binom{n}{i}a_i b_{n-i}\)(이항 합성곱)로 나옵니다.
"순열·분할처럼 라벨을 섞는" 문제에서 유용합니다.


5. 언제 쓰나 · 복잡도

  • 분할·거스름·조합 카운팅에서 점화식을 세우기 어려울 때 곱으로 구조화.
  • 계수를 \(N\) 까지 구하는 다항식 곱은 소박하게 \(O(N^2)\), FFT/NTT 로 \(O(N\log N)\).
    구현은 2강에서 \(O(N^2)\) 정확 버전을 다룹니다(큰 \(N\) 은 NTT 필요).
Lesson 2강 · 다항식 곱 구현과 카운팅 선택 8m

1. 다항식 곱 (\(O(N^2)\), 모듈러)

계수를 \(N\) 차까지만 유지하며 합성곱을 직접 계산합니다. 답이 크면 보통
\(MOD=998244353\)(NTT 친화 소수) 또는 \(10^9+7\) 로 나눕니다.

#include <bits/stdc++.h>
using namespace std;
const long long MOD = 998244353;

// a, b 를 곱해 x^N 차까지 반환
vector<long long> mul(const vector<long long>& a,
                      const vector<long long>& b, int N) {
    vector<long long> c(N + 1, 0);
    for (int i = 0; i <= N && i < (int)a.size(); i++) {
        if (!a[i]) continue;
        for (int j = 0; j + i <= N && j < (int)b.size(); j++)
            c[i + j] = (c[i + j] + a[i] * b[j]) % MOD;  // a[i]*b[j] < MOD^2 < 2^63
    }
    return c;
}

오버플로 주의. \(a_i,b_j 이므로 곱은 \(<2^{60}\), long long
에 안전합니다. 하지만 int 로 받으면 즉시 넘칩니다 — 계수 배열은 항상
long long.


2. 동전 거스름 방법 수 (무한 개수)

\(\prod \frac{1}{1-x^{c_i}}\) 의 계수는 곱을 다 만들 필요 없이, 동전마다
"밀어 더하기"로 \(O(m\cdot N)\) 에 구합니다. 이는 생성 함수 곱의 실전 형태입니다.

vector<long long> coin_ways(vector<int> coins, int N) {
    vector<long long> dp(N + 1, 0);
    dp[0] = 1;
    for (int c : coins)
        for (int v = c; v <= N; v++)        // 1/(1-x^c) 를 곱하는 것과 동치
            dp[v] = (dp[v] + dp[v - c]) % MOD;
    return dp;                               // coins={1,2}, N=4 -> dp[4]=3
}
def coin_ways(coins, N, MOD=998244353):
    dp = [0] * (N + 1); dp[0] = 1
    for c in coins:
        for v in range(c, N + 1):
            dp[v] = (dp[v] + dp[v - c]) % MOD
    return dp                                 # coins=[1,2], N=4 -> dp[4]=3

개수 제한이 있으면(각 동전 최대 \(k\) 개) 인수는 \(\frac{1-x^{c(k+1)}}{1-x^c}\)
가 되며, 유한 등비 합으로 계수를 조정하거나 이진 그룹핑을 씁니다.


3. 파이썬 일반 다항식 곱

def poly_mul(a, b, N, MOD=998244353):
    c = [0] * (N + 1)
    for i, ai in enumerate(a):
        if i > N or ai == 0: continue
        for j, bj in enumerate(b):
            if i + j > N: break
            c[i + j] = (c[i + j] + ai * bj) % MOD
    return c

수치 검증. poly_mul([1,1,1,1,1], [1,0,1,0,1], 4) = \(x^4\) 까지의
\((1+x+x^2+x^3+x^4)(1+x^2+x^4)\) 계수 → [1, 1, 2, 2, 3]. \(x^4\) 계수 3 은
동전 \(\{1,2\}\), \(n=4\) 결과와 일치합니다.


4. 변형과 함정

  • \(N\): \(O(N^2)\)\(N\gtrsim 10^5\) 에서 느립니다. NTT(\(MOD=998244353\))
    또는 FFT 로 \(O(N\log N)\). 개념은 동일(계수 = 합성곱)이니 라이브러리 교체만.
  • 역원·나눗셈: \(\frac{1}{A(x)}\)\(a_0\neq0\) 일 때 존재하며 계수 점화식
    또는 뉴턴 반복으로 구합니다. 모듈러에서는 \(a_0\)모듈러 역원 필요.
  • 음수 계수: 포함-배제로 부호가 섞이면 (x % MOD + MOD) % MOD 로 보정.
  • EGF: 계수를 \(a_n/n!\) 로 보관 — 팩토리얼과 그 모듈러 역원을 전처리.
Practice problem 제한 있는 동전 채우기 선택 25m
R00651

제한 있는 동전 채우기

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem 연결된 라벨 그래프의 셈 선택 25m
R00684

연결된 라벨 그래프의 셈

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Lesson 1강 · 이론: 독립성 공리와 그리디 필수 8m

왜 매트로이드인가

"가장 무거운 것부터 욕심껏 골라라"는 그리디 는 어떤 문제에선 최적이고 어떤 문제에선 틀린다. 매트로이드(matroid)는 그리디가 최적이 되는 조합 구조의 정확한 조건 을 포착하는 추상화다. 최소 신장 트리의 크루스칼 알고리즘이 왜 옳은지, 스케줄링·색칠·매칭의 여러 그리디가 왜 통하는지를 하나의 정리로 설명한다.

정의

매트로이드 는 유한 바탕집합(ground set) \(E\) 와 그 부분집합들의 모임 \(\mathcal{I} \subseteq 2^E\) (독립집합(independent sets))의 쌍 \(M = (E, \mathcal{I})\) 로, 다음 세 공리를 만족한다.

  • (I1) 비어있지 않음: \(\emptyset \in \mathcal{I}\).
  • (I2) 유전성(hereditary): \(A \subseteq B\) 이고 \(B \in \mathcal{I}\) 이면 \(A \in \mathcal{I}\). (독립집합의 부분집합은 독립)
  • (I3) 교환·증대성(exchange): \(A, B \in \mathcal{I}\) 이고 \(|A| < |B|\) 이면, 어떤 \(x \in B \setminus A\) 가 존재해 \(A \cup \{x\} \in \mathcal{I}\).

(I3)이 매트로이드의 심장이다. "더 큰 독립집합에서 원소 하나를 빌려와 작은 독립집합을 키울 수 있다"는 성질로, 벡터공간의 선형 독립성이 가진 교환 보조정리를 그대로 추상화한 것이다.

랭크, 기저, 서킷

  • 기저(base): 극대 독립집합. 모든 기저는 크기가 같다 (I3의 결과). 이 공통 크기가 매트로이드의 랭크 \(r(M)\).
  • 랭크 함수: \(r(S) = \max\{|A| : A \subseteq S,\ A \in \mathcal{I}\}\). 부분모듈러(submodular)하다: \(r(S)+r(T) \ge r(S\cup T)+r(S\cap T)\).
  • 서킷(circuit): 극소 종속집합(자기 자신은 종속이지만 진부분집합은 모두 독립). 독립집합 \(A\) 에 원소 \(x\) 를 더해 종속이 되면, \(A+x\) 안에는 \(x\) 를 포함하는 유일한 서킷이 생긴다 — 매트로이드 교집합의 교환 그래프를 세우는 근거다.

대표적인 매트로이드

이름 바탕집합 \(E\) 독립 조건
그래프 매트로이드 그래프의 간선 사이클이 없다(숲)
벡터/선형 매트로이드 벡터(행렬의 열) 선형 독립
유니폼 \(U_{k,n}\) \(n\)개 원소 크기 \(\le k\)
분할/색채(partition) 매트로이드 색이 칠해진 원소 \(i\) 원소를 \(c_i\)개 이하
횡단(transversal) 매트로이드 이분그래프 한쪽 정점 상대편으로 매칭 가능한 집합

특히 그래프 매트로이드 에서 기저는 신장 숲(연결이면 신장 트리), 랭크는 정점 수 − 연결요소 수다. 색채 매트로이드 는 "각 그룹에서 정해진 개수만" 뽑는 제약을 그대로 모델링한다.

매트로이드 위의 그리디 = Rado–Edmonds 정리

각 원소에 가중치 \(w: E \to \mathbb{R}\) 가 있을 때, 최대 가중치 기저(또는 최대 가중치 독립집합) 를 구하는 그리디는 다음과 같다.

가중치 내림차순으로 원소를 훑으며, 현재 집합에 더해도 여전히 독립이면 채택한다.

Rado–Edmonds 정리: 이 그리디가 모든 가중치에 대해 최적 인 것은 \((E, \mathcal{I})\)매트로이드일 때, 그리고 그때뿐이다. 즉 매트로이드는 그리디 최적성과 정확히 동치인 구조다.

크루스칼의 MST가 이 정리의 특수 경우다: 그래프 매트로이드에서 가중치를 음수로 뒤집으면 "최소 가중치 기저 = MST"가 된다.

최적성의 직관 (교환 논증)

그리디 해 \(G\) 와 임의의 최적 독립집합 \(O\) 가 있고 \(|G| < |O|\) 라면, (I3)에 의해 \(O\) 에서 원소 \(x\) 를 빌려와 \(G+x\) 를 독립으로 키울 수 있다. 그리디가 \(x\) 를 지나쳤다면 그 시점에서 종속이었어야 하는데, 유전성과 교환성을 함께 쓰면 그런 일이 최적해와 모순됨을 보일 수 있다. 결국 그리디가 매 단계에서 "그 시점까지의 부분해 중 최선"을 유지한다.

작은 예시: 색채 매트로이드

원소 5개, 색과 가중치가 아래와 같고 "각 색 최대 1개"라 하자.

원소 가중치
a 빨강 9
b 빨강 7
c 파랑 6
d 파랑 5
e 초록 3

가중치 내림차순 \(a(9), b(7), c(6), d(5), e(3)\). 그리디: \(a\) 채택 → \(b\) 는 빨강 중복이라 기각 → \(c\) 채택 → \(d\) 는 파랑 중복 기각 → \(e\) 채택. 결과 \(\{a, c, e\}\), 가중치 \(9+6+3 = 18\). 각 색에서 최댓값을 고른 것과 일치하며 최적이다.

복잡도

바탕집합 크기 \(n\), 독립성 판정(오라클) 1회 비용 \(Q\) 라 하면, 정렬 \(O(n \log n)\) 과 오라클 \(n\) 회로 그리디는

$$ O(n \log n + n \cdot Q) $$

에 동작한다. 그래프 매트로이드라면 유니온-파인드로 \(Q = O(\alpha(n))\).

Lesson 2강 · 구현: 오라클과 그리디 선택 8m

독립성 오라클 인터페이스

매트로이드 알고리즘은 구조 자체보다 "이 집합이 독립인가?" 를 답하는 오라클 로 추상화하면 재사용성이 높다. 두 가지 스타일이 있다.

  • 상태 없는(stateless) 오라클: 부분집합 전체를 받아 독립 여부 반환. 느리지만 일반적.
  • 증분(incremental) 오라클: 현재 독립집합에 원소 하나를 더해도 독립인지 판정. 그리디에 적합하고 빠르다.

그래프 매트로이드 — 유니온-파인드 증분 오라클

#include <bits/stdc++.h>
using namespace std;

struct DSU {
    vector<int> p, r;
    DSU(int n) : p(n), r(n, 0) { iota(p.begin(), p.end(), 0); }
    int find(int x) { return p[x] == x ? x : p[x] = find(p[x]); }
    bool unite(int a, int b) {          // 합쳐지면 true(=독립 유지)
        a = find(a); b = find(b);
        if (a == b) return false;       // 사이클 → 종속
        if (r[a] < r[b]) swap(a, b);
        p[b] = a; if (r[a] == r[b]) r[a]++;
        return true;
    }
};

// 간선 (u,v,w) 들에서 최대 가중치 신장 숲(그래프 매트로이드 기저)
struct Edge { int u, v; long long w; };
long long maxWeightForest(int n, vector<Edge> es) {
    sort(es.begin(), es.end(),
         [](const Edge& a, const Edge& b){ return a.w > b.w; });
    DSU dsu(n);
    long long total = 0;
    for (auto& e : es)
        if (dsu.unite(e.u, e.v)) total += e.w;   // 독립이면 채택
    return total;
}

가중치를 음수로 뒤집고 오름차순으로 정렬하면 그대로 크루스칼 MST 다. 매트로이드 그리디와 MST가 같은 알고리즘임을 코드로 확인할 수 있다.

색채(분할) 매트로이드 오라클

각 원소가 색 col[i] 를 가지고, 색 \(g\)cap[g] 개까지 허용한다.

struct ColorMatroid {
    vector<int> col, cap, used;
    ColorMatroid(vector<int> col, vector<int> cap)
        : col(col), cap(cap), used(cap.size(), 0) {}
    bool add(int i) {                    // i를 더해도 독립인가
        int g = col[i];
        if (used[g] < cap[g]) { used[g]++; return true; }
        return false;
    }
    void reset() { fill(used.begin(), used.end(), 0); }
};

일반 그리디 (상태 없는 오라클 버전)

임의의 매트로이드에 대한 최대 가중치 독립집합. indep(S) 는 부분집합 S 가 독립인지 반환.

// 원소 인덱스 0..n-1, 가중치 w, 독립성 오라클 indep
vector<int> greedyIndep(int n, const vector<long long>& w,
                        function<bool(const vector<char>&)> indep) {
    vector<int> ord(n);
    iota(ord.begin(), ord.end(), 0);
    sort(ord.begin(), ord.end(),
         [&](int a, int b){ return w[a] > w[b]; });
    vector<char> chosen(n, 0);
    vector<int> res;
    for (int i : ord) {
        chosen[i] = 1;
        if (indep(chosen)) res.push_back(i);   // 독립 유지 → 채택
        else chosen[i] = 0;                    // 아니면 되돌림
    }
    return res;
}

음수 가중치 처리

최대 가중치 독립집합 은 음수 원소를 아예 안 뽑는 게 이득일 수 있으니 가중치가 양수인 것만 후보로 둔다. 반면 최대 가중치 기저 는 크기가 랭크로 고정이므로 음수라도 채워야 한다. 문제 요구가 "기저"인지 "독립집합"인지 구분하라.

벡터(선형) 매트로이드 오라클

\(\mathbb{F}_p\) 위 벡터들의 선형 독립성은 가우스 소거의 증분 형태 로 판정한다. 각 채택된 벡터를 계단형으로 유지하고, 새 벡터를 소거해 0이 되면 종속.

const long long MOD = 998244353;
long long power(long long,long long,long long=MOD);  // 앞 강의 참고
struct LinearMatroid {
    int dim;
    vector<vector<long long>> basis;     // 계단형 기저(피벗별)
    LinearMatroid(int dim) : dim(dim), basis(dim) {}
    bool add(vector<long long> v) {       // 독립이면 편입하고 true
        for (int c = 0; c < dim; c++) if (v[c]) {
            if (basis[c].empty()) {
                long long iv = power(v[c], MOD - 2, MOD);
                for (auto& x : v) x = x * iv % MOD;
                basis[c] = v; return true;
            }
            long long f = v[c];
            for (int j = 0; j < dim; j++)
                v[j] = (v[j] - f * basis[c][j]) % MOD + MOD, v[j] %= MOD;
        }
        return false;                     // 모두 소거됨 → 종속
    }
};

복잡도 요약

매트로이드 오라클 1회
그래프 \(O(\alpha(n))\) (유니온-파인드)
색채/유니폼 \(O(1)\)
벡터(\(d\)차원) \(O(d^2)\) (증분 소거)

그리디 전체는 정렬 \(O(n\log n)\) 에 오라클 \(n\) 회를 더한 값이다.

Lesson 3강 · 심화·응용: 쌍대·합·확장 선택 8m

쌍대 매트로이드 (dual)

매트로이드 \(M = (E, \mathcal{I})\)쌍대 \(M^*\) 는 바탕집합이 같고, 기저가 "\(M\) 의 기저의 여집합"인 매트로이드다. 랭크는

$$ r^*(S) = |S| - r(E) + r(E \setminus S). $$

그래프 매트로이드의 쌍대는 cographic 매트로이드 로, 평면 그래프에서는 쌍대 그래프의 그래프 매트로이드와 대응한다. "간선을 지워도 연결이 유지되는가" 같은 여집합 관점 문제를 매트로이드 언어로 옮길 때 쓴다.

매트로이드 합 (union) 과 분할

여러 매트로이드 \(M_1, \dots, M_k\) \(M_1 \vee \cdots \vee M_k\) 는 "각 \(M_i\) 에서 독립인 조각들로 쪼갤 수 있는 집합"을 독립으로 삼는 매트로이드다(Nash-Williams). 대표 응용:

  • 그래프를 최소 개수의 숲으로 분할(수목성, arboricity): 그래프 매트로이드의 합.
  • \(k\)개의 서로소 신장 트리 존재성 판정.

이 문제들은 매트로이드 합의 랭크 공식과 매트로이드 교집합/매칭 알고리즘으로 다항 시간에 풀린다.

왜 "매트로이드 교집합"으로 넘어가는가

한 매트로이드 위의 그리디는 완벽하지만, 두 제약을 동시에 요구하면(예: "숲이면서 각 색 1개 이하") 단순 그리디가 깨진다. 두 매트로이드의 공통 독립집합 중 최대 를 구하는 문제는 놀랍게도 여전히 다항 시간이며, 이것이 [[매트로이드-교집합]]이다. 반면 세 개 매트로이드의 공통 최대는 NP-난해로 급변한다 — 매트로이드의 "경계"가 정확히 둘까지임을 기억하라.

전형적 문제 형태:

  • 이분그래프 완전 매칭 = 두 분할 매트로이드의 교집합.
  • 색 제약 신장 트리(각 색 간선 개수 제한) = 그래프 매트로이드 ∩ 색채 매트로이드.
  • 방향 그래프의 가지치기(branching, arborescence) = 그래프 매트로이드 ∩ 분할 매트로이드.

매트로이드가 아닌 것 구별하기

그리디가 통할 것 같아도 매트로이드가 아니면 최적이 깨진다. 판별 체크리스트:

  • (I2) 유전성이 깨지는가? "정확히 \(k\)개" 같은 제약은 부분집합이 독립이 아니게 되어 유전성 위반 → 매트로이드 아님(단, 기저 관점에선 다룰 수 있음).
  • (I3) 교환성 반례가 있는가? 크기가 다른 두 극대 독립집합이 존재하면(즉 극대인데 크기가 제각각) 즉시 매트로이드가 아니다. 매칭 이 대표적: 이분이 아닌 일반 그래프의 매칭은 극대 크기가 달라 매트로이드가 아니다.

가중 최적화의 두 목표 구분

목표 그리디 방향 크기
최대 가중치 독립집합 큰 것부터, 양수만 가변
최대 가중치 기저 큰 것부터, 랭크까지 채움 \(r(M)\) 고정
최소 가중치 기저 작은 것부터(MST형) \(r(M)\) 고정

실전 팁

  • 문제를 만나면 먼저 "제약이 매트로이드 하나인가, 둘인가?" 를 판단하라. 하나면 그리디, 둘이면 교집합, 셋 이상이면 다른 접근(흐름·DP·근사).
  • 오라클을 증분 형태 로 짜면 그리디가 빠르고, 상태 없는 형태 로 짜면 교집합 알고리즘에 그대로 재사용된다. 둘 다 준비해 두면 편하다.
  • 그래프·색채·유니폼·벡터 4종 오라클은 템플릿으로 상비하라. 대부분의 대회 문제는 이들의 조합이다.

정리

개념 요지
공리 비어있음 · 유전성 · 교환성
핵심 정리 그리디 최적 ⇔ 매트로이드(Rado–Edmonds)
랭크 함수 부분모듈러, 모든 기저 크기 동일
확장 쌍대, 합(수목성), 교집합
한계 2개 교집합은 다항, 3개 이상은 NP-난해

매트로이드는 "언제 욕심내도 되는가"에 대한 최종 답이다. 이 언어를 익히면 서로 무관해 보이는 그리디·매칭·신장 트리 문제가 하나의 틀로 보인다.

Practice problem 색색의 숲 선택 25m
R00696

색색의 숲

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Lesson 1강 · 이론: 곱 트리와 나머지 내려보내기 필수 8m

두 개의 쌍대 문제

차수 \(n\) 미만의 다항식과 \(n\)개의 점을 놓고 서로 반대 방향의 두 문제를 생각한다.

  • 다중점 평가(multipoint evaluation): 다항식 \(P(x)\) 와 점 \(x_0,\dots,x_{n-1}\) 가 주어질 때 \(P(x_0),\dots,P(x_{n-1})\) 을 모두 구한다.
  • 보간(interpolation): 점 \((x_0,y_0),\dots,(x_{n-1},y_{n-1})\) 가 주어질 때 \(P(x_i)=y_i\) 를 만족하는 차수 \(n\) 미만의 유일한 \(P\) 를 복원한다.

한 점 평가는 호너법으로 \(O(n)\), 그러니 순진하게 하면 다중점 평가는 \(O(n^2)\), 라그랑주 보간도 \(O(n^2)\) 이다. 두 문제 모두 곱 트리(product / subproduct tree) 와 다항식 나눗셈을 결합하면 \(O(n\log^2 n)\) 으로 내려간다.

언제 쓰나

  • 하나의 다항식을 수백만 개 점에서 값매김해야 할 때(예: 여러 질의를 다항식으로 환원).
  • \(10^5\) 개가 넘는 점을 보간해야 하는데 \(O(n^2)\) 이 시간 초과일 때.
  • NTT 소수 위에서 정확한 정수 결과가 필요할 때. 이 단원의 모든 것은 \(\bmod\ 998244353\) 위에서 돈다.

곱 트리

점 집합에 대해 리프가 \(x - x_i\) 인 이진 세그먼트 트리를 만든다. 내부 노드는 두 자식 다항식의 이다.

$$ T_{[l,r)}(x) = \prod_{i=l}^{r-1} (x - x_i) $$

루트는 \(M(x)=\prod_i (x-x_i)\), 차수 \(n\) 의 모닉 다항식이다. 각 층에서 곱해지는 다항식 길이의 총합이 \(O(n)\) 이고 층이 \(O(\log n)\) 개, 한 곱이 NTT로 \(O(\text{len}\log\text{len})\) 이므로 트리 구축은 \(O(n\log^2 n)\).

평가: 나머지를 내려보낸다

핵심 항등식은 다음과 같다. 임의의 점 \(a\) 에 대해

$$ P(a) = P(x) \bmod (x - a) $$

이다(나머지 정리). 이를 트리로 확장한다. 노드 \(v\) 에서 다항식

$$ R_v = P \bmod T_v $$

를 들고 있으면, 자식으로 내려갈 때 \(R_{\text{left}} = R_v \bmod T_{\text{left}}\), \(R_{\text{right}} = R_v \bmod T_{\text{right}}\) 로 다시 나눈다. \(T_{\text{left}} \mid T_v\) 이므로 \(P\bmod T_{\text{left}} = (P\bmod T_v)\bmod T_{\text{left}}\) 가 성립해 재귀가 정당하다. 리프 \(i\) 에 도달하면 \(R = P \bmod (x-x_i)\) 는 상수 \(P(x_i)\) 다.

루트에서는 \(\deg P < n = \deg M\) 이므로 \(P \bmod M = P\) 로 시작한다. 각 층의 다항식 나눗셈 비용 합이 \(O(n\log n)\), 층이 \(O(\log n)\) 개라 전체 \(O(n\log^2 n)\).

작은 예시

\(P(x) = 1 + 2x + 3x^2\)\(x=1,2\) 에서 평가하자. 리프는 \(x-1,\ x-2\), 루트 \(M=(x-1)(x-2)=x^2-3x+2\).

  • \(R_{\text{root}} = P \bmod M\): \(3x^2 + 2x + 1 = 3(x^2-3x+2) + (11x - 5)\) 이므로 \(R = 11x - 5\).
  • 왼쪽: \((11x-5)\bmod(x-1) = 11\cdot 1 - 5 = 6 = P(1)\). 실제 \(1+2+3=6\). ✓
  • 오른쪽: \((11x-5)\bmod(x-2) = 11\cdot 2 - 5 = 17 = P(2)\). 실제 \(1+4+12=17\). ✓

보간: 라그랑주를 트리로

라그랑주 공식은

$$ P(x) = \sum_{i} y_i \prod_{j\ne i} \frac{x-x_j}{x_i-x_j}. $$

여기서 분모 \(\prod_{j\ne i}(x_i-x_j)\)\(M(x)=\prod_j (x-x_j)\)도함수 를 대입한 값과 같다:

$$ M'(x_i) = \prod_{j\ne i} (x_i - x_j). $$

따라서 (1) 곱 트리로 \(M\)\(M'\) 을 만들고, (2) \(M'\) 을 모든 \(x_i\) 에서 다중점 평가\(d_i = M'(x_i)\) 를 얻은 뒤, (3) 계수 \(c_i = y_i / d_i\) 로 두면

$$ P(x) = \sum_i c_i\,\frac{M(x)}{x - x_i}. $$

이 합을 트리를 타고 올라오며 합친다. 노드 \(v\) 에서

$$ F_v(x) = \sum_{i\in v} c_i \prod_{j\in v,\ j\ne i}(x - x_j) $$

를 정의하면 합치는 규칙은

$$ F_v = F_{\text{left}}\cdot T_{\text{right}} + F_{\text{right}}\cdot T_{\text{left}} $$

이고, 리프에서는 \(F = c_i\) (상수)다. 루트의 \(F\) 가 바로 \(P\). 역시 \(O(n\log^2 n)\).

복잡도 정리

연산 순진한 방법 곱 트리
곱 트리 구축 \(O(n\log^2 n)\)
다중점 평가 \(O(n^2)\) \(O(n\log^2 n)\)
보간 \(O(n^2)\) \(O(n\log^2 n)\)
메모리 \(O(n)\) \(O(n\log n)\) (트리 저장)

로그가 두 개 붙는 이유는 "층마다 다항식 나눗셈"(\(\log\))을 "\(\log\) 개의 층"에 대해 반복하기 때문이다. 상수가 제법 크므로 \(n \lesssim 2\times 10^5\) 정도가 현실적인 사용 범위다.

Lesson 2강 · 구현: 곱 트리 · 다중점 평가 · 보간 선택 8m

준비물

아래 코드는 FFT/NTT 단원에서 만든 합성곱

vector<long long> multiply(vector<long long> a, vector<long long> b); // mod 998244353

와 다항식 나눗셈에 필요한 다항식 역원(다항식 연산 단원)을 전제로 한다. 먼저 모듈러 유틸과 다항식 나눗셈부터 세운다.

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef vector<ll> poly;
const ll MOD = 998244353;

ll power(ll a, ll b){ ll r=1; a%=MOD; if(a<0)a+=MOD;
    for(;b;b>>=1,a=a*a%MOD) if(b&1) r=r*a%MOD; return r; }
ll inv(ll a){ return power(a, MOD-2); }

// 앞의 n개 계수로 자름
poly cut(poly a, int n){ a.resize(n); return a; }

// 형식적 멱급수 역원: a[0] != 0, 결과는 x^n 까지 (뉴턴법)
poly poly_inverse(poly a, int n){
    poly b = { inv(a[0]) };
    for(int k=1; k<n; k<<=1){
        int m = k<<1;
        poly a2 = cut(a, m);
        poly t = multiply(b, multiply(a2, b));   // b * a * b
        b.resize(m, 0);
        for(int i=0;i<m;i++){
            ll two_b = (2*b[i]) % MOD;
            ll ti = (i<(int)t.size()? t[i]:0);
            b[i] = (two_b - ti % MOD + 2*MOD) % MOD;   // b = 2b - a b^2
        }
    }
    return cut(b, n);
}

다항식 나눗셈 (몫과 나머지)

\(A = Q\cdot B + R\), \(\deg R < \deg B\). reverse 트릭으로 몫을 먼저 구한다. \(\mathrm{rev}(P)\) 는 계수를 뒤집은 다항식이고, \(B\) 가 모닉이면 \(\mathrm{rev}(B)\) 의 상수항이 \(1\) 이라 역원이 존재한다.

poly rev(poly a){ reverse(a.begin(), a.end()); return a; }

// A mod B  (B는 모닉일 필요 없음; 여기선 곱 트리라 항상 모닉)
pair<poly,poly> divmod(poly A, poly B){
    while(A.size() && A.back()==0) A.pop_back();
    while(B.size() && B.back()==0) B.pop_back();
    int n=A.size(), m=B.size();
    if(n < m) return { poly{0}, A };
    int qd = n-m+1;
    poly Ar=rev(A), Br=rev(B);
    poly Bri = poly_inverse(cut(Br, qd), qd);
    poly Q = cut(multiply(cut(Ar,qd), Bri), qd);
    Q = rev(Q);                                   // 몫
    poly BQ = multiply(B, Q);
    poly R(m>1? m-1 : 0, 0);
    for(int i=0;i<(int)R.size();i++){
        ll v = (i<(int)A.size()? A[i]:0) - (i<(int)BQ.size()? BQ[i]:0);
        R[i] = ((v)%MOD+MOD)%MOD;
    }
    return { Q, R };
}

곱 트리

int N;                 // 점의 개수
vector<poly> tree;     // 세그먼트 트리, 크기 4N
vector<ll> xs;

void build(int node, int l, int r){
    if(l==r){ tree[node] = { (MOD - xs[l])%MOD, 1 }; return; } // (x - x_l)
    int m=(l+r)/2;
    build(2*node, l, m);
    build(2*node+1, m+1, r);
    tree[node] = multiply(tree[2*node], tree[2*node+1]);
}

다중점 평가

루트에서 \(P\bmod M\) 로 시작해 나머지를 내려보낸다.

vector<ll> ys;         // 결과: ys[i] = P(xs[i])

void eval(int node, int l, int r, poly cur){
    cur = divmod(cur, tree[node]).second;   // cur %= T_node
    if(l==r){ ys[l] = cur.empty()?0:cur[0]; return; }
    int m=(l+r)/2;
    eval(2*node, l, m, cur);
    eval(2*node+1, m+1, r, cur);
}

vector<ll> multipoint_eval(poly P, vector<ll> pts){
    xs = pts; N = pts.size();
    ys.assign(N, 0);
    tree.assign(4*N, {});
    build(1, 0, N-1);
    eval(1, 0, N-1, P);
    return ys;
}

보간

\(M'\) 을 다중점 평가해 분모 \(d_i\) 를 구하고, 트리를 올라오며 \(F_v = F_L\ T_R + F_R\ T_L\) 을 합친다.

poly deriv(poly a){
    if(a.size()<=1) return {0};
    poly d(a.size()-1);
    for(int i=1;i<(int)a.size();i++) d[i-1] = a[i]*i % MOD;
    return d;
}

vector<ll> cs;         // 리프 계수 c_i = y_i / M'(x_i)

poly interp_up(int node, int l, int r){
    if(l==r) return { cs[l] };
    int m=(l+r)/2;
    poly L = interp_up(2*node, l, m);
    poly R = interp_up(2*node+1, m+1, r);
    poly a = multiply(L, tree[2*node+1]);
    poly b = multiply(R, tree[2*node]);
    a.resize(max(a.size(), b.size()), 0);
    for(int i=0;i<(int)b.size();i++) a[i] = (a[i]+b[i])%MOD;
    return a;
}

poly interpolate(vector<ll> X, vector<ll> Y){
    xs = X; N = X.size();
    tree.assign(4*N, {});
    build(1, 0, N-1);
    poly Md = deriv(tree[1]);            // M'
    ys.assign(N, 0);
    eval(1, 0, N-1, Md);                 // ys[i] = M'(x_i)
    cs.assign(N, 0);
    for(int i=0;i<N;i++) cs[i] = Y[i] % MOD * inv(ys[i]) % MOD;
    return interp_up(1, 0, N-1);         // 계수 벡터 P
}

사용 예

int main(){
    poly P = {1,2,3};                    // 1 + 2x + 3x^2
    auto v = multipoint_eval(P, {1,2});  // {6, 17}
    auto Q = interpolate({1,2,4}, {6,17,57}); // P를 3점에서 복원 -> {1,2,3}
}

multiplya.size()+b.size()-1 길이를 돌려준다고 가정한다. 빈 다항식(길이 0) 처리를 위해 divmod/eval 에서 empty() 를 항상 챙긴다.

Lesson 3강 · 심화·응용: 변형과 함정 선택 8m

자주 터지는 버그

체크리스트

  • 모닉 가정. 곱 트리 노드는 항상 모닉 \((x-x_i)\) 곱이라 divmod 의 reverse 역원이 안전하다. 일반 \(B\) 로 나눌 때 \(B\) 의 최고차항 역원을 곱해 모닉으로 맞춘 뒤 되돌려야 한다.
  • 역원 길이. poly_inverse(cut(Br, qd), qd) 에서 몫의 차수 \(qd = n-m+1\) 만큼만 필요하다. 더 길게 뽑으면 시간 낭비, 짧으면 몫이 틀린다.
  • 빈 다항식. NTT multiply 는 길이 0 입력에서 이상 동작할 수 있으니 리프·나머지를 항상 최소 길이 1로 유지한다.
  • 중복 점. 보간에서 \(x_i\) 가 겹치면 \(M'(x_i)=0\) 이 되어 역원이 폭발한다. 입력이 서로 다른 점임을 반드시 보장.
  • 음수 계수. 모든 산술 뒤 (v%MOD+MOD)%MOD 로 정규화.

특수한 점 집합은 더 빠르다

일반 점에서는 \(\log^2\) 이지만, 점이 구조적 이면 훨씬 빠른 대안이 있다.

점 집합 평가/보간 방법
등비수열 \(x_i = a\ r^i\) \(O(n\log n)\) Chirp-Z(Bluestein) 변환
연속 정수 \(x_i = i\) \(O(n\log n)\) 이항계수 이동(shift of sampling)
단위근 \(x_i = \omega^i\) \(O(n\log n)\) 그냥 NTT/FFT

특히 "값을 연속한 정수 \(0,1,\dots,n-1\) 에서 알고, 다른 연속 구간 \(c,c+1,\dots\) 의 값을 원한다"면 곱 트리 없이 라그랑주 보간의 접두·접미 곱만으로 \(O(n)\) 에 처리할 수 있다(샘플 이동). 곱 트리는 점이 임의 일 때의 범용 도구다.

응용: 다항식으로 환원되는 질의

  • 집합의 곱을 여러 점에서: \(\prod(x-x_i)\) 를 곱 트리로 얻으면 임의의 \(t\) 에서 \(\prod(t-x_i)\) 를 즉시 평가.
  • 분수식 합 \(\sum \frac{c_i}{x - x_i}\): 보간의 올라오기 규칙과 동일한 구조라, 통분한 분자·분모를 곱 트리로 동시에 구한다.
  • 선형 점화식의 특성 근이 서로 다를 때: 초기값 보간 → 임의 항을 다중점 평가로 뽑는 식의 파이프라인.

NTT 소수 밖 모듈러

답 모듈러가 \(998244353\) 이 아니면, 곱 트리의 모든 곱에서 계수가 커진다. 임의 모듈러가 필요하면 세 개의 NTT 소수로 곱하고 Garner(CRT)로 복원하는 합성곱을 multiply 자리에 끼운다. 이때 상수가 3배가 되지만 차수는 그대로 \(O(n\log^2 n)\).

정수(비모듈러) 보간이 필요하면 값이 지수적으로 커져 큰 정수 연산이 불가피하므로, 대개는 답이 자연스레 모듈러로 주어지는 문제에서 쓴다.

복잡도 재확인

세 연산 모두 \(O(n\log^2 n)\) 시간, \(O(n\log n)\) 메모리(트리). 상수 요인이 커서 같은 \(n\) 이라도 단순 NTT 곱보다 5~10배 느리다. 점이 특수 구조면 항상 \(\log\) 하나를 떼는 전용 알고리즘을 먼저 검토하는 것이 정석이다.

Practice problem 형식적 멱급수의 역원 선택 25m
R00634

형식적 멱급수의 역원

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem 한 줄로 늘어선 표본점 선택 25m
R00683

한 줄로 늘어선 표본점

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Lesson 1강 · 이론: 교환 그래프와 증대 경로 필수 8m

문제

같은 바탕집합 \(E\) 위의 두 매트로이드 \(M_1 = (E, \mathcal{I}_1)\), \(M_2 = (E, \mathcal{I}_2)\) 가 주어질 때, 두 매트로이드 모두에서 독립인 집합 중 크기가 최대인 것

$$ \max \{ |I| : I \in \mathcal{I}_1 \cap \mathcal{I}_2 \} $$

을 찾는 문제가 매트로이드 교집합(matroid intersection) 이다. [[매트로이드]] 하나 위에서는 그리디가 최적이지만, 두 제약을 동시에 만족시키려면 그리디가 깨진다. 놀랍게도 두 개까지는 다항 시간 에 풀리며(세 개는 NP-난해), 그 도구가 증대 경로(augmenting path) 다.

대표 응용

  • 이분 매칭: 좌·우 정점의 두 분할 매트로이드 교집합. 최대 매칭이 곧 최대 공통 독립집합.
  • 색 제약 신장 트리: 그래프 매트로이드 ∩ 색채 매트로이드 (각 색 간선 개수 제한).
  • 방향 신장 가지(arborescence): 그래프 매트로이드 ∩ 분할 매트로이드(각 정점 진입 간선 \(\le 1\)).

교환 그래프 (exchange graph)

현재 공통 독립집합 \(I \in \mathcal{I}_1 \cap \mathcal{I}_2\) 가 있다고 하자. \(I\) 를 한 원소 키우기 위해 방향 그래프 \(D_I\) 를 세운다. 정점은 \(E\) 전체, 간선은 다음과 같다. (\(x \in I\), \(y \notin I\))

  • \(x \to y\): \(I - x + y \in \mathcal{I}_1\) (즉 \(M_1\) 에서 \(x\) 를 빼고 \(y\) 를 넣으면 독립).
  • \(y \to x\): \(I - x + y \in \mathcal{I}_2\) (즉 \(M_2\) 에서 교환 가능).

그리고 두 종류의 특수 정점 을 정의한다.

  • 출발원(source) \(X_1 = \{ y \notin I : I + y \in \mathcal{I}_1 \}\): 그냥 더해도 \(M_1\) 에서 독립인 원소.
  • 도착지(sink) \(X_2 = \{ y \notin I : I + y \in \mathcal{I}_2 \}\): 그냥 더해도 \(M_2\) 에서 독립인 원소.

증대 정리 (Lawler / Schrijver)

\(X_1\) 에서 \(X_2\) 로 가는 최단(간선 수 최소) 경로 \(P\) 가 존재하면, \(I' = I \,\triangle\, V(P)\) (대칭차) 는 크기가 \(|I| + 1\) 인 공통 독립집합이다. 그런 경로가 없으면 \(I\) 가 이미 최대다.

경로 위 정점들은 \(I\) 밖(짝수 위치, 새로 들어옴)과 \(I\) 안(홀수 위치, 빠져나감)이 번갈아 나타나며, 대칭차를 취하면 정확히 원소 하나가 순증한다. 최단 경로여야 함 이 결정적이다 — 지름길(chord)이 있는 경로로 대칭차를 취하면 독립성이 깨질 수 있는데, BFS로 얻은 최단 경로는 이 위험이 없음이 증명되어 있다.

정지 조건과 최적성 (min-max)

증대 경로가 더 없을 때 \(I\) 가 최대임은 다음 최소-최대 정리 로 보장된다.

$$ \max_{I \in \mathcal{I}_1 \cap \mathcal{I}_2} |I| \;=\; \min_{S \subseteq E} \big( r_1(S) + r_2(E \setminus S) \big) $$

(\(r_1, r_2\) 는 각 매트로이드의 랭크 함수.) 이분 매칭에서의 쾨니그 정리, 흐름에서의 최대흐름-최소절단 정리를 일반화한 형태다.

작은 예시: 그래프 ∩ 색채

정점 \(\{1,2,3\}\) 위 간선 4개. \(M_1\) = 숲(그래프 매트로이드), \(M_2\) = "빨강 간선 최대 1개".

간선 연결
a 1–2 빨강
b 2–3 빨강
c 1–3 파랑
d 1–2 파랑

목표: 사이클 없고 빨강 \(\le 1\) 인 최대 간선 집합(=색 제약 신장 트리). \(\{a, c\}\) 는 숲이고 빨강 1개 → 크기 2. 여기서 \(b\) 는 빨강 2개가 되어 불가, \(d\)\(a\) 와 평행이라 \(\{a,d\}\) 는 사이클... \(\{c, d\}\) 는 숲이고 빨강 0개, \(+a\) 하면 사이클, \(+b\) 하면 1–3–2 경로에 2–3 추가로 사이클. 최대 크기는 2(신장 트리는 정점 3개이므로 간선 2개가 상한). 증대는 \(\{c\}\) 에서 시작해 \(X_1, X_2\) 를 오가며 두 번 만에 크기 2에 도달한다.

복잡도

바탕집합 크기 \(n\), 랭크 \(r = |I_{\max}|\), 오라클 1회 비용 \(Q\) 라 하면:

  • 증대는 총 \(r\) 번.
  • 한 번의 증대에서 교환 그래프를 세우는 데 원소쌍마다 오라클을 부르므로 \(O(n^2)\) 오라클, BFS는 \(O(n^2)\).

따라서 기본형은

$$ O(r \cdot n^2 \cdot Q) $$

규모다. Cunningham의 개선으로 \(O(n^{1.5})\) 회 증대·재사용을 쓰면 오라클 호출을 \(\tilde{O}(n^{1.5} r Q)\) 급까지 줄일 수 있으나, 대회에서는 기본형으로 충분한 경우가 많다.

Lesson 2강 · 구현: 증대 루프 선택 8m

오라클 설계

교집합 알고리즘은 "현재 집합 in 에서 원소 하나를 넣거나 뺀 뒤 각 매트로이드에서 독립인가?" 를 반복해서 묻는다. 가장 재사용하기 좋은 형태는 vector<char> in (멤버십 표시)을 받아 독립 여부를 돌려주는 상태 없는 오라클 이다.

#include <bits/stdc++.h>
using namespace std;

struct MatroidIntersection {
    int n;
    vector<char> in;                 // 현재 공통 독립집합 멤버십
    function<bool(vector<char>&)> ind1, ind2;   // 두 독립성 오라클

    MatroidIntersection(int n,
        function<bool(vector<char>&)> f1,
        function<bool(vector<char>&)> f2)
        : n(n), in(n, 0), ind1(f1), ind2(f2) {}

    bool augment() {
        vector<vector<int>> g(n);
        vector<char> src(n, 0), snk(n, 0);

        // 출발원 X1, 도착지 X2 판정: I + y 가 각 매트로이드에서 독립인가
        for (int y = 0; y < n; y++) if (!in[y]) {
            in[y] = 1;
            if (ind1(in)) src[y] = 1;   // I + y ∈ M1
            if (ind2(in)) snk[y] = 1;   // I + y ∈ M2
            in[y] = 0;
        }

        // 교환 간선: x∈I, y∉I 에 대해 I - x + y 검사
        for (int x = 0; x < n; x++) if (in[x])
        for (int y = 0; y < n; y++) if (!in[y]) {
            in[x] = 0; in[y] = 1;
            if (ind1(in)) g[x].push_back(y);   // I-x+y ∈ M1 : x→y
            if (ind2(in)) g[y].push_back(x);   // I-x+y ∈ M2 : y→x
            in[x] = 1; in[y] = 0;
        }

        // X1 에서 X2 로 가는 최단 경로(BFS) — 최단이어야 정당함
        vector<int> par(n, -1), dist(n, -1);
        queue<int> q;
        for (int y = 0; y < n; y++) if (src[y]) { dist[y] = 0; q.push(y); }
        int hit = -1;
        while (!q.empty()) {
            int u = q.front(); q.pop();
            if (snk[u]) { hit = u; break; }     // 첫 도달 = 최단
            for (int v : g[u]) if (dist[v] < 0) {
                dist[v] = dist[u] + 1; par[v] = u; q.push(v);
            }
        }
        if (hit < 0) return false;              // 증대 경로 없음 → 최대

        for (int v = hit; v != -1; v = par[v])  // 대칭차: 경로 토글
            in[v] ^= 1;
        return true;
    }

    int solve() {                                // 최대 공통 독립집합 크기
        while (augment()) {}
        return count(in.begin(), in.end(), (char)1);
    }
};

반드시 BFS 최단 경로

DFS로 아무 경로나 잡으면 지름길(chord) 때문에 대칭차 후 집합이 독립이 아닐 수 있다. BFS로 얻은 최단 경로 만 증대 정리를 만족한다. snk[u] 를 큐에서 꺼낼 때(=최소 거리 확정 시) 멈추는 것이 핵심이다.

구체 예시 오라클: 그래프 ∩ 색채

간선 배열을 전역에 두고, 멤버십 in 에서 켜진 간선들이 (1) 숲을 이루는지, (2) 각 색 용량을 지키는지 판정한다.

int N;                          // 정점 수
vector<int> eu, ev, ecol;       // 각 간선의 양끝, 색
vector<int> cap;                // 색별 용량

int find(vector<int>& p, int x){ return p[x]==x? x : p[x]=find(p,p[x]); }

bool isForest(vector<char>& in) {          // M1: 사이클 없음
    vector<int> p(N); iota(p.begin(), p.end(), 0);
    for (int i = 0; i < (int)in.size(); i++) if (in[i]) {
        int a = find(p, eu[i]), b = find(p, ev[i]);
        if (a == b) return false;          // 사이클 → 종속
        p[a] = b;
    }
    return true;
}
bool colorOK(vector<char>& in) {           // M2: 색 용량
    vector<int> use(cap.size(), 0);
    for (int i = 0; i < (int)in.size(); i++) if (in[i])
        if (++use[ecol[i]] > cap[ecol[i]]) return false;
    return true;
}

// 사용 예
int solveColorTree(int n, vector<array<int,3>> edges, vector<int> capacity) {
    N = n; cap = capacity;
    int m = edges.size();
    eu.resize(m); ev.resize(m); ecol.resize(m);
    for (int i = 0; i < m; i++)
        { eu[i]=edges[i][0]; ev[i]=edges[i][1]; ecol[i]=edges[i][2]; }
    MatroidIntersection mi(m, isForest, colorOK);
    return mi.solve();          // 최대 색제약 숲의 간선 수
}

이분 매칭으로의 사상

이분 매칭은 두 분할 매트로이드 의 교집합이다. 바탕집합은 간선, \(M_1\) = "각 좌측 정점에 붙은 간선 \(\le 1\)", \(M_2\) = "각 우측 정점에 붙은 간선 \(\le 1\)". 두 오라클 모두 색채 매트로이드 형태이며, 교집합 알고리즘을 돌리면 최대 매칭이 나온다(전용 홉크로프트–카프보다 느리지만 정당성 확인용으로 좋다).

정당성 자체 검증

구현이 맞았는지 확인하려면 매 증대 후 ind1(in) && ind2(in) 가 참인지 assert 하라. 최단 경로를 지키면 항상 참이어야 한다.

assert(ind1(in) && ind2(in));   // 증대 직후 불변식
Lesson 3강 · 심화·응용: 가중 교집합과 함정 선택 8m

가중 매트로이드 교집합

각 원소에 가중치 \(w\) 가 있고, 크기 \(k\) 인 공통 독립집합 중 가중치 최대(또는 최소) 를 모든 \(k\) 에 대해 구하고 싶을 때가 있다. 이때 교환 그래프의 간선을 가중치가 있는 것으로 바꾸고, "가중치 기준 최적, 동률이면 간선 수 최소"인 경로를 따라 증대한다.

  • 교환 그래프에서 \(I\) 밖 정점 \(y\)(들어올 후보)에 비용 \(-w_y\), \(I\) 안 정점 \(x\)(나갈 후보)에 비용 \(+w_x\) 를 부여.
  • 정점 비용을 간선 비용으로 옮긴 뒤, 음수 간선을 허용하는 최단 경로(벨만–포드 / SPFA)를 돌린다. 그래프에는 음수 사이클이 생기지 않음이 보장된다.
  • 1차 기준 = 총비용 최소(=가중치 최대), 2차 기준 = 간선 수 최소로 사전식 비교.
// 가중 교집합: 각 크기 k 에서 최소비용 증대. dist는 (비용, 간선수) 사전식.
bool augmentWeighted(int n, vector<char>& in,
        const vector<long long>& w,
        function<bool(vector<char>&)> ind1,
        function<bool(vector<char>&)> ind2) {
    vector<vector<int>> g(n);
    vector<char> src(n,0), snk(n,0);
    for (int y=0;y<n;y++) if(!in[y]){ in[y]=1;
        if(ind1(in)) src[y]=1; if(ind2(in)) snk[y]=1; in[y]=0; }
    for (int x=0;x<n;x++) if(in[x])
      for (int y=0;y<n;y++) if(!in[y]){
        in[x]=0; in[y]=1;
        if(ind1(in)) g[x].push_back(y);
        if(ind2(in)) g[y].push_back(x);
        in[x]=1; in[y]=0; }
    // 정점비용: 들어올 y = -w[y], 나갈 x = +w[x]
    auto cost = [&](int v){ return in[v] ? w[v] : -w[v]; };
    const long long INF = LLONG_MAX/4;
    vector<long long> d(n, INF); vector<int> len(n,0), par(n,-1);
    // SPFA (음수 간선 허용)
    deque<int> q; vector<char> inq(n,0);
    for (int y=0;y<n;y++) if(src[y]){ d[y]=cost(y); len[y]=1; q.push_back(y); inq[y]=1; }
    while(!q.empty()){
        int u=q.front(); q.pop_front(); inq[u]=0;
        for(int v:g[u]){
            long long nd=d[u]+cost(v); int nl=len[u]+1;
            if(nd<d[v] || (nd==d[v] && nl<len[v])){
                d[v]=nd; len[v]=nl; par[v]=u;
                if(!inq[v]){ inq[v]=1; q.push_back(v); }
            }
        }
    }
    int best=-1;
    for(int y=0;y<n;y++) if(snk[y] && d[y]<INF)
        if(best<0 || d[y]<d[best] || (d[y]==d[best] && len[y]<len[best])) best=y;
    if(best<0) return false;
    for(int v=best; v!=-1; v=par[v]) in[v]^=1;
    return true;
}

크기를 하나씩 키우며 매번 그때의 최소비용 을 유지하면, 각 \(k\) 에서의 최적해를 순차적으로 얻는다(가중치 최대라면 부호를 뒤집어 최소 비용으로 처리).

흔한 함정

  • 최단 경로를 안 지킴. 가장 치명적. DFS나 임의 경로로 증대하면 대칭차 후 독립성이 깨진다. 무가중은 BFS, 가중은 "비용 최소 + 동률 시 간선 수 최소".
  • 출발원/도착지 혼동. \(X_1\) 은 "\(M_1\) 에 그냥 넣어도 독립", \(X_2\) 는 "\(M_2\) 기준". 교환 간선 방향(\(x\to y\)\(M_1\), \(y\to x\)\(M_2\))과 짝을 맞춰야 한다. 두 매트로이드를 바꿔 넣으면 조용히 틀린다.
  • 오라클 상태 오염. 상태 없는 오라클은 in 을 검사 후 원상 복구 해야 한다(위 코드의 in[x]=1; in[y]=0;). 하나라도 빠지면 그래프가 오염된다.
  • 세 매트로이드로 확장 시도. 세 개 이상의 공통 최대 독립집합은 NP-난해 다. 세 제약이 필요하면 이 알고리즘을 쓸 수 없다 — 흐름·ILP·근사로 우회하라.
  • 가중 버전의 음수 사이클 오해. 정당하게 세운 교환 그래프에는 음수 사이클이 없으므로 SPFA가 종료한다. 만약 무한 완화가 관측되면 비용 부호나 간선 방향이 틀린 것이다.

성능 개선 방향

  • 오라클 캐싱·증분화. 그래프 매트로이드는 유니온-파인드를, 색채는 카운터를 증분으로 유지하면 오라클을 \(O(1)\)~\(O(\alpha)\) 로. 상태 없는 재계산 \(O(n)\) 을 없앤다.
  • Cunningham 스케일링. 이론적으로 증대 횟수·탐색을 줄여 \(O(n^{1.5})\) 규모 오라클 호출로 개선. 대회에선 대개 불필요.
  • 문제 특화 대체. 이분 매칭이면 홉크로프트–카프, 최소 비용이면 MCMF가 훨씬 빠르다. 교집합은 범용성 이 무기이지 속도가 무기가 아니다.

정리

항목 무가중 가중
경로 선택 BFS 최단 비용 최소(+간선수) SPFA
목표 최대 크기 각 크기의 최적 가중치
증대 횟수 \(r\) \(r\)
오라클 호출/회 \(O(n^2)\) \(O(n^2)\)
한계 2개까지 다항 3개↑ NP-난해

매트로이드 교집합은 이분 매칭·색 제약 신장 트리·방향 가지 등 서로 달라 보이는 문제를 하나의 증대 경로 틀 로 통합한다. [[매트로이드]]의 그리디가 "제약 하나"의 답이라면, 교집합은 "제약 둘"의 정확한 경계다.

Practice problem 색색의 숲 선택 25m
R00696

색색의 숲

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Lesson 1강 · 이산 로그와 Baby-step Giant-step 필수 8m

문제 정의

\(m\) 에서

$$ a^{x}\equiv b\pmod{m} $$

를 만족하는 가장 작은 비음 정수 \(x\) 를 찾는 것이 이산 로그(discrete
logarithm)
입니다. 지수 \(x\) 가 미지수라는 점에서 거듭제곱(\(a^x\) 계산)과 정반대
방향입니다. 이 문제의 어려움 이 Diffie–Hellman·ElGamal 등 공개키 암호의
기반이지만, 경시 범위의 \(m\) 에서는 \(O(\sqrt m)\) 알고리즘으로 풉니다.


1. Baby-step Giant-step (BSGS)

\(\gcd(a,m)=1\) 이라 하고 \(n=\lceil\sqrt m\,\rceil\) 로 둡니다. 지수를

$$ x=in-j,\qquad 1\le i\le n,\ 0\le j

로 쓰면(모든 \(0\le x 은 이렇게 표현 가능, \(n^2\ge m\)),

$$ a^{in-j}\equiv b \;\Longleftrightarrow\; a^{in}\equiv b\cdot a^{j}\pmod m. $$

전략.

  1. Baby steps: \(j=0,1,\dots,n-1\) 에 대해 \(b\cdot a^{j}\bmod m\) 을 모두 해시
    맵에 저장(값 → 가장 큰 \(j\)).
  2. Giant steps: \(i=1,2,\dots,n\) 에 대해 \(a^{in}\bmod m=(a^{n})^{i}\) 를 하나씩
    계산하며 맵에 같은 값이 있으면 매칭. 그때 \(x=in-j\).

각 단계 \(O(\sqrt m)\), 맵 조회 평균 \(O(1)\) → 전체 시간·공간 \(O(\sqrt m)\).


2. 정당성

\(a\) 가 가역이면 순서(order) \(\mathrm{ord}(a)\le m\) 이 존재하고 답 \(x\) 가 있으면
\(0\le x<\mathrm{ord}(a)\le m\le n^2\) 이므로 \(x=in-j\) 형태로 반드시 표현됩니다.
\(i\) 를 오름차순으로 훑으며 첫 매칭을 취하면(같은 \(i\) 안에서는 큰 \(j\) = 작은 \(x\))
가장 작은 \(x\) 를 얻습니다.


3. 수치 예제

\(2^{x}\equiv 22\pmod{29}\) 를 풀어봅시다. \(n=\lceil\sqrt{29}\,\rceil=6\).

Baby steps \(b\cdot a^{j}=22\cdot2^{j}\bmod29\) 를 맵에 저장:

\(j\) 0 1 2 3 4 5
22 15 1 2 4 8

Giant steps \((a^{n})^{i}=(2^{6})^{i}=6^{i}\bmod29\):
\(i{=}1\!:6,\ i{=}2\!:7,\ i{=}3\!:13,\ i{=}4\!:20,\ i{=}5\!:4\).

\(i=5\) 에서 giant 값 \(4\) 가 baby 표의 \(j=4\) 와 매칭됩니다. 따라서

$$ x=in-j=5\cdot6-4=26. $$

검증: \(2^{26}\bmod29=22\) 로 일치합니다(아래 2강 코드도 \(26\) 을 반환). 여러 해는
\(x\equiv26\pmod{\mathrm{ord}(2)}\) 로 주기적입니다.


4. \(\gcd(a,m)\neq1\) 로의 일반화

\(a\)\(m\) 이 서로소가 아니면 \(a\) 가 가역이 아니라 위 변형이 깨집니다. 공통
인수를 단계적으로 제거합니다.

반복: \(g=\gcd(a,m)\). \(g\nmid b\) 이고 \(b\ne1\) 이면 해 없음. \(g\mid b\) 이면

$$ \frac{a}{g}\cdot a^{x-1}\equiv\frac{b}{g}\pmod{\frac{m}{g}} $$

로 줄이고 \(x\) 를 1 감소시킨 부분 문제를 풉니다. 이 "제거 후 BSGS" 가 확장
BSGS
입니다(2강 구현).


5. 복잡도 · 언제 쓰나

항목 비용
시간 \(O(\sqrt m\log m)\) (맵/거듭제곱 포함)
공간 \(O(\sqrt m)\)

암호 수준의 거대한 군에선 비현실적이지만, 경시 \(m\)(\(\le10^{18}\) 정도)에선 충분히
빠릅니다. \(a^x\equiv b\) 꼴 방정식, 순환군에서의 지수 역산 문제에 사용합니다.

Lesson 2강 · BSGS 구현과 확장 선택 8m

1. 기본 BSGS (\(\gcd(a,m)=1\))

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long u64;

u64 mulmod(u64 a, u64 b, u64 m) { return (__int128)a * b % m; }  // 오버플로 방지
ll power(ll a, ll b, ll m) {
    a %= m; if (a < 0) a += m;
    ll r = 1 % m;
    while (b > 0) { if (b & 1) r = mulmod(r, a, m); a = mulmod(a, a, m); b >>= 1; }
    return r;
}

// 최소 비음 x 로 a^x ≡ b (mod m), 없으면 -1.  gcd(a,m)=1 가정.
ll bsgs(ll a, ll b, ll m) {
    a %= m; b %= m;
    if (m == 1) return 0;
    if (b == 1 % m) return 0;                 // a^0 = 1
    ll n = (ll)ceil(sqrt((double)m)) + 1;
    unordered_map<ll, ll> table;
    ll cur = b % m;
    for (ll j = 0; j < n; j++) {               // baby: b * a^j, 값→최대 j
        table[cur] = j;
        cur = mulmod(cur, a, m);
    }
    ll an = power(a, n, m);                     // a^n
    ll giant = an;
    for (ll i = 1; i <= n; i++) {               // giant: (a^n)^i
        auto it = table.find(giant);
        if (it != table.end()) {
            ll x = i * n - it->second;          // x = i*n - j
            if (x >= 0) return x;
        }
        giant = mulmod(giant, an, m);
    }
    return -1;
}

table[cur]=j 를 매번 덮어쓰면 같은 값에 대해 가장 큰 \(j\) 가 남고, \(i\)
오름차순으로 보므로 최소 \(x\) 가 나옵니다. bsgs(2,22,29)26
(검증: \(2^{26}\bmod29=22\)).


2. 파이썬 구현

def bsgs(a, b, m):
    a %= m; b %= m
    if m == 1: return 0
    if b == 1 % m: return 0
    n = int(m ** 0.5) + 1
    table = {}
    cur = b % m
    for j in range(n):                 # baby steps
        table[cur] = j
        cur = cur * a % m
    an = pow(a, n, m)                   # 파이썬 pow: 모듈러, 큰 수 안전
    giant = an
    for i in range(1, n + 1):          # giant steps
        if giant in table:
            x = i * n - table[giant]
            if x >= 0: return x
        giant = giant * an % m
    return -1
# bsgs(2, 22, 29) == 26,  bsgs(3, 1, 7) == 0

3. 확장 BSGS (\(\gcd(a,m)\neq1\))

공통 인수를 제거해 가역이 되게 만든 뒤 기본 BSGS 를 호출합니다.

ll gcd_(ll a, ll b){ return b ? gcd_(b, a % b) : a; }

ll exbsgs(ll a, ll b, ll m) {
    a %= m; b %= m;
    if (b == 1 % m || m == 1) return 0;
    ll cnt = 0, d, ad = 1;
    while ((d = gcd_(a, m)) > 1) {
        if (b % d) return -1;                   // 해 없음
        b /= d; m /= d; ad = mulmod(ad, a / d, m);
        cnt++;
        if (ad == b) return cnt;                 // a^cnt 자체가 답
    }
    // 이제 gcd(a,m)=1.  ad * a^y ≡ b  →  a^y ≡ b * ad^{-1}
    // (m 이 소수가 아닐 수 있으니 ad 의 역원은 확장유클리드로; 여기선 이항)
    ll y = bsgs_shifted(a, b, m, ad);           // b·ad^{-1} 를 목표로 BSGS
    return y == -1 ? -1 : y + cnt;
}

여기서 bsgs_shifted 는 baby step 을 \(ad\cdot a^{j}\) 로 시작(즉 목표를
\(b\) 로 두고 좌변에 남은 계수 \(ad\) 를 곱해 나가는 형태)해 역원 계산을 피하는
표준 변형입니다. 핵심은 매 제거 단계에서 \(x\) 를 1씩 늘리고(\(cnt\)),
\(b\)\(d\) 로 나누어떨어지지 않으면 해가 없다는 것입니다.


4. 함정 정리

  • 오버플로: \(m\)\(10^{18}\) 급이면 곱 \(a\cdot b\)\(2^{63}\) 을 넘습니다.
    반드시 __int128 기반 mulmod 사용(위 코드). 파이썬은 자동 처리.
  • 자명 해: \(b\equiv1\) 이면 \(x=0\) 을 먼저 반환. \(a\equiv0\) 등 퇴화 경우 주의.
  • 최소성: 같은 값에 최대 \(j\) 저장 + \(i\) 오름차순이 최소 \(x\) 를 보장.
  • 없는 해: 모든 giant step 에서 매칭이 없으면 \(-1\). 확장 BSGS 에서
    \(g\nmid b\) 인 순간 즉시 \(-1\).
  • 순서(order): 답은 \(\bmod\ \mathrm{ord}(a)\) 로 주기적 — "모든 해"를 원하면
    최소 해에 \(\mathrm{ord}(a)\) 의 배수를 더합니다.
Practice problem 이산 로그를 찾아라 선택 25m
R00638

이산 로그를 찾아라

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem 이산 로그 선택 25m
R00702

이산 로그

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Lesson 이론 — 형식적 로그와 지수 필수 8m

정의와 존재 조건

형식적 멱급수에서 로그와 지수는 미적분의 정의를 그대로 옮긴다.

$$ \log A(x) = \int \frac{A'(x)}{A(x)}\,dx,\qquad \exp A(x)=\sum_{k\ge0}\frac{A(x)^k}{k!} $$

  • 로그: \(\log A\) 가 (상수항 \(0\) 인) 멱급수가 되려면 \(a_0=1\) 이어야 한다. 그러면 \(\log A\) 의 상수항은 \(\log 1 = 0\).
  • 지수: \(\exp A\) 가 잘 정의되려면 \(a_0=0\) 이어야 한다(그래야 \(A^k/k!\) 급수가 각 차수에서 유한합). \(\exp A\) 의 상수항은 \(1\).

이 둘은 서로 역연산이다: \(\exp(\log A)=A\), \(\log(\exp A)=A\).

왜 필요한가

지수는 "지수 생성함수(EGF)의 집합/사이클 구조" 를 조립하는 핵심이다. 연결 구조의 EGF 를 \(C(x)\) 라 하면 그 구조들의 집합(순서 없는 여러 개)의 EGF 는 \(\exp C(x)\) 다. 라벨된 그래프·순열의 사이클·분할수·삼림→나무 관계 등 수많은 조합 항등식이 \(\log/\exp\) 한 줄로 계산된다. 또한 일반 거듭제곱 \(A(x)^k = \exp(k\log A)\) 로 임의(심지어 큰) 지수의 멱을 \(O(n\log n)\) 에 얻는다.

로그의 계산

\(\log A\) 자체를 직접 급수로 더하면 느리다. 대신 미분해서 유리식으로 만든 뒤 적분한다.

$$ \big(\log A\big)' = \frac{A'}{A}\quad\Rightarrow\quad \log A = \int A' \cdot A^{-1}\,dx. $$

\(A^{-1}\) 은 다항식 역원(앞 단원), \(A'\) 는 항별 미분, 적분은 항별 나눗셈. 모두 \(O(n\log n)\).

작은 예제

\(A = 1 + x\). \(A' = 1\), \(A^{-1}=1-x+x^2-\cdots\). 곱은 \(1-x+x^2-\cdots\), 적분하면 \(x-\tfrac{x^2}{2}+\tfrac{x^3}{3}-\cdots = \log(1+x)\). 정확히 테일러 급수와 일치.

지수의 계산 — 뉴턴 반복

\(\exp\) 는 방정식 \(\log B - A = 0\) 의 근 \(B=\exp A\) 를 뉴턴으로 찾는다. \(f(B)=\log B - A\), \(f'(B)=B^{-1}\) 이므로

$$ B_{2k} \equiv B_k\big(1 - \log B_k + A\big) \pmod{x^{2k}} $$

초깃값 \(B_1 = 1\)(\(a_0=0\) 이므로 \(\exp\) 의 상수항은 \(1\)). 매 단계 \(\log B_k\) 를 계산해야 하므로 지수 한 번에 로그(=역원+곱)가 들어가, 상수는 크지만 차수는 여전히 \(O(n\log n)\).

작은 예제

\(A = x\). \(B_1=1\). \(\log B_1 = 0\) 이므로 \(B_2 = 1\cdot(1-0+x)=1+x\). 다음 단계에서 \(\tfrac{x^2}{2}\) 항이 붙어 \(\exp x = 1+x+\tfrac{x^2}{2}+\cdots\) 로 수렴한다.

언제 쓰나 / 복잡도

연산 조건 복잡도
\(\log A\) \(a_0=1\) \(O(n\log n)\)
\(\exp A\) \(a_0=0\) \(O(n\log n)\) (상수 큼)
\(A^k = \exp(k\log A)\) \(a_0=1\) \(O(n\log n)\)

전형적 용도: EGF 조립(집합=\(\exp\)), 대량 조합수, 임의 지수 멱, 분할수·삼림수 같은 항등식의 직접 계산.

Lesson 구현 — 미분·적분·log·exp 선택 8m

보조 연산: 미분과 적분

vector<ll> deriv(vector<ll> a) {           // a' : 계수 i*a[i] 를 한 칸 당김
    if (a.size() <= 1) return {0};
    vector<ll> r(a.size() - 1);
    for (int i = 1; i < (int)a.size(); i++) r[i - 1] = a[i] * i % MOD;
    return r;
}
vector<ll> integ(vector<ll> a) {           // ∫a : a[i]/(i+1) 를 한 칸 밀음
    vector<ll> r(a.size() + 1, 0);
    for (int i = 0; i < (int)a.size(); i++)
        r[i + 1] = a[i] * power(i + 1, MOD - 2, MOD) % MOD;
    return r;                              // r[0] = 0 (적분상수)
}

실전에서는 \(1,2,\dots,n\) 의 모듈러 역원을 선형 시간에 미리 전처리해 integ 안의 power 를 없앤다.

로그

\(a_0=1\) 필요. \(\log A = \int A'A^{-1}\).

vector<ll> poly_log(vector<ll> A, int n) {  // A[0] == 1
    vector<ll> res = multiply(deriv(A), poly_inv(A, n));
    res.resize(n - 1);
    res = integ(res);                       // 크기 n
    res.resize(n);
    return res;
}

지수 — 뉴턴 반복

\(a_0=0\) 필요. \(B \equiv B(1 - \log B + A)\).

vector<ll> poly_exp(vector<ll> A, int n) {  // A[0] == 0
    vector<ll> B = {1};
    for (int k = 1; k < n; k <<= 1) {
        int m = k << 1;
        vector<ll> L = poly_log(B, m);       // log of current B, 정밀도 m
        vector<ll> t(m, 0);
        for (int i = 0; i < m; i++) {
            ll ai = (i < (int)A.size()) ? A[i] : 0;
            t[i] = ((ai - L[i]) % MOD + MOD) % MOD;  // A - log B
        }
        t[0] = (t[0] + 1) % MOD;             // 1 + A - log B
        B = multiply(B, t);
        B.resize(m);
    }
    B.resize(n);
    return B;
}

검증

int main(){
    // exp(x) 의 앞 5항: 1, 1, 1/2, 1/6, 1/24  (모듈러 역팩토리얼)
    vector<ll> A = {0, 1};                  // A = x
    auto E = poly_exp(A, 5);
    for (ll c : E) printf("%lld ", c);      // 1  1  inv2  inv6  inv24
    puts("");
    // log(exp(x)) == x 를 되돌려 확인
    auto back = poly_log(E, 5);
    for (ll c : back) printf("%lld ", c);   // 0 1 0 0 0
}

inv2 = (MOD+1)/2, inv6, inv24 는 각 팩토리얼의 모듈러 역원이다. poly_log 로 되돌리면 원래 \(A=x\) 가 정확히 복원된다.

흔한 실수

  • 상수항 조건 위반. poly_log\(a_0\neq 1\), poly_exp\(a_0\neq 0\) 을 넣으면 결과가 무의미하다. 호출 전 반드시 검사.
  • 정밀도 불일치. poly_exp 안에서 poly_log(B, m) 은 반드시 현재 정밀도 \(m\) 으로. 짧게 부르면 고차 항이 틀어진다.
  • 미분/적분 인덱스. 미분은 한 칸 당기고(\(i\to i-1\)), 적분은 한 칸 미는(\(i\to i+1\)) 것을 헷갈리지 말 것.
Lesson 심화·응용 — 임의 지수 멱과 함정 선택 8m

임의 지수 다항식 거듭제곱 \(A^k\)

\(A(x)^k\)\(k\) 가 아주 클 때도 \(O(n\log n)\) 에 구하는 표준 기법:

$$ A^k = \exp\big(k\log A\big),\qquad a_0=1\ \text{일 때}. $$

vector<ll> poly_pow(vector<ll> A, ll k, int n) {   // A[0] == 1 가정
    vector<ll> L = poly_log(A, n);
    for (auto& x : L) x = x * (k % MOD) % MOD;      // k * log A
    return poly_exp(L, n);
}

\(a_0 \neq 1\) 인 일반 경우

\(A\) 의 최저 차수 항을 \(a_t\ x^t\) 라 하면 \(A^k\) 의 최저 차수는 \(tk\) 다. \(tk \ge n\) 이면 답은 \(0\). 아니면 \(A = a_t x^t (1 + \cdots)\) 로 뽑아내

$$ A^k = a_t^{\,k}\, x^{tk}\, \big(\underbrace{1+\cdots}_{a_0=1}\big)^k $$

로 만들어 위 공식을 적용하고, \(x^{tk}\) 만큼 시프트, \(a_t^{k}\) 를 곱한다. 주의: 지수 \(k\)\(a_t^k\) 계산에선 \(\bmod (p-1)\)(페르마), \(\exp\) 안의 \(k\log A\) 계수에선 \(\bmod p\) 로 서로 다른 모듈러를 쓴다.

생성함수 응용 스케치

  • 집합 구조 = \(\exp\): 연결 성분의 EGF 가 \(C(x)\) 이면 전체(성분들의 집합)의 EGF 는 \(\exp C(x)\). 반대로 전체에서 연결만 뽑으려면 \(\log\).
  • 분할수 계열: \(\prod (1-x^i)^{-1}\) 같은 무한곱은 로그를 취해 \(\sum \log(1-x^i)^{-1}=\sum\sum \frac{x^{ij}}{j}\) 로 바꾸면 조화합 형태가 되어 \(O(n\log n)\) 에 지수로 복원한다.
  • 다중집합/사이클: 사이클 구조는 \(\sum \frac{C(x^i)}{i}\) 류 변형 후 \(\exp\).

함정 총정리

  • 로그의 상수항. \(a_0=1\) 이라야 \(\log A\) 의 상수항이 \(0\) 이 되어 형식적 급수로 성립. \(a_0\neq1\) 이면 상수 \(\log a_0\) 가 정의되지 않으므로 반드시 정규화(공통인수를 뽑아 \(a_0=1\) 로).
  • 지수의 상수항. \(a_0=0\) 이 아니면 \(\exp\) 급수가 발산(각 차수 무한합). \(A\) 에서 상수항을 떼고 마지막에 \(e^{a_0}\) 를 곱하는 방식은 형식적 멱급수에선 불가하니 항상 \(a_0=0\) 을 강제.
  • \(k\) 의 두 모듈러. 임의 지수 멱에서 계수 곱의 지수는 \(\bmod (p-1)\), \(\exp\) 내부 계수는 \(\bmod p\). 혼동하면 틀린다.
  • 정밀도 앨리어싱. 모든 뉴턴 반복 공통: 곱을 \(\bmod x^{2k}\) 로 잘라라.
  • 상수 폭. \(\exp\) 는 매 배가 단계마다 \(\log\)(=역원+미분+곱+적분)를 부르므로 곱셈 대비 상수가 크다. 정밀도 \(n\sim 10^5\) 까진 실전 통과하지만, 필요 이상 큰 \(n\) 은 피한다.

복잡도 요약

연산 복잡도 상수
\(\log A\) \(O(n\log n)\)
\(\exp A\) \(O(n\log n)\)
\(A^k=\exp(k\log A)\) \(O(n\log n)\)
Practice problem 형식적 멱급수의 역원 선택 25m
R00634

형식적 멱급수의 역원

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem 한 줄로 늘어선 표본점 선택 25m
R00683

한 줄로 늘어선 표본점

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Lesson 1강 · 이론: 자기 참조 합성곱과 분할 정복 필수 8m

문제: 결과가 입력에 되먹임될 때

보통의 합성곱은 두 수열 \(a,b\)미리 전부 주어질\(c_k=\sum_{i+j=k} a_ib_j\) 를 한 번의 NTT로 구한다. 그런데 다음처럼 \(c\) 자신이 재귀적으로 정의되면 그 방법을 쓸 수 없다.

$$ c_0 = 1,\qquad c_n = \sum_{i=1}^{n} f_i\, c_{n-i}\quad(n\ge 1) $$

여기서 \(f\) 는 고정 수열이지만, \(c_n\) 을 계산하려면 \(c_0,\dots,c_{n-1}\) 이 이미 있어야 한다. 즉 "\(c_n\) 을 구하는 데 \(c\) 의 이전 값이 필요한" 온라인(online) / 완화(relaxed) 합성곱 이다. 순진하게 정의대로 계산하면 \(O(n^2)\).

언제 쓰나

  • 생성함수 DP에서 \(c_n\)\(c_{ 과 고정 커널의 합성곱으로 정의될 때.
  • \(\exp\), 카탈랑류, 트리·괄호열 개수 등 자기 참조 점화식.
  • 다항식 뉴턴법(\(\exp/\log\))을 쓰기 애매한, 항별 비선형 관계가 섞인 DP.

목표는 이 온라인 제약을 지키면서 전체를 \(O(n\log^2 n)\) 에 계산하는 것이다.

핵심 아이디어: 기여를 미리 밀어 넣기(CDQ)

\(c_i\) 가 확정되는 순간, 그것이 앞으로의 어떤 \(c_k\,(k>i)\) 에 얼마나 기여하는지는 이미 안다: \(c_i\)\(f_{k-i}\) 를 통해 \(c_k\) 로 들어간다. 문제는 이 기여를 하나씩 더하면 \(O(n^2)\) 이라는 점. 그래서 구간 단위로 묶어 한 번의 NTT로 밀어 넣는다. 이것이 CDQ 분할 정복(온라인 FFT)이다.

구간 \([l,r)\) 에 대한 재귀 solve(l, r) 를 다음 불변식으로 정의한다.

solve(l, r) 호출 시점에는, 인덱스 \(l\) 미만의 모든 확정값 이 이 구간의 \(c\) 들에 이미 기여를 마친 상태다. 이 호출은 \([l,r)\) 내부에서 생기는 기여만 처리한다.

분할은 이렇게 한다(\(m=\lfloor(l+r)/2\rfloor\)):

  1. solve(l, m) — 왼쪽 절반을 먼저 완전히 확정한다.
  2. 이제 \(c_l,\dots,c_{m-1}\) 이 확정됐으니, 왼쪽 블록이 오른쪽 블록에 주는 기여 를 한 번의 합성곱으로 계산해 \(c_m,\dots,c_{r-1}\) 에 더한다:
    $$ \Delta_k \mathrel{+}= \sum_{i=l}^{m-1} c_i\, f_{k-i},\qquad k\in[m,r). $$
    이는 길이 \(m-l\) 짜리 조각 \(c[l..m)\)\(f[0..r-l)\) 의 합성곱이다.
  3. solve(m, r) — 이제 오른쪽 절반은 "\(l\) 미만 + 왼쪽 절반"의 기여를 모두 받았으므로 같은 방식으로 재귀한다.

리프 \([l,l{+}1)\) 에서는 \(c_l\) 이 완전히 확정되었으니 그 자리에서 점화식의 나머지 부분(예: 기저항, 혹은 \(c_l\) 로부터 \(f_l\) 을 정하는 비선형 관계)을 적용한다.

왜 이중 계산이 안 일어나나

한 쌍 \((i,\text{커널 }f_{k-i})\) 의 기여는, \(i\)\(k\)처음으로 서로 다른 절반에 갈라지는 유일한 재귀 단계에서 딱 한 번 더해진다. 위 2단계는 "왼쪽에서 오른쪽으로"만 밀기 때문에 \(i 인 인과적 기여만, 정확히 한 번 처리된다. 자기 자신(\(k=i\), 커널 \(f_0\))은 오른쪽 구간에 들어가지 않으므로 자연히 배제된다.

복잡도

각 재귀 층에서 처리하는 합성곱 길이의 총합이 \(O(n)\), 한 합성곱이 \(O(\text{len}\log\text{len})\), 층이 \(O(\log n)\) 개다. 정리하면

$$ T(n) = 2T(n/2) + O(n\log n) = O(n\log^2 n). $$

작은 예시

\(f=(f_1,f_2,\dots)=(1,1,\dots)\), \(c_0=1\), \(c_n=\sum_{i=1}^n f_i c_{n-i}\)\(n=3\) 까지.

  • \(c_0=1\).
  • \(c_1=f_1c_0=1\).
  • \(c_2=f_1c_1+f_2c_0=1+1=2\).
  • \(c_3=f_1c_2+f_2c_1+f_3c_0=2+1+1=4\).

CDQ로 보면 solve(0,4): 왼쪽 solve(0,2)\(c_0,c_1\) 확정 → \(c[0..2)\ast f\)\(c_2,c_3\) 에 밀어 넣고 → solve(2,4)\(c_2,c_3\) 을 마저 확정. 결과 \(c=(1,1,2,4)\).

Lesson 2강 · 구현: CDQ 온라인 FFT 선택 8m

자기 참조 점화식 버전

가장 흔한 형태 \(c_0=1,\ c_n=\sum_{i=1}^{n} f_i c_{n-i}\) 를 그대로 구현한다. multiply 는 NTT 합성곱이다.

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll MOD = 998244353;
vector<ll> multiply(vector<ll> a, vector<ll> b); // NTT, 길이 a+b-1

int N;
vector<ll> f, c;   // f[1..N], c[0..N]

void solve(int l, int r){          // 반열린 구간 [l, r)
    if(r - l == 1){
        // c[l] 은 이 시점에 완전히 확정됨 (l==0 이면 기저값 그대로)
        return;
    }
    int m = (l + r) / 2;
    solve(l, m);
    // c[l..m) 와 f[0..r-l) 의 합성곱을 c[m..r) 에 더한다
    int lenA = m - l, lenB = r - l;
    vector<ll> A(c.begin()+l, c.begin()+m);        // c[l..m)
    vector<ll> B(f.begin(), f.begin()+lenB);       // f[0..r-l)  (f[0]=0)
    vector<ll> C = multiply(A, B);
    // A idx a -> c[l+a], B idx b -> f[b]; 곱 인덱스 a+b -> c[l+a+b] 에 기여
    for(int k = m; k < r; k++){
        int idx = k - l;                           // = a + b
        if(idx < (int)C.size())
            c[k] = (c[k] + C[idx]) % MOD;
    }
    solve(m, r);
}

int main(){
    // 예: f[i]=1 (i>=1), c[0]=1
    N = 8;
    f.assign(N+1, 0); c.assign(N+1, 0);
    for(int i=1;i<=N;i++) f[i]=1;
    c[0]=1;
    solve(0, N+1);
    for(int i=0;i<=N;i++) printf("%lld ", c[i]); // 1 1 2 4 8 16 32 64 128
}

f[0]=0 으로 두는 것이 중요하다. 그래야 \(c_k\) 가 자기 자신(\(f_0\) 항)으로 되먹임되지 않는다. 위 예시에서 \(c_n=2c_{n-1}\) 이 되어 \(2^n\) 이 나온다.

두 수열 일반형 (relaxed multiplication)

\(h_k=\sum_{i+j=k} f_i g_j\) 에서 \(f_k,g_k\)\(h_{ 에 의존하는 완전 일반형은, 왼쪽→오른쪽 밀기를 양쪽 수열에 대해 하되 왼쪽 최상위 블록의 이중 계산만 조심하면 된다.

vector<ll> F, G, H;   // 모두 크기 N (0-based)
void process(int k);  // H[k] 확정 시 호출: F[k], G[k] 를 정한다

void relaxed(int l, int r){
    if(r - l == 1){ process(l); return; }
    int m = (l + r) / 2;
    relaxed(l, m);
    // (1) F[l..m) * G[0..r-l)  ->  H[m..r)
    {
        vector<ll> A(F.begin()+l, F.begin()+m);
        vector<ll> B(G.begin(), G.begin()+(r-l));
        vector<ll> C = multiply(A, B);
        for(int k=m;k<r;k++){ int idx=k-l; if(idx<(int)C.size()) H[k]=(H[k]+C[idx])%MOD; }
    }
    // (2) 대칭 항 G[l..m) * F[0..r-l)  ->  H[m..r)
    //     단 l==0 이면 두 블록이 [0..m)x[0..m) 를 공유해 (1)과 겹치므로 생략
    if(l != 0){
        vector<ll> A(G.begin()+l, G.begin()+m);
        vector<ll> B(F.begin(), F.begin()+(r-l));
        vector<ll> C = multiply(A, B);
        for(int k=m;k<r;k++){ int idx=k-l; if(idx<(int)C.size()) H[k]=(H[k]+C[idx])%MOD; }
    }
    relaxed(m, r);
}

여기서 왜 \(l=0\) 일 때 (2)를 생략하는가? \(l=0\) 이면 (1)은 이미 \(F[0..m)\cdot G[0..m)\) 의 모든 쌍을 담고, (2)를 또 하면 대칭 쌍을 두 번 세게 된다. \(l\ne 0\) 이면 \(F[l..m)\)\(F[0..r-l)\) 의 인덱스 대역이 겹치지 않아 이중 계산이 없다.

실전 팁

  • 구간 길이가 작을 때(예: \(\le 32\))는 재귀·NTT 오버헤드를 피해 \(O(\text{len}^2)\) 직접 합성곱으로 분기하면 상당히 빨라진다.
  • multiply(A, B) 호출 전 두 조각 길이를 확인해, 한쪽이 비면 건너뛴다.
  • process 안에서 \(F,G\) 를 나눗셈으로 정의한다면 모듈러 역원을 쓴다(\(h[k]\) 로부터 \(f[k]\) 복원 등).
Lesson 3강 · 심화·응용: 뉴턴법과의 비교, 함정 선택 8m

온라인 FFT vs 다항식 뉴턴법

같은 자기 참조 생성함수 문제를 두 방향으로 풀 수 있다.

기준 온라인 FFT(CDQ) 뉴턴법(\(\exp/\log/\text{inverse}\))
복잡도 \(O(n\log^2 n)\) \(O(n\log n)\)
적용 범위 항별 비선형 관계도 OK 관계가 형식적 멱급수 항등식으로 닫혀야 함
구현 난이도 중 (분할 정복 하나) 상 (\(\exp,\log\) 스택)
상수 매우 큼

닫힌 형식(예: \(C=e^{F}\))으로 표현되면 뉴턴법이 \(\log\) 하나 더 빠르다. 하지만 "\(c_n\) 을 알아야 \(f_n\) 이 정해지는" 식의 온라인·비선형 결합에는 CDQ가 거의 유일한 범용 해법이다. 그래서 온라인 합성곱은 뉴턴법으로 안 닫히는 DP의 만능 열쇠로 통한다.

자주 하는 실수

함정

  • \(f_0\) 를 0으로 안 둠. 자기 참조 버전에서 \(f_0\ne 0\) 이면 \(c_k\) 가 자기 자신을 참조해 값이 오염된다. 커널의 상수항을 반드시 분리.
  • 구간을 반열린 \([l,r)\)/닫힌 \([l,r]\) 로 혼용. 인덱스 off-by-one 이 가장 흔한 버그. 하나로 통일하라. 위 구현은 반열린이다.
  • 밀어 넣는 대상 인덱스 오류. 조각 c[l..m) 의 국소 인덱스 \(a\) 는 전역 \(l+a\) 다. 곱 인덱스 \(a+b\) 가 전역 \(l+a+b=k\) 이므로 idx = k - l 로 접근해야 한다.
  • 이중 계산(두 수열 버전). \(l=0\) 최상위 블록에서 대칭 항 생략을 잊으면 대각 쌍이 2배가 된다.
  • 작은 구간 NTT 낭비. 길이 1~2 구간까지 NTT를 부르면 상수가 폭발한다. 기저 분기 필수.

응용 예시(문제 이름 없이 유형만)

  • 자기 참조 생성함수: \(C(x)=1+x\,C(x)^2\) 류(카탈랑)를 \(c_n=\sum c_i c_{n-1-i}\) 온라인으로. 곱이 \(c\ast c\) 라 두 수열 버전에서 \(F=G=c\) 로 두고 process 에서 \(c_k\) 를 확정.
  • 분할·트리 개수 DP: \(a_n = \sum_{i} a_i\, w_{n-i}\) 꼴에 고정 가중치 \(w\).
  • 확률/기댓값 점화식: 각 항이 이전 항들의 합성곱으로 정의되는 흡수 마르코프류.

복잡도·확장

  • 기본 \(O(n\log^2 n)\). 상수를 줄이려면 CDQ 내부 NTT에서 이미 변환된 \(f\) 조각을 캐싱하는 최적화가 알려져 있지만 구현이 복잡하다.
  • 여러 개의 서로 다른 커널이 섞이면 각 커널마다 밀기 단계를 추가하되 층당 총 길이가 여전히 \(O(n)\) 이면 복잡도는 유지된다.
  • 임의 모듈러가 필요하면 multiply 를 3-소수 CRT 합성곱으로 교체(차수 동일, 상수 3배).
Practice problem 알파카컵 1회: J - 알파카의 별빛 목장 선택 25m
A00010

알파카컵 1회: J - 알파카의 별빛 목장

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기
Practice problem 물음표가 섞인 패턴 찾기 선택 25m
R00632

물음표가 섞인 패턴 찾기

힌트를 열기 전에 상태와 전이를 먼저 찾아보세요.

Unrated 레이팅 미적용 지금 풀기