多项式笔记

多项式笔记

本文为阅读 OI-Wiki 书籍 学长slide 网上博客的笔记
多项式一般用来对一些函数的计算加速。

前置知识

多项式插值

\(n + 1\) 个横坐标互不相同的点可以唯一确定一个 \(n\) 次多项式。

复数

复数运算

已知 \(i^2 = -1\), 那么复数加减乘除全部就可以自己手推出来。

辐角和模长

复数的模与辐角是复数三角形式表示的两个基本元素,复数所对应的向量长度称为复数的幅值,该向量与实轴正方向的夹角为复数的辐角。辐角的大小有无穷多,但是辐角主值唯一确定。利用复数的模和辐角,可以将复数表示成三角表示式和指数表示式,并可以和代数表示式之间互相转化,以方便讨论不同问题时的需要。 ----百度百科

复数表示

代数表示法: \(z = x + yi\)
由辐角和模长定义可知三角表示法: $$z = r(cos\theta + isin\theta)$$
由欧拉公式可知 $$ e^{i\theta }=\cos \theta +i\sin \theta $$
指数表示法:\(z = re^{i\theta}\)

从指数表示法我们就可以看出, 复数乘法就是模长相乘, 辐角相加。

单位根

引理1:任意一个复数系一元 \(n\) 次多项式方程在复数域上恰好 \(n\) 个根。

我们记 \(x^n - 1 = 0\)\(n\) 个根分别记为 \(w_0, w_1...w_{n - 1}\)

\(w_{i}=\cos \dfrac{2i\pi }{n}+i\sin \dfrac{2i\pi }{n}\)

这式子可以从复数乘法的几何意义来来理解, 模长必定为1, 辐角乘 \(n\) 以后要与 \(x\) 轴正半轴重合, 所以单位就是把单位圆切割为正多边形的顶点。

性质:

\[\large \begin{aligned} \omega_n^n&=1\\ \omega_n^k&=\omega_{2n}^{2k}\\ \omega_{2n}^{k+n}&=-\omega_{2n}^k\\ \end{aligned} \]

这些性质的证明: 都可以通过几何意义来理解。

快速傅里叶变换 FFT

算法思路

我们知道 \(F(x)\)\(G(x)\), 我们要求 \(H(x) = F(x)G(x)\), 考虑暴力的话就是卷积 \(O(n^2)\), 为了避免卷积, 我们考虑插值, 如果插值的话, 就有 \(\forall_i H(x_i) = F(x_i)G(x_i)\)

但现在就有一个问题, 我们怎么实现快速插值并且将插值快速变换为多项式呢?

DFT

考虑我们要快速求出某个函数 \(f(x)\)\(x\) 取值为 \(\begin{Bmatrix}w_n^0, w_n^1...w_n^n\end{Bmatrix}\) 的值 \(F = \begin{Bmatrix}y_0, y_1...y_n \end{Bmatrix}\)。 则我们有 \(F(x) = \sum_{i = 0}^{n}f_ix^i\)。 我们想要快速求出 \(F\) 函数, 可以这么做:

\(F(x)\) 将系数 \(f_i\)\(x\) 奇偶分类则有:

\[f(x)=G\left(x^2\right) + x \times H\left(x^2\right) \]

考虑单位根有这样的性质, $\large ( w_n^k )^2 = w_n^2k = w_{ \frac{n}{2} }^k $

\[\large \begin{aligned} f(\omega_n^k) &= G((\omega_n^k)^2) + \omega_n^k \times H((\omega_n^k)^2) \\ &= G(\omega_n^{2k}) + \omega_n^k \times H(\omega_n^{2k}) \\ &= G(\omega_{n/2}^k) + \omega_n^k \times H(\omega_{n/2}^k) \end{aligned} \]

和:

\[ \large \begin{aligned} f(\omega_n^{k+n/2}) &= G(\omega_n^{2k+n}) + \omega_n^{k+n/2} \times H(\omega_n^{2k+n}) \\ &= G(\omega_n^{2k}) - \omega_n^k \times H(\omega_n^{2k}) \\ &= G(\omega_{n/2}^k) - \omega_n^k \times H(\omega_{n/2}^k) \end{aligned} \]

考虑我们用总共 \(n\) 个插值(包含 \(G\)\(H\)\(\frac{n}{2}\) 个), 就可以求出 \(f\)\(n\) 个插值, 本来我们暴力插值的话 一个插值 \(O(n)\) 总共 \(n^2\), 现在我们递归到 \(n = 1\) 是就可以 \(O(1)\) 算插值, 然后递归上来, 就可以在 \(O(nlogn)\) 的时间复杂度算出 \(f\)\(n\) 个插值。

IDFT

现在我们要从插值还原到多项式的系数。
考虑从线性代数的角度理解:

\[\large \begin{bmatrix}y_0 \\ y_1 \\ y_2 \\ y_3 \\ \vdots \\ y_{n-1} \end{bmatrix} = \begin{bmatrix}1 & 1 & 1 & 1 & \cdots & 1 \\ 1 & \omega_n^1 & \omega_n^2 & \omega_n^3 & \cdots & \omega_n^{n-1} \\ 1 & \omega_n^2 & \omega_n^4 & \omega_n^6 & \cdots & \omega_n^{2(n-1)} \\ 1 & \omega_n^3 & \omega_n^6 & \omega_n^9 & \cdots & \omega_n^{3(n-1)} \\ \vdots & \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & \omega_n^{n-1} & \omega_n^{2(n-1)} & \omega_n^{3(n-1)} & \cdots & \omega_n^{(n-1)^2} \end{bmatrix} \begin{bmatrix} a_0 \\ a_1 \\ a_2 \\ a_3 \\ \vdots \\ a_{n-1} \end{bmatrix} \]

左边向量为插值, 右边向量为系数。 考虑我们要求出系数, 就是给插值向量左乘一个 \(w\) 的逆矩阵。 逆矩阵就是 \(w_i^j\) 的倒数再除以 \(n\), 所以我们再把 \(y_i\) 当成一个新函数的系数, 以 $$\large\frac{1}{w_n^0 n}, \frac{1}{w_n^1 n}...$$ 为 \(x\) 的插值算出来就是系数。

考虑 \(w_n^i\) 的倒数就等于 \(\large\frac{1}{\omega_k}=\omega_k^{-1}=\mathrm{e}^{-\frac{2\pi \mathrm{i}}{k}}=\cos\left(\frac{2\pi}{k}\right)+\mathrm{i} \sin\left(-\frac{2\pi}{k}\right)\)。 所以我们只需要在 DFT 的代码上更改一个地方即可。

代码

点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef double db;
const db pi = acos(-1);
const int N = 3e6 + 10;
struct C{
	db x, y;
	C() { x = y = 0; }
	C(db _x, db _y) : x(_x), y(_y) {}
	inline C operator + (const C &a) { return C(x + a.x, y + a.y); }
	inline C operator - (const C &a) { return C(x - a.x, y - a.y); }
	inline C operator * (const C &a) { return C(x * a.x - y * a.y, x * a.y + y * a.x); }  
};
int n, m, lim, bit, rev[N];
void FFT(vector<C> &v, int op) {
	for(int i = 0; i < lim; i++) if(i < rev[i]) swap(v[i], v[rev[i]]);
	for(int k = 1; k < lim; k <<= 1) {
		C omega(cos(pi / k), op * sin(pi / k));
		for(int i = 0; i < lim; i += (k << 1)) {
			C w(1, 0);
			for(int j = 0; j < k; j++, w = w * omega) {
				C s = v[i + j], t = v[i + j + k] * w; 
				v[i + j] = s + t, v[i + j + k] = s - t;
			}
		}
	}
	if(op == -1)  for(int i = 0; i < lim; i++) v[i].x /= lim;
}
int main() {
	scanf("%d%d", &n, &m); 
	lim = 1 << (32 - __builtin_clz(n + m)), bit = 32 - __builtin_clz(n + m);
	vector<C> A(lim), B(lim);	
	for(int i = 0; i <= n; i++) scanf("%lf", &A[i].x);
	for(int i = 0; i <= m; i++) scanf("%lf", &B[i].x);
	for(int i = 0; i < lim; i++) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (bit - 1));
	FFT(A, 1); FFT(B, 1);
	for(int i = 0; i < lim; i++) A[i] = A[i] * B[i];
	FFT(A, -1); 
	for(int i = 0; i <= n + m; i++) printf("%lld ", (long long)(A[i].x + 0.5));
	return 0;
} 

快速数论变换 NTT

NTT 和 FFT 十分相似, 常见于多项式计算需要取模时使用, 考虑在 \(mod p\) 意义下的原根的具有和单位根相似的性质,也就是构成一个循环节。

其他和FFT非常类似, 就不写了。

多项式牛顿迭代

泰勒展开

image

就是一个用多项式逼近某个函数的公式, 非常的牛, 我也不会, 背住就好了。

牛顿迭代解决这样的一个问题:

给定多项式 \(g\left(x\right)\),已知有 \(f\left(x\right)\) 满足:

\[g\left(f\left(x\right)\right)\equiv 0\pmod{x^{n}} \]

求出模 \(x^{n}\) 意义下的 \(f\left(x\right)\)

算法

from OI_WIKI

\(g\left(f\left(x\right)\right) 在 f_{0}\left(x\right)\) 处进行泰勒展开,有:

\[\sum_{i=0}^{+\infty}\frac{g^{\left(i\right)}\left(f_{0}\left(x\right)\right)}{i!}\left(f\left(x\right)-f_{0}\left(x\right)\right)^{i}\equiv 0\pmod{x^{n}} \]

因为 \(f\left(x\right)-f_{0}\left(x\right)\) 的最低非零项次数最低为 \(\left\lceil\frac{n}{2}\right\rceil\),故有:

\[\forall 2\leqslant i:\left(f\left(x\right)-f_{0}\left(x\right)\right)^{i}\equiv 0\pmod{x^{n}} \]

则:

\[\sum_{i=0}^{+\infty}\frac{g^{\left(i\right)}\left(f_{0}\left(x\right)\right)}{i!}\left(f\left(x\right)-f_{0}\left(x\right)\right)^{i}\equiv g\left(f_{0}\left(x\right)\right)+g'\left(f_{0}\left(x\right)\right)\left(f\left(x\right)-f_{0}\left(x\right)\right)\equiv 0\pmod{x^{n}} \]

\[f\left(x\right)\equiv f_{0}\left(x\right)-\frac{g\left(f_{0}\left(x\right)\right)}{g'\left(f_{0}\left(x\right)\right)}\pmod{x^{n}} \]

这里需要求导:

所以比较常用的是这几个:
\(a' = 0\)

\((x^a)' = a(x^{a - 1})\)

\((a^x)' = (a^x)lna\)

\((log_ax)' = \large\frac{1}{xlna}\)

复合函数求导: \(f'(g(x)) = f'(g(x)) * g'(x)\)

多项式求逆

\[g(f(x)) \equiv \frac{1}{f(x)} - h(x) \equiv 0 \]

\(h(x)\) 给定。

带入公式即可。

多项式开根

\[g(f(x)) \equiv f^2(x) - h(x) \equiv 0 \]

多项式求导

就按照上面的求导规则即可, \((ax^2 + bx)' = (ax^2)' + (bx)'\)

多项式积分

把求导反着来。

多项式ln

\((lnf(x))' = \frac{1}{f(x)}\), 我们求个导再求个逆元再积分即可。

多项式exp

\[g(f(x)) \equiv ln(f(x)) - h(x) \equiv 0 \]

快速沃尔什变换 FWT

给两个长为 \(2^n\) 的序列 \(A\)\(B\)。 我们规定 \(A \times B = C\) 的运算规则是:

\[C_i = \sum_{j \bigotimes k = i} A_j * B_k \]

\(\bigotimes\) 常见的是 or, and, xor。 我们需要在 \(nlogn\) 时间得到 \(C\), 这里的 \(n\)\(2^n\)

我们参照 FFT, 通过插值来变换, 但是这里显然不能插值, 但是我们可以借鉴思想。 构造某种变换, 使得其可以快速变换和反演, 这也是算法精髓所在。

or

接下来我们讲 or 的做法。

我们构造 \(FWT[a]_i\) 表示通过序列 \(a\) 得出的 \(FWT[a]\) 序列, \(i\) 就是下标, 考虑 \(FWT[a]_i = \sum_{i or j = i} a_j\)

那么我们就有 \(FWT[a]_i * FWT[b]_i = FWT[c]_i\), 证明也很简单, 就不写了。

那么现在就考虑怎么快速得到 \(FWT[a]\), 我们可以根据下标的二进制把 \(FWT\) 分治, 分成以最高位为 \(1\) \(a1\), 和最高位为 \(0\) \(a0\) 的两段, 考虑边界就是每段长度为 1, 可以轻松得到, 现在我们就考虑怎么通过这两段分别的FWT推出一整段的FWT, 考虑\(FWT[a] = merge(FWT[a0], FWT[a0] + FWT[a1])\), 因为我们要枚举后半段的子集, 后半段的子集可以分为最高位为 1, 后面的随便选, 和最高位为 0, 后面的随便选。

现在我们就考虑怎么将 \(FWT[a]\) 还原回 \(a\), 考虑我们怎么从 \(a\) 来的, 其实就是FWT的逆过程, 我们把 \(FWT[a] 还原成FWT[a0] 和 FWT[a1]\) 再递归下去, 等到长度为 1 就可以直接是 \(a\)了。

那么就是 \(FWT[a0]\) 不变, \(FWT[a1] = FWT[a] - FWT[a0]\), 这里 \(FWT[a]\) 取后半段。

void OR(int *v, int type = 1) {
	for(int lim = 2, k = 1; lim <= n; lim <<= 1, k <<= 1) 
		for(int i = 0; i < n; i += lim) 
			for(int j = 0; j < k; j++) 
				v[i + j + k] = add(v[i + j + k], mul(v[i + j], type));	
}

参考代码理解, 写的迭代形式。

and

考虑仿照上面的做法, \(FWT[a]_i\) 就是 \(\sum_{i and j = i} a_j\)\(FWT[a] = merge(FWT[a1] + FWT[a0], FWT[a1])\)

xor

对于 xor, \(FWT[a] = merge(FWT[a0] + FWT[a1], FWT[a0] - FWT[a1])\)

拉格朗日插值法

如果 $$

posted @ 2024-07-08 10:53  qqrj  阅读(13)  评论(0编辑  收藏  举报