多项式笔记

多项式笔记

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

前置知识

多项式插值

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

复数

复数运算

已知 i2=1, 那么复数加减乘除全部就可以自己手推出来。

辐角和模长

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

复数表示

代数表示法: z=x+yi
由辐角和模长定义可知三角表示法: z=r(cosθ+isinθ)
由欧拉公式可知 eiθ=cosθ+isinθ
指数表示法:z=reiθ

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

单位根

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

我们记 xn1=0n 个根分别记为 w0,w1...wn1

wi=cos2iπn+isin2iπn

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

性质:

ωnn=1ωnk=ω2n2kω2nk+n=ω2nk

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

快速傅里叶变换 FFT

算法思路

我们知道 F(x)G(x), 我们要求 H(x)=F(x)G(x), 考虑暴力的话就是卷积 O(n2), 为了避免卷积, 我们考虑插值, 如果插值的话, 就有 iH(xi)=F(xi)G(xi)

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

DFT

考虑我们要快速求出某个函数 f(x)x 取值为 {wn0,wn1...wnn} 的值 F={y0,y1...yn}。 则我们有 F(x)=i=0nfixi。 我们想要快速求出 F 函数, 可以这么做:

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

f(x)=G(x2)+x×H(x2)

考虑单位根有这样的性质, (wnk)2=wn2k=wn2k

f(ωnk)=G((ωnk)2)+ωnk×H((ωnk)2)=G(ωn2k)+ωnk×H(ωn2k)=G(ωn/2k)+ωnk×H(ωn/2k)

和:

f(ωnk+n/2)=G(ωn2k+n)+ωnk+n/2×H(ωn2k+n)=G(ωn2k)ωnk×H(ωn2k)=G(ωn/2k)ωnk×H(ωn/2k)

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

IDFT

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

[y0y1y2y3yn1]=[111111ωn1ωn2ωn3ωnn11ωn2ωn4ωn6ωn2(n1)1ωn3ωn6ωn9ωn3(n1)1ωnn1ωn2(n1)ωn3(n1)ωn(n1)2][a0a1a2a3an1]

左边向量为插值, 右边向量为系数。 考虑我们要求出系数, 就是给插值向量左乘一个 w 的逆矩阵。 逆矩阵就是 wij 的倒数再除以 n, 所以我们再把 yi 当成一个新函数的系数, 以 1wn0n,1wn1n...x 的插值算出来就是系数。

考虑 wni 的倒数就等于 1ωk=ωk1=e2πik=cos(2πk)+isin(2πk)。 所以我们只需要在 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 十分相似, 常见于多项式计算需要取模时使用, 考虑在 modp 意义下的原根的具有和单位根相似的性质,也就是构成一个循环节。

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

多项式牛顿迭代

泰勒展开

image

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

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

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

g(f(x))0(modxn)

求出模 xn 意义下的 f(x)

算法

from OI_WIKI

g(f(x))f0(x) 处进行泰勒展开,有:

i=0+g(i)(f0(x))i!(f(x)f0(x))i0(modxn)

因为 f(x)f0(x) 的最低非零项次数最低为 n2,故有:

2i:(f(x)f0(x))i0(modxn)

则:

i=0+g(i)(f0(x))i!(f(x)f0(x))ig(f0(x))+g(f0(x))(f(x)f0(x))0(modxn)

f(x)f0(x)g(f0(x))g(f0(x))(modxn)

这里需要求导:

所以比较常用的是这几个:
a=0

(xa)=a(xa1)

(ax)=(ax)lna

(logax)=1xlna

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

多项式求逆

g(f(x))1f(x)h(x)0

h(x) 给定。

带入公式即可。

多项式开根

g(f(x))f2(x)h(x)0

多项式求导

就按照上面的求导规则即可, (ax2+bx)=(ax2)+(bx)

多项式积分

把求导反着来。

多项式ln

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

多项式exp

g(f(x))ln(f(x))h(x)0

快速沃尔什变换 FWT

给两个长为 2n 的序列 AB。 我们规定 A×B=C 的运算规则是:

Ci=jk=iAjBk

常见的是 or, and, xor。 我们需要在 nlogn 时间得到 C, 这里的 n2n

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

or

接下来我们讲 or 的做法。

我们构造 FWT[a]i 表示通过序列 a 得出的 FWT[a] 序列, i 就是下标, 考虑 FWT[a]i=iorj=iaj

那么我们就有 FWT[a]iFWT[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 就是 iandj=iajFWT[a]=merge(FWT[a1]+FWT[a0],FWT[a1])

xor

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

拉格朗日插值法

如果 $$

posted @   qqrj  阅读(18)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
点击右上角即可分享
微信分享提示