多项式与生成函数
# 多项式与生成函数
# 生成函数
母函数(又翻为生成函数),是求解递推关系巧妙的数学方法,他通过代数手段解决组合计数问题
先思考一个简单的问题:从数字 中取出一个或多个相加(每个数字最多只能用一次)能组合成几个数?每个数有几种组合?
我们构造一个多项式,把他展开
惊人的发现,每个数的组合数量和系数一样
母函数的作用就是这样:把组合问题的加法与幂级数的乘幂对应起来
普通母函数的定义:对于序列 构造函数 ,称 为序列 的母函数
母函数的实质是无限可微分函数的泰勒计数,有:
利用母函数的泰勒计数表达,有时可以简单的演算组合计数问题
在袋子中装 个水果,要求苹果是偶数个,香蕉数是 的倍数,句子最多 个,梨有 个或 个,给出 ,共有多少种情况?
写出生成函数:
最后一步根据牛顿二项式定理得到
# 幂级数
- 多项式:
- 形式幂级数:
形式幂级数之间可以有一些运算:
设
- 加法:
- 乘法:
我们发现,乘法和多项式乘法有共同之处
# 形式幂级数
记形式幂级数(或多项式) 的 项的系数为
形式幂级数的本质是序列,幂级数的本质是极限
# 形式幂级数的逆元
- 形式幂级数 的逆元:
- 逆元存在的条件:
常见的逆:
- 的逆元为
- 的逆元为
- 的逆元为 ,即
# 形式幂级数的求导
假设
定义 的导数为 记作 或
# 形式幂级数的积分
假设
定义 的积分为 记作
# 形式幂级数的复合
假设 ,
复合 定义为 ,满足 ,记为
# 形式幂级数的其他运算
- 设形式幂级数 满足 由此可以定义 和 ,或者可以定义 则可以计算
# 常生成函数
一个数列 对应的常数生成函数为
例: 有两种物品,其中取 个第 种物品的方案数为 ,取 个第二种物体的方案数为 ,求取 个物体的方案数
设
答案为
# 递推关系
斐波那契数列满足 ,求其生成函数
设
解得
我们可以通过这个常生成函数来求数列的第 项
把分数拆开,有:
那么:
上面的 可以使用待定系数法求解
# 指数生成函数
一个数列 对应的指数生成函数为
有两个物体,其中取 个第 种物品的方案数为 ,取 个第二种物体的方案数为 ,求取 个物体并排成一列的方案数
设
所以答案为
使用 函数就可以实现生成函数之间的运算了,比如
# FFT
朴素多项式乘法,求逆,开根号等的复杂度为
FFT / NTT 可以 内实现两个 次多项式的乘积
# 多项式的表示形式
假设 是一个 次多项式,则 的系数表示为
则 的点值表示为
那么也就是说 个点值可以表示一个 次多项式
在点值表示下 次多项式的乘法复杂度为
# 复数与单位根
复数的指数形式
,其中 ,
单位根
在复数域上的根称为 次单位根。 次单位根有 个,形式为
单位根的性质:
# 快速傅里叶变换
DFT(离散傅里叶变换)
将多项式 转化为其点值形式
这里如何求这个
这里有傅里叶变换的公式:
由于这个式子是一个线性的,所以我们能写成矩阵的形式
但是我们暴力求 也是 的,现在考虑一种分治做法
我们有:
把 拆成奇数项和偶数项:
设
则
- 当 时
- 当 时
不妨把 减小 继续把 控制在 的范围内
IDFT(逆离散傅里叶变换)
将多项式的点值表示 , 转化为其系数表示
我直接给出逆变换的公式
但是现在要思考如何理解这个公式
对于上面那个 DFT 的变换的矩阵
现在相当于已知 和 求 ,那么只需要求出 即可
尝试构造 ,我们尝试共轭矩阵
观察答案矩阵 的第 行 第 列
我们发现这是一个公差为 的等差数列求和,根据等差数列求和公式
得出答案矩阵为:
,
这样就可以求出 ,也就是我们上面的公式
现在我们已知了 DFT 和 IDFT 的公式,那么我们就可以进行 FFT 了
直接来看代码实现
首先,我们需要一个复数类,尽管 C++ 有复数类,常数太大了
struct Complex {
double x, y;
Complex (double x = 0, double y = 0) : x(x), y(y) {}
};
Complex operator + (Complex a, Complex b) { return Complex(a.x + b.x, a.y + b.y); }
Complex operator - (Complex a, Complex b) { return Complex(a.x - b.x, a.y - b.y); }
Complex operator * (Complex a, Complex b) { return Complex(a.x * b.x - a.y * b.y, a.x * b.y + a.y * b.x); }
2
3
4
5
6
7
8
递归实现 FFT
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1 << 22;
const double eps = 1e-6, pi = acos(-1);
complex<double> a[MAXN], b[MAXN], c[MAXN];
int n, m;
void FFT(complex<double> *a, int n, int inv) { // inv 为 1 时为 FFT, inv 为 -1 时为 IFFT
if (n == 1) return;
int mid = n >> 1;
complex<double> A1[mid + 1], A2[mid + 1];
for (int i = 0; i <= n; i += 2) {
A1[i >> 1] = a[i];
A2[i >> 1] = a[i + 1];
}
FFT(A1, mid, inv); FFT(A2, mid, inv);
complex<double> w0(1, 0), wn(cos(2 * pi / n), inv * sin(2 * pi / n));
for (int i = 0; i < mid; i++, w0 *= wn) {
a[i] = A1[i] + w0 * A2[i];
a[i + mid] = A1[i] - w0 * A2[i];
}
}
int main() {
scanf("%d %d", &n, &m);
for (int i = 0; i <= n; i++) {double x; scanf("%lf", &x); a[i].real(x);}
for (int i = 0; i <= m; i++) {double x; scanf("%lf", &x); b[i].real(x);}
int len = 1 << max((int)ceil(log2(n + m)), 1); //由于FFT需要项数为2的整数次方倍,所以多项式的次数len为第一个大于 n+m 的二的正整数次方
FFT(a, len, 1); FFT(b, len, 1);
for (int i = 0; i < len; i++) c[i] = a[i] * b[i];
FFT(c, len, -1);
for (int i = 0; i <= n + m; i++)
printf("%.0f ",(c[i].real() / len + eps));
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 蝴蝶变换
观察原序列和后序列的二进制之后,惊奇的发现,其实际上就是把二进制翻转了,我们可以用 来实现
实际上可以用 的递推式来实现,定义 表示 二进制反转后对应的值,有递推式:
通过这个递推式能求出
void change(complex<double> A[], int n) {
for (int i = 0; i < n; i++)
R[i] = (R[i >> 1] >> 1) + ((i & 1) ? n >> 1 : 0);
for (int i = 0; i < n; i++)
if (i < R[i]) swap(A[i], A[R[i]]); // 只需要换一次
}
2
3
4
5
6
然后有了这个思路,就可以直接构造出最后一层的下标然后从下往上模拟递归合并的过程了
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1 << 22;
const double eps = 1e-6, pi = acos(-1);
complex<double> a[MAXN], b[MAXN], c[MAXN];
int R[MAXN];
int n, m;
void change(complex<double> A[], int n) {
for (int i = 0; i < n; i++)
R[i] = (R[i >> 1] >> 1) + ((i & 1) ? n >> 1 : 0);
for (int i = 0; i < n; i++)
if (i < R[i]) swap(A[i], A[R[i]]);
}
void FFT(complex<double> *a, int n, int inv) { // inv 为 1 时为 FFT, inv 为 -1 时为 IFFT
change(a, n);
for (int m = 2; m <= n; m <<= 1) {
complex<double> w1(cos(2 * pi / m), inv * sin(2 * pi / m));
for (int i = 0; i < n; i += m) {
complex<double> wk(1, 0);
for (int j = 0; j < m / 2; j++, wk *= w1) {
auto x = a[i + j], y = wk * a[i + j + m / 2];
a[i + j] = x + y; a[i + j + m / 2] = x - y;
}
}
}
}
int main() {
scanf("%d %d", &n, &m);
for (int i = 0; i <= n; i++) {double x; scanf("%lf", &x); a[i].real(x);}
for (int i = 0; i <= m; i++) {double x; scanf("%lf", &x); b[i].real(x);}
int len = 1 << max((int)ceil(log2(n + m)), 1); //由于FFT需要项数为2的整数次方倍,所以多项式的次数len为第一个大于 n+m 的二的正整数次方
FFT(a, len, 1); FFT(b, len, 1);
for (int i = 0; i < len; i++) c[i] = a[i] * b[i];
FFT(c, len, -1);
for (int i = 0; i <= n + m; i++)
printf("%.0f ",(c[i].real() / len + eps));
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# NTT
# 原根
欧拉定理
若正整数 ,满足 ,则
阶
若正整数 ,满足 ,则 的最小正整数 称为 对模 的阶,记作
若 ,则称 为 的一个原根
阶的性质
假设 ,则
- 在模 意义下两两不同
原根的存在与判定
只有模 存在原根( 是奇质数)
原根的判定定理
设 , 为正整数且 。则 是 的原根当且仅当对于任意 的质因子 ,
如何寻找原根,假设这里我们需要寻找 的原根,那么有 在模 意义下各不相同,如果 是一个质数,那么这里有
一个数原根的个数为 但是第一个原根往往不是特别大,所以从小到大枚举 然后 去 check
若 不是原根,那么一定存在 使得 那么有 ,所以说如果出现一个 使得 ,那么就说明 不是原根
int rt = 1; // P 是一个质数
while (true) {
bool ok = true;
int v = 1;
for (int i = 1; i < P - 1; i++) {
v = v * rt % P;
if (v == 1) {
ok = false;
break;
}
}
if (ok) break;
rt += 1;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
设 ,又有 ,所以有
于是,我们只需要预处理出 的的因数 ,对于一个 如果存在一个 那么 就不是原根
指标
对于质数 ,假设 是 的一个原根,则 在模 意义下是 的一个排列。假设对于 有 则称 的指标为 ,记作
指标有着非常优秀的性质,也被称为离散对数:
如何求一个 可以使用 BSGS 算法
求所有的 可以使用 的递推
vector<ll> lg(P, 0);
for (int i = 0, v = 1; i < P - 1; i++) {
lg[v] = i;
v = v * rt % P;
}
2
3
4
5
# NTT 实现
假设质数 , 是 的原根,和 FFT 类似,我们用 代替
我们发现,这样子去选取 一样满足一些性质:
和 FFT 类似,只不过这里用原根来替换单位根,来进行一个模意义下的运算,FFT 中的 DFT,IDFT,依然成立
- NTT 的优点:快,精确
- NTT 的限制:模数需要是满足 的质数 ,
下面有些常见的模数:
非递归的 NTT
//NTT
int rev[MAXN], limit, bit;
void NTT (vector<ll> &a, int op) {
for (int i = 0; i < limit; i++)
if (i < rev[i]) swap(a[i], a[rev[i]]);
for (int m = 2; m <= limit; m <<= 1) {
ll gn = qpow(3, (MOD - 1) / m);
if (op == -1) gn = qpow(gn, MOD - 2);
for (int i = 0; i < limit; i += m) {
ll gk = 1;
for (int j = 0; j < m / 2; j++) {
ll x = a[i + j], y = gk * a[i + j + m / 2] % MOD;
a[i + j] = (x + y) % MOD;
a[i + j + m / 2] = (x - y + MOD) % MOD;
gk = gk * gn % MOD;
}
}
}
}
void convolution(vector<ll> A, vector<ll> B, vector<ll> &C) {
int n = A.size() - 1, m = B.size() - 1;
limit = 1, bit = 0;
while (limit <= n + m) limit <<= 1, bit += 1;
for (int i = 0; i < limit; i++)
rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (bit - 1));
A.resize(limit), B.resize(limit); C.resize(limit);
for (int i = n + 1; i < limit; i++) A[i] = 0;
for (int i = m + 1; i < limit; i++) B[i] = 0;
NTT(A, 1), NTT(B, 1);
for (int i = 0; i < limit; i++)
C[i] = A[i] * B[i] % MOD;
NTT(C, -1);
ll inv = qpow(limit, MOD - 2);
for (int i = 0; i < limit; i++)
C[i] = C[i] * inv % MOD;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 牛顿迭代
给定多项式 ,求满足 的形式幂级数
当 时, 可以求出
假设现在已经得到了 的解
有递归式:
# 多项式求逆
给定函数 ,有方程
应用牛顿迭代可得:
于是,我们就得到了 的递推是
根据主定理,时间复杂度为
# 多项式开方
设给定函数为 有方程:
应用牛顿迭代可得:
时间复杂度
# 多项式 exp
给定函数 ,有方程:
应用牛顿迭代可得:
时间复杂度为
# 拉格朗日差值
个点值 ,满足 ,它们唯一确定一个 次多项式
拉格朗日差值定理用于点值表示转化成多项式形式,我们需要构造出一个多项式,满足
首先,把 拆成 个函数相加的形式
特别的,我们可以令 当且仅当在 时 ,其他时候都为
那么很显然可以构造出 ,其中 是一个系数,我们把 带入
回代:
则:
考虑计算
- 特殊情况一:
只需要求一个 ,那么暴力 算即可
- 特殊情况二:
只需要求一个 ,并且满足 ,是可以优化到 的