Processing math: 1%

浅谈快速沃尔什变换

快速沃尔什变换(fwt)

fwt是一种快速计算位运算卷积的算法,一般包括按位或卷积,按位与卷积和异或卷积。

按位或(or)卷积

对于多项式A,B,C,定义\oplus为卷积符号,即A\oplus B = C

那么,按位或卷积就是:

C_k=\sum_{i~or~j=k}A_i\cdot B_j

类比于FFT,现在,我们的任务就是找到一种变换,记这种变换为fwt(A),则要满足fwt(A)\times fwt(B)=fwt(C),其中\times表示每一位相乘,且A\oplus B=C

经过前人的大力研究,可以发现:

fwt(A)_i=\sum_{j~or~i=i}A_j

是满足性质的,证明很简单,直接带进去可得:

\begin{align} fwt(C)_k&=\sum_{j~or~k=k}\sum_{a~or~b=j}A_a\cdot B_b\\ &=\sum_{a~or~k=k}A_a\cdot \sum_{b~or~k=k}B_b\\ &=fwt(A)_k\cdot fwt(B)_k \end{align}

即得证。

那么,考虑怎样快速的进行fwt变换。

然后有一个这样的式子:

fwt(A)= \begin{cases} (fwt(A_1),fwt(A_1)+fwt(A_2))&n>0\\ A_0&n=0 \end{cases}

其中,(A,B)表示把两个多项式的系数拼起来,感性理解一下就好了。

A_1表示多项式前半段,A_2表示后半段。

n=0的时候显然,我们只需要关心上面那个是为什么就好了。

对于前半段的第i项,i的最高位肯定是0,那么后半段显然对他没有影响,前半段的影响就是fwt(A_1)_i

对于后半段的第i项,i的最高位是1,所以最高位取0时是fwt(A_1)_i,取1时是fwt(A_2)_i,所以一共就是fwt(A_1)+fwt(A_2)

然后这玩意形式其实和FFT差不太多,复杂度也是O(n\log n)

代码:

void fwt_or(int *r) {
	for(int i=1;i<n;i<<=1)
		for(int j=0;j<n;j+=(i<<1))
			for(int k=0;k<i;k++)
                r[i+j+k]=(r[i+j+k]+r[j+k])%mod;
}

按位与(and)卷积

和上面差不多的,定义:

fwt(A)_i=\sum_{j\&i=i}A_i

证明也差不多,这里不赘述了。

那么,算的话就是:

fwt(A)= \begin{cases} (fwt(A_1)+fwt(A_2),fwt(A_2))&n>0\\ A_0&n=0 \end{cases}

只要考虑按位与的性质,高位为1时只能选高位为1的,否则都能选。

代码:

void fwt_and(int *r) {
	for(int i=1;i<n;i<<=1)
		for(int j=0;j<n;j+=(i<<1))
			for(int k=0;k<i;k++)
                r[j+k]=(r[i+j+k]+r[j+k])%mod;
}

异或(xor)卷积

这里的定义就不是很相同了。

定义:

fwt(A)_i=\sum_{j=0}^{n}(-1)^{cnt(i\&j)}A_j

其中,i\&j表示按位与,cnt(x)表示x二进制下1的个数。(这到底是怎么想到的。。)

带进去交换下枚举顺序可得:

\begin{align} fwt(C)_i&=\sum_{j=0}^{n}(-1)^{cnt(i\&j)}C_j\\ &=\sum_{j=0}^{n}(-1)^{cnt(i\&j)}\sum_{a\oplus b=j}A_aB_b\\ &=\sum_{a=0}^{n}A_a\sum_{b=0}^{n}B_b(-1)^{cnt(i\&(a\oplus b))} \end{align}

我们考虑下指数上的那一块东西:cnt(i\&(a\oplus b)),分情况讨论下这个与cnt(i\&a)+cnt(i\&b)的关系:(由于多位和一位没有区别,这里只讨论一位)

i0,显然这一位不计入答案,不管。

a,b都为1的话,a\oplus b=0,不计入答案,但是注意到这里是(-1)的指数,其实(-1)^0=(-1)^2,不妨看做是2,那么这两个相等。

a,b有一个为1,前后显然相等,都为1

a,b都为0,显然也相等,都为0

所以式子可以改写成这样:

\begin{align} fwt(C)_i&=\sum_{a=0}^{n}A_a\sum_{b=0}^{n}B_b(-1)^{cnt(i\&a)+cnt(i\&b)}\\ &=\sum_{a=0}^{n}(-1)^{cnt(i\&a)}A_a\sum_{b=0}^{n}(-1)^{cnt(i\&b)}B_b\\ &=fwt(A)_i\cdot fwt(B)_i \end{align}

所以,证毕。

那么,快速做这个的式子:

fwt(A)= \begin{cases} (fwt(A_1)+fwt(A_2),fwt(A_1)-fwt(A_2))&n>0\\ A_0&n=0 \end{cases}

具体的,考虑前一半的时候,最高位为0,直接加起来就好了。

对于后一半,最高位为1,如果选的数最高位也为1cnt就多了1,也就是整体多乘了个-1,所以就是fwt(A_1)-fwt(A_2)

代码:

void fwt_xor(int *r) {
	for(int i=1;i<n;i<<=1)
		for(int j=0;j<n;j+=(i<<1))
			for(int k=0;k<i;k++) {
				int x=r[j+k],y=r[i+j+k];
                r[j+k]=(x+y)%mod,r[i+j+k]=(x-y)%mod;
			}
}

逆沃尔什变换

知道了上面的,这玩意其实就很简单了。

对于按位或,就是知道了fwt(A_1)fwt(A_1)+fwt(A_2),求出两个分别是多少,直接减一下就完了:

ifwt(A)=(ifwt(A_1),ifwt(A_2)-ifwt(A_1))

对于按位与,也差不多:

ifwt(A)=(ifwt(A_1)-ifwt(A_2),ifwt(A_2))

对于异或,是知道fwt(A_1)+fwt(A_2)fwt(A_1)-fwt(A_2),那么加起来除以2就是第一个,减一下除以2就是第二个,即:

ifwt(A)=(\frac{ifwt(A_1)+ifwt(A_2)}{2},\frac{ifwt(A_1)-ifwt(A_2)}{2})

模板

给一个模板大全吧,题目来自luogu4717

#include<bits/stdc++.h>
using namespace std;
 
void read(int &x) {
    x=0;int f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar()) if(ch=='-') f=-f;
    for(;isdigit(ch);ch=getchar()) x=x*10+ch-'0';x*=f;
}
 
void print(int x) {
    if(x<0) putchar('-'),x=-x;
    if(!x) return ;print(x/10),putchar(x%10+48);
}
void write(int x) {if(!x) putchar('0');else print(x);putchar('\n');}

const int maxn = 2e5+10;
const int mod = 998244353;
const int inv2 = 499122177;

int bit,n,a[maxn],b[maxn],c[maxn],ina[maxn],inb[maxn];

void fwt_or(int *r,int op) {
	for(int i=1;i<n;i<<=1)
		for(int j=0;j<n;j+=(i<<1))
			for(int k=0;k<i;k++)
				if(op==1) r[i+j+k]=(r[i+j+k]+r[j+k])%mod;
				else r[i+j+k]=(r[i+j+k]-r[j+k])%mod;
}

void fwt_and(int *r,int op) {
	for(int i=1;i<n;i<<=1)
		for(int j=0;j<n;j+=(i<<1))
			for(int k=0;k<i;k++)
				if(op==1) r[j+k]=(r[i+j+k]+r[j+k])%mod;
				else r[j+k]=(r[j+k]-r[i+j+k])%mod;
}

void fwt_xor(int *r,int op) {
	for(int i=1;i<n;i<<=1)
		for(int j=0;j<n;j+=(i<<1))
			for(int k=0;k<i;k++) {
				int x=r[j+k],y=r[i+j+k];
				if(op==1) r[j+k]=(x+y)%mod,r[i+j+k]=(x-y)%mod;
				else r[j+k]=1ll*(x+y)*inv2%mod,r[i+j+k]=1ll*(x-y)*inv2%mod;
			}
}

int main() {
	read(bit);n=1<<bit;
	for(int i=0;i<n;i++) read(ina[i]);
	for(int i=0;i<n;i++) read(inb[i]);
	// or
	memcpy(a,ina,sizeof ina);memcpy(b,inb,sizeof inb);
	fwt_or(a,1),fwt_or(b,1);for(int i=0;i<n;i++) a[i]=1ll*a[i]*b[i]%mod;
	fwt_or(a,-1);for(int i=0;i<n;i++) printf("%d ",(a[i]+mod)%mod);puts("");
	// and 
	memcpy(a,ina,sizeof ina);memcpy(b,inb,sizeof inb);
	fwt_and(a,1),fwt_and(b,1);for(int i=0;i<n;i++) a[i]=1ll*a[i]*b[i]%mod;
	fwt_and(a,-1);for(int i=0;i<n;i++) printf("%d ",(a[i]+mod)%mod);puts("");
	// xor
	memcpy(a,ina,sizeof ina);memcpy(b,inb,sizeof inb);
	fwt_xor(a,1),fwt_xor(b,1);for(int i=0;i<n;i++) a[i]=1ll*a[i]*b[i]%mod;
	fwt_xor(a,-1);for(int i=0;i<n;i++) printf("%d ",(a[i]+mod)%mod);puts("");
	return 0;
}
posted @   Hyscere  阅读(457)  评论(0编辑  收藏  举报
编辑推荐:
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· 用 C# 插值字符串处理器写一个 sscanf
· Java 中堆内存和栈内存上的数据分布和特点
· 开发中对象命名的一点思考
· .NET Core内存结构体系(Windows环境)底层原理浅谈
阅读排行:
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· DeepSeek 解答了困扰我五年的技术问题。时代确实变了!
· 本地部署DeepSeek后,没有好看的交互界面怎么行!
· 趁着过年的时候手搓了一个低代码框架
· 推荐一个DeepSeek 大模型的免费 API 项目!兼容OpenAI接口!
点击右上角即可分享
微信分享提示