位运算的诱惑,FFT摇身一变FWT(快速沃尔什变换)(未完待续)

烂漫一生 提交于 2020-01-13 10:41:41

真的想点接触这个玩意。

cmd写的OI多项式💊是真的顶。

参考文献

人称OI药丸:https://www.luogu.com.cn/blog/command-block/wei-yun-suan-juan-ji-yu-ji-kuo-zhan

ZFY:你又从洛谷日报上抄袭,烦不烦呀。
如果能有源码我还自己打!QAQ

例题

例题

符号约定

ZFY:抄袭的第一步。。。

文中的多项式和下标均为向量,\([]\)表示向量拼接(但是\([\)命题\(]\)就是当命题成立时为\(1\))。

\(A_{i}\)表示\(A\)中的第\(i\)位,\(i_{j}\)表示\(i\)的二进制中从高往低的第\(j\)位。

ZWQ:你这和照搬有什么区别。。。为什么不能有自己的思考呢?像我一样。

\(⊕\)为位运算,&为和运算,^为异或,|为非运算。

规定\(n\)为总位数,且一定是\(2\)的次幂,记为\(2^u\)

\(C[k]=\sum\limits_{i⊕j==k,0≤i,j≤n-1}A[i]*B[i]\),写成\((A*B=C)\),称为位运算\(⊕\)的卷积形式。

\(DWT(A)\)\(A\)线性代换后的向量。别看线性代换这个词很高端,这里的线性代换操作其实就是你有一行长度为\(k\)数组,和一个长度为\(k*k\)的矩阵,你用这个数组和矩阵第一行的每个数乘起来,相加(也就是\(a_1*b_1+a_2*b_2+...+a_k*b_k\)),得到一个数字,然后对每一行都做一遍,得到了一个新的数组,这个数组就是代换后的向量,然后此操作称为线性代换。(但是不是严格定于,这个是我个人理解,线性知识是大学知识呀,我哪知道。)

其实就是矩阵乘法,不过是一行乘一行而已。

矩阵\(c\)的第\(i\)行,第\(j\)列的数字就是\(c(i,j)\),称为变换系数。

通用思路

规律性

现在给你一个位运算的规则,我们一开始看毫无头绪,但是仔细想想也不会,FFT是怎么做的?

不就是通过线性代换后的点值直接乘吗,我们自然也希望这样子做。

但是首先就是确定矩阵是否具有某些性质,毕竟不是所有的矩阵都可以这么做!!!

也就是\(DWT(A)_{i}=\sum\limits_{j=0}^{n-1}c(i,j)A_{j}\)

然后满足当\(C=A*B\)时,\(DWT(C)_{i}=DWT(A)_{i}*DWT(B)_{i}\)

也就是\(\sum\limits_{j=0}^{n-1}c(i,j)A_{j}*\sum\limits_{k=0}^{n-1}c(i,k)B_{k}=\sum\limits_{z=0}^{n-1}c(i,z)C_{j}\)

因为\(A*B=C\)

所以\(\sum\limits_{j=0}^{n-1}c(i,j)A_{j}*\sum\limits_{k=0}^{n-1}c(i,k)B_{k}=\sum\limits_{z=0}^{n-1}c(i,z)\sum\limits_{q⊕p==z}A_{q}B_{p}\)

\(\sum\limits_{j=0}^{n-1}\sum\limits_{k=0}^{n-1}c(i,k)c(i,j)A_{j}B_{k}=\sum\limits_{z=0}^{n-1}c(i,z)\sum\limits_{q⊕p==z}A_{q}B_{p}=\sum\limits_{q=0}^{n-1}\sum\limits_{p=0}^{n-1}A_{q}B_{p}c(i,q⊕p)\)

观察左右,发现了\(c(i,k)c(i,j)=c(i,j⊕k)\)

限制性

首先,\(c\)矩阵的每一行不能是相似的,也不能某行某列全是\(0\)。(也就是不能是倍数关系或者相同,这种矩阵一般没有逆,似乎都是肯定的,因为可以证明,全是\(0\)的怎么乘都是\(0\),相似的话不可能一行的某个位置为\(0\),另外一行的相对位置不为\(0\))因为在后面求逆矩阵的时候严格要求此地方不能没有逆矩阵。

为了方便后面的操作,我们规定我们构造的矩阵满足一下性质:\(c(i,j)=c(i_0,j_0)c(i_1,j_1)...\)。这个我曾经问过cmd本人,他也没有持反对态度。貌似也没有持支持态度。。。

所以,我们可以用一个\(2*2\)的矩阵表示\(c\),我们称为\(cc\),其实\(cc\)就是\(c\)的第一行到第二行,第一列到第二列,也叫位矩阵。

但是,其实我们还是要证明一个东西,就是这个构造和上面的规律有没有冲突。

位运算的一个很大的特点,就是每一位只与这一位有关。

所以我们对于\(c(i,j)*c(i,k)=c(i,j⊕k)\)

\(i⊕k=t\)

我们把前面的式子拆开就是:\((c(i_0,j_0)*c(i_1,j_1)*...*c(i_u,j_u))(c(i_0,j_0)*c(i_1,j_1)*...*c(i_u,j_u))*(c(i_0,j_0)*c(i_1,j_1)*...*c(i_u,j_u))=(c(i_0,j_0)*c(i_0,k_0))*(c(i_1,j_1)*c(i_1,k_1))*...*(c(i_u,j_u)*c(i_u,k_u))=c(i_0,t_0)*c(i_1,t_1)*...*c(i_u,t_u)=c(i,t)\)

所以互不影响。

可行性

这个可以用来证明为什么需要求逆矩阵,这是cmd所缺少的估计是他觉得太弱智了,也是为什么我要写这篇博客。

首先,这个\(c\)矩阵我们给他沿对角线翻转一下:
例如:\(\begin{Bmatrix}1 & 2 \\ 3 & 4\end{Bmatrix}\)翻折成了\(\begin{Bmatrix}1 & 3 \\ 2 & 4\end{Bmatrix}\)

那么\(c\)矩阵翻折后变成了\(y\)矩阵。

那么现在的DWT真的就是原向量与\(y\)矩阵的矩阵乘法了。

那么我们求出了\(A*y\)(这里是矩阵乘法),但是怎么求出\(A\)呢?因为矩阵满足结合律,所以我们只要求出有\(y^{-1}\),即逆矩阵,然后再一乘,出来了。

现在我们又要证一个东西,\(cc^{-1}\)按照限制性构造可以构造出\(c^{-1}\)

这个看起来正确但是还是要证的。

假设命题成立。

首先,对于\(c\)\(c^{-1}\)\(\sum\limits_{j=0}^{n-1}c_{i,j}*c^{-1}_{j,k}=[i=k]\)

那么我们拆分一下:\(\sum\limits_{j=0}^{n-1}(cc(i_0,j_0)*cc(i_1,j_1)*...*cc(i_u,j_u))*(cc^{-1}(j_0,k_0)*cc^{-1}(j_1,k_1)*...*cc^{-1}(j_u,k_u))=[i=k]\)

然后我们针对第\(u\)位,也就是最后一位来看(即针对最后一位为\(0\)和为\(1\)的进行合并),乘法分配律一下就是:\(cc(i_0,j_0)*cc(i_1,j_1)*...*cc(i_{u-1},j_{u-1})*cc^{-1}(j_0,k_0)*cc^{-1}(j_1,0)*...*cc^{-1}(j_{u-1},k_{u-1})(cc(i_u,0)*cc^{-1}(0,k_u)+cc(i_u,1)*cc^{-1}(1,k_u))=cc(i_0,j_0)*cc(i_1,j_1)*...*cc(i_{u-1},j_{u-1})*cc^{-1}(j_0,k_0)*cc^{-1}(j_1,0)*...*cc^{-1}(j_{u-1},k_{u-1})*[j_0=k_0]\)

\(j_0≠k_0\)时,式子直接为\(0\)

否则我们就当作没有\(u\)这个位,然后对\(u-1\)位进行这个操作。

如果\(i=j\),那么二进制一定有一位不一样变成\(0\),否则到最后就是\(1\)吗,这么简单的事情。

所以,事实证明是可行的。

相信看了这么一大坨的你,会更容易理解cmd的博客的。

可分治性

既然刚刚我们构造了一些条件,我们就要用这些条件去为我们的题目服务。

像FFT那样,我们对于\(DWT\)左右拆半,然后化成了这样的东西:

\(DWT(A)_{i}=\sum\limits_{j=0}^{\frac{n}{2}-1}c(i,j)A_{j}+\sum\limits_{j=0}^{\frac{n}{2}-1}c(i,j+\frac{n}{2})A_{j+\frac{n}{2}}\)

然后我们去掉首位,就是(其中的\(i',j'\)是去掉首位的\(i,j\)):

\(c(i_{0},0)\sum\limits_{j=0}^{\frac{n}{2}-1}c(i',j')A_{j}+c(i_{0},1)\sum\limits_{j=0}^{\frac{n}{2}-1}c(i',j')A_{j}\)

猛然发现了新大陆,我们把:\(DWT(A)_{i}\)\(DWT(A)_{i+\frac{n}{2}}\)进行对比。

\(DWT(A)_{i}=c(0,0)\sum\limits_{j=0}^{\frac{n}{2}-1}c(i',j')A_{j}+c(0,1)\sum\limits_{j=0}^{\frac{n}{2}-1}c(i',j')A_{j}\)

\(DWT(A)_{i+\frac{n}{2}}=c(1,0)\sum\limits_{j=0}^{\frac{n}{2}-1}c(i',j')A_{j}+c(1,1)\sum\limits_{j=0}^{\frac{n}{2}-1}c(i',j')A_{j}\)

那么我们就可以把过程分成\(log_{2}{n}\)层,用下一层合并到上一层,类似归并排序那样,会FFT的同学可以直接理解为FFT套板子,这个过程叫\(DWT\)

但是\(IDWT\)呢?就是把向量数组转会异或数组,我们只需要再做一遍\(DWT\),只不过用的矩阵是\(cc\)的逆矩阵。

总体时间复杂度为:\(O(n\log{n})\)

完结撒花。

矩阵

这么快就完结撒花,你是不是在想桃子。

我们中间的矩阵都没说呢。

\(cc\)有以下要求:

  1. 满足:\(cc(i,j)*cc(i,k)=cc(i,j⊕k)\),这个常常用来检验\(cc(0,0)\)一般是\(1\)还是\(-1\)还是其他,当然检验其他格子也是可以的。
  2. 存在逆矩阵。

和运算

\(c(0,0)*c(0,0)=c(0,0)\),所以\(c(0,0)\)只能是\(0,1\)
同理可证所有的都只能是\(0,1\)

所以检验之后只有以下矩阵:

\(\begin{Bmatrix} 0 & 1\\ 1 & 1\end{Bmatrix}\)\(\begin{Bmatrix} 1 & 1\\ 0 & 1\end{Bmatrix}\)

秉承着一抄抄到底的态度我们采用第二种。

求逆即为:\(\begin{Bmatrix}1 &-1 \\ 0 & 1\end{Bmatrix}\)

非运算

和和运算一样,也全都是\(0,1\)

检验之后有以下矩阵:\(\begin{Bmatrix} 1&1 \\1 & 0\end{Bmatrix}\)\(\begin{Bmatrix} 1& 0\\ 1&1 \end{Bmatrix}\)

在这里插入图片描述
-----cmd奆佬,Orz

我们依旧采用第二个矩阵,逆矩阵为:\(\begin{Bmatrix} 1&0 \\-1 & 1\end{Bmatrix}\)

异或运算

\(cc(0,0)*cc(0,y)=cc(0,0\)^\(y)=cc(0,y)\),所以\(cc(0,0)\)一定是\(1\)

\(cc(0,1)*cc(0,1)=cc(0,1\)^\(1)=cc(0,0)=1\),所以\(cc(0,0)\)\(1\)\(-1\)

\(cc(1,?)\)也是同理。

那么,经过检验有以下矩阵(这里cmd好像有点错误):\(\begin{Bmatrix} 1&1 \\1 & -1\end{Bmatrix}\)\(\begin{Bmatrix} 1& -1\\ 1&1 \end{Bmatrix}\)

这里我们采用第一个矩阵。逆矩阵可以直接抄的事情他不香吗。

逆矩阵为:\(\begin{Bmatrix} 0.5& 0.5\\ 0.5 & -0.5\end{Bmatrix}\)

幸亏题目有\(\%\),不然就难受了。

代码

#include<cstdio>
#include<cstring>
#define  N  210000
#define  mod  998244353
#define  inv2  499122177ll
using  namespace  std;
typedef  long  long  LL;
int  n,m;
LL  cor[2][2]={{1,0},{1,1}},can[2][2]={{1,1},{0,1}},cxo[2][2]={{1,1},{1,mod-1}};
//矩阵
LL  ior[2][2]={{1,0},{mod-1,1}},ian[2][2]={{1,mod-1},{0,1}},ixo[2][2]={{inv2,inv2},{inv2,mod-inv2}};
//逆矩阵
LL  a[N],b[N],aa[N],bb[N],an1[N],an2[N],an3[N];
void  fwt(LL  *a,LL  f[2][2])
{
    for(int  i=2;i<=m;i<<=1)
    {
        int  mid=(i>>1);
        for(int  j=1;j<=m;j+=i)
        {
            int  ed=j+mid-1;
            for(int  k=j;k<=ed;k++)
            {
                LL  x=a[k],y=a[k+mid];
                a[k]=x*f[0][0]+y*f[0][1];a[k]%=mod;
                a[k+mid]=x*f[1][0]+y*f[1][1];a[mid+k]%=mod;
            }
        }
    }
}
void  cpy(){for(int  i=1;i<=m;i++)aa[i]=a[i],bb[i]=b[i];}
int  main()
{
    scanf("%d",&n);
    m=1;for(int  i=1;i<=n;i++)m<<=1;
    for(int  i=1;i<=m;i++)scanf("%lld",&a[i]);
    for(int  i=1;i<=m;i++)scanf("%lld",&b[i]);
    cpy();
    fwt(aa,cor);
    fwt(bb,cor);
    for(int  i=1;i<=m;i++)an1[i]=(aa[i]*bb[i])%mod;
    fwt(an1,ior);
    
    cpy();
    fwt(aa,can);
    fwt(bb,can);
    for(int  i=1;i<=m;i++)an2[i]=(aa[i]*bb[i])%mod;
    fwt(an2,ian);
    
    cpy();
    fwt(aa,cxo);
    fwt(bb,cxo);
    for(int  i=1;i<=m;i++)an3[i]=(aa[i]*bb[i])%mod;
    fwt(an3,ixo);
    for(int  i=1;i<m;i++)printf("%lld ",an1[i]);printf("%lld\n",an1[m]);
    for(int  i=1;i<m;i++)printf("%lld ",an2[i]);printf("%lld\n",an2[m]);
    for(int  i=1;i<m;i++)printf("%lld ",an3[i]);printf("%lld\n",an3[m]);
    return  0;
}

小结

规律性其实就是针对行的,为了能够分治,我们添加规则,使其满足性质,真是精致。

未完待续

  1. 秉承着抄到底的理念,下一次就抄完!!!
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!