FWT学习笔记


FWT

快速沃尔什变换,用来解决位运算相关的卷积。常见的有与、或、异或三种。

P4717 【模板】快速莫比乌斯/沃尔什变换 (FMT/FWT)#

给定长度为 2n 两个序列 A,B,设

Ci=jk=iAj×Bk

分别当 是 or,and,xor 时求出 C

n17

merge(A,B) 表示将数列 B 拼接到数列 A 后面形成的新数列。

考虑构造一种可逆的线性变换,记 A 变换后为 A^,那么满足

C=ABC^=A^×B^

其中 × 号表示对应位相乘, 号是卷积。然后求出 C^ 的逆变换即可。

因为变换是线性的,所以 C=A+BC^=A^+B^

然后考虑如何构造这个变换。

or#

设现在数列长度为 2nA0 表示 A 的前 2(n1) 个数,其下标二进制下第 n 位为 0A1 表示 A 的后 2(n1) 个数,其下标二进制下第 n 位为 1B0,B1,C0,C1 类似。

显然 C0 只需要 A0,B0 的信息,但是 C1 需要 A0,A1,B0,B1 的信息。不妨构造 A^=merge(A^0,A^0+A^1),当序列长度为 1Ai^=Ai。证明这样变换是正确的:

因为有 C0=A0B0,C1=A0B1+A1B0+A1B1

所以有 C^0=A^0×B^0,C^1=A^0×B^1+A^1×B^0+A^1×B^1

merge(A^0,A^0+A^1)×merge(B^0,B^0+B^1)=merge(A^0×B^0,A^0×B^0+A^0×B^1+A^1×B^0+A^1×B^1)=merge(C^0,C^0+C^1)=C

既然正变换是 A^=merge(A^0,A^0+A^1),那么逆变换应该是 A=merge(A0,A1A0)。不论是正,逆变换,都可以使用分治在 O(n2n) 的时间复杂度内完成。

void FWT_OR(int* a, int v) {
    for (int k = 2; k <= (1 << n); k <<= 1)
        for (int len = k >> 1, i = 0; i < (1 << n); i += k)
            for (int j = i; j < i + len; j++) (a[j + len] += (a[j] * v + mod) % mod) %= mod;
}

v = 1 时是正变换,v = -1 时是逆变换。

事实上,观察上面的代码(或者手玩几组数据)可以发现最终的 A^i=j|i=iAj。代入到式子里面也是正确的。

可以看出来 or 卷积是高维前缀和。

and#

和 or 操作类似的,构造 A^=merge(A^0+A^1,A^0),容易证明这样变换是正确的。逆变换是 A=merge(A0A1,A1)

容易发现 A^i=j&i=iAj

可以看出来 and 卷积实际上是高维后缀和。

void FWT_AND(int* a, int v) {
    for (int k = 2; k <= (1 << n); k <<= 1)
        for (int len = k >> 1, i = 0; i < (1 << n); i += k)
            for (int j = i; j < i + len; j++) (a[j] += (a[j + len] * v + mod) % mod) %= mod;
}

xor#

因为有 popcount(x)+popcount(y)popcount(xxory) (mod 2) 所以 (1)popcount(x)(1)popcount(y)=(1)popcount(xxory)

x&(yxorz)=(xxory)&(xxorz) 所以可以构造 A^i=j=02n1(1)popcount(i&j)Aj。也可以写成 j=02n1(1)|ij|Aj

根据式子,可以得到变换 A^=merge(A^0+A^1,A^0A^1)。这是因为原来 A^0,A^1 都没有考虑下标二进制下的第 n 位,而 A^ 要考虑第 n 位了。当原先不考虑第 n 位,现在两个下标第 n 位都是 1,那么 popcount(i&j)=popcount(i&j)+1(1)popcount(i&j) 符号就反过来了。如果其中有一个下标二进制下第 n 位为 0,那么 popcount(i&j)=popcount(i&j),符号不变。

显然其逆变换是 A=merge(A0+A12,A0A12)

看起来挺像 FFT 的,事实上 xor 卷积是高维循环卷积。

void FWT_XOR(int* a, int v) {
    for (int k = 2; k <= (1 << n); k <<= 1)
        for (int len = k >> 1, i = 0; i < (1 << n); i += k)
            for (int j = i; j < i + len; j++) {
                int x = a[j], y = a[j + len];
                a[j] = 1ll * (x + y) * v % mod;
                a[j + len] = 1ll * (x - y + mod) * v % mod;
            }
}
Code of P4717
#include<cstdio>
const int M = 2e5 + 10;
const int mod = 998244353;

int n, a[M], b[M], c[M];

void FWT_OR(int* a, int v) {
    for (int k = 2; k <= (1 << n); k <<= 1)
        for (int len = k >> 1, i = 0; i < (1 << n); i += k)
            for (int j = i; j < i + len; j++) (a[j + len] += (a[j] * v + mod) % mod) %= mod;
}

void FWT_AND(int* a, int v) {
    for (int k = 2; k <= (1 << n); k <<= 1)
        for (int len = k >> 1, i = 0; i < (1 << n); i += k)
            for (int j = i; j < i + len; j++) (a[j] += (a[j + len] * v + mod) % mod) %= mod;
}

void FWT_XOR(int* a, int v) {
    for (int k = 2; k <= (1 << n); k <<= 1)
        for (int len = k >> 1, i = 0; i < (1 << n); i += k)
            for (int j = i; j < i + len; j++) {
                int x = a[j], y = a[j + len];
                a[j] = 1ll * (x + y) * v % mod;
                a[j + len] = 1ll * (x - y + mod) * v % mod;
            }
}

int main() {
    scanf("%d", &n);
    for (int i = 0; i < (1 << n); i++) scanf("%d", &a[i]);
    for (int i = 0; i < (1 << n); i++) scanf("%d", &b[i]);

    FWT_OR(a, 1), FWT_OR(b, 1);
    for (int i = 0; i < (1 << n); i++) c[i] = 1ll * a[i] * b[i] % mod;
    FWT_OR(c, -1), FWT_OR(a, -1), FWT_OR(b, -1);
    for (int i = 0; i < (1 << n); i++) printf("%d ", c[i]);
    putchar('\n');

    FWT_AND(a, 1), FWT_AND(b, 1);
    for (int i = 0; i < (1 << n); i++) c[i] = 1ll * a[i] * b[i] % mod;
    FWT_AND(c, -1), FWT_AND(a, -1), FWT_AND(b, -1);
    for (int i = 0; i < (1 << n); i++) printf("%d ", c[i]);
    putchar('\n');

    FWT_XOR(a, 1), FWT_XOR(b, 1);
    for (int i = 0; i < (1 << n); i++) c[i] = 1ll * a[i] * b[i] % mod;
    FWT_XOR(c, (mod + 1) >> 1);
    for (int i = 0; i < (1 << n); i++) printf("%d ", c[i]);
    return 0;
}

CF662C Binary Table#

link of Luogu

有一个 nm 列的表格,每个元素都是 0/1

每次操作可以选择一行或一列,把 0/1 翻转,即把 0 换为 1 ,把 1 换为 0

请问经过若干次操作后,表格中最少有多少个 1

1n20,1m105

n 很小,可以将每一列看做一个状态。

每行只有翻转和不翻转之分。考虑翻转的行的集合为 S,原先这一列的状态为 x,翻转之后变成 xxorS

Ax 为状态为 x 的列的数量,Bx=max(popcount(x),npopcount(x))。那么翻转行的集合为 S 时答案就是 x=02n1AxBxxorS,即 xxory=SAxBy,FWT 优化即可。

Code of CF662C
#include<cstdio>
#include<algorithm>
const long long M = 2e6 + 10;

long long A[M], B[M], C[M];
int n, m, a[21][100010];

void FWT_XOR(long long* a, int op) {
    for (int k = 2; k <= (1 << n); k <<= 1)
        for (int len = k >> 1, i = 0; i < (1 << n); i += k)
            for (int j = i; j < i + len; j++) {
                long long x = a[j], y = a[j + len];
                if (op == 1)
                    a[j] = x + y, a[j + len] = x - y;
                if (op == -1)
                    a[j] = (x + y) / 2, a[j + len] = (x - y) / 2;
            }
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++) scanf("%1d", &a[i][j]);
    for (int j = 0; j < m; j++) {
        int S = 0;
        for (int i = 0; i < n; i++) S += a[i][j] * (1 << i);
        A[S]++;
    }
    for (int i = 1; i < (1 << n); i++) B[i] = B[i - (i & -i)] + 1;
    for (int i = 0; i < (1 << n); i++) B[i] = std::min(B[i], n - B[i]);

    FWT_XOR(A, 1), FWT_XOR(B, 1);
    for (int i = 0; i < (1 << n); i++) C[i] = A[i] * B[i];
    FWT_XOR(C, -1);
    long long ans = 10000000000007;
    for (int i = 0; i < (1 << n); i++) ans = std::min(ans, C[i]);
    printf("%lld\n", ans);
    return 0;
}

HDU5823 Color II#

给定一张无向图,设点集 S 至少要 g(S) 种颜色染色,能够使得其中有边相连的两点不同颜色。求 Sg(S)×233S mod232

n18

fx,Sx 种颜色给点集 S 染色的方案数。那么有 fx,S=S1S2=Sfx1,S1f1,S2,即先用 x1 种颜色染 S1,然后再将 S2 中的点涂上新的颜色。g(S) 即为 fx,S 不为 0 的最小的 x

先预处理出 f1,S,然后 n 次 FWT 求出每个 fx,S 即可。时间复杂度 O(n22n)

Code of HDU5823
#pragma GCC optimize("Ofast")
#include <cstdio>
#include <cstring>
const int N = 1 << 18, M = 20;

typedef unsigned int uint;

int T, n;
bool G[M][M], vis[N];
uint f[M][N], pw233[N];

void FWT_OR(uint* a, int op) {
    for (int k = 2; k <= (1 << n); k <<= 1)
        for (int len = k >> 1, i = 0; i < (1 << n); i += k)
            for (int j = i; j < i + len; j++) a[j + len] += a[j] * op;
}

inline void clear();
int main() {
    pw233[0] = 1;
    for (int i = 1; i < N; i++) pw233[i] = pw233[i - 1] * 233;
    scanf("%d", &T);
    while (T--) {
        clear();
        scanf("%d", &n);
        for (int i = 0; i < n; i++)
            for (int j = 0; j < n; j++) scanf("%1d", &G[i][j]);

        for (int S = 1; S < (1 << n); S++) {
            bool flag = true;
            for (int i = 0; i < n && flag; i++)
                for (int j = 0; j < n && flag; j++)
                    if ((S & (1 << i)) && (S & (1 << j)) && G[i][j]) flag = false;
            if (flag) f[1][S] = 1;
        }

        uint ans = 0;
        for (int i = 1; i < n; i++) {
            for (int S = 1; S < (1 << n); S++)
                if (f[i][S] && !vis[S]) vis[S] = true, ans += i * pw233[S];
            FWT_OR(f[i], 1);
            for (int S = 0; S < (1 << n); S++) f[i + 1][S] = f[1][S] * f[i][S];
            FWT_OR(f[i + 1], -1);
        }
        for (int S = 1; S < (1 << n); S++)
            if (f[n][S] && !vis[S]) ans += n * pw233[S];
        printf("%u\n", ans);
    }
    return 0;
}

inline void clear() {
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++) G[i][j] = 0;
    for (int i = 0; i < n; i++)
        for (int j = 0; j < (1 << n); j++) f[i][j] = 0;
    for (int i = 0; i < (1 << n); i++) vis[i] = false;
}

UOJ310 黎明前的巧克力#

长为 n 的序列 a,选出两个互不相交的集合,满足这两个集合内元素异或和相同且不同时为空,问方案数。

n106,ai106

两个集合元素异或和相同,说明将选出的所有元素异或起来和为 0

fi,x 为前 i 个数选出若干个分成两个集合,两个集合元素异或和异或起来为 x 的方案数。

显然有转移 fi,x=fi1,x+2fi1,xxorai,但是 O(n2) 的。

FWT 优化转移,构造向量 gi 满足 gi,0=1,gi,ai=2,其余元素都为 0。那么 fi^x=fi1^xgi^x,即 fn^x=i=1ngi^x

但是这样要做 n 轮异或卷积,会 T 飞。

考虑异或卷积的变换:

Ax=y=02n1(1)|xy|Ay

但是 gi 里面只有 gi,0gi,ai 不为 0,可以简化:

Ax=(1)|0x|A0+(1)|aix|Aai=1+2×(1)|aix|

gi,x 的取值只有可能为 13。设 g,x 共有 p3np1,那么最终 i=1ngi,x=3p(1)np

考虑如何快速求得 p。因为只有两种取值,可以求出 i=1ngi^x,然后鸡兔同笼就能求出 p。而 FWT 是线性变换,所以可以全部加起来之后再变换。最终得到 h^x=i=1ngi^x,那么 3p(np)=h^xp=h^x+n4

于是 i=1ngi^x 就容易求得。和初始状态 f0,0=1 卷积即可。

其实直接对 i=1ngi^x 做 IFWT 即可,因为只有 f0,0=1 时,f0^ 的每个元素都是 1

答案是 (i=1ngi^0)^

Code of UOJ310
#include <cstdio>
#include <cassert>
#include <cstring>
#include <algorithm>
#define int long long

const int N = 1 << 20, M = N + 10;
const int mod = 998244353;

int n, a[M], f[M], g[M], h[M], pw3[M];

void FWT_XOR(int* a, int v, bool flag = true) {
    for (int k = 2; k <= N; k <<= 1)
        for (int l = k >> 1, i = 0; i < N; i += k)
            for (int j = i; j < i + l; j++) {
                int x = a[j], y = a[j + l];
                a[j] = 1ll * (x + y) * v;
                a[j + l] = 1ll * (x - y) * v;
                if (flag) ((a[j] %= mod) += mod) %= mod, ((a[j + l] %= mod) += mod) %= mod;
            }
}

signed main() {
    pw3[0] = 1;
    for (int i = 1; i < M; i++) pw3[i] = 3ll * pw3[i - 1] % mod;

    scanf("%lld", &n);
    for (int i = 1; i <= n; i++) scanf("%lld", &a[i]);
    for (int i = 1; i <= n; i++) h[0]++, h[a[i]] += 2;
    FWT_XOR(h, 1, false);
    int mn = 0, mx = 0;
    for (int i = 0; i < N; i++) mn = std::min(mn, h[i]), mx = std::max(mx, h[i]);

    for (int i = 0; i < N; i++) {
        int _3 = (h[i] + n) / 4;
        h[i] = ((n - _3) & 1) ? (mod - pw3[_3]) : pw3[_3];
    }
    FWT_XOR(h, (mod + 1) >> 1, true);
    printf("%d\n", (h[0] + mod - 1) % mod);
    return 0;
}

其他的题还有 BZOJ4589 Hard Nim | sol


其他位运算#

FWT 不止可以应用于 or, and, xor 卷积。

FWT 可以看做一个向量 A 乘上变换矩阵 G 得到 A^。显然矩阵 G 可逆。

C^x=A^xB^xi=0nGx,iCi=j=0nGx,jAjk=0nGx,kBki=0nGx,ijk=iAjBk=j=0nk=0nGx,jGx,kAjBkj=0nk=0nGx,ijAjBk=j=0nk=0nGx,jGx,kAjBkGx,jk=Gx,jGx,k

所以如果矩阵 G 满足上述条件,那么 G 就是一个合法的变换矩阵。

例如 or 卷积,G=[1101]G[A0A1]=[A0A0+A1]。而 G1=[1011] 恰好是逆变换的矩阵。

HDU6966 I love sequences#

给定三个长为 n 的序列 a,b,c

dp,k=k=ijaibj,其中 1i,jnp 代表三进制下的按位 gcd

p=1nk=1+dp,kcpk

n2×105

枚举 pnp=O(nlnn),所以对于每个 p 暴力求出 dp

虽然是三进制,但是还是位运算卷积,考虑 FWT。

列出关于变换矩阵 G 的方程组:

{Gx,02=Gx,0Gx,1Gx,0=Gx,1Gx,12=Gx,1Gx,2Gx,0=Gx,2Gx,2Gx,1=Gx,1Gx,22=Gx,2

容易解得 (Gx,0,Gx,1,Gx,2)=(0,0,0),(1,0,1),(1,1,1),(1,0,0)。显然不能取 (0,0,0) 因为会导致不存在 G1。故最终 G=[100101111]

其实可以交换任意两行,仍然满足转移矩阵的性质。

A^=merge(A^0,A^0+A^2,A^0+A^1+A^2),逆变换是 A=merge(A0,A2A1,A1A0)

对于每个 p,FWT 求出数组 dp 即可。时间复杂度 O(nlog2n)

Code of HDU6966
#pragma GCC optimize("Ofast")
#include <cstdio>
#define int long long

const int mod = 1e9 + 7;
const int M = 6e5 + 10;

int n, a[M], b[M], c[M], d[M], a0[M], b0[M];
int _lg3[M], pw3[M];

inline int read() {
    char ch = getchar();
    int x = 0;
    while (ch < '0' || ch > '9') ch = getchar();
    while (ch >= '0' && ch <= '9') x = x * 10 + ch - '0', ch = getchar();
    return x;
}

inline void FWT(int* a, int op, int L) {
    for (int k = 3; k <= L; k *= 3)
        for (int len = k / 3, i = 0; i < L; i += k)
            for (int j = i; j < i + len; j++) {
                int x = a[j], y = a[j + len], z = a[j + 2 * len];
                if (op == 1) {
                    a[j] = x, a[j + len] = (x + z) % mod, a[j + 2 * len] = (x + y + z) % mod;
                } else {
                    a[j] = x, a[j + len] = (z - y + mod) % mod, a[j + 2 * len] = (y - x + mod) % mod;
                }
            }
}

signed main() {
    _lg3[0] = -1, pw3[0] = 1;
    for (int i = 1; i < M; i++) _lg3[i] = _lg3[i / 3] + 1;
    for (int i = 1; i <= 20; i++) pw3[i] = pw3[i - 1] * 3;

    n = read();
    for (int i = 1; i <= n; i++) a[i] = read();
    for (int i = 1; i <= n; i++) b[i] = read();
    for (int i = 1; i <= n; i++) c[i] = read();

    int ans = 0;
    for (int p = 1; p <= n; p++) {
        int L = 1;
        while (L <= n / p) L *= 3;
        //int L = pw3[_lg3[n / p] + 1];
        for (int i = 1; i <= n / p; i++) a0[i] = a[i], b0[i] = b[i];
        FWT(a0, 1, L), FWT(b0, 1, L);
        for (int i = 0; i < L; i++) d[i] = 1ll * a0[i] * b0[i] % mod;
        FWT(d, -1, L);
        //for (int i = 0; i < L; i++) (ans += 1ll * qpow(c[p], i) * d[i] % mod) %= mod;
        int pwc = 1;
        for (int i = 0; i < L; i++)
            (ans += 1ll * pwc * d[i] % mod) %= mod, pwc = 1ll * pwc * c[p] % mod;

        for (int i = 0; i < L; i++) d[i] = a0[i] = b0[i] = 0;
    }
    printf("%lld\n", ans);
    return 0;
}

子集卷积

P6097 【模板】子集卷积#

给定两个长度为 2n 的序列 a,b,求出序列 c

ck=i&j=0i  j=kaibj

n20

只有 ij=k 的限制的话是好求的。考虑如何加上 i&j=0 这个限制。

注意到这两个限制相当于 |S1|+|S2|=|S|S1S2=S,考虑 dp。

f|i|,i=ai,g|j|,j=bj,h|k|,k=ck,其余项都为 0,那么有:

hx,S=y+z=xS1  S2=Sfy,S1gz,S2

暴力枚举 y,z,然后后面部分用 FWT 优化即可。时间复杂度 O(n22n)

Code of P6097
#include <cstdio>
const int M = 21, N = 1 << 20;
const int mod = 1e9 + 9;

int n, a[M][N], b[M][N], c[M][N];
int pcnt[N];

inline void FWT_OR(int* a, int op) {
    for (int k = 2; k <= (1 << n); k <<= 1)
        for (int l = k >> 1, i = 0; i < (1 << n); i += k)
            for (int j = i; j < i + l; j++) (((a[j + l] += 1ll * op * a[j] % mod) %= mod) += mod) %= mod;
}

int main() {
    for (int i = 1; i < N; i++)
        pcnt[i] = pcnt[i - (i & -i)] + 1;
    scanf("%d", &n);
    for (int i = 0; i < (1 << n); i++) scanf("%d", &a[pcnt[i]][i]);
    for (int i = 0; i < (1 << n); i++) scanf("%d", &b[pcnt[i]][i]);
    for (int i = 0; i <= n; i++) FWT_OR(a[i], 1), FWT_OR(b[i], 1);
    for (int i = 0; i <= n; i++)
        for (int j = 0; i + j <= n; j++)
            for (int S = 0; S < (1 << n); S++)
                (c[i + j][S] += 1ll * a[i][S] * b[j][S] % mod) %= mod;
    for (int i = 0; i <= n; i++) FWT_OR(c[i], -1);
    for (int i = 0; i < (1 << n); i++) printf("%d ", c[pcnt[i]][i]);
    return 0;
}

作者:zzxLLL

出处:https://www.cnblogs.com/zzxLLL/p/17810165.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   zzxLLL  阅读(24)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示