从FFT 到 FWHT 快速沃尔什-阿达玛转换(Walsh Hadamard transform)
FWHT
快速沃尔什-阿达玛转换(Fast Walsh-Hadamard transform), 一种广义傅立叶变换(FWHT)
解决什么问题
FWHT 是用于解决对下标进行位运算卷积问题的方法
$c_{i} = \sum_{i=j \bigoplus k}a_{j} b_{k}$
并且没有fft中会涉及到double
前置知识 FFT(DFT)
DFT:
${\displaystyle {\begin{bmatrix}f_{0}\\
f_{1}\\
\vdots \\
f_{n-1}\end{bmatrix}}={\begin{bmatrix}1&1&1&\cdots &1\\
1&\alpha &\alpha ^{2}&\cdots &\alpha ^{n-1}\\1&\alpha ^{2}&\alpha ^{4}&\cdots &\alpha ^{2(n-1)}\\\vdots &\vdots &\vdots &&\vdots \\1&\alpha ^{n-1}&\alpha ^{2(n-1)}&\cdots &\alpha ^{(n-1)(n-1)}\\\end{bmatrix}}{\begin{bmatrix}v_{0}\\v_{1}\\\vdots \\v_{n-1}\end{bmatrix}}.}$
Atcoder abc212
G - Power Pair
题目
https://atcoder.jp/contests/abc212/tasks/abc212_g
给定 质数$p$
问 $x,y\in[0,p-1]$ 中有多少对$(x,y)$满足
存在$n$, 使得$x^n = y \pmod p$
范围
p $10^{12}$
2s
1024mb
题解
我的思路
显然x=0 只有y = 0, y=0也只有x=0
然后如果x是原根 那么 方案数$p-1$
如果$r|(p-1)$ 那么 $x^r \pmod p$的方案数为$\frac{p-1}{r}$
或者$x$的最小幂次$t$让$x^t = 1 \pmod p$, 则答案为$t$
但是即使这样, 如果每个去枚举依然是$O(p log(p))$
反过来考虑说有没有办法求$x^t = 1$ 的方案数,
如果能快速计算出,那么 方案数减去它的t因子对应的方案数 就恰好是 = t的方案数
而$t$的取值只会是 $p-1$的因数
$t = 1$ $x = 1$
$t = 2$ $x = 1,-1$
$t = 4$
$t = 7$
$t = 8$
t = 2k时, $x^{2k} - 1 = 0 \pmod p$
$(x^k+1)(x^k-1) = 0 \pmod p$, 相当于$x^k = 1 \pmod p, x^k = -1 \pmod p $的解的并
并不会
官方
原根的想法没问题, 然后就变成了我们指定原根
$x^i = j$, $(i,j)$ 是一一对应, 且取$[1,p-1]$范围内的所有值
这样的话
要求 $x^i$ 的最小让 幂次等于1的t
注意到 和我思路一样的 $x^i$当$i | (p-1)$时, 方案数 $=\frac{p-1}{i}$
而这里$i$可能不是$p-1$的因子, 而易证明 方案数为 $\frac{p-1}{gcd(p-1,i)}$
这里问题变成了, 统计不同 $gcd(p-1,i) = k$ 的数量即可
$g | (p-1)$
$\sum_{g|(p-1)} count(g 倍数 且非(p-1)因子) $
代码
https://atcoder.jp/contests/abc212/submissions/33524525
1 | #include <bits/stdc++.h> |
H/Ex - Nim Counting
https://atcoder.jp/contests/abc212/tasks/abc212_h
给正整数 长度k的 数组A, 值两两不同
T和A轮流游戏, T先
选一个 >= 1 的石堆, 移除任意正整数个
谁取了最后一个胜利
问题是
对于长度[1,N] 每个元素属于A中的一个的 初始状态, 有多少种状态是 T 胜利
模 998244353
范围
n 2e5
k 65536
$a_i$ [1, 65536]
题解
我的思路
首先nim游戏作为博弈的常见基础, 很明显 就是 xor 和 != 0 时 T胜利
那么无非是求 所有 !=0 的方案数, 或者 是 == 0 的方案数, 去总方案减去 ==0的方案数
那么对于一个选择来说因为Ai两两不同, 偶数次被选的Ai 不影响xor,奇数次被选的Ai影响一次
问题变成了说
选x个Ai,让它们 xor = 0, 那么
对于长度x 贡献是 x!
对长度x+2 贡献是 ?? 还是x, 但是剩余两个是一样的, 这两个一样的如果属于x个值内注意不要重复统计,不属于x个数内,则穿插即可
官方
对于给定的数组长度M
$C=(C_0,C_1,…C_{2^{16}-1})$ 表示 下标的值 是否存在, 相当于选择了一次
定义xor的卷积 $Z_k = \sum_{i\oplus j=k} X_i Y_j$
那么$C$的M次卷积的结果$R$中的$R_0$, 就是期望值
快速沃尔什-阿达玛转换(Fast Walsh Hadamard transform), 一种广义傅立叶变换
FWT/FWHT 是用于解决对下标进行位运算卷积问题的方法, 见下面我的博客链接
$C_{i} = \sum_{i=j \bigoplus k}A_{j} B_{k}$
因为 xor 的卷积满足结合率, 所以可以考虑快速幂来算
注意到$C * C = ifwt(fwt(C)\odot fwt(C))$
而$C * C * C= ifwt(fwt(C * C) \odot fwt(C))$
$= ifwt(fwt(ifwt(fwt(C)\odot fwt(C))) \odot fwt(C))$
$= ifwt(fwt(C)\odot fwt(C) \odot fwt(C))$
即是 $C^n = ifwt(\left( fwt(C)_ i^n\right))$
所以 $C$的$n$次+/xor/or/and卷积等于 正变换每个位置的$n$次方后的逆变换, 这个在dft(fft)/ntt/fwt 同理
令 $I = C^0 = (1,0,0,0,0,0,\cdots)$
答案 $R = C + C * C + \cdots + C^n$
$R * C = C^2 + C^3 + \cdots + C^{n+1}$
$R * (C-I) = C^{n+1} - C$
$fwt(R) \odot fwt(C-I) = fwt(C^{n+1} - C)$
$fwt(R) = fwt(C^{n+1} - C) \oslash fwt(C-I)$
$R = ifwt(fwt(C^{n+1} - C) \oslash fwt(C-I))$
注意到$fwt$ 实际是线性变换, 所以也有$fwt(a+b) = fwt(a) + fwt(b),fwt(a-b) = fwt(a) - fwt(b),$
$R = ifwt( (fwt(C^{n+1}) - fwt(C)) \oslash (fwt(C)-fwt(I)))$
注意到 $fwt(I) = (1,1,1,1,1,\cdots)$
$R = ifwt(\left(\frac{fwt(C)_ i^{n+1} - fwt(C)_ i}{fwt(C)_ i - 1}\right))$
至此就很好写了
代码
https://atcoder.jp/contests/abc212/submissions/33545657
1 | #include <bits/stdc++.h> |
总结
G
没观察到选了一个原根以后 整个范围 都有幂次和值的一一映射
H(Ex)
首先这个C和这个卷积 就很神奇, 完全没有向这个方向的思路
学了一手FWT/FWHT
参考
从FFT 到 NTT(快速数论变换)
NTT
竟然Div2能出现NTT 虽然是在最后一题, 还是得学一学
解决什么问题
是多项式乘法带模数的情况计算卷积
并且没有fft中会涉及到double,
NTT 也就是关于任意 环(数学术语) 上的离散傅立叶变化(DFT), 有限域的情况下,通常成为数论变换
离散傅立叶变换
回顾离散傅立叶变换, 就是
原函数(原向量) $\to$ DFT(离散傅立叶) $\to$ 点乘 $\to$ IDFT(逆傅立叶) $\to$ 结果函数(结果向量)
DFT:
$\hat{x}[k]=\sum_{n=0}^{N-1} e^{-i\frac{2\pi}{N}nk}x[n] \qquad k = 0,1,\ldots,N-1.$
IDFT:
$x\left[n\right]={1 \over N}\sum_{k=0}^{N-1} e^{ i\frac{2\pi}{N}nk}\hat{x}[k] \qquad n = 0,1,\ldots,N-1.$
有时系数可以是两个$\frac{1}{\sqrt{N}}$
矩阵表示的DFT, 是一个线性算子
${\displaystyle {\begin{bmatrix}f_{0}\\f_{1}\\\vdots \\f_{n-1}\end{bmatrix}}={\begin{bmatrix}1&1&1&\cdots &1\\1&\alpha &\alpha ^{2}&\cdots &\alpha ^{n-1}\\1&\alpha ^{2}&\alpha ^{4}&\cdots &\alpha ^{2(n-1)}\\\vdots &\vdots &\vdots &&\vdots \\1&\alpha ^{n-1}&\alpha ^{2(n-1)}&\cdots &\alpha ^{(n-1)(n-1)}\\\end{bmatrix}}{\begin{bmatrix}v_{0}\\v_{1}\\\vdots \\v_{n-1}\end{bmatrix}}.}$
Educational Codeforces Round 132
E
https://codeforces.com/contest/1709/problem/E
给你个一个树, 每个点上有值, 修改尽量少的点为任意值
让任何简单路径的 xor都不为零
范围
n 2e5
ai [1,2^30]
3s
256MB
题解
我的思路
显然啊, 类似树上差分的思想, a…b xor = 0
意味着 a…root ^ root …b = 0
所以直接变成 计算每个数到根的xor
然后问题变成, 任意两个相同值之间的连线, 需要至少选一个点
或者说, 需要xor树最终两两不等
不知道往后怎么搞
一个思路是dp
但是 遇到同值点不是 祖先关系,而是分叉的关系 ,也不知打怎么记录
题解
虽然的确有前缀意义,但其实 我想的总思路没问题, 但是细节有问题
并不是简单路径 = 到根的 路径 xor
因为它们的lca 被算了两次
所以其实是 path[u]^path[v] = a[lca[u,v]]
那么 对于一个点x = lca(u,v) , 且 u..v 的xor = 0
那么 (x,y) 上至少有一点需要改变
然后 我们对所有的有这种情况的x 按深度从深到浅考虑
找深度最深的x, 那么修改 x的方案不会比修改 u…v 的更差
因为是最深的, 所以其它满足的链 如果经过它的子树 则必定经过它,因为它需要修改, 所以而至少一个, 而对于其它经过它的链来说是至多一个
dfs(u){
对于u的所有子树 dfs(v)
每个点获取 从根到它的子节点 能得到的 xor 集合
一旦有来自两个子树 中有值 xor == u 那么u就必然被选
而被选的u对于它的父节点的返回是 空集合
未被选的u对于它的父节点的返回是它所有子集合可达值 xor 它自身
}
怎么看都是n^2 啊 , 为啥 O(nlog^2 n)
因为虽然dfs一次就可以把所有点到根的xor 算完
但是每次做 set 的合并时, 一个点就是会在它的 父节点 中被运算
那每个点被参与比较的次数可以达到 链的长度 不就是 n^2 吗
然后神奇的是 这个启发式合并的复杂度不是n方
启发式合并(DSU on Tree)
重儿子, 儿子节点子节点个数最多的
重边, 当前点连向其中一个重儿子的边
轻边 非重边
根到任意点 轻边不超过log n 条
反证法, 如果大于log n 条,
则最深的轻边的根至少还额外连了一个点大小为1的子树
第二深的轻边的根至少还额外连了一个点大小为3的子树
第三深的轻边的根至少还额外连了一个点大小为7的子树
…
第m深的轻边的根至少还额外连了一个点大小为2^m-1的子树
显然 m > log n , 则 2^m - 1 > n - 1 >= n
性质得证
那么每个节点 只有在它的某个祖先u的视角, 它是u的轻边的那一部分时, 才会作为被遍历的
所以每个节点的被遍历次数 = 它到根的 轻边数
所以 O(n log n)
代码
https://codeforces.com/contest/1709/submission/165472602
1 | #include <bits/stdc++.h> |
F
题目
https://codeforces.com/contest/1709/problem/F
读入 n, k ,f
包含长度等于n的字符串 的可重集合 beautiful 定义为
对于长度 [1,n] 之间任意字符串s, c[s] >= 集合中以s为前缀的字符串个数
任务 对于[1,n] 的所有字符串s, 你需要计算有多少 中映射c[s] 的选择方式
max(beautiful的集合的元素个数 )= f
其中 0 <= c[s] <= k
范围
n 15
k,f 2e5
6s
512ms
题解
我的思路
显然关于前缀
如果 s0 是 s1的前缀
那么显然 s0 出现的次数 >= s1 出现的次数
那么 如果 c[s0] <= c[s1] , c[s1] 就没什么作用
反过来, 如果 c[s0] >= sum c[s0的所有下一级后缀], 那么c[s0] 也不是紧限制
因此 真正有效的内容是
c[s1] <= c[s0] ,c[s0] <= sum c[s0的所有下一级后缀],
而这样约束以后, 方案数 就 = c[0] + c[1], 因为每个都表示能达到的最大个数
问题来了, 这样如何 算原始的c呢
题解
转化一下题目
高度n的满二叉树
你需要对每个父子之间做最大流量限制,[0,k] 之间整数
让根源, 所有叶子汇, 的最大流恰好 = f
求方案数 % 998244353
对于给定k
a(n,f) = 高度n, 最大恰好f的方案数
考虑状态转移
那么也就是 左边的n-1高度 最大流为v的时候,右边为f-v
但是这个 左边的最大流 可能是被根到左边这个边限制的, 也可能是本身n-1,v 的方案
定义 b(n,f) 等于 高度n 且根上多一个[0,k] 的限制的最大流 = k,方案数
如果是下面子树最大f, 那么对于根上可以选择 f~k, 所以有(k-f+1)a(n,f) 种
如果是下面子树最大 > f, 那么对于根上仅可选择f, 所以有 $\sum_{i=f+1}^{2k} a(n,i) 种
综上$b(n,f) = (k-f+1)a(n,f) + \sum_{i=f+1}^{2k} a(n,i)$ , 前缀和每个状态 均摊O(1) 可算
那么对于a就是最开始的转移
$a(n,f) = \sum_{i=0}^f b(n-1,i)\cdot b(n-1,f-i)$, 卷积, 需要FFT或者NTT就可以搞
那么答案 = $a(n,f)$, 也说明 $f > 2k$ 无解
最后边界处理, $b(n,f) = 0, f > k$
关于实现, 似乎直接NTT c++17会被卡, 可能要c++20
因为这里 相当于 是$a[n] = b[n-1]^2$, 所以平方可以少算一次NTT
然后 注意计算前把 末尾0删了, 不然可能长度倍增
以及我本地测 带了-fsanitize
的编译, c++17/20 都4.5s, 而不带的都是1s
代码
https://codeforces.com/contest/1709/submission/165613575
1 | #include <bits/stdc++.h> |
总结
E
一个是细节想错, lca多算一次 没想到
另一个是 考虑最深的lca的根 ,而不是考虑端点
这两点都是能想到没想到
然后关键的来了, 就是启发式合并的知识点, 这下学会了又
F
一个是变成树的 “流计算”, 很神奇 我好想 竖着觉得是树 ,横着就没发现了,
一个是学一下NTT, 以及NTT中的平方