다항식 연산 알고리즘 정리

다항식 곱셈

두 다항식 F(x)G(x)가 주어졌을 때, H(x) = F(x)G(x)를 계산한다.

NTT를 활용해 점값 표현으로 변환한 후 점별 곱셈을 수행하고, 역변환으로 결과를 얻는다. 시간 복잡도는 O(n log n)이다.

void poly_multiply() {
    read(n, m);
    FOR(i, 0, n) read(f[i]);
    FOR(i, 0, m) read(g[i]);
    
    int sz = 1, bit = 0;
    while(sz <= n + m) sz <<= 1, ++bit;
    FOR(i, 0, sz) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (bit - 1));
    
    ntt(f, sz, 1);
    ntt(g, sz, 1);
    FOR(i, 0, sz) f[i] = mul(f[i], g[i]);
    ntt(f, sz, -1);
    
    FOR(i, 0, n + m) print(f[i]);
}

다항식 역원

F(x)G(x) ≡ 1 (mod x^n)을 만족하는 G(x)를 구한다.

상수항 G(x)_0 = F(x)_0^(-1)로 시작하여 배수 확장한다. mod x^⌈n/2⌉의 해 G_0(x)를 이용해:

G(x) ≡ 2G_0(x) - F(x)G_0(x)^2 (mod x^n)

시간 복잡도는 T(n) = T(n/2) + O(n log n) = O(n log n)이다.

int tmp[N];
void poly_inv(int* src, int* dst, int deg) {
    if(deg == 1) {
        dst[0] = mod_pow(src[0], MOD - 2);
        return;
    }
    poly_inv(src, dst, (deg + 1) >> 1);
    
    int sz = 1, bit = 0;
    while(sz < deg << 1) sz <<= 1, ++bit;
    FOR(i, 0, sz) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (bit - 1));
    
    FOR(i, 0, deg) tmp[i] = src[i];
    FOR(i, deg, sz) tmp[i] = 0;
    
    ntt(dst, sz, 1);
    ntt(tmp, sz, 1);
    FOR(i, 0, sz) dst[i] = mul(dst[i], sub(2, mul(dst[i], tmp[i])));
    ntt(dst, sz, -1);
    FOR(i, deg, sz) dst[i] = 0;
}

다항식 제곱근

G(x)^2 ≡ F(x) (mod x^n)을 만족하는 G(x)를 구한다.

상수항은 F(x)_0의 제곱근이며, 배수 확장 공식은:

G(x) ≡ (G_0(x)/2) + (F(x)/(2G_0(x))) (mod x^n)

시간 복잡도는 O(n log n)이다.

다항식 로그함수와 지수함수

테일러 전개를 통해 다항식에 대한 lnexp를 정의한다:

  • exp(x) = Σ x^k/k! (x의 상수항이 0일 때 수렴)
  • ln(1+x) = Σ (-1)^(k+1) x^k/k (x의 상수항이 1일 때 수렴)

다항식 로그함수

G(x) ≡ ln(F(x)) (mod x^n), 단 F(x)_0 = 1

미분과 적분을 활용: G(x) = ∫(F'(x)/F(x)) dx

int der[N], inv_arr[N];
void poly_ln(int* src, int* dst, int deg) {
    int sz = 1, bit = 0;
    while(sz < deg << 1) sz <<= 1, ++bit;
    FOR(i, 0, sz) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (bit - 1));
    
    FOR(i, 0, sz) inv_arr[i] = 0;
    poly_inv(src, inv_arr, deg);
    
    FOR(i, 1, deg) der[i - 1] = mul(i, src[i]);
    FOR(i, deg - 1, sz) der[i] = 0;
    
    ntt(der, sz, 1);
    ntt(inv_arr, sz, 1);
    FOR(i, 0, sz) der[i] = mul(der[i], inv_arr[i]);
    ntt(der, sz, -1);
    
    FOR(i, 1, deg) dst[i] = mul(der[i - 1], mod_inv[i]);
    dst[0] = 0;
}

뉴턴 반복법

F(G(x)) ≡ 0 (mod x^n)의 근 G(x)를 구한다.

테일러 전개를 통해: G(x) ≡ G_0(x) - F(G_0(x))/F'(G_0(x)) (mod x^n)

다항식 지수함수

G(x) ≡ exp(F(x)) (mod x^n), 단 F(x)_0 = 0

ln(G(x)) - F(x) = 0의 근으로 변환하여 뉴턴 반복법 적용:

G(x) ≡ G_0(x)(1 - ln(G_0(x)) + F(x)) (mod x^n)

int buf[N];
void poly_exp(int* src, int* dst, int deg) {
    if(deg == 1) {
        dst[0] = 1;
        return;
    }
    poly_exp(src, dst, (deg + 1) >> 1);
    
    FOR(i, 0, deg) buf[i] = 0;
    poly_ln(dst, buf, deg);
    FOR(i, 0, deg) buf[i] = sub(src[i], buf[i]);
    add(buf[0], 1);
    
    int sz = 1, bit = 0;
    while(sz < deg << 1) sz <<= 1, ++bit;
    FOR(i, 0, sz) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (bit - 1));
    FOR(i, deg, sz) buf[i] = 0;
    
    ntt(dst, sz, 1);
    ntt(buf, sz, 1);
    FOR(i, 0, sz) dst[i] = mul(dst[i], buf[i]);
    ntt(dst, sz, -1);
    FOR(i, deg, sz) dst[i] = 0;
}

다항식 거듭제곱

G(x) ≡ F(x)^k (mod x^n), 단 F(x)_0 = 1

로그-지수 변환: G(x) = exp(k · ln(F(x)))

연속 점값 이동

f(0), f(1), ..., f(n)가 주어졌을 때 f(m), f(m+1), ..., f(m+n)를 계산한다.

라그랑주 보간법과 합성수 분해를 활용한 컨볼루션 최적화:

f(m+x) = Π(m+x-i) · Σ F(i)G(x+n-i)

여기서 F(i) = f(i)/(i!(n-i)!(-1)^(n-i)), G(i) = 1/(m-n+i)

Chirp Z-변환

다항식 F(x)에 대해 F(c^0), F(c^1), ..., F(c^(m-1))를 계산한다.

지수 항 ij를 이항계수로 분해: ij = C(i+j,2) - C(i,2) - C(j,2)

이를 통해 컨볼루션 형태로 변환: F(c^i) = c^(-C(i,2)) · Σ (F_j · c^(-C(j,2))) · c^(C(i+j,2))

임의 모듈러 다항식 곱셈

NTT를 지원하지 않는 모듈러 P에 대해, 세 NTT 소수 998244353, 469762049, 1004535809에서 각각 계산 후 CRT로 복원한다.

struct tri_int {
    int v1, v2, v3;
    // 연산자 오버로딩...
    int crt(int mod) {
        ll x = 1LL * P1 * mul_mod2(sub_mod2(v2, v1 % P2), INV1) + v1;
        int k = mul_mod3(sub_mod3(v3, x % P3), INV2);
        return (x + 1LL * k * (1LL * P1 * P2 % mod)) % mod;
    }
};

빠른 팩토리얼

n! mod PO(√n log n)에 계산한다.

블록 크기 B = √n으로 설정하고, F_d(x) = Π(i=1 to d)(x+i)의 점값 F_d(0), F_d(B), ..., F_d(dB)를 배수 확장으로 계산한다.

스털링 수

제2종 스털링 수 (행)

S(n, i) = Σ(j=0 to i) (-1)^(i-j) · j^n / (j!(i-j)!)

F_j = j^n/j!, G_j = (-1)^j/j!로 설정하여 컨볼루션 수행

제2종 스털링 수 (열)

EGF 접근: (e^x - 1)^k / k!의 계수

제1종 스털링 수 (행)

상승계수 x^(n̄) = Σ S₁(n,i) x^i를 배수 확장으로 계산

x^(2n̄) = x^(n̄) · (x+n)^(n̄), 후자는 점값 이동과 컨볼루션으로 획득

제1종 스털링 수 (열)

EGF 접근: ln(1/(1-x))^k / k!의 계수

태그: NTT FFT 다항식 스털링수 턴반복법

6월 29일 22:19에 게시됨