2020.10: 进行了小修小补。
2020.11: 重制了更严谨的 “k进制不进位加法卷积” 部分。
2020.11: 加更集合幂级数。
0x01.FWT 概论
众所周知,多项式乘法是加法卷积,因为第
i 项和第
j 项的乘积贡献到第
i+j 项。
类似地定义位运算卷积 : 第
i 项和第
j 项的乘积贡献到第
i⊕j 项。其中
⊕ 是某种
位运算。
即
S[k]=i⊕j=k∑A[i]B[j] ,记作卷积式
A∗B=S 。
众所周知,
FFT 把多项式转换成点值之后,从卷积变为了直接点积。
我们自然也期望把位运算卷积转化成点积。
设
FWT(A) 是幂级数
A 经过
FWT 变换之后得到的幂级数。
我们需要令其满足 :
A∗B=C⟺FWT(A)⋅FWT(B)=FWT(C) (点积)。
FFT 是一个
线性变换,我们也希望
FWT 变换是线性的。
我们还不知道怎么变换,于是设
c(i,j) 为变换系数,即
A[j] 对
FWT(A)[i] 的贡献系数。
则
FWT(A)[i]=j=0∑n−1c(i,j)Aj
根据
FWT(A)⋅FWT(B)=FWT(C) ,得到 :
FWT(A)[i]FWT(B)[i]=FWT(C)[i]
j=0∑n−1c(i,j)A[j]k=0∑n−1c(i,k)B[k]=p=0∑n−1c(i,p)C[p]
j=0∑n−1k=0∑n−1c(i,j)c(i,k)A[j]B[k]=p=0∑n−1c(i,p)C[p]
根据
A∗B=C ,又能得到 :
C[p]=t1⊕t2=p∑A[t1]B[t2]
p=0∑n−1c(i,p)C[p]=p=0∑n−1c(i,p)t1⊕t2=p∑A[t1]B[t2]
j=0∑n−1k=0∑n−1c(i,j)c(i,k)A[j]B[k]=p=0∑n−1c(i,p)t1⊕t2=p∑A[t1]B[t2]
j=0∑n−1k=0∑n−1c(i,j)c(i,k)A[j]B[k]=p=0∑n−1t1⊕t2=p∑A[t1]B[t2]c(i,t1⊕t2)=t1=0∑n−1t2=0∑n−1A[t1]B[t2]c(i,t1⊕t2)
对比左右两边,得到
c(i,j)c(i,k)=c(i,j⊕k) ,只需要满足这个就好了。
另外,由于位运算每一位的独立性,
c(i,j) 也有一个重要性质: 可以分位考虑。
假设我们已经(根据真值表)求出满足要求的
c([0,1],[0,1]),我们能这样构造出所有的
c :
设二进制数
a 的每一位分别为 :
a0,a1,a2...
则有
c(i,j)=c(i0,j0)c(i1,j1)c(i2,j2)... ,就是
把每一位的变换系数乘起来。
那么 : 对于每个
t ,都有
c(it,jt)c(it,kt)=c(it,jt⊕kt)⟺c(i,j)c(i,k)=c(i,j⊕k)
这就符合我们的条件。
好了,现在假设我们已经有了符合要求的
c ,如何快速求解
FWT 变换呢?
FWT(A)[i]=j=0∑n−1c(i,j)A[j]
根据这个式子直接求和,复杂度至少是
O(n2) 的,并没有起到优化作用。
我们考虑按位拆半:
=j=0∑(n/2)−1c(i,j)A[j]+j=(n/2)∑n−1c(i,j)A[j]
设
i′ 为
i 去掉二进制首位剩下的数。
在首位分开考虑 :
=j=0∑(n/2)−1c(i0,j0)c(i′,j′)A[j]+j=(n/2)∑n−1c(i0,j0)c(i′,j′)A[j]
=c(i0,0)j=0∑(n/2)−1c(i′,j′)A[j]+c(i0,1)j=(n/2)∑n−1c(i′,j′)A[j]
考虑到
c(i′,j′) 就是去掉最高位的子变换,这里规模减半了。
设
A0 为幂级数下标首位为
0 的部分,类似地有
A1。
FWT(A)[i]=c(0,0)FWT(A0)[i]+c(0,1)FWT(A1)[i](i∈[0,n/2))
FWT(A)[i+(n/2)]=c(1,0)FWT(A0)[i]+c(1,1)FWT(A1)[i](i∈[0,n/2))
我们就能以
O(n) 的代价,根据上列式子合并两个规模为
n/2 的子变换。
所以,若
n=2m ,需要合并
m 次,复杂度为
O(m2m)。
( 可能有点抽象,但是您如果写过FFT,看到代码就会懂了 )
此外,逆变换
IFWT 就是对
c 矩阵求个逆,具体见下文。
( 一个重要的地方是,这个构造出来的
c 矩阵
一定要有逆,否则就变换不回去TAT )
0x02.基础位运算卷积
针对不同的位运算,根据
c(i,j)c(i,k)=c(i,j⊕k) 构造出
c([0,1],[0,1]) 即可。
我们把这个矩阵称为位矩阵。
构造的过程可能有些繁杂,可以直接记结论,或者去后面看扩展版的。
1.1 Or 卷积
设位矩阵为
c=[c(0,0)c(1,0)c(0,1)c(1,1)]
起点 :
c(i,j)c(i,k)=c(i,j∣k)
-
c(0,0)c(0,0)=c(0,0∣0)
⇒c(0,0)=1 或
0。
同理不难推知
c(_,_)∈{0,1}
-
c(0,1)c(0,0)=c(0,1∣0)
⇒c(0,1)=0 或
c(0,0)=c(0,1)=1
-
c(1,1)c(1,0)=c(1,1∣0)
⇒c(1,1)=0 或
c(1,0)=c(1,1)=1
首先,如果有一排0或者一列0那么这个矩阵就没有逆,那么可以构造出:
两种情况 :
[1110] 或
[1101]。
Tips :
Or 卷积的上面第二个矩阵
FWT 相当于子集求和。
原因:第二个矩阵相当于
c(i,j)=[i&j=j]
Ai′=i&j=j∑Ai等价于
Ai′=j∈i∑Ai。
(也可以使用高维前缀和来推导)
(下面采用第二个矩阵)
FWT(A)[i]=FWT(A0)[i]
FWT(A)[i+(n/2)]=FWT(A0)[i]+FWT(A1)[i]
对于逆变换,把矩阵求个逆可得
[1−101] 。
IFWT(A)[i]=IFWT(A0)[i]
IFWT(A)[i+(n/2)]=IFWT(A1)[i]−IFWT(A0)[i]
1.2 And 卷积
起点 :
c(i,j)c(i,k)=c(i,j&k)。
同上,容易得到
c(_,_)∈{0,1}。
-
c(0,1)c(0,0)=c(0,1&0)
⇒c(0,0)=0 或
c(0,0)=c(0,1)=1
-
c(1,1)c(1,0)=c(1,1&0)
⇒c(1,0)=0 或
c(1,0)=c(1,1)=1
还是老样子,如果有一排
0 或者一列
0 那么这个矩阵就没有逆,那么可以构造出:
两种情况 :
[0111] 或
[1011],下面采用第二种。
FWT(A)[i]=FWT(A0)[i]+FWT(A1)[i]
FWT(A)[i+(n/2)]=FWT(A1)[i]
把矩阵求个逆可得
[10−11]:
IFWT(A)[i]=IFWT(A0)[i]−IFWT(A1)[i]
IFWT(A)[i+(n/2)]=IFWT(A1)[i]
1.3 Xor 卷积
起点 :
c(i,j)c(i,k)=c(i,j xor k)
-
对于任意的
x,y ,均有
c(0,0)c(x,y)=c(x,y xor 0)=c(x,y)
⇒c(0,0)=1.
-
c(1,1)c(1,1)=c(1,0)
此时若
c(1,1)=c(1,0)=0,则一行为
0 ,矩阵无逆。
所以
c(1,1),c(1,0) 必然都非
0。
-
c(1,0)c(1,1)=c(1,1)
刚才说
c(1,1) 非
0,所以此处
c(1,0) 一定是1.
-
c(0,1)c(0,1)=c(0,0)
⇒c(0,1)∈{−1,1}
两种情况 :
[1−111] 或
[111−1] ,下面采用第二种。
附 :不难观察出
c(i,j)=(−1)∣i&j∣
FWT(A)i=FWT(A0)i+FWT(A1)i
FWT(A)i+(n/2)=FWT(A0)i−FWT(A1)i
求逆可得
[0.50.50.5−0.5]
IFWT(A)i=2IFWT(A0)i+IFWT(A1)i
IFWT(A)i+(n/2)=2IFWT(A0)i−IFWT(A1)i
1.4 模板题 & Code:
CPP#include<algorithm>
#include<cstring>
#include<cstdio>
#define Maxn 135000
#define ll long long
using namespace std;
const int mod=998244353,inv2=499122177;
inline int read(){
int X=0;char ch=0;
while(ch<48||ch>57)ch=getchar();
while(ch>=48&&ch<=57)X=X*10+(ch^48),ch=getchar();
return X;
}
const ll
Cor[2][2] ={{1,0},{1,1}},
Cand[2][2] ={{1,1},{0,1}},
Cxor[2][2] ={{1,1},{1,mod-1}},
ICor[2][2] ={{1,0},{mod-1,1}},
ICand[2][2]={{1,mod-1},{0,1}},
ICxor[2][2]={{inv2,inv2},{inv2,mod-inv2}};
void FWT(ll *F,const ll c[2][2],int n)
{
for (int len=1;len<n;len<<=1)
for (int p=0;p<n;p+=len+len)
for (int i=p;i<p+len;i++){
ll sav0=F[i];
F[i]=(c[0][0]*F[i]+c[0][1]*F[i+len])%mod;
F[i+len]=(c[1][0]*sav0+c[1][1]*F[i+len])%mod;
}
}
void bitmul(ll *F,ll *G,const ll C[2][2],const ll IC[2][2],int n)
{
FWT(F,C,n);FWT(G,C,n);
for (int i=0;i<n;i++)F[i]=F[i]*G[i]%mod;
FWT(F,IC,n);
}
void print(ll *s,int n){
for (int i=0;i<n;i++)
printf("%lld ",s[i]);puts("");
}
#define cpy(f,g,n) memcpy(f,g,sizeof(ll)*(n))
ll f[Maxn],g[Maxn],a[Maxn],b[Maxn];
int n;
int main()
{
n=(1<<read());
for (int i=0;i<n;i++)f[i]=read();
for (int i=0;i<n;i++)g[i]=read();
cpy(a,f,n);cpy(b,g,n);
bitmul(a,b,Cor,ICor,n);
print(a,n);
cpy(a,f,n);cpy(b,g,n);
bitmul(a,b,Cand,ICand,n);
print(a,n);
cpy(a,f,n);cpy(b,g,n);
bitmul(a,b,Cxor,ICxor,n);
print(a,n);
return 0;
}
留个代码以备将来研究……
与前后文无关。
CPP#include<algorithm>
#include<cstdio>
#define Maxn 140000
#define mod 998244353
#define ll long long
using namespace std;
inline int read()
{
register int X=0;
register char ch=0;
while(ch<48||ch>57)ch=getchar();
while(ch>=48&&ch<=57)X=X*10+(ch^48),ch=getchar();
return X;
}
int n,pn,inv2;
ll f[Maxn],g[Maxn];
ll a[Maxn],b[Maxn],c[Maxn];
void mulor(ll *a,ll *b,ll *c,int len)
{
if (!(len>>=1)){
c[0]=(a[0]*b[0])%mod;
return ;
}for (int i=0;i<len;i++){
a[i+len]=(a[i+len]+a[i])%mod;
b[i+len]=(b[i+len]+b[i])%mod;
}mulor(a,b,c,len);
mulor(a+len,b+len,c+len,len);
for (int i=0;i<len;i++)
c[i+len]=(c[i+len]-c[i]+mod)%mod;
}
void muland(ll *a,ll *b,ll *c,int len)
{
if (!(len>>=1)){
c[0]=(a[0]*b[0])%mod;
return ;
}for (int i=0;i<len;i++){
a[i]=(a[i+len]+a[i])%mod;
b[i]=(b[i+len]+b[i])%mod;
}muland(a,b,c,len);
muland(a+len,b+len,c+len,len);
for (int i=0;i<len;i++)
c[i]=(c[i]-c[i+len]+mod)%mod;
}
void mulxor(ll *a,ll *b,ll *c,int len)
{
if (!(len>>=1)){
c[0]=(a[0]*b[0])%mod;
return ;
}for (int i=0;i<len;i++){
ll sava=a[i],savb=b[i];
a[i]=(a[i+len]-a[i]+mod)%mod;
b[i]=(b[i+len]-b[i]+mod)%mod;
a[i+len]=(a[i+len]+sava)%mod;
b[i+len]=(b[i+len]+savb)%mod;
}mulxor(a,b,c,len);
mulxor(a+len,b+len,c+len,len);
for (int i=0;i<len;i++){
ll savc=c[i];
c[i]=(savc+c[i+len])*inv2%mod;
c[i+len]=(c[i+len]-savc+mod)*inv2%mod;
}
}
void print(ll *s)
{
for (int i=0;i<n;i++)
printf("%lld ",s[i]);puts("");
}
void copy(ll *f,ll *g)
{for (int i=0;i<n;i++)f[i]=g[i];}
int main()
{
inv2=(mod+1)/2;
scanf("%d",&n);n=(1<<n);
for (int i=0;i<n;i++)f[i]=read();
for (int i=0;i<n;i++)g[i]=read();
copy(a,f);copy(b,g);
mulor(a,b,c,n);
print(c);
copy(a,f);copy(b,g);
muland(a,b,c,n);
print(c);
copy(a,f);copy(b,g);
mulxor(a,b,c,n);
print(c);
return 0;
}
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
0x03.FWT 变换的一些性质
前面讲了,
FWT 变换的本质是线性变换。
这意味着
FWT(A+B)=FWT(A)+FWT(B) ,且
FWT(cA)=cFWT(A)
此外,如果需要卷积的向量只有少数非
0 项(
2,3 个)可能会有分类讨论的神奇的解法。
例题(我只能做到这些了):
题做多了再来填坑吧。
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
0x04.位值域的扩展
其实
位运算的本质是对一个
n 维
01 向量的运算。
或运算就是每一维取
max。且运算就是每一维取
min。
异或运算则是每一维对应相加再
mod 2。
位运算有个特点 : 向量的每一位都是独立的。
我们把
{0,1}扩展到
[0,k)∩Z 也就是扩展到
k 进制,看看会得到什么?
每一维取 max
可得
c(x,y)c(x,y)=c(x,max(y,y))=c(x,y)
又可得
c(x,y1)c(x,y2)=c(x,max(y1,y2))
假如有
c(x,y1)=1, c(x,y2)=0,可得
c(x,y1)c(x,y2)=c(x,max(y1,y2))
又因为
1∗0=0 所以
max(y1,y2)=y2 ,可得
y1<y2.
也就是说,每一行以内,
1 总是在
0 的前面。
接下来,除了矩阵有逆之外,再没有别的要求了。
如果要有逆的话,每一行都必须不同,那么
1 的个数分别就是
1...n ,随意排列都可以。
1111011100110001或者
1111101110101000等等
4!种。
为了方便,一般取用第一种,求逆可得
1−10001−10001−10001
暴力做的话
O(kn+1n)。
这个矩阵本质上就是前缀和和差分……所以可以优化到
O(knn)。
(宏观上就是高维前缀和)
每一维取 min
类似的,整个矩阵中只有
0 或
1 ,每一行以内,
1 总是在
0 的后面。
1000110011101111或者
1000101011101111等等
4!种。
为了方便,一般取用第一种,求逆可得
1000−11000−11000−11
这个矩阵本质上就是后缀和和差分……同样能优化到
O(knn)
(宏观上就是高维后缀和)
每一维加起来 mod k
这玩意就比较复杂了,需要动用单位根……
我们知道
c(x,y1)c(x,y2)=c(x,(y1+y2)mod k)
c(x,y)=ωky 就能满足要求:
c(x,y1)c(x,y2)=ωky1ωky2=ωk(y1+y2)mod k
但是,每一行都一样的话,矩阵就没有逆。
你会发现直接把 FFT 的范德蒙德矩阵拿来用就好了:
111...11wk1wk2...wkk−11wk2wk4...wk2(k−1)...............1wkk−1wk2(k−1)...wk(k−1)(k−1)
逆矩阵我们也知道,那就是:
k1111...11wk−1wk−2...wk−(k−1)1wk−2wk−4...wk−2(k−1)...............1wk−(k−1)wk−2(k−1)...wk−(k−1)(k−1)
暴力计算显然是
O(kn+1n) 的。
可以使用
FFT 优化到
O(knnlogk)。
但是,单位根在模意义很可能不存在。考虑扩充我们“数”的表示。
首先想到的是,人为地定义代数对象
x 满足
xk=1 (且
x 的阶恰为
k),用其代替单位根
wk,不难验证其满足前文构造矩阵的条件。
此举相当于用
(modxk−1) 下的循环卷积多项式替换了“数”。
我们的位矩阵长这样 :
C1=111...11x1x2...xk−11x2x4...x2(k−1)...............1xk−1x2(k−1)...x(k−1)(k−1)
C2=k1111...11x−1x−2...x−(k−1)1x−2x−4...x−2(k−1)...............1x−(k−1)x−2(k−1)...x−(k−1)(k−1)
接下来的推导可能需要抽象代数和高等代数相关知识。详情可见 :
近世代数乱编
问题在于,此时
(modxk−1) 下的循环卷积多项式并不是域,其存在零因子。
这样就会导致两个位矩阵并非严格互逆。
具体而言,我们原本希望
C1∗C2=I ,这需要满足
i=0∑k−1C1[a][i]C2[i][b]=[a=b]。
当
a=b 时,有
i=0∑n−1C1[a][i]C2[i][a]=k1i=0∑k−1xaix−ai=1 ,这不需要消去律也成立,这部分贡献是没有差错的。
当
a=b 时,有
i=0∑n−1C1[a][i]C2[i][b]=k1i=0∑k−1xaix−bi=k1i=0∑k−1x(a−b)i
若
x 的阶恰为
k ,则
x(a−b) 必不为
1。在经典的范德蒙德矩阵证明中,由此可得上式
=0
但是,此时由于无法做除法,等比数列求和公式不再生效,无法证明上式
=0。
但是,仍然恒有
0=(xk−1)=(x−1)(1+x+x2+...+xk) 存在,也就是说,上面求和的结果虽然可能非
0 ,但必然是零因子。
也就是说,我们算得的结果是由 (真实答案)+(零因子的线性组合) 构成的。
接下来要找到一种合适的扩域方法,这样就能避免零因子问题。
不妨取分圆多项式
Φk(x) ,下面不加证明地给出两个定理 :
(相关知识可见《近世代数乱编》)
-
这保证了
(modΦk(x)) 意义下的“数”是个域,不存在零因子。
这里注意,还要验证该多项式在
Fp 下是否能够分解,一般情况下是不能的,此时可以正常使用。
若计算时没有利用
Fp 的性质(如求逆元),则可以看作大整数计算,最后将答案取模,此时不需要检查
Fp 下是否能够分解。
-
② 在
(modΦk(x)) 意义下,
x 的阶恰好为
k。
这又保证了我们构造的前提成立。
这样,只需在
(modΦk(x)) 下计算,即可得到满足题意的答案。
但是,模多项式的计算较为繁琐,常数较大且不易优化。
而更好的消息是,由于
Φk(x)∣(xk−1) ,则有
F(x)modΦk(x)=F(x)mod(xk−1)modΦk(x)
这表明我们可以先用“循环卷积多项式”代替严格的扩域,到最后再对
Φk(x) 取模。
此时任意两个数的乘法就变成
O(k2) 多项式循环卷积,复杂度升高了
O(k2)。
观察矩阵可得,每次乘的都是一个单项式,复杂度就只需升高
O(k)。
暴力做的话,总的复杂度是
O(kn+2n) 。
可以用
FFT 优化到
O(kn+1nlogk) ,不过大多数时候不实用。
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
0x04.子集卷积 & 集合幂级数
作用 : 求
S[s]=i⊆s∑F[i]G[s xor i]
另一种说法 :
S[s]=i∣j=si&j=0∑F[i]G[j]
组合意义就是每次挑选不交的两个集合拼成新的集合。
好像在某些
DP 题目里面见过? 这确实可以用来优化某些毒瘤子集
DP。
使用枚举子集的技巧即可做到
O(3n) 计算。考虑使用卷积知识加速计算。
直接套用前面学习的位运算卷积只能求
S[s]=i∣j=s∑F[i]G[j] ,无法再满足
i&j=0 。
考虑到
i∣j=s, i&j=0 等价于
i∣j=s,∣i∣+∣j∣=∣s∣。
( 其中
∣i∣ 表示
i 在二进制下的
1 个数 )
我们可以将原来的
F[k] 替换成占位多项式
F[k]x∣k∣。
这样,计算
or 卷积之后,利用
x 上的加法卷积,
[xp]S[k] 就是满足
i∣j=s,∣i∣+∣j∣=p 的
(i,j) 的贡献。
我们取出
[x∣k∣]S[k] 即为我们想要的答案。
考虑计算的复杂度,多项式加法和数乘的复杂度是
O(n) 的,故
FWT 变换的复杂度变为
O(2nn2)。
中间还需要点乘,多项式乘法用暴力实现,复杂度也是
O(2nn2)。
在具体实现中,可以把
x 维度放在前面以减小常数。(代码中并没有这样做)
CPP#include<algorithm>
#include<cstdio>
#define MaxN 1050000
using namespace std;
const int mod=1000000009;
int read(){
int X=0;char ch=0;
while(ch<48||ch>57)ch=getchar();
while(ch>=48&&ch<=57)X=X*10+(ch^48),ch=getchar();
return X;
}
int m;
struct Poly
{
int a[21];
void operator += (const Poly &B){
for (int i=0;i<=m;i++)
a[i]=(a[i]+B.a[i])%mod;
}
void operator -= (const Poly &B){
for (int i=0;i<=m;i++)
a[i]=(a[i]-B.a[i])%mod;
}
Poly operator * (const Poly &B) const{
Poly R;
for (int i=0;i<=m;i++)R.a[i]=0;
for (int i=0;i<=m;i++)
for (int j=0;i+j<=m;j++)
R.a[i+j]=(R.a[i+j]+1ll*a[i]*B.a[j])%mod;
return R;
}
};
void DWT(Poly *f,int n)
{
for (int l=1;l<n;l<<=1)
for (int p=0;p<n;p+=l+l)
for (int k=0;k<l;k++)
f[p|l|k]+=f[p|k];
}
void IDWT(Poly *f,int n)
{
for (int l=1;l<n;l<<=1)
for (int p=0;p<n;p+=l+l)
for (int k=0;k<l;k++)
f[p|l|k]-=f[p|k];
}
Poly F[MaxN],G[MaxN],T[MaxN];
int n,c[MaxN];
int main()
{
m=read();n=(1<<m);
for (int i=1;i<n;i++)c[i]=c[i>>1]+(i&1);
for (int i=0;i<n;i++)F[i].a[c[i]]=read();
for (int i=0;i<n;i++)G[i].a[c[i]]=read();
DWT(F,n);DWT(G,n);
for (int i=0;i<n;i++)
F[i]=F[i]*G[i];
IDWT(F,n);
for (int i=0;i<n;i++)
printf("%d ",(F[i].a[c[i]]+mod)%mod);
return 0;
}
这就是一道很好的模板题 (不过值域只有
217 所以很多人
317 艹过去了)。
令
cnt[i]=j∑[s[j]=i]
求出
cnt 和本身的子集卷积
F1 ,
cnt 和本身的异或卷积
F2。
令
G1[i]=fib(F1[i]) ,
G2 类似,
cnt′[i]=fib(cnt[i])
最后把
G1,G2,cnt′ 卷在一起就好。
-
半在线子集卷积
给出集合幂级数
W,C ,求幂级数
S 使得 :
S[s]=C[s]t⊊s∑S[t]W[s−t]
对于
S[s] ,需要得知所有为
s 的真子集的位置的
S 才能计算。
我们可以按照
∣s∣ 的顺序计算
S[s] ,这样就能保证需要的值已经被计算好了。
此时为了方便需要将
x 维度放在前面。具体实现见例题。
-
集合幂级数运算进阶
下面以
F(x)=s∑F[s]xs 来描述一个集合幂级数。
-
求逆
变换后将占位多项式求逆,然后逆变换回来即可。
集合幂级数卷积的单位元是
x∅ ,可以将其视作常数项
1。
-
设有集合幂级数
F ,模仿多项式级数来定义其
exp,ln。
定义
expF=k=0∑k!Fk ,要求
[x∅]F=0 。
定义
lnF 为
exp 的逆运算,要求
[x∅]F=1 。
也有定义式
lnF=k=1∑k(−1)k+1(F−x∅)k
也只需要变换后对占位多项式进行
exp,ln 即可计算。
集合幂级数的运算也有和生成函数类似的组合意义。
-
不难发现,对于序列中的
A[i] 写出集合幂级数
(x∅+xA[i]) ,我们的目标是出所有集合幂级数的卷积。
考虑模仿快速
01 背包的套路,对其先取
ln 后
exp。
P(x)=i=1∏n(x∅+xA[i])
=expi=1∑nln(x∅+xA[i])
=expi=1∑nk=1∑k(−1)k+1(xA[i])k
而
(xA[i])k 显然只有
k=1 时为
xA[i] ,其他情况为
0。(注意这是子集卷积)
=expi=1∑nxA[i]
不过要注意对
A[i]=0 的情况特殊处理。
不难发现“划分”就对应生成无序集合,这即为
exp 的组合意义。
本题要求划分的集合数
≤k ,则是一个部分
exp :
i=0∑kk!F(x)k
对占位多项式做部分
exp 即可。
有
ODE :
G=i=0∑kk!Fk⇒G′=F′∗(G−(Fk/k!))
常数莫名其妙的大,跑不过
O(2nn3) 实锤……
设
G[s] 为点集
s 的联通生成子图个数,
F[s] 为点集
s 的生成子图个数。
F 是易求的,有
F[s]=2s内部边数。
根据
exp 的组合意义显然有
F=expG ,则
G=lnF。
设
P[s] 为点集
s 的生成子图的连通值之和。
枚举连通块数目则有
P=k=0∑k!Gkk!=1−G1。
求逆即可。
能够发现无环图所有边反向之后还是无环图,互反的一对图翻转边数和恰为
m。所以答案为方案数乘以
m/2。
设
F[s] 表示点集
s 内适当反转后无环的方案数。
考虑拆
DAG 为子问题,每次去除入度为
0 的所有点。
钦定入度为
0 的点集
t⊆s ,
t 内部必须没有边,即
t 为独立集。
t 和其余点
s−t 之间的边方向确定。所以方案数为
[t是独立集]F[s−t]
由于我们只能钦定一些点入度为
0 ,所以还需容斥。
可得
F[s]=t⊆s∑(−1)∣t∣−1[t是独立集]F[s−t]。
设
G=(−1)∣t∣−1[t是独立集]
即
F=F∗G+1。可得
F=1−G1,求逆即可。
首先要求出偶度图的集合幂级数,然后求
ln 即可得到欧拉图。
现在问题变为对每个点集求生成偶度图个数。
找出任意一个生成森林,将其余的边随意选取,此时能得到目前每个点的奇偶性。
由于森林无环的性质,每种电度奇偶性都能通过恰当地选取森林中的边得到,且方案数恰好为
1。
因此,偶度图个数为
2边数−生成森林边数。