今日も、明日も、輝いている。|

Aquizahv

园龄:2年粉丝:0关注:7

2024-10-17 17:17阅读: 43评论: 0推荐: 0

【学习笔记】多项式

多项式,OI的魅力,OIer的噩梦。

多项式

这个大家应该都会。

对于式子 i=0kaixi,且 ak0,那么我们称它是关于 xk 次多项式。k 为次数。

就是这么简单

(一般型)生成函数

注:本小节的“生成函数”都指一般型生成函数。

定义:对于序列 a0,a1,a2,a3,...,其生成函数为

f(x)=a0+a1x+a2x2+a3x3+...

组合意义

举几个例子:

  1. ai= i  0 ,则有 f1(x)=1+x+x2+x3+x4+...

  2. ai= i  1 ,则有 f2(x)=0+x+2x2+3x3+4x4+...

  3. ai=i  Alice  Bob ,则有 f3(x)=0+x+22x2+32x3+42x4+...

好了,这就是生成函数的定义。但是现在你肯定会问,这东西有啥用呢?

幼儿园我们就学过,这种有无限项相加的式子可以用“错位相减”之类的办法化成一个有限的表达方式。

那么我们就试试生成函数是否同样可以:

f1(x)=i=0xixf1(x)=i=1xi(1x)f1(x)=1f1(x)=11xf2(x)=i=0ixixf2(x)=i=0ixi+1=i=1(i1)xi(1x)f2(x)=i=0ixii=1(i1)xi=i=1ixii=1(i1)xi=i=1xi=i=0xi1=f1(x)1=11x1=x1xf2(x)=x(1x)2

确实可以。即使它看起来很反常识,但这就是将一个无穷序列“浓缩”起来的表示方式。

类似地,还有f3(x)=i=0i2xi=x2+x(1x)3,请读者自己尝试推导。

顺带一提,无限项的式子可以浓缩,有限也可以。比如可以利用等比数列求和公式把 1+x+x2+x3+...+xk 写成 1xk+11x

所以到底有啥用

别急嘛,接下来看几个例题你就明白了。

例题 1:有一个多重集 A,其中值为 i 的元素有 ai 个。还有一个多重集 B,其中值为 i 的元素有 bi 个。问:多重集 C=AB 中值为 i 的有多少个?

显然,是 ci=ai+bi。算出 AB 的生成函数然后两式相加即可。

例题 2:还是刚刚的 AB,你可以从两集合内各取一个元素,然后将和扔进 C。问: C 中值为 i 的有多少个?

你想了一下,然后还是很快反应过来了——将两多项式相乘即可。

那如果。。。我想取出 k 的倍数,或者不超过 k 个,那么阁下又该如何应对?

例题 3:现在有 A,B,C,D 四种水果,每种都有无限个。现在要恰好取出 n 个水果,但水果 A 必须取偶数个,水果 B 必须取 5 的倍数个,水果 C 最多只能取 4 个,水果 D 最多只能取 1 个。问:方案数是多少?

首先考虑倍数怎么办。A 必须取 2 的倍数个,也就是说偶次项系数为 1,奇次项系数为 0。即 A(x)=1+x2+x4+x6+...,不难得出 A(x)=11x2

同理可得 B(x)=1+x5+x10+x15+...=11x5

此时,一条显然的规律也出来了:i=0,d|ixi=11xd

然后再考虑「至多」,此时的你应该也能发现,这就是上面说的有限项数的函数。

比如,C(x)=1+x+x2+x3+x4,利用公式写成 1x51x

同理,D(x)=1+x=1x21x

最后总方案数的生成函数就是:

A(x)×B(x)×C(x)×D(x)=11x2×11x5×1x51x×1x21x=1(1x)2

1(1x)2 的意义可以参考例题二,也就是两个集合内都有无限个元素 0,1,2,3,...,然后每个集合选一个出来,再求和。

因此你就可以通过它的意义来还原多项式:

1(1x)2=1+2x+3x2+4x3+...

解释:0=0+01=0+1=1+02=0+2=1+1=2+0...

是不是很神奇?那么复杂的问题,它的答案就是 n+1

不信?那你可以随意拿几个数试试哦。

洛谷P2000 拯救世界

不用多说,就是刚刚那题的加强版。建议先手动推一推。

最后你会发现答案是 1(1n)5,也就是选 5 个非负整数,使得和为 n 的方案数。这个东西就是小学学的插板法,n 个苹果分给 5 个小朋友,可以有小朋友没有,方案数是 (n+5151)=(n+44)=(n+1)(n+2)(n+3)(n+4)24

别急,再看看数据范围——呃?这题还卡普通高精乘,看来得用 FFT qwq。

不过你用 Ruby 写倒也不是不行

LOJ #6077. 「2017 山东一轮集训 Day7」逆序对

题解

线性递推

定理:若对于生成函数 f(x),其第 n 项系数 an=c1an1+c2an2+...+cmanm,则称其为「m 阶递推」,该生成函数为

f(x)=a0+a1x+a2x2+...+am1xm11c1xc2x2...cmxm

比较经典的一个例子是 Fibonacci 数列。其中,F0=0,F1=1,并且后一项为前两项之和。则根据上面定理,不难得到关于它的生成函数

F(x)=x1xx2

可以拿 Fibonacci 数列 x+x2+2x3+3x4+5x5+8x6+... 乘上分母验证一下。

P4451 [国家集训队] 整数的lqp拆分

题意

序列 a 满足:

m>0a1,a2,...,am>0a1+a2+...+am=n

i=1mFai,对 109+7 取模。

1n1010000

题解

一道线性递推生成函数的综合运用题。

首先,根据乘法原理,m 项时的生成函数 fm(x)=Fm(x)

因此,答案为下面生成函数的第 n 项系数:

g(x)=m=1fm(x)=m=1Fm(x)=m=0Fm(x)1=11F(x)1=11x1xx21=1xx212xx21=x12xx2

然后我们就求出了答案的递推序列!

a0=0,a1=1,后一项为两倍的前一项加上再前一项。

a0=0,a1=1,a2=2,a3=5,a4=12,...

矩阵快速幂即可。时间复杂度 O(logn)

n=1010000 时,数量级差不多是 8×33219

拉格朗日插值法

众所周知,在平面直角坐标系中,(p+1) 个横坐标互不相同的点就可以唯一确定一个 p 次函数。这一点在代码上可以用高斯消元来解决。那么就来看看这个题:洛谷P4781 【模板】拉格朗日插值

这时候我们看了眼数据范围:

遗憾地发现高斯消元过不去了。

由于这题并不需要解出函数,只需求出 f(k) 的值,所以就可以引入一个新东西:拉格朗日插值法

第一步,我们构造 (p+1)fi(x),分别为「将 xi 带入可以返回 yi,但是将别的 xj 带入都会返回 0」的函数。显然可以用以下方式构造:

fi(x)=yiijxxjxixj

第二步,我们计算出 f(x)=f1(x)+f2(x)+...+fp+1(x),显然题目给出的所有坐标都在此函数上,并且次数为 p。所以它就是我们需要求的函数!这样一来,把 k 带入每个 fi,返回值加起来,就能得到 f(k)

聪明的你这时也一定发现了,这个思想和中国剩余定理有着异曲同工之妙:对每个条件针对性构造,且同时对于其余条件都返回单位元,最后求和(积)即可。

不过由于这题需要取模,所以需要求逆元。如果模拟上式的计算,时间复杂度是 O(n2logp)

观察上式,发现可以每轮单独记录分子之积和分母之积,因此枚举完 j 再算总分母的逆元即可。时间复杂度降至 O(n2+nlogp)

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 2e3 + 5, MOD = 998244353;
int n, k, x[N], y[N], ans;
inline int madd(int x, int y) { return x + y >= MOD ? x + y - MOD : x + y; }
inline int msub(int x, int y) { return madd(x, MOD - y); }
inline int mmul(int x, int y) { return 1ll * x * y % MOD; }
inline int qpow(int x, int y) { int res = 1, t = x % MOD;
while (y) { if (y & 1) res = mmul(res, t);
t = mmul(t, t); y >>= 1; }
return res; }
inline int mdiv(int x, int y) { return mmul(x, qpow(y, MOD - 2)); }
int main()
{
cin >> n >> k;
for (int i = 1; i <= n; i++)
{
scanf("%d%d", x + i, y + i);
if (k == x[i])
{
cout << y[i] << endl;
return 0;
}
}
for (int i = 1; i <= n; i++)
{
int bdiv = 1, div = 1;
for (int j = 1; j <= n; j++)
if (i != j)
{
bdiv = mmul(bdiv, msub(k, x[j]));
div = mmul(div, msub(x[i], x[j]));
}
ans = madd(ans, mmul(y[i], mdiv(bdiv, div)));
}
cout << ans << endl;
return 0;
}

[省选联考 2022] 填树

一道很好的拉插优化 dp 题。

Easy version

n200ri,K200000

假设树上非零最小权为 x。那么此时树上的非零权值的范围是 [x,x+K]。于是对于每个节点 i,其取值范围是 [max(li,x),min(ri,x+K)]。然后就可以树形 dp 了。

具体地,我们记 fu 表示以 u 为根的链的方案数,gu 表示以 u 为根的链的权值之和。

那么转移就类似求直径的方法,加入儿子时先计算对答案的贡献,然后更新 dp 数组的值。时间复杂度是 O(n) 的。这个大家都会,就只放代码了。

点击查看代码
int f[N], g[N], sum1, sum2;
inline void add(int &to, int from) // op为1表示加,-1表示减,后面会介绍为什么有加有减
{
if (op > 0)
to = madd(to, from);
else
to = msub(to, from);
}
void dfs(int u, int fa)
{
int ln = max(L, l[u]), rn = min(R, r[u]);
if (ln > rn)
ln = 1, rn = 0;
int x = rn - ln + 1, y = sum(ln, rn); // 本节点的可取值的个数、可取值的和
f[u] = x, g[u] = y;
add(sum1, f[u]);
add(sum2, g[u]);
for (int v : G[u])
{
if (v == fa)
continue;
dfs(v, u);
// 统计答案
add(sum1, mmul(f[u], f[v]));
add(sum2, madd(mmul(g[u], f[v]), mmul(g[v], f[u])));
// 更新dp
f[u] = madd(f[u], mmul(f[v], x));
g[u] = madd(g[u], madd(mmul(y, f[v]), mmul(g[v], x)));
}
}

以上是对一个 [x,x+K] 的 dp,那现在就要滑动一个长度为 (K+1) 的区间,每个区间进行 dp。但是我们发现一个问题:对于 [x,x+K][x+1,x+K+1] 的答案,在区间 [x+1,x+K] 的答案会被计算两次。因此滑动一个单位前还要减去相交的部分的答案。

于是你已经成功切掉了 Easy version。

Hard version

n200ri,K109

我们再看一下整个过程,无非就是移动一个区间,然后每次进行一回 dp。但是每次 dp 只是取值不同,过程是一样的。是不是有点浪费了呢?

考虑某一个节点,在移动 [x,x+K] 的区间时,它的「可取值的个数」是先 0(没有交),然后变大、不变、变小,最后又变成 0。不难发现可以分成几个「段」,「分段点」分别是 [liK,li],[li,li+K],[riK,ri],[ri,ri+K]。在每个「段」,「可取值的个数」是一个至多一次的函数。同理,「可取值的和」是一个至多二次的函数。

设输出的第一问的答案为 sum1,第二问的答案为 sum2

如果我们把根据个节点的「分段点」划分成若干「段」,根据 dp 的过程,对于 sum1 的贡献可以看成一个至多 n 次的多项式,理由是它最高次相当于至多 n 个「可取值的个数」的一次式相乘的级别,并且这个贡献不可能超过所有节点的「可取值的个数」的乘积。

同理,对于 sum2 的贡献可以看成一个至多 n+1 次的多项式,因为其最高次相当于某一个节点的「可取值的和」的二次式,与其余 n1 个「可取值的个数」的一次式相乘的级别。

综上,对于每个「段」,对 sum1 的贡献是可以看成一个关于 xn 次多项式,对 sum2 的贡献是可以看成一个关于 xn+1 次多项式。于是就可以每「段」求出 n+2 个 dp 值,然后……等等!由于我们算的是整个「段」的答案,即 dp(x),因此对每个 x 拉插出答案还不如 dp 呢!不过这个好办,我们记录 dp 的前缀和 S(x) 即可。显然这样每「段」构成多项式,次数还是 nn+1

分析时间复杂度,对于每个「段」,时间复杂度分为 O(n) 次的 O(n) 的 dp 和 O(n2) 的拉插。由于「段」的个数是 O(n) 级别的,所以总复杂度 O(n3)。于是这题就做完了。

代码很好写的,如果有些地方不太能理解的话建议看看代码。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int N = 205, MOD = 1e9 + 7;
int n, k, m;
int l[N], r[N], L, R, op, pos[N << 2];
vector<int> G[N];
inline int madd(int x, int y) { return x + y >= MOD ? x + y - MOD : x + y; }
inline int msub(int x, int y) { return madd(x, MOD - y); }
inline int mmul(int x, int y) { return 1ll * x * y % MOD; }
inline int qpow(int x, int y) { int res = 1, t = x % MOD;
while (y) { if (y & 1) res = mmul(res, t);
t = mmul(t, t); y >>= 1; }
return res; }
inline int mdiv(int x, int y) { return mmul(x, qpow(y, MOD - 2)); }
int sum(int l, int r)
{
return 1ll * (l + r) * (r - l + 1) / 2 % MOD;
}
int f[N], g[N], sum1, sum2;
inline void add(int &to, int from)
{
if (op > 0)
to = madd(to, from);
else
to = msub(to, from);
}
void dfs(int u, int fa)
{
int ln = max(L, l[u]), rn = min(R, r[u]);
if (ln > rn)
ln = 1, rn = 0;
int x = rn - ln + 1, y = sum(ln, rn);
f[u] = x, g[u] = y;
add(sum1, f[u]);
add(sum2, g[u]);
for (int v : G[u])
{
if (v == fa)
continue;
dfs(v, u);
add(sum1, mmul(f[u], f[v]));
add(sum2, madd(mmul(g[u], f[v]), mmul(g[v], f[u])));
f[u] = madd(f[u], mmul(f[v], x));
g[u] = madd(g[u], madd(mmul(y, f[v]), mmul(g[v], x)));
}
}
int X[N], Y[2][N];
int lag(int resx, int p, int *x, int *y)
{
int resy = 0;
for (int i = 0; i <= p; i++)
{
int bd = 1, d = 1;
for (int j = 0; j <= p; j++)
if (i != j)
{
bd = mmul(bd, msub(resx, x[j]));
d = mmul(d, msub(x[i], x[j]));
}
resy = madd(resy, mmul(y[i], mdiv(bd, d)));
}
return resy;
}
int main()
{
#ifdef aquazhao
freopen("data.in", "r", stdin);
freopen("data.out", "w", stdout);
#endif
cin >> n >> k;
m = 1;
for (int i = 1; i <= n; i++)
{
scanf("%d%d", l + i, r + i);
// 每个区间的四个分段点
pos[++m] = max(0, l[i] - k);
pos[++m] = l[i];
pos[++m] = max(0, r[i] - k);
pos[++m] = r[i];
pos[1] = max(pos[1], r[i] + 1);
}
sort(pos + 1, pos + m + 1);
m = unique(pos + 1, pos + m + 1) - pos - 1;
int u, v;
for (int i = 1; i < n; i++)
scanf("%d%d", &u, &v), G[u].push_back(v), G[v].push_back(u);
for (int i = 1; i < m; i++)
{
L = pos[i], R = pos[i] + k;
int j;
for (j = 0; pos[i] + j < pos[i + 1] && j < n + 2; j++)
{
op = 1;
dfs(1, 0);
L++;
op = -1;
dfs(1, 0);
R++;
X[j] = pos[i] + j;
Y[0][j] = sum1;
Y[1][j] = sum2;
}
if (pos[i] + j < pos[i + 1])
{
sum1 = lag(pos[i + 1] - 1, j - 1, X, Y[0]);
sum2 = lag(pos[i + 1] - 1, j - 1, X, Y[1]);
}
}
R--;
op = -1;
dfs(1, 0);
cout << sum1 << endl << sum2 << endl;
return 0;
}

指数型生成函数

前置知识:一般型生成函数。

先通过几个问题来引入:

问题 A:求 n 个相同的红球摆成一排的方案数。
问题 B:求 n 个相同的红球摆成一排的方案数。
问题 C:求相同的红球和蓝球共 n 个摆成一排的方案数。

对于问题 A、B,三岁小孩都知道答案是 1

对于问题 C,幼儿园小朋友都知道答案是 2n

但我们作为 OIer,当然要使用生成函数解决。

首先,三个问题的一般型生成函数为:

  1. A(x)=1+x+x2+x3+...
  2. B(x)=1+x+x2+x3+...
  3. C(x)=1+2x+4x2+8x3+...

乍一看,问题 C 和问题 A、B 有某种关系,但是我们发现无法用一般型生成函数表达这种关系……

尝试将 AB 相乘,得到的多项式 C 的系数 cn

i=0naibni

然而实际上 cn 长酱紫:

i=0naibni(ni)

n!i=0naibnii!(ni)!

则有

cnn!=i=0naii!bni(ni)!

诶!那我们定义:

  • fA(x)=fB(c)=10!+x1!+x22!+x33!+...
  • fC(x)=10!+2x1!+4x22!+8x33!+...

于是就有 fC=fA×fB。这就是指数型生成函数。

根据高等数学中的泰勒展开,10!+x1!+x22!+x33!+...=ex

于是就有 fA(x)=fB(x)=ex,fC(x)=e2x

现在你已经学会指数型生成函数了,来做道题吧:

化简 10!+x22!+x44!+x66!+...

答案:

ex+ex2

同理,x1!+x33!+x55!+x77!+...=exex2

[CTS2019] 珍珠

放道题,大家有兴趣可以做做,可能需要 FFT。我不会做找时间再来补()

快速傅立叶变换(FFT)

前言:虽然说在 noi 大纲里,FFT 是 10 级算法,NOI 以下都不用掌握,但是 FFT 并不难,而且也有必要掌握。我刚开始打算看 OI-wiki 学 FFT,但是他写的太深奥,压根看不懂。后来我是去了洛谷题解区才发现原来 FFT 这么简单,只需要基本的复数知识就能理解。

FFT 可以以 O(nlogn) 的时间复杂度解决:求两个长度 O(n) 级别的多项式的卷积。即

Ci=j+k=iAjBk

其思想是:

  1. 对两个多项式分别带入若干值,得到若干个 (x,y) 的点值。但是每次代 x 一般都是 O(n) 的,代 n 次肯定不行。而 FFT 代的 x 很特殊,可以通过分治做到 O(nlogn) 得到 O(n) 个点值。这个转换为点值的变换叫做快速傅立叶变换(FFT)。

  2. 把点值们的 y 直接相乘,这个没什么好说的,O(n)

  3. 现在有一些点值,它们在最终多项式上。但是要把多项式确定出来,拉插的 O(n2) 都不行。但是快速傅立叶逆变换(IFFT)可以 O(nlogn) 把它变成系数序列,即多项式。

一句话就是: {AFFTBFFT}IFFTA×B

这个思想很重要,后面的 FWT 也是这个思想。至于 FFT 的具体操作,我推荐 attack 大佬的这篇博客,写的很好,我觉得这里没有必要再赘述了。(真不是因为懒)

学会之后可以把洛谷模版题给写了。

NTT

NTT mod table
P=k×2m+1 k m g
3 1 1 2
5 1 2 2
17 1 4 3
97 3 5 5
193 3 6 5
257 1 8 3
7681 15 9 17
12289 3 12 11
40961 5 13 3
65537 1 16 3
786433 3 18 10
5767169 11 19 3
7340033 7 20 3
23068673 11 21 3
104857601 25 22 3
167772161 5 25 3
469762049 7 26 3
998244353 119 23 3
1004535809 479 21 3
2013265921 15 27 31
2281701377 17 27 3
3221225473 3 30 5
7516927681 35 31 3
77309411329 9 33 7
206158430209 3 36 22
2061584302081 15 37 7
2748779069441 5 39 3
6597069766657 3 41 5
39582418599937 9 42 5
79164837199873 9 43 5
263882790666241 15 4 7
1231453023109121 35 45 3
1337006139375617 19 46 3
3799912185593857 27 47 5
4222124650659841 15 48 19
7881299347898396 7 50 6
31525197391593473 7 52 3
180143985094819841 5 55 6
1945555039024054237 27 56 5
4179340454199820289 29 57 3

快速莫比乌斯变换(FMT)

FMT 用来处理高维前后缀的和/差分。然后就可以做或卷积、与卷积了。

如果对长为 2n 序列 f 做高维前缀和,则有

gi=jifj

那为啥它叫“高维前缀和”?这很好理解,n 维前缀和就是对于每个 ij 把所有比 ij 小的也加上。那上面的式子就相当于一个 n 层循环,每个循环里的 i0/1,所以每一轮的 (i1,i2,...,in) 就要把其子集们也加上。

以下代码可以对本身进行一次高维前缀和,时间复杂度 O(n2n)

for (int i = 0; i < n; i++)
for (int mask = 0; mask < (1 << n); mask++)
if (mask & (1 << i))
f[mask] += f[mask ^ (1 << i)];

解释一下为什么可以这样:一开始,每个下标的值就是自己。当进行第一轮(i=0)后,所有包含 i 那一位的下标的值,就变为了“第 i 位选或不选”构成的下标上的值的和。同理,当进行第二轮(i=1)后,包含 i 那一位的下标的值就包含了 i 位可以不选的值。所以下标 (11)2 就有 (01)2(10)2,以及 (00)2 上的值(当然还有自己)。

至于高维前缀差分,把加法改成减法就行。

for (int i = 0; i < n; i++)
for (int mask = 0; mask < (1 << n); mask++)
if (mask & (1 << i))
f[mask] += f[mask ^ (1 << i)];

你可能会问,既然是前缀和的逆运算,不应该把过程反过来吗?

确实,但是正着做也正确。理由是对于同一个 i,内层循环可以以任意顺序更新,因为有 i 位的下标只会从没有 i 位的下标更新;而外层的 i 就更没有固定顺序了。

高维后缀和同理:

hi=ijfj

点击查看代码
for (int i = 0; i < n; i++)
for (int mask = 0; mask < (1 << n); mask++)
if (!(mask & (1 << i)))
f[mask] += f[mask ^ (1 << i)];

把加改成减就是高维后缀差分。

以上应该就是 FMT(?)

快速沃尔什变换(FWT)

FFT 可以以 O(nlogn) 的时间复杂度解决:求两个长度 O(n) 级别的多项式的或、与、异或卷积。即

Ci=jk=iAjBk

其中 分别为 orandxor

剩下的就去看 xht 的这篇博客吧()

不过我看完之后有个问题。。【待补】

本文作者:Aquizahv's Blog

本文链接:https://www.cnblogs.com/aquizahv/p/18456034

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   Aquizahv  阅读(43)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起