Contents
多项式合集
拉格朗日插值
问题背景
给出 个点 ,令这 个点确定的多项式为 ,求 的值。
结论
其中每个 为拉格朗日基本多项式,表达式为
其特点是 , 有
推导
抛开拉插,这道题明显可以列方程组然后使用高斯消元求解,但是复杂度为 且精度问题明显,所以拉格朗日是这样考虑的:
对于每个点 ,构造一个 次多项式 使其在 上取值为 ,在其余 上为 。构造的结果就是上面的结论:
这个多项式的正确性还是很显然的。然后我们也知道这个多项式它就是唯一的。
然后考虑构造答案:很显然对于点 ,只有 的取值为 ,其他的都为 。所以答案的正确性也是比较显然的:对于 ,只有 产生了贡献,其余的都是 。故这个多项式是正确的。
所以回到一开始,我们需要的就是
由于模数是质数,所以使用费马小定理求逆元,跑得飞快。
复杂度 ,求逆元就是个很小的常数
#include <cstdio>
#include <cctype>
#define il inline
typedef long long ll;
inline ll read()
{
char c = getchar();
ll s = 0;
bool x = 0;
while (!isdigit(c))
x = x | (c == '-'), c = getchar();
while (isdigit(c))
s = 10 * s + c - '0', c = getchar();
return x ? -s : s;
}
const ll maxn = 2e3 + 5, mod = 998244353;
ll x[maxn], y[maxn];
ll pow(ll base, ll p)
{
ll ans = 1;
base = (base + mod) % mod;
for (; p; p >>= 1)
{
if (p & 1)
ans = ans * base % mod;
base = base * base % mod;
}
return ans;
}
il ll inv(ll n)
{
return pow(n, mod - 2);
}
int main()
{
ll n = read(), k = read();
for (int i = 1 ; i <= n; ++i)
x[i] = read(), y[i] = read();
ll ans = 0;
for (int i = 1; i <= n; ++i)
{
ll prod1 = 1, prod2 = 1;
for (int j = 1; j <= n; ++j)
{
if (i == j)
continue;
prod1 = prod1 * (k - x[j]) % mod;
prod2 = prod2 * (x[i] - x[j]) % mod;
}
ans = (ans + prod1 * y[i] % mod * inv(prod2) % mod + mod) % mod;
}
printf("%lld\n", ans);
return 0;
}
拉格朗日插值与范德蒙矩阵
可以考虑将这 个点值表示为如下形式:
左边这个矩阵就是所谓的范德蒙德矩阵,记作 ,系数列向量记作 ,右边的记作 ,则很明显:
打开来看清楚些实际就是多项式 在每个点处的值:
我们把两边都乘上 :
就得到了 一定可以表示为某种形如
的形式,即 只与 与 有关。
所以不难发现对于一个要求的 ,都可以被表示为如下形式
构造的过程即需要考虑 时 ,其中 。 说明每一个 都要有 这个因式,然后又因为 ,所以最终构造出来就是上面的结果:
我们其实也可以利用拉格朗日插值来求范德蒙矩阵的逆阵,复杂度
快速傅里叶变换(FFT)
复数基础
定义虚数单位 ,把形如 的数称为复数,所有复数的集合称为复数集 。
复数一般使用 表示,表示为 ,这种形式称为复数的代数形式。 被称为复数的实部, 称为复数的虚部,未加说明的情况下一般认为 。很明显地,当 时,这个复数为纯虚数,当 时,这个复数为实数。
每个复数 都能对应平面直角坐标系里面的一个点 ,同样的也可以对应一个向量 。故定义复数的模为 。
定义复数的加法与乘法:
这都是比较显然的。
容易看出复数满足很多实数的运算律。
定义复数 的共轭复数为 ,不难发现 与 关于实轴对称。
复数既然可以对应平面直角坐标系中的向量,不难发现其可以使用其模长与辐角来表示:
其中 为 的模长, 为其辐角。即我们可以把一个复数表示成二元组 的形式。
现在考虑两个复数 与 相乘得到的结果:
于是我们可以概括复数乘法的法则:模长相乘,辐角相加。(上述推导需要掌握基本的三角恒等变换)
从欧拉公式到单位圆
给出复数指数幂的定义:
这个公式是由我也不会证明的泰勒展开推导出来的:
将 代入进去即可推导。
如果 ,我们就得到大名鼎鼎的欧拉公式:
更特殊地,如果 ,得到的就是下面这个神奇的式子:
复平面上我们可以定义类似于平面直角坐标系上的单位圆,单位圆上的所有复数构成集合 。这些复数都可以表示为 或 的形式。
多项式的表示法
系数表示法:顾名思义
点值表示法:
我们知道由一个多项式在 个点上的取值是可以唯一确定一个多项式的,其本质也就是线性方程组的解。所以一个 次多项式可以用 个点表示:
通过下面的这个形式我们看得出来其就是一个典型的线性方程组的形式,不难证明其解的唯一性。
并且我们发现点值表示法有一个很明显的优势:可以在 的时间内将两个多项式乘起来,只需把对应点的 乘起来即可。
通俗的来说,FFT 实现的就是快速求多项式乘法的过程:先把系数表示法转成点值表示法(DFT,离散傅里叶变换),乘完之后再把点值还原为插值(IDFT,离散傅里叶逆变换)。可是朴素的 DFT 需要的时间复杂度为 ,IDFT 还回其系数需要高斯消元是 的。而 FFT 利用了一些很特殊很特殊的值加速了 DFT 和 IDFT 的过程,使得总时间复杂度降低到了 。
单位复数根
解这个方程:
我们会发现这个方程在实数范围内只有 或者 个解。然而代数基本定理告诉我们这样的方程有 个复数域上的解。由模长相乘辐角相加我们知道因为最终 ,所以这些满足条件的 的模长必定也是 。然后需要满足他们的辐角的 倍能被 整除。
不难发现其就是 等分单位圆:
我们记 次单位根的第 个记为 ,不难发现 。由此可见,单位复数根具有一些非常好的性质比如:
利用这些性质,我们可以加速 DFT 的过程。FFT 就是利用分治思想加速求每个 的值
DFT
此时 DFT 的分治思想就是分开考虑奇次项和偶次项:
考虑
将其分为两个多项式
考虑两个新多项式:
不难发现
利用单位复数根的性质:
其中 。不难发现只要我们求得出 与 的话,就可以同时求出 和 。接下来再对 与 递归 DFT 即可。其时间复杂度函数是形如下面这样的:
所以总复杂度为
实际实现的时候一定要注意传进去的系数一定要是 个的,不然分治的过程中左右不一样会出问题。第一次传进去的时候就高位补 ,补成最高项次数为 的多项式。
void dft(int lim, complex *a)
{
if (lim == 1) return;//常数项直接返回
complex a1[lim >> 1], a2[lim >> 1];
for (int i = 0; i < lim; i += 2)
a1[i >> 1] = a[i], a2[i >> 1] = a[i + 1];//把系数按照奇偶分开
dft(lim >> 1, a1, type);//求 DFT(f_0())
dft(lim >> 1, a2, type);//求 DFT(f_1())
complex Wn = complex(cos(2.0 * pi / lim), sin(2.0 * pi / lim)), w = complex(1, 0);
for (int i = 0; i < (lim >> 1); ++i, w = w * Wn)
{
a[i] = a1[i] + w * a2[i];//求 DFT(f(\omega_n^k))
a[i + (lim >> 1)] = a1[i] - w * a2[i];//求 DFT(f(\omega_n^{k+\fracn2}))
}
return;
}
IDFT
好了现在假装我们已经求出了两个多项式的点值表达并已经将他们乘起来,但是我们最终还是要把他还原回去到系数表示的。这个过程就叫做 IDFT。
其实就是我们需要求解下面关于 的线性方程组:
我们将其乘上左边矩阵的逆:
模相同的正交列向量构成的矩阵的逆是转置的模分之一倍,所以:
所以不难发现,IDFT 其实就是再做了一遍 DFT,只不过是反起来的。只是算出来最后的系数结果都要除以点值的个数,反应在代码里面就是那个 lim
变量。
不难发现 的共轭就是虚部取反,所以可以在 DFT 函数里面传一个参数表示是否为 IDFT。
这样子一个递归版的 FFT 就写完了,总体的代码如下:
#include <cstdio>
#include <cctype>
#include <cmath>
#define FOR(i, a, b) for (int i = a; i <= b; ++i)
const int maxn = 2e6 + 5;
const double pi = acos(-1.0);
inline int read()
{
char c = getchar();
int s = 0;
while (!isdigit(c))
c = getchar();
while (isdigit(c))
s = 10 * s + c - '0', c = getchar();
return s;
}
struct complex
{
double x, y;
complex(double xx = 0, double yy = 0)
{
x = xx, y = yy;
}
} a[maxn], b[maxn];
complex operator+(const complex &a, const complex &b) {return complex(a.x + b.x, a.y + b.y);}
complex operator-(const complex &a, const complex &b) {return complex(a.x - b.x, a.y - b.y);}
complex operator*(const complex &a, const complex &b) {return complex(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x);}
void dft(int lim, complex *a, int type)//type = 1 DFT;type = -1 IDFT
{
if (lim == 1) return;//返回常数项
complex a1[lim >> 1], a2[lim >> 1];
for (int i = 0; i < lim; i += 2)
a1[i >> 1] = a[i], a2[i >> 1] = a[i + 1];
dft(lim >> 1, a1, type);
dft(lim >> 1, a2, type);
complex Wn = complex(cos(2.0 * pi / lim), type * sin(2.0 * pi / lim)), w = complex(1, 0);
for (int i = 0; i < (lim >> 1); ++i, w = w * Wn)
{
a[i] = a1[i] + w * a2[i];
a[i + (lim >> 1)] = a1[i] - w * a2[i];
}
return;
}
int main()
{
int n = read(), m = read();
FOR(i, 0, n) a[i].x = read();
FOR(i, 0, m) b[i].x = read();
int lim = 1;
while (lim <= n + m) lim <<= 1;//lim一定要大于 n + m
dft(lim, a, 1);
dft(lim, b, 1);
FOR(i, 0, lim)
a[i] = a[i] * b[i];//点值乘起来
dft(lim, a, -1);//IDFT还回去
FOR(i, 0, n + m)
printf("%d ", (int)(a[i].x / lim + 0.5));//最后要除那个数然后还原回去,四舍五入
return 0;
}
位逆序置换
然而,上面的代码连模板都跑不过去……
考虑继续优化 DFT 的过程。递归的过程中开了大量的空间并且常数巨大,考虑非递归写法。
只考虑我们对 到 操作:
递归的过程:
original 0 1 2 3 4 5 6 7
recursion#1 0 2 4 6 1 3 5 7
recursion#2 0 4 2 6 1 5 3 7
recursion#3 0 4 2 6 1 5 3 7
original bin 000 001 010 011 100 101 110 111
now bin 000 100 010 110 001 101 011 111
可见递归到最后的结果无非就是一个二进制反转。
所以我们可以考虑非递归,一开始就先把所有的数放到最后的位置,然后迭代的时候一步步还回去即可。这个过程就是位逆序置换(蝴蝶变换)
考虑处理出 二进制位翻转之后的数 。易知 。我们可以从小到大求 。很明显, 的二进制位是 右移一位,那么如果知道了 就可以很容易的求出 ,再分 的奇偶性判断就可以了。
举个例子:翻转 ,首先我们知道它的二分之一倍为 ,其翻转结果为 ,除以二变为 ,由于它是偶数所以前面不用补 。不难发现其就是一开始要求的翻转结果。
预处理翻转结果的代码:
while (lim <= n + m) lim <<= 1;
FOR(i, 0, lim - 1)
rev[i] = ((rev[i >> 1] >> 1) | (((i & 1) ? (lim >> 1) : 0)));
然后在处理翻转的时候只需要下面几行:
FOR(i, 0, lim - 1)
if (i < rev[i])
myswap(a[i], a[rev[i]]);
不难验证其正确性。
而且观察我们在求 时我们需要算两遍 ,复数的乘法常数很大,考虑使用临时变量记录以降低常数。
这样子的话迭代版的 DFT 过程就很好写了:
void DFT(int lim, complex *a, int type)
{
FOR(i, 0, lim - 1)
if (i < rev[i])
myswap(a[i], a[rev[i]]);//先预处理翻转完了的结果
for (int p = 2; p <= lim; p <<= 1)//模拟合并答案的过程,即为所谓的 n
{
int len = p >> 1;//即上面的 n / 2
complex Wp = complex(cos(2 * pi / p), type * sin(2 * pi / p));//处理出 p 次单位根
for (int k = 0; k < lim; k += p)//对每一个进行合并
{
complex w = complex(1, 0);//处理 \omega_p^0
for (int l = k; l < k + len; ++l, w = w * Wp)//开始合并
{
//此时的 a[l] 就是之前的 a1[i],a[len + l] 就是之前的 a2[i]
complex tmp = w * a[len + l];
a[len + l] = a[l] - tmp;//相当于上面的 a[i + (lim >> 1)] = a1[i] - w * a2[i]
a[l] = a[l] + tmp;//相当于上面的 a[i] = a1[i] + w * a2[i]
}
}
}
}
例题
总的一个非递归版 FFT 的实现如下(洛谷 P3803):
#include <cstdio>
#include <cctype>
#include <cmath>
#define FOR(i, a, b) for (int i = a; i <= b; ++i)
const int maxn = 3e6 + 5;
const double pi = acos(-1.0);
inline int read()
{
char c = getchar();
int s = 0;
while (!isdigit(c))
c = getchar();
while (isdigit(c))
s = 10 * s + c - '0', c = getchar();
return s;
}
template<typename T> inline void myswap(T &a, T &b)
{
T t = a;
a = b;
b = t;
return;
}
struct complex
{
double x, y;
complex(double xx = 0, double yy = 0)
{
x = xx, y = yy;
}
} a[maxn], b[maxn];
int rev[maxn];
complex operator+(const complex &a, const complex &b) {return complex(a.x + b.x, a.y + b.y);}
complex operator-(const complex &a, const complex &b) {return complex(a.x - b.x, a.y - b.y);}
complex operator*(const complex &a, const complex &b) {return complex(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x);}
void DFT(int lim, complex *a, int type)
{
FOR(i, 0, lim - 1)
if (i < rev[i])
myswap(a[i], a[rev[i]]);//先预处理翻转完了的结果
for (int p = 2; p <= lim; p <<= 1)//模拟合并答案的过程,即为所谓的 n
{
int len = p >> 1;//即上面的 n / 2
complex Wp = complex(cos(2 * pi / p), type * sin(2 * pi / p));//处理出 p 次单位根
for (int k = 0; k < lim; k += p)//对每一个进行合并
{
complex w = complex(1, 0);//处理 \omega_p^0
for (int l = k; l < k + len; ++l, w = w * Wp)//开始合并
{
//此时的 a[l] 就是之前的 a1[i],a[len + l] 就是之前的 a2[i]
complex tmp = w * a[len + l];
a[len + l] = a[l] - tmp;//相当于上面的 a[i + (lim >> 1)] = a1[i] - w * a2[i]
a[l] = a[l] + tmp;//相当于上面的 a[i] = a1[i] + w * a2[i]
}
}
}
}
int main()
{
int n = read(), m = read();
FOR(i, 0, n) a[i].x = read();
FOR(i, 0, m) b[i].x = read();
int lim = 1;
while (lim <= n + m) lim <<= 1;//补齐高位
FOR(i, 0, lim - 1)
rev[i] = ((rev[i >> 1] >> 1) | (((i & 1) ? (lim >> 1) : 0)));//先处理翻转完的结果
DFT(lim, a, 1);//DFT
DFT(lim, b, 1);//DFT
FOR(i, 0, lim)
a[i] = a[i] * b[i];//对处理出来的点值进行乘法
DFT(lim, a, -1);//IDFT
FOR(i, 0, n + m)
printf("%d ", (int)(a[i].x / lim + 0.5));
return 0;
}
使用 FFT 来求高精度整数乘法的实现(洛谷 P1919):
#include <cstdio>
#include <cstring>
#include <cmath>
#define FOR(i, a, b) for (int i = a; i <= b; ++i)
#define DEC(i, a, b) for (int i = a; i >= b; --i)
template<typename T> inline void myswap(T &a, T &b) {T t = a; a = b; b = t; return;}
typedef double db;
const int maxn = 3000000 + 5;
const db pi = acos(-1.0);
struct cmplx
{
db x, y;
cmplx(db xx = 0, db yy = 0) {x = xx, y = yy;}
} a[maxn], b[maxn];
cmplx operator+(const cmplx &a, const cmplx &b) {return cmplx(a.x + b.x, a.y + b.y);}
cmplx operator-(const cmplx &a, const cmplx &b) {return cmplx(a.x - b.x, a.y - b.y);}
cmplx operator*(const cmplx &a, const cmplx &b) {return cmplx(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x);}
char s1[maxn], s2[maxn];
int rev[maxn], ans[maxn];
void DFT(cmplx *f, int lim, int type)
{
FOR(i, 0, lim - 1)
if (i < rev[i])
myswap(f[i], f[rev[i]]);
for (int p = 2; p <= lim; p <<= 1)
{
int len = p >> 1;
cmplx Wp(cos(2.0 * pi / p), type * sin(2.0 * pi / p));
for (int k = 0; k < lim; k += p)
{
cmplx w(1, 0);
for (int l = k; l < k + len; ++l, w = w * Wp)
{
cmplx tmp = w * f[l + len];
f[l + len] = f[l] - tmp;
f[l] = f[l] + tmp;
}
}
}
return;
}
int main()
{
scanf("%s\n%s", s1, s2);
int n1 = -1, n2 = -1;
DEC(i, strlen(s1) - 1, 0) a[++n1].x = s1[i] - '0';
DEC(i, strlen(s2) - 1, 0) b[++n2].x = s2[i] - '0';
int lim = 1;
while (lim <= n1 + n2) lim <<= 1;
FOR(i, 0, lim - 1)
rev[i] = ((rev[i >> 1] >> 1) | (((i & 1) ? (lim >> 1) : 0)));
DFT(a, lim, 1);
DFT(b, lim, 1);
FOR(i, 0, lim)
a[i] = a[i] * b[i];
DFT(a, lim, -1);
FOR(i, 0, lim)
ans[i] = (int)(a[i].x / lim + 0.5);
FOR(i, 0, lim)
if (ans[i] >= 10) ans[i + 1] += ans[i] / 10, ans[i] %= 10, lim += (i == lim);
while (!ans[lim] && lim > -1) --lim;
if (lim == -1) puts("0");
else DEC(i, lim, 0) printf("%d", ans[i]);
return 0;
}
当然,千万要记得 IDFT 还回去的时候要除以 lim
,实在怕记不住就在 DFT 函数里面加几句话直接处理好
if (type == -1)
FOR(i, 0, lim - 1)
f[i].x /= lim;
可以应用 FFT 的一道题:洛谷 P3338 [ZJOI2014]力
题意:给定 ,定义
求
考虑暴力的话,这道题是 的,过不去,考虑转化式子:
我们尝试将其化为卷积的形式,令 ,且 ;,且 ,回代:
左边的部分已经是一个卷积的形式了,考虑继续化简右边。此时我们可以使用一个翻转的技巧,令 ,,则右半边的式子可以化为 。现在两边都化为卷积的形式了,可以愉快的使用 FFT 加速了。
即我们设多项式 ,,。再令 ,,不难发现答案 ,其中 和 分别为 和 中 的系数。
#include <cstdio>
#include <cmath>
#define FOR(i, a, b) for (int i = a; i <= b; ++i)
typedef double db;
const int maxn = 3e5 + 5;
const db pi = acos(-1.0);
struct cmplx
{
db x, y;
cmplx(db xx = 0, db yy = 0) {x = xx, y = yy;}
} a[maxn], b[maxn], c[maxn];
cmplx operator+(const cmplx &a, const cmplx &b) {return cmplx(a.x + b.x, a.y + b.y);}
cmplx operator-(const cmplx &a, const cmplx &b) {return cmplx(a.x - b.x, a.y - b.y);}
cmplx operator*(const cmplx &a, const cmplx &b) {return cmplx(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x);}
int rev[maxn];
template<typename T> inline void swap(T &a, T &b) {T t = a; a = b; b = t; return;}
void DFT(cmplx *f, int lim, int type)
{
FOR(i, 0, lim - 1)
if (i < rev[i])
swap(f[i], f[rev[i]]);
for (int p = 2; p <= lim; p <<= 1)
{
int len = p >> 1;
cmplx Wp(cos(pi / len), type * sin(pi / len));
for (int k = 0; k < lim; k += p)
{
cmplx w(1, 0);
for (int l = k; l < k + len; ++l, w = w * Wp)
{
cmplx tmp = w * f[l + len];
f[l + len] = f[l] - tmp;
f[l] = f[l] + tmp;
}
}
}
if (type == -1)
FOR(i, 0, lim - 1)
f[i].x /= lim;
return;
}
int main()
{
int n; scanf("%d", &n);
FOR(i, 1, n)
{
scanf("%lf", &a[i].x);
b[i].x = (1.0 / i / i);
c[n - i].x = a[i].x;
}
int lim = 1;
while (lim <= (n << 1)) lim <<= 1;
FOR(i, 0, lim)
rev[i] = ((rev[i >> 1] >> 1) | ((i & 1) ? (lim >> 1) : 0));
DFT(a, lim, 1), DFT(b, lim, 1), DFT(c, lim, 1);
FOR(i, 0, lim)
a[i] = a[i] * b[i], c[i] = b[i] * c[i];
DFT(a, lim, -1), DFT(c, lim, -1);
FOR(i, 1, n)
printf("%.3lf\n", a[i].x - c[n - i].x);
return 0;
}
留言