RiseOJ는 solved.ac와 제휴 관계가 없습니다. 티어 아이콘 © solved.ac. solved.ac
← 루비 III

다중점 평가와 보간

다항식을 N개 점에서 O(N log² N)에 평가한다.

수학 Ruby III 루비 III
선수 지식: 다항식 연산
1강 다항식 다중점 평가와 보간 공식

문제

차수 \(< n\)인 다항식 \(P(x)\)와 점 \(x_0, \dots, x_{n-1}\)이 주어졌을 때:

  • 다중점 평가: 모든 \(P(x_i)\)를 구한다.
  • 보간(역연산): 점-값 쌍 \((x_i, y_i)\)에서 \(P\)를 복원한다.

순진하게 하면 평가는 \(O(n^2)\)(호너 \(n\)번), 보간은 라그랑주 \(O(n^2)\). 목표는 둘 다 \(O(n \log^2 n)\).

핵심 도구: subproduct tree

점들로부터 곱-다항식을 분할정복으로 미리 만든다.

$$ Z(x) = \prod_{i=0}^{n-1} (x - x_i). $$

리프는 \((x - x_i)\), 내부 노드는 두 자식의 곱(NTT). 트리 전체를 만드는 비용:

$$ T(n) = 2T(n/2) + O(n \log n) = O(n \log^2 n). $$

각 노드는 자기 구간의 점들에 대한 \(\prod (x - x_j)\)를 저장한다.

다중점 평가: 나머지로 내려가기

핵심 보조정리:

$$ P(x_i) = \big(P \bmod (x - x_i)\big). $$

더 일반적으로, 구간 점들의 곱 \(Z_L = \prod_{i \in L}(x - x_i)\)에 대해

$$ P(x_i) = \big((P \bmod Z_L) \bmod (x - x_i)\big) \quad (i \in L). $$

따라서 루트에서 \(P\)를 들고, 각 단계에서 왼/오른쪽 자식의 곱-다항식으로 다항식 나머지 를 취하며 내려간다. 나머지 연산이 NTT로 \(O(d \log d)\)이므로 전체 \(O(n \log^2 n)\).

eval(node, P):
    if leaf: return P(x_node)        # P는 상수
    P_L = P mod Z[left]
    P_R = P mod Z[right]
    return eval(left, P_L) ++ eval(right, P_R)

보간: 라그랑주 + 미분

라그랑주 보간:

$$ P(x) = \sum_{i} y_i \prod_{j \ne i} \frac{x - x_j}{x_i - x_j} = \sum_i \frac{y_i}{Z'(x_i)} \cdot \frac{Z(x)}{x - x_i}. $$

여기서 분모 \(\prod_{j\ne i}(x_i - x_j) = Z'(x_i)\)다(곱의 미분, 다른 항들이 0이 되어 남는 것). 따라서:

  1. \(Z(x) = \prod (x - x_i)\)와 그 도함수 \(Z'(x)\)를 구한다.
  2. 다중점 평가\(Z'(x_i)\)를 한꺼번에 계산한다.
  3. \(c_i = y_i / Z'(x_i)\).
  4. \(\sum_i c_i \cdot \dfrac{Z(x)}{x - x_i}\)를 subproduct tree 위에서 분할정복으로 합친다:
    $$ \frac{Z}{x - x_i} = Z_R \cdot \frac{Z_L}{x - x_i} + Z_L \cdot \frac{Z_R}{x - x_i} $$
    형태로 자식 결과를 결합(좌/오른쪽 곱을 교차 곱). 전체 \(O(n \log^2 n)\).

복잡도 요약

연산 순진 고속
다중점 평가 \(O(n^2)\) \(O(n \log^2 n)\)
보간 \(O(n^2)\) \(O(n \log^2 n)\)
subproduct tree \(O(n \log^2 n)\)

다음 강의에서 NTT 위에 다항식 나머지와 트리 구현을 본다.

2강 구현: subproduct tree와 나머지 공식

전제: NTT 다항식 연산

poly_mul(a, b)(NTT 곱)과 poly_inv(a, m)(역원 \(\bmod x^m\), 뉴턴법)이 있다고 가정한다. 모든 계산은 NTT 친화 소수(998244353) 위.

다항식 나머지: \(P \bmod D\)

차수 \(\deg P = n-1\), \(\deg D = m-1\). 뒤집기 트릭 을 쓴다. \(P = QD + R\)에서 \(Q\)를 먼저 구한다.

$$ Q = \left( \text{rev}(P) \cdot \text{rev}(D)^{-1} \bmod x^{\,n-m+1} \right)\text{를 다시 뒤집음}. $$

vector<ll> rev_(vector<ll> a){ reverse(a.begin(), a.end()); return a; }

vector<ll> poly_mod(const vector<ll>& P, const vector<ll>& D) {
    int n = P.size(), m = D.size();
    if (n < m) return P;
    int qsz = n - m + 1;
    vector<ll> rp = rev_(P), rd = rev_(D);
    rp.resize(qsz); rd.resize(qsz);
    vector<ll> q = poly_mul(rp, poly_inv(rd, qsz));
    q.resize(qsz); q = rev_(q);                  // 몫 Q
    vector<ll> qd = poly_mul(q, D);
    vector<ll> R(m - 1);
    for (int i = 0; i + 1 < m; i++)
        R[i] = (P[i] - (i < (int)qd.size() ? qd[i] : 0) % MOD + MOD) % MOD;
    return R;
}

subproduct tree 구축

vector<vector<ll>> tree;   // 1-indexed segment-tree 형태
int N;                     // 점 개수(2의 거듭제곱으로 패딩)

void buildTree(int node, int lo, int hi, const vector<ll>& xs) {
    if (lo == hi) { tree[node] = { (MOD - xs[lo]) % MOD, 1 }; return; } // (x - x_lo)
    int mid = (lo + hi) >> 1;
    buildTree(2*node, lo, mid, xs);
    buildTree(2*node+1, mid+1, hi, xs);
    tree[node] = poly_mul(tree[2*node], tree[2*node+1]);
}

다중점 평가

void evalDC(int node, int lo, int hi, vector<ll> P, vector<ll>& out) {
    if (lo == hi) { out[lo] = P.empty() ? 0 : P[0]; return; }   // P는 상수
    int mid = (lo + hi) >> 1;
    evalDC(2*node,   lo,    mid, poly_mod(P, tree[2*node]),   out);
    evalDC(2*node+1, mid+1, hi,  poly_mod(P, tree[2*node+1]), out);
}

vector<ll> multipoint(const vector<ll>& P, const vector<ll>& xs) {
    N = xs.size(); tree.assign(4 * N, {});
    buildTree(1, 0, N - 1, xs);
    vector<ll> out(N);
    evalDC(1, 0, N - 1, poly_mod(P, tree[1]), out);   // 먼저 P mod Z 로 차수 절감
    return out;
}

보간

vector<ll> deriv(const vector<ll>& a) {
    vector<ll> d(max(0, (int)a.size() - 1));
    for (int i = 1; i < (int)a.size(); i++) d[i-1] = a[i] * i % MOD;
    return d;
}

void interpDC(int node, int lo, int hi, const vector<ll>& c,
              vector<ll>& res) {
    if (lo == hi) { res = { c[lo] }; return; }
    int mid = (lo + hi) >> 1;
    vector<ll> L, R;
    interpDC(2*node,   lo,    mid, c, L);
    interpDC(2*node+1, mid+1, hi,  c, R);
    // L * Z[right] + R * Z[left]
    res = poly_add(poly_mul(L, tree[2*node+1]), poly_mul(R, tree[2*node]));
}

vector<ll> interpolate(const vector<ll>& xs, const vector<ll>& ys) {
    N = xs.size(); tree.assign(4 * N, {});
    buildTree(1, 0, N - 1, xs);
    vector<ll> Zp = deriv(tree[1]);              // Z'(x)
    vector<ll> denom(N);
    evalDC(1, 0, N - 1, poly_mod(Zp, tree[1]), denom);  // Z'(x_i)
    vector<ll> c(N);
    for (int i = 0; i < N; i++)
        c[i] = ys[i] % MOD * modpow(denom[i], MOD - 2) % MOD;
    vector<ll> res;
    interpDC(1, 0, N - 1, c, res);
    return res;
}

검증과 함정

  • 평가 결과를 보간에 넣어 원래 다항식이 복원되는지 round-trip 테스트.
  • 점이 서로 달라야 함(\(Z'(x_i) \ne 0\)). 중복 점은 미리 제거.
  • poly_mod 전에 Ptree[1]로 한 번 줄이면 상수 폭을 줄인다.

복잡도는 평가·보간 모두 \(O(n \log^2 n)\). \(n \le 10^5\)급에서 안정적이다.