多项式基础内容小记

0. 基础知识:

关于多项式的定义:

  • 多项式:一个形如 \(f(x) = \sum_{i = 0} ^n a_i x^i\) 的有限和式被称为多项式。

  • 系数:多项式第 \(i\) 项的系数在上面就表示为 \(a_i\)

  • 度(次数):多项式中最高次数的项的次数就被称为该多项式的度,也称次数。

多项式表示法:

多项式有两种表示法:分别是 系数表示法点值表示法。其中系数表示法就形如上面给出的形式。

而点值表示法则是用不同的 \(n + 1\) 个点来表示一个 \(n\) 次多项式。其中 两点确定一条直线 就是一个很好的例子。如何快速的在两者中快速转化呢?这是我们要研究的问题。其中从点值表示法到系数表示法的过程叫插值,从系数表示法到点值表示法的过程叫求值。

1.拉格朗日插值:

这个东西的作用其实很大,可以用来优化在值域上很大,但是 转移与值域无关转移都是简单加乘\(\rm DP\),此时这个 \(\rm DP\) 值其实是关于值域的一个多项式,若可以归纳证明它是一个 有限次数且次数较小 的多项式,可以拉插优化。

接下来先研究一下如何快速插值。首先一个简单的想法是直接列出 \(n + 1\) 条方程式并使用高斯消元,但这样是 \(O(n^3)\) 的,能不能更快呢?

我们尝试换一种多项式的表现方式:考虑给每个 \(y_i\) 前面都加上一个系数 \(k_i\) 并求和,并当 \(x = x_i\) 时该 \(k_i\) 取到 \(1\),对于其他的情况取到 \(0\)。但这样还是比较困难的,注意到我们只关心该多项式的表达式在这 \(n + 1\) 的表现,于是考虑将 \(k_i\) 的定义变为:\(x\) 为任意 \(x_j(j \ne i)\) 时,\(k_i\) 取到 \(0\),其他情况取到 \(1\)。形式化的:令 \(f(x) = \sum_{i = 1} ^ {n + 1} k_iy_i\),其中 \(k_i = [\forall j(1 \le j \le n + 1, j \ne i), x \ne x_j]\)

于是我们只需要构造出合适的 \(k_i\) 满足上述条件即可,显然 \(\prod_{j \ne i} (x - x_j)\) 会在 \(x\) 取到任意 \(x_j, j \ne i\) 时取到 \(0\),但是当 \(x = x_i\) 时,该式子不是 \(1\),那就把多出来的除掉即可。于是得到 \(k_i = \prod_{j \ne i} \frac{x-x_j}{x_i-x_j}\)。于是 \(f(x) = \sum_{i = 1}^{n + 1}y_i \prod_{j \ne i} \frac{x-x_j}{x_i-x_j}\)。算法时间复杂度降到了 \(O(n^2)\),这便是拉格朗日插值。

值得一提的是,若 \(x\) 值连续,如 \(x_i = i\),那么不难发现柿子上下都是某个阶乘扣掉一个数,预处理阶乘和逆元容易做到 \(O(n)\)

P4781 【模板】拉格朗日插值

qwq
#include<bits/stdc++.h>
#define int long long
#define inv(x) qpow(x, mod - 2)
using namespace std;

const int N = 2e3 + 10, mod = 998244353;

int n, k, ans;
struct point{
	int x, y;
}p[N];

int qpow(int x, int y){
	int ret = 1;
	for(; y; y >>= 1, x = x * x % mod) if(y & 1) ret = ret * x % mod;
	return ret;
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> n >> k;
	for(int i = 1; i <= n; i++) cin >> p[i].x >> p[i].y;
	for(int i = 1; i <= n; i++){
		int sum1 = p[i].y, sum2 = 1;
		for(int j = 1; j <= n; j++){
			if(i == j) continue;
			sum1 = (sum1 * ((k - p[j].x + mod)) % mod) % mod;
			sum2 = (sum2 * ((p[i].x - p[j].x + mod) % mod)) % mod;
		}
		ans = (ans + sum1 * inv(sum2) % mod) % mod;		
	}
	cout << ans;

	// system("pause");
	return 0;
}

2.快速傅里叶变换(FFT)

接下来研究如何快速对多项式做乘法,其实也就是如何快速对两个系数数列做卷积。首先暴力卷积显然是 \(O(n^2)\) 的,很不好,考虑如何优化这个过程。

两个多项式乘法是 \(O(n^2)\) 的,但是把两个多项式不同的 \(n\) 个点值乘起来是 \(O(n)\) 的。于是现在我们可以把乘法拆成三个步骤:

  • 对于一个 \(n\) 次多项式 \(A\),找到任意 \(x_0, x_1 ..., x_n\), 求出 \(A(x_0), A(x_1) ..., A(x_n)\)\(B\) 同理。\((\rm DFT)\)

  • 求出 \(C_i = A(x_i) \times B(x_i)\)

  • 通过 \(C_{0, 1, ..., n + m}\) 插值插出 \(C\) 的系数序列。\((\rm IDFT)\)

DFT

先考虑 \(\rm DFT\) 如何做。首先考虑分治划分子问题,我们把 \(F\) 奇偶拆半,即 \(F(x) = F_e(x^2) + xF_o(x^2)\),其中 \(F_e\)\(F\) 中的偶次数项,\(F_o\)\(F\) 中的奇数次数项。但是对于每个 \(x\) 如果每次都这样做一次,显然太慢了。我们希望在同一层的 \(F_e, F_o\) 每次都可以重复利用以减小时间复杂度,但这样就要求 \(x\) 序列有一些特殊的性质。

这个时候就需要单位根啦!定义 \(n\) 次单位根 \(\omega_n = \cos(\frac{2\pi}{n}) + i\sin(2\pi)\),其实就是复平面单位圆上 \(n\) 等分的结果,他的 \(k\) 次方就相当于在上面转了 \(\frac{2\pi k}{n}\) 的弧度。(这个是因为复数相乘,模长相乘,幅角相加,在单位圆上模长都是 \(1\))。因此有以下结论:

  • \(\omega_n^{2k} = \omega_{n / 2}^k\)

  • \(\omega_n^{n + k} = \omega_n^{k}\)

  • \(\omega_n^{\frac{n}{2} + k} = -\omega_n^{k}\),其中 \(2 | n\)

这些性质均可以通过在单位圆上旋转来理解。

于是我们考虑到单位根的特殊性质,不妨直接令 \(x_i = \omega_n^{i}\),其中 \(n\)不小于多项式次数的最小 \(2\) 的整数次幂。(原因下面会讲)

那么 \(F(\omega_n^i) = F_e(\omega_{\frac{n}{2}}^i) + \omega_n^i F_o(\omega_{\frac{n}{2}}^i)\)

这里用到了第一个性质,每次都要满足 \(2 | n\),因此分治树上的每一层 \(n\) 都应该为偶数,那么 \(n\) 显然需要是一个 \(2\) 的整数次幂。然后不难发现,这里需要的分治下去的点值我们都已经求出来了,相当于一个子问题,时间复杂度 \(T(n) = 2T(n / 2) + O(n)\),即 \(T(n) = O(n \log n)\)

优化

先不急着讲 \(\rm IDFT\),可以看到 \(\rm FFT\) 的常数是非常大的其中精度问题也有。那么接下来给出一个常见的优化:蝴蝶变换。

考虑 \(\rm DFT\) 的过程:

  • \(a_i\) 的贡献向两边分,即从上至下的建立分治树

  • 从下往上的合并两个儿子的信息。

考虑画出这个分治树:

image


(不会画图就偷了一张/lh)

可以看到对于一个数 \(i\),最后在根节点上表现出来的位置是 \(rev(i)\)\(rev(i)\) 表示把 \(i\) 的二进制位翻转,比如 \(011\) 变为 \(110\)。那么不妨直接把这个东西提前交换过来。这样就不需要 \(O(n \log n)\) 的建树了,然后算答案时可以直接从下往上迭代,避免 \(\mathrm {vector}\) 传递带来的巨大开销。

IDFT

考虑 \(\rm DFT\) 的转移矩阵:

\[\begin{bmatrix} (\omega ^ 0) ^ 0 & (\omega ^ 0) ^ 1 & \cdots & (\omega ^ 0) ^ {n - 1} \\ (\omega ^ 1) ^ 0 & (\omega ^ 1) ^ 1 & \cdots & (\omega ^ 1) ^ {n - 1} \\ \vdots & \vdots & \ddots & \vdots \\ (\omega ^ {n - 1}) ^ 0 & (\omega ^ {n - 1}) ^ 1 & \cdots & (\omega ^ {n - 1}) ^ {n - 1} \\ \end{bmatrix} \]

对其求逆可以得到:

\[\frac 1 n \begin{bmatrix} (\omega ^ {-0}) ^ 0 & (\omega ^ {-0}) ^ 1 & \cdots & (\omega ^ {-0}) ^ {n - 1} \\ (\omega ^ {-1}) ^ 0 & (\omega ^ {-1}) ^ 1 & \cdots & (\omega ^ {-1}) ^ {n - 1} \\ \vdots & \vdots & \ddots & \vdots \\ (\omega ^ {-(n - 1)}) ^ 0 & (\omega ^ {-(n - 1)}) ^ 1 & \cdots & (\omega ^ {-(n - 1)}) ^ {n - 1} \\ \end{bmatrix} \]

那么 \(\rm IDFT\) 就相当于把一个点值组成的向量乘上这个逆矩阵,其实就是 \(\rm DFT\),只不过值不一样。但是由于 \(\omega_n^{-i} = \omega_n{n - i}\),那么只需要对求出来的点值序列先做一遍正常的 \(\rm DFT\),然后翻转一下就可以了。

至此,多项式乘法就可以在 \(O(n \log n)\) 时间解决了。

qwq
#include<bits/stdc++.h>
#define ll long long
#define db double
using namespace std;

const int N = 1 << 21 | 50;
const db pai = acos(-1);

struct CP{
    db a, b; // a + bi
    CP(db x = 0, db y = 0){a = x; b = y;}
    CP operator +(CP x){return CP(a + x.a, b + x.b);}
    CP operator -(CP x){return CP(a - x.a, b - x.b);}
    CP operator *(CP x){return CP(a * x.a - b * x.b, a * x.b + b * x.a);}
    CP operator /(int x){return CP(a / x, b / x);}
}w[N];

using Poly = vector<CP>;
int rev[N];

void init(int n){
    for(int i = 2; i <= n; i <<= 1){
        const int ls = i >> 1;
        for(int k = 0; k < n / 2; k++)
            w[ls + k] = CP(cos(2 * pai * k / i), sin(2 * pai * k / i));
    }
    const int mid = n >> 1;
    for(int i = 1; i < n; i++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) ? mid : 0);
}

void DFT(Poly &F){
    const int n = F.size();
    for(int i = 0; i < n; i++) if(i < rev[i]) swap(F[i], F[rev[i]]);
    for(int i = 2; i <= n; i <<= 1){
        const int mi = i >> 1;
        for(int j = 0; j < n; j += i){
            for(int k = 0; k < mi; k++){
                CP qwq = w[mi + k] * F[j + k + mi];
                F[j + k + mi] = F[j + k] - qwq; F[j + k] = F[j + k] + qwq;
            }
        }
    }
}

void IDFT(Poly &F){
    DFT(F); int n = F.size();
    reverse(&F[1], &F[n]);
    for(int i = 0; i < n; i++) F[i] = F[i] / n;
}

Poly operator *(Poly A, Poly B){
    int resiz = A.size() + B.size() - 1, n;
    for(n = 1; n < resiz; n <<= 1);
    A.resize(n); B.resize(n); init(n);
    DFT(A); DFT(B);
    for(int i = 0; i < n; i++) A[i] = A[i] * B[i];
    IDFT(A); return A;
}

#define gc getchar()

int read(){
    char c = gc; int x = 0;
    while(c < '0' || c > '9') c = gc;
    while(c >= '0' && c <= '9') x = (x << 3) + (x << 1) + c - '0', c = gc;
    return x;
}

void _out(int x){
     if(!x) return;
    _out(x / 10); putchar(x % 10 + '0');
}

void print(int x, char sp){
    _out(x);
    if(x == 0) putchar('0'); putchar(sp);
}

signed main(){
    int n, m; n = read(); m = read();
    Poly A(n + 1), B(m + 1);
    for(int i = 0; i <= n; i++) A[i].a = read();
    for(int i = 0; i <= m; i++) B[i].a = read();
    Poly C = A * B;
    for(int i = 0; i <= n + m; i++) print((int)(C[i].a + 0.1), ' ');

    return 0;
}

3. FMT/FWT

我们拓展一下卷积的形式,变为位运算卷积。即 or, and, xor。即给出多项式 \(A = (a_0, a_1 \dots a_n), B = (b_0, b_1, \dots b_n)\),求出 \(C = (c_0, c_1, \dots c_n)\),其中 \(c_i = \sum_{j \oplus k} a_j \times a_k\)\(\oplus\) 为 or, and, xor 中的一种。

我们类似 FFT 的思路,设计一个变换 \(F(A) \to B\),和其逆变换 \(iF(B) \to A\),满足 \(F, iF\) 都可以快速计算,同时还要满足 \(F(A) \times F(B) = F(A \oplus B)\)

首先注意到 or 和 and 都是相类似的,从高维视角看,一个是集合的并集而一个是集合的交集。下文考虑 or,那么我们考虑类似容斥的东西,设计 \(F(A) = \sum_{S' \subseteq A} a_{S'}\),那么容易有 \(F(A) \times F(B) = F(C)\)。那么考虑 \(iF(A)\) 的求法,这个容易有 \(iF(A) = \sum_{S' \subseteq A} (-1)^{|A - S'|} a_{S’}\)

考虑如何快速求出 \(F(A)\),我们从底到高依次考虑每一位 \(i\),那么此时对于 \(i - 1\) 位以下的卷积已经做好了,直接对第 \(i\) 位做卷积即可,那么此时只要让该位为 \(1\) 的加上对应的该位为 \(0\) 的即可。时间复杂度 \(O(n \log n)\)

对于 \(iF\) 的话,这个就相当于每次加入一个 \(0\) 就要乘上 \(-1\),那么此时就相当于该位为 \(1\) 的减去该位为 \(0\) 的。其他都是一样的。

对于 and 来说也是一样的,只不过换成后缀和。

这上面的都是基于 \(F\) 是高维前/后缀和的变换,该变换叫做 \(\rm FMT\)(莫比乌斯变换)。而对于 xor 的形式比较特殊,会给出另一种形式的变换,叫做 \(\rm FWT\)(快速沃尔士变换)。

(可以跳过这段)

还是考虑如何构造一个函数,即考虑构造 \(F(A) \times F(B) = F(A \operatorname{xor} B)\)。这里有一个人类智慧。令 \(h(a) = \operatorname{popcnt}(a) \bmod 2\),则有 \(h(a \operatorname{and} c) \operatorname{xor} h(b \operatorname{and} c) = h((a \operatorname{xor} b) \operatorname{and} c)\),这是因为 \(a \operatorname{xor} b\) 只会让原来有贡献的位要么贡献 -2 要么不变。也就是假设 \(g(a, b) = (\operatorname{popcnt}(a \operatorname{and} b) \bmod 2)\),则 \((g, \operatorname{xor})\) 具有结合律。那么对于 \(F\) 来说,我们要求的是 \((\times, \operatorname{xor})\) 具有交换律,我们该如何做一个 \(g(a, b) \to \times\) 的映射呢?可以考虑放到系数上。

具体的,我们构造 \(F(A) = \sum_{S} (-1)^{|S \cap A|} a_S\)。下面证明 \(F(A) \times F(B) = F(A \operatorname{xor} B)\)。(这里的 \(\operatorname{xor}\) 指的是异或卷积后的结果)。我们下面只考察某个集合 \(S\)\(F(A \operatorname{and} B)\) 的表现。

\[\begin{aligned} F(A \operatorname{xor} B) &= \sum_{A} \sum_{B} (-1)^{|S \cap (A \operatorname{xor} B)|} a_A b_B \\ &= \sum_A (-1)^{|S \cap A|} a_A \sum_{B} (-1)^{S \cap B} b_B \\ &= F(A) \times F(B) \end{aligned} \]

那么最关键的构造就搞好了。还是类似 \(\rm FMT\) 的思路,考虑第 \(i\) 位如何合并答案,注意到当选择的两个集合该位都为 \(1\)\(-1\) 才会产生贡献。于是相当于 \(0 = 0 + 1\), \(1 = 0 - 1\)。然后考虑 \(iF\) 的计算方式。对这个东西求逆还是比较抽象的,反正我不会,结论是乘上 \(\frac{1}{n}\) 就好了。

模板题代码
#include<bits/stdc++.h>
#define ll long long
#define pb emplace_back
#define pir pair<int, ll>
#define fi first
#define se second
#define inv(x) qpow(x, mod - 2)
#define il inline
#define mkpir make_pair
#define ull unsigned long long
#define umap unordered_map
using namespace std;

const int N = 2e5 + 10, M = 2e5 + 10;
const ll mod = 998244353, iv2 = (mod + 1) / 2ll;

/*
struct edge{
  int v, next;
}edges[M << 1];
int head[N], idx;

void add_edge(int u, int v){
  edges[++idx] = {v, head[u]};
  head[u] = idx;
}
*/

il ll qpow(ll x, ll y){
  ll ret = 1;
  for(; y; y >>= 1, x = x * x % mod) if(y & 1) ret = ret * x % mod;
  return ret;
}
il void chkmin(ll& x, ll y){if(y < x) x = y;}
il void chkmax(ll& x, ll y){if(y > x) x = y;}
il void chkmin(int& x, int y){if(y < x) x = y;}
il void chkmax(int& x, int y){if(y > x) x = y;}
il void chkmod(ll& x){x = (x + mod) % mod;}
il void ADD(ll& x, ll y){x += y; (x >= mod) ? (x -= mod) : 0;}
il void MUL(ll& x, ll y){x = x * y % mod;}
//#define int long long

struct Poly{
  int n, K;
  ll a[N]; // op = 1 : fmt/fwt, op = -1 : ifmt/ifwt
  void fmt_or(ll op){
    chkmod(op);
    for(int k = 1; k < n; k <<= 1)
      for(int i = 0; i < n; i += (k << 1))
        for(int j = 0; j < k; j++)
          ADD(a[i + j + k], a[i + j] * op % mod);
   }     
  void fmt_and(ll op){
    chkmod(op);
    for(int k = 1; k < n; k <<= 1)  
      for(int i = 0; i < n; i += (k << 1))
        for(int j = 0; j < k; j++)
          ADD(a[i + j], a[i + j + k] * op % mod);
  }
  void fwt_xor(ll op){
    if(op == -1) op = iv2;
    for(int k = 1; k < n; k <<= 1)
      for(int i = 0; i < n; i += (k << 1))
        for(int j = 0; j < k; j++){
          ll a0 = a[i + j], a1 = a[i + j + k];
          a[i + j] = op * (a0 + a1) % mod;
          a[i + j + k] = op * (a0 - a1 + mod) % mod;
        }
  } 
};

Poly operator |(Poly P1, Poly P2){
  //cerr << 1 << "\n";
  P1.fmt_or(1); P2.fmt_or(1);
  Poly ret; ret.n = P1.n;
  for(int i = 0; i < ret.n; i++) ret.a[i] = P1.a[i] * P2.a[i] % mod;
  ret.fmt_or(-1); return ret;
}
Poly operator &(Poly P1, Poly P2){
  P1.fmt_and(1); P2.fmt_and(1);
  Poly ret; ret.n = P1.n;
  for(int i = 0; i < ret.n; i++) ret.a[i] = P1.a[i] * P2.a[i] % mod;
  ret.fmt_and(-1); return ret;
}
Poly operator ^(Poly P1, Poly P2){
  P1.fwt_xor(1); P2.fwt_xor(1);
  Poly ret; ret.n = P1.n;
  for(int i = 0; i < ret.n; i++) ret.a[i] = P1.a[i] * P2.a[i] % mod;
  ret.fwt_xor(-1); return ret;
}
ostream& operator <<(ostream &out, Poly P){
  for(int i = 0; i < P.n; i++) out << P.a[i] << " ";
  out << "\n"; return out;
}
istream& operator >>(istream &in, Poly &P){
  for(int i = 0; i < P.n; i++) in >> P.a[i];
  return in;
}

signed main(){
  ios::sync_with_stdio(0);
  cin.tie(0); cout.tie(0);
  Poly A, B;
  cin >> A.K; B.K = A.K; A.n = B.n = (1 << A.K);
  cin >> A >> B;
  cout << (A | B) << (A & B) << (A ^ B);

  return 0;
}

posted @ 2024-07-30 20:33  Little_corn  阅读(48)  评论(0)    收藏  举报