update in 2020/03/12:本篇内容几乎已经完全重构。

@0 - 参考资料@

我觉得这么好的博客应该发送给全世界观看。

yyb 的讲解(FWT)

popoqqq 的讲解(FWT)

zjp 的讲解(FMT)

Dance-Of-Faith 的讲解(FMT)

@1 - 位运算(子集)卷积@

现在已知两个 n = 2^m 维的向量 \(A = (a_0, a_1, ..., a_{n-1}), B = (b_0, b_1, ..., b_{n-1})\)。定义异或卷积式:

\[A\oplus B = (\sum_{i\oplus j = 0}a_i*b_j,\sum_{i\oplus j=1}a_i*b_j,\dots,\sum_{i\oplus j=n-1}a_i*b_j)
\]

类似地可以定义出或卷积、与卷积:

\[A | B=(\sum_{i | j=0}a_i*b_j,\sum_{i | j=1}a_i*b_j,\dots,\sum_{i | j=n-1}a_i*b_j)\\
A \& B=(\sum_{i \& j=0}a_i*b_j,\sum_{i \& j=1}a_i*b_j,\dots,\sum_{i \& j=n-1}a_i*b_j)\]

嘛,你也可以理解为 m 个元素构成的全集的所有子集进行子集对称差、子集并、子集交卷积。

定义加减法 \(A\pm B=(a_0\pm b_0,a_1\pm b_1,...,a_{n-1}\pm b_{n-1})\),再定义乘积 \(A*B=(a_0*b_0,a_1*b_1,...,a_{n-1}*b_{n-1})\)。

@2 - 关于 FWT(快速沃尔什变换)@

可以发现暴力做是 \(O(n^2) = O(4^m)\) 的,需要优化。

@2.1 - 从高维卷积角度看 FWT@

由于异或运算相当于二进制的不进位加法,那么异或卷积相当于是 m 元函数,每一维都进行二维循环卷积。

从集合角度来描述,将原来的向量记成一个 m 元函数 \(A(S) = \sum_{T\subset S}a_T\sum_{i\in T}x_{i}\)。

那么异或卷积相当于 \(A(S)\oplus B(S) = \sum_{T_1\subset S, T_2\subset S}a_{T_1}b_{T_2}\sum x_{i}^{([i\in T_1] + [i\in T_2]) \mod 2}\)。

那么我们就可以套用 fft 的方法。注意到二维情况单位根为 -1, 1,因此问题会简化很多。

不过直接用看起来比较复杂,考虑具体情况怎么实现。

定义 \(A_0=(a_0,a_1,...a_{\frac{n}{2}-1}), A_1=(a_{\frac{n}{2}},a_{\frac{n}{2}+1},...,a_{n-1})\),则有 \(A=(A_0, A_1)\)。

这里可以把 \(A_0\) 理解成不包含 0 号元素的子集,\(A_1\) 为包含 0 号元素的子集。

变换后应该有 \(fwt(A) = (fwt(A_0) + fwt(A_1), fwt(A_0) - fwt(A_1))\)。

其实很好理解,因为前面不包含 0 号元素,所以不管 0 号元素代什么值都是正;而后面包含 0 号元素,所以当 0 号元素代 -1 时(即 \(fwt(A_1)\))时才变成负。

逆变换根据 fft 的方法,类似地有 \(ifwt(A) = (\frac{ifwt(A_0) + ifwt(A_1)}{2}, \frac{ifwt(A_0) - ifwt(A_1)}{2})\)。

这个高维卷积好像也可以使用于另两种卷积,不过就不是 fft 了,而是自己去配每一维的变换。

@2.2 - 从分治乘法角度看 FWT@

在计算多项式乘法 \(A(x)B(x)\) 时,我们暴力做是 \(O(n^2)\) 的复杂度。

如果我们把 \(A(x)\) 分成等半的 \(A_0(x) + A_1(x)x^{\frac{n}{2}}\) 同理把 \(B(x)\) 分成 \(B_0(x) + B_1(x)x^{\frac{n}{2}}\),则:

\[A(x)B(x) = (A_0(x) + A_1(x)x^{\frac{n}{2}})(B_0(x) + B_1(x)x^{\frac{n}{2}})\\
= A_0(x)B_0(x) + (A_0(x)B_1(x) + A_1(x)B_0(x))x^{\frac{n}{2}} + A_1(x)B_0(x)x^n\]

正常做的话需要递归 4 次卷积,即时间复杂度 \(T(n) = 4T(n/2) + O(n)\),解出来 \(T(n) = n^2\)。

不过注意到 \((A_0(x) + A_1(x))(B_0(x) + B_1(x)) = A_0(x)B_0(x) + A_0(x)B_1(x) + A_1(x)B_0(x) + A_1(x)B_1(x)\),所以如果再算出 \(A_0(x)B_0(x), A_1(x)B_1(x)\) 即可递归 3 次解决。

时间复杂度 \(T(n) = 3T(n/2) + O(n)\),解出 \(T(n) = O(n^{\log_{2}3})\)。

这就是分治乘法的基本思路,利用结合律减少乘法的计算次数。我们可以把这种思路拓展在位运算卷积上,以或卷积为例。

定义 \(A_0=(a_0,a_1,...a_{\frac{n}{2}-1}), A_1=(a_{\frac{n}{2}},a_{\frac{n}{2}+1},...,a_{n-1})\),则有 \(A=(A_0, A_1)\)。

可以把 \(A_0, A_1\) 理解为向量 \(A\) 的前半部分与后半部分,也可以理解为 \(A\) 中二进制最高位为 0 的部分与二进制最高位为 1 的部分。

或卷积可以写作 \(A | B = (A_0, A_1) | (B_0, B_1) = (A_0 | B_0, A_0 | B_1 + A_1 | B_0 + A_1 | B_1)\)。一样地,正常情况下一次需要递归做 4 次卷积。

很简单地可以发现只需要求 \(A_0|B_0\) 与 \((A_0 + A_1)|(B_0 + B_1)\) 即可,由 \(T(n) = 2T(n/2) + O(n)\) 解出总时间复杂度为 \(O(n\log n)\)。

但是这样分治下来不太好拓展。我们尝试构造一个变换 \(A' = (A_0, A_0 + A_1)\)。

因此有 \((A|B)' = (A_0|B_0, (A_0 + B_0)|(A_1 + B_1)) = (A_0'|B_0', A_1'|B_1')\)。

这种变换的好处是我们把两部分给分离开了,因此可以分别递归做。

如果我们再对第二维,第三维一直到第 m 维都做一遍这个变换,那么最后只需要对应项相乘 —— 那么就和 fft 是一个道理了。

由此,我们可以构造出或卷积的 fwt 变换:

\[fwt(A) = (fwt(A_0), fwt(A_0) + fwt(A_1)) = (A_0', A_1') = A'
\]

至于逆变换是什么,考虑列方程组:

\[\begin{cases}
A_0' = fwt(A_0)&\\
A_1' = fwt(A_0) + fwt(A_1)
\end{cases}\]

解出来:

\[\begin{cases}
A_0 = ifwt(A_0')&\\
A_1 = ifwt(A_1') - ifwt(A_0')
\end{cases}\]

这个思路一样可以拓展成与卷积、异或卷积。不过异或卷积有点难配出来就是了。

补充一下与卷积:

\[\begin{cases}
A_0' = fwt(A_0) + fwt(A_1)&\\
A_1' = fwt(A_1)
\end{cases}\]

逆变换:

\[\begin{cases}
A_0 = ifwt(A_0') - ifwt(A_1')&\\
A_1 = ifwt(A_1')
\end{cases}\]

@3 - 参考代码实现@

本代码为 洛谷P4717 的 AC 代码。

#include<cstdio>
const int MOD = 998244353;
const int INV = (998244353 + 1)/2;
const int MAXN = (1<<17);
void fwt_or(int *a, int n, int type) {
for(int s=2;s<=n;s<<=1) {
int t = (s>>1);
for(int i=0;i<n;i+=s) {
for(int j=0;j<t;j++) {
int x = a[i+j], y = a[i+j+t];
a[i+j] = (type == -1) ? x : x;
a[i+j+t] = (type == -1) ? (y + MOD - x)%MOD : (y + x)%MOD;
}
}
}
}
void fwt_and(int *a, int n, int type) {
for(int s=2;s<=n;s<<=1) {
int t = (s>>1);
for(int i=0;i<n;i+=s) {
for(int j=0;j<t;j++) {
int x = a[i+j], y = a[i+j+t];
a[i+j] = (type == -1) ? (x + MOD - y)%MOD : (x + y)%MOD;
a[i+j+t] = (type == -1) ? y : y;
}
}
}
}
void fwt_xor(int *a, int n, int type) {
for(int s=2;s<=n;s<<=1) {
int t = (s>>1);
for(int i=0;i<n;i+=s) {
for(int j=0;j<t;j++) {
int x = a[i+j], y = a[i+j+t];
a[i+j] = (type == -1) ? 1LL*(x + y)%MOD*INV%MOD : (x + y)%MOD;
a[i+j+t] = (type == -1) ? 1LL*(x + MOD - y)%MOD*INV%MOD : (x + MOD - y)%MOD;
}
}
}
}
int A[MAXN + 5], B[MAXN + 5], C[MAXN + 5];
int main() {
int N; scanf("%d", &N); N = (1<<N);
for(int i=0;i<N;i++)
scanf("%d", &A[i]);
for(int i=0;i<N;i++)
scanf("%d", &B[i]); fwt_or(A, N, 1); fwt_or(B, N, 1);
for(int i=0;i<N;i++)
C[i] = 1LL*A[i]*B[i]%MOD;
fwt_or(A, N, -1); fwt_or(B, N, -1); fwt_or(C, N, -1);
for(int i=0;i<N;i++)
printf("%d ", C[i]);
puts(""); fwt_and(A, N, 1); fwt_and(B, N, 1);
for(int i=0;i<N;i++)
C[i] = 1LL*A[i]*B[i]%MOD;
fwt_and(A, N, -1); fwt_and(B, N, -1); fwt_and(C, N, -1);
for(int i=0;i<N;i++)
printf("%d ", C[i]);
puts(""); fwt_xor(A, N, 1); fwt_xor(B, N, 1);
for(int i=0;i<N;i++)
C[i] = 1LL*A[i]*B[i]%MOD;
fwt_xor(A, N, -1); fwt_xor(B, N, -1); fwt_xor(C, N, -1);
for(int i=0;i<N;i++)
printf("%d ", C[i]);
puts("");
}

@4 - 关于 FMT(快速莫比乌斯变换)@

感觉……其实也不是很有用的样子。

完全可以被 FWT 替代啊。(可能常数要小一点?)

FMT 用于解决这样一类卷积问题(子集并卷积):

\[f(S) = \sum_{T1\bigcup T2 = S}g(T1)*h(T2)
\]

\(S, T1, T2\) 都是集合,其中 \(T1,T2\subset S\),f, g, h 是定义在集合上的一个权值。

FMT 仿照 FFT 的方法,将集合转换为另一种处理起来更为方便的形式。定义:

\[\hat f(S)=\sum_{T\subset S}f(T)
\]

即 S 集合的所有子集权值之和。

依照这个定义,就有:

\[\hat f(S)=\sum_{T1\bigcup T2 \subset S}g(T1)*h(T2)
\]

我们知道子集之间的并集还是子集,所以就有:

\[\hat f(S)=\sum_{T1,T2 \subset S}g(T1)*h(T2)
\]

根据乘法分配律,可以得到:

\[\hat f(S)=(\sum_{T \subset S}g(T))*(\sum_{T \subset S}h(T))=\hat g(S)*\hat h(S)
\]

然后我们的目的就达成了,我们把卷积转换成了乘积。

再考虑怎么进行逆变换。根据容斥原理,可以得到:

\[f(S)=\sum_{T\subset S}(-1)^{|S|-|T|}\hat f(T)
\]

可以考虑 S 每一个子集对答案的贡献,是组合数代数和的形式,即 C(n,0) - C(n,1) + C(n,2) ... 。这是一个经典的容斥式子,只有当 n = 0 时它等于 1,所以这个逆变换是成立的。

考虑代码实现,一种暴力的想法是 3^n 枚举子集计算。

但是我们发现这个式子长得非常 dp,所以我们考虑用递推来解决这一问题。

定义 dp[i][s] 表示我们通过删去元素 1 ... i 中的一些,能得到的子集之和。

则通过对第 i 个元素的讨论,可以得到转移式 dp[i][s] = dp[i-1][s] + dp[i-1][s - {i}]。

然后第一维可以直接滚动数组。

逆变换的话,我们的 dp 转移式就变成了 dp[i][s] = dp[i-1][s] - dp[i-1][s - {i}]。因为每多一个元素,上面公式中 -1 的幂就要增加 1。

代码比较简短(type 为 1 时为正变换,type 为 -1 时为逆变换):

void fmt(int A[], int n, int type) {
for(int i=1;i<n;i<<=1)
for(int j=0;j<n;j++)
if( i & j ) A[j] = (A[j] + type*A[i^j])%MOD;
}

好了。为什么我说这个东西可以被 FWT 替代呢?因为如果把集合用相应的二进制数表示,我们所说的子集并卷积其实就是或卷积。包括我在网上看到的子集卷积,子集逆卷积都可以用或卷积来替代。因为其实我们并没有用到 FMT 独特的一些性质,只是使用 FMT 来将卷积转换为乘积,而这个我们 FWT 也可以干。

可能 FMT 的优势就在于代码稍微短一些,理解起来容易一些,更方便背代码。

@5 - 例题与应用@

@dp 优化@

身为一个卷积怎么可以不用来优化 dp?

假如我们列出了 dp 转移式子,发现它具有异或卷积的特征,我们就可以使用 fwt 进行优化。

比如:hdu - 5909 Tree Cutting 这道题就非常的典型。

另外,我们 fwt 也可以像 fft 一样进行卷积快速幂,比如这一道例题:bzoj - 4589 Hard Nim

@子集卷积@

有些情况下,可能题目在异或卷积的前提下会加上更多的限制:比如一个集合必须与另一个集合无交集(对应位运算即 i xor j = i+j),或者说一个集合必须是另外一个集合的子集(对应位运算即 i xor j = i-j)。

在这个时候,我们通常会去限定二进制表示下 1 的个数,从而达到题目给出的限制。

比如这道题:hdu - 6057 Kanade's convolution

@卷积逆运算@

当然有可能会让你作卷积除法。方法比较简单,fwt 作正变换过后直接相除即可。(为什么 fft 不可以这样做呢?因为 fft 得到的结果项数可能是无穷多的,而 fwt 得到的结果项数是完全已知的)。

如果是子集卷积作除法,先限制二进制 1 的个数,然后 fwt 正变换完过后,因为二进制 1 的个数不会很大,所以直接暴力求解即可。

有一道雅礼集训出现的题目:union

@总结 - 2@ 位运算卷积/子集卷积 —— FWT/FMT的更多相关文章

  1. 【bzoj2073】【[POI2004]PRZ】位运算枚举子集的特技

    (上不了p站我要死了) Description 一只队伍在爬山时碰到了雪崩,他们在逃跑时遇到了一座桥,他们要尽快的过桥. 桥已经很旧了, 所以它不能承受太重的东西. 任何时候队伍在桥上的人都不能超过一 ...

  2. 逻辑、集合运算上的卷积一览(FMT、FWT,……)

    \oplus=\and,\or,\veebar 简介 对于逻辑\(\oplus\)的卷积,而且你不能N方豹草 \[ A_k=\sum_{i\oplus j=k} B_i\times C_k\\ \] ...

  3. FMT 和 子集卷积

    FMT 和 子集卷积 FMT 给定数列 $ a_{0\dots 2^{k}-1} $ 求 $ b $ 满足 $ b_{s} = \sum_{i\in s} a_i $ 实现方法很简单, for( i ...

  4. 位运算卷积-FWT

    问题 给出两个幂级数 \(f,g\) ,求 \[ h=\sum _i\sum _jx^{i\oplus j}f_ig_j \] 其中 \(\oplus\) 是可拆分的位运算. 算法 由于位运算具有独立 ...

  5. 【学习笔记】fwt&&fmt&&子集卷积

    前言:yyb神仙的博客 FWT 基本思路:将多项式变成点值表达,点值相乘之后再逆变换回来得到特定形式的卷积: 多项式的次数界都为\(2^n\)的形式,\(A_0\)定义为前一半多项式(下标二进制第一位 ...

  6. CF914G Sum the Fibonacci FWT、子集卷积

    传送门 一道良心的练习FWT和子集卷积的板子-- 具体来说就是先把所有满足\(s_a \& s_b = 0\)的\(s_a \mid s_b\)的值用子集卷积算出来,将所有\(s_a \opl ...

  7. CF 914 G Sum the Fibonacci —— 子集卷积,FWT

    题目:http://codeforces.com/contest/914/problem/G 其实就是把各种都用子集卷积和FWT卷起来算即可: 注意乘 Fibonacci 数组的位置: 子集卷积时不能 ...

  8. Comet Contest#11 F arewell(DAG计数+FWT子集卷积)

    传送门. 题解: 4月YY集训时做过DAG计数,和这个基本上是一样的,但是当时好像直接暴力子集卷积,不然我省选时不至于不会,这个就多了个边不选的概率和子集卷积. DAG计数是个套路来的,利用的是DAG ...

  9. CF838C(博弈+FWT子集卷积+多项式ln、exp)

    传送门: http://codeforces.com/problemset/problem/838/C 题解: 如果一个字符串的排列数是偶数,则先手必胜,因为如果下一层有后手必赢态,直接转移过去,不然 ...

随机推荐

  1. leetcode 563 - 653

    563. Binary Tree Tilt Input: 1 / \ 2 3 Output: 1 Explanation: Tilt of node 2 : 0 Tilt of node 3 : 0 ...

  2. 字符串常用方法(转载--https://www.cnblogs.com/ABook/p/5527341.html)

    一.String类String类在java.lang包中,java使用String类创建一个字符串变量,字符串变量属于对象.java把String类声明的final类,不能有类.String类对象创建 ...

  3. webpack4配置react开发环境

    webpack4大大提高了开发效率,简化了配置复杂度,作为一个大的版本更新,作为一个对开发效率执着的爱折腾的程序员,已经忍不住要尝尝鲜了 首先是cli和webpack的分离,开发webpack应用程序 ...

  4. 学习写Js的动画

    说起前端,要说动画是最有乐子的东西了.玩好动画一定会很轻易的享受到前端的乐趣. 这里我不会讲述什么css3 的 transform animation keyframes,也不会讲述jquery的an ...

  5. plsql中学习job

    --1.plsql中学习job --学习job --建表 create table test_job(para_date date); commit; insert into test_job val ...

  6. iOS播放器横竖屏切换

    http://www.cocoachina.com/cms/wap.php?action=article&id=20292 http://feihu.me/blog/2015/how-to-h ...

  7. 强强联合 阿里云 RDS for SQL Server 与 金蝶 K/3 WISE 产品实现兼容适配

    强强联合 阿里云 RDS for SQL Server 与 金蝶 K/3 WISE 产品实现兼容适配,原K/3 WISE用户通过简单配置就可以无缝搭配RDS SQL Server使用,不需再费时费力自 ...

  8. 【CS Round #44 (Div. 2 only) C】Check DFS

    [链接]点击打开链接 [题意] 给你一个n节点,m条边的无向联通图. 给你一个节点访问的顺序.(1..n的排列) 你可以改变每个点优先访问的出度.(但必须按照dfs的规则); 问你能不能按照所给的访问 ...

  9. Vue--公有组件以及组件的使用和特点

    组件的作用:为了能够让功能与功能之间互不影响,使代码更加清晰整洁 1 <!DOCTYPE html> <html lang="en"> <head&g ...

  10. laravel-admin

    laravel-admin 文档地址: http://laravel-admin.org/docs/#/zh/