从SOSDP到FWT

本文原名 SOSDP ,2025.08.20添加了FWT部分。

符号:

如无特殊说明,\(\cap\) 表示按位与,\(\cup\) 表示按位或, \(\oplus\) 表示按位异或。

\(i\subseteq x\) 表示的是 \(i\) 作为二进制表示的集合是 \(x\) 的子集。形式化地,\(i\cap x=i\)

SOSdp

原文链接

SOSdp(sum over subsets dynamic programming)用来解决这样的问题:

给定数组 \(a\) ,定义 \(F(x)=\sum_{i \subseteq x} a_i\),求每个 \(F(x)\) 的值。
也即对 \(x\) 二进制意义下的子集作为下标的 \(a\) 求和。

为了解决这一问题,我们定义 \(f_{x,i}\) 表示二进制下仅有后 \(i\) 位与 \(x\) 不同,且是 \(x\) 子集的数作为下标的 \(a\) 求和。位数从 \(0\) 开始编号,如 \(101\) 是两位。

举个例子,\(f_{\bold{101}1010,3}=a_{\bold{101}1010}+a_{\bold{101}1000}+a_{\bold{101}0010}+a_{\bold{101}0000}\) ,加粗部分代表一定相同。

现在考虑转移。

\[f_{x,i}= \begin{cases} f_{x,i-1} &i^{th}~bit =0\\ f_{x\oplus 2^i,i-1}+f_{x,i-1} & i^{th}~bit=1\\ \end{cases} \]

简单解释一下:若 \(x\) 的第 \(i\) 位为 \(0\) ,那么此时被求和的元素的这一位都一定是 \(0\) ,可以直接推到 \(f_{x,i-1}\) ,否则被求和元素分为该位是 \(0/1\) 的两部分, 式子中后一项为选 \(1\) 的,前一项中异或 \(2^i\) 从而该位变为 \(0\) 。正确性得证。

于是,我们便可以在 \(O(n\log n)\) 的时间内解决问题!( \(n\)为数组元素数,若 \(n\) 为二进制下的位数则复杂度为原博客的 \(O(n2^n)\) )

压掉第二维非常好写!

//假设n为位数,101为两位数(
for(int i=0;i<n;i++)
    for(int x=(1<<i);x<(1<<n);x++)
        if(x&(1<<i))f[x]+=f[x^(1<<i)];

哇这也太优美了111!!!

其他应用

sosdp除了算子集还能算超集!!(超集:若 \(A\)\(B\) 的子集,则 \(B\)\(A\) 的超集)

for(int i=0;i<n;i++)
    for(int x=(1<<i);x<(1<<n);x++)
        if(!(x&(1<<i)))f[x]+=f[x^(1<<i)];

唯一的区别在于 if 中的条件。

sosdp的思维还能优化高维前缀和!!!

这里是思维,我绕了好久,并不是直接用sosdp!!!!!!!!!

先将 sum 每个位置赋值为原数组该位上的值,然后做三次这个,第一次不变,第二次line 7的i-1变为i,我们对j方向累加,第三次同理

for(int i=1;i<=n;i++)
{
    for(int j=1;j<=m;j++)
    {
        for(int k=1;k<=l;k++)
        {
            sum[i][j][k]+=sum[i-1][j][k];
        }
    }
}

如果你每次从旁边几个加过来然后再容斥,复杂度应为 \(O(nml\cdot2^d)\) ,而这样可以优化到 \(O(dnml)\) !!

太伟大了sosdp。

原博客下给出了很多例题,帮大家把在 cf 上的搬运过来了(

CF165E
CF383E
CF499D
CF800D

FWT

之前一直以为 FWT 是和 FFT 之类的多项式内容强相关的,今天发现不是。

FWT一般用来解决神秘位运算卷积,形式化地,拿到两个序列 \(A,B\) ,然后我们要求的 \(C\) 由如下方式得到:

\[C_i=\sum_{j\odot k=i}A_j\cdot B_k \]

其中,\(\odot\) 为按位与,按位或,按位异或中的一种。

按位与、按位或

这两种的处理方法较为类似。下面先以按位或为例。

首先,这个要求 \(j\cup k=i\) ,比较困难,但是我们发现求 \(j\cup k\subseteq i\) 是更好求的。

我们设一个新的数组:\(FC\),满足

\[FC_i=\sum_{j\cup k\subseteq i}A_j\cdot B_k \]

我们来简单转化一下,可以发现

\[FC_i=\sum_{j\subseteq i}A_j\cdot\sum_{j\subseteq i}B_j \]

后面这两个相乘的式子,对 \(i\) 的所有子集求和……这不是我们刚刚才学的 SOSdp 吗?

直接再设两个数组 \(FA,FB\)\(FA_i=\sum_{j\subseteq i}A_j\)\(FB\) 同理。则 \(FC_i=FA_i\cdot FB_i\)

观察我们求出来的 \(FC\)

\[FC_i=\sum_{j\cup k\subseteq i} A_j\cdot B_k=\sum_{t\subseteq i}\sum_{j\cup k=t} A_j\cdot B_k=\sum_{j\subseteq i}C_j \]

Wow,和 \(FA,FB\) 的结构一模一样。(这同时意味着,如果你要对多个数组做这个卷积,可以直接拿两个数组得到的这个 \(F\) 接着做下去!)

要从 \(FC\) 还原出 \(C\),我们只要倒着再做一遍 SOSdp。like this:

for(int i=n-1;i>0;i--)
    for(int x=(1<<i);x<(1<<n);x++)
        if(x&(1<<i))f[x]-=f[x^(1<<i)];

到这里大家肯定都猜到按位与怎么做了。只需要把上述流程中的子集变成超集就可以了。

但是,要通过洛谷上的板子,还有一种运算。

为了更好的理解下面这个更抽象的卷积,我们先来小结一下。其实刚刚的 \(FA\) 数组就是 \(A\) 数组在按位与/按位或意义下沃尔什变换得到的新序列。如果你学过 FFT 的话,你肯定能发现这两种算法之间的共同点:

  • 先通过一种方式,使用神秘的变换,把原来的序列变换成另一个。
  • 此时,原来的卷积在新序列上转化成了 按位相乘 的形式。
  • 变换存在逆变换。也即,我们可以通过变换后得到的序列求解出原来的序列。

现在,你已经明白了沃尔什变换的核心思想,让我们掌声有请……

按位异或!

SOSdp 是对于单一序列做的,所以当然不能弄出来出什么 “怎么怎么异或就得到 \(i\)” 的数组。我们没法直接套用上面的方法。

借鉴我们刚刚的思路:我们需要通过某种方式得到一个 \(F\) 数组的构造方式,将原来的卷积转化为按位相乘。

这个构造个人觉得其实难以注意到。我们定义 \(x \circ y\) 表示 \(x \cap y\)\(1\) 数量的奇偶性,即 \(x \circ y = \mathrm{popcnt}(x \cap y) \bmod 2,\) 那么有 \((x \circ y) \oplus (x \circ z) = x \circ (y \oplus z).\)

简单口胡一下证明:把这个运算拆开来就变成

\[\mathrm{popcnt}(x \cap y)\bmod 2\oplus\mathrm{popcnt}(x \cap z)\bmod 2=\mathrm{popcnt}(x \cap (y\oplus z))\bmod 2\\ \mathrm{popcnt}(x \cap y)\oplus\mathrm{popcnt}(x \cap z)\equiv\mathrm{popcnt}(x \cap (y\oplus z))\pmod 2\\ \mathrm{popcnt}(x \cap y)\oplus\mathrm{popcnt}(x \cap z)\equiv\mathrm{popcnt}((x\cap y)\oplus (x\cap z))\pmod 2\\ \]

\(y'=y\cap x,z'=z\cap x\)

\[\mathrm{popcnt}(y')\oplus\mathrm{popcnt}(z')\equiv\mathrm{popcnt}(y'\oplus z')\pmod 2\\ \]

大概感知一下,\(y'\oplus z'\) 只可能将若干 \(1\) 变成 \(0\),所以不改变奇偶性。

那我设 \(FA_i=\sum_{i\circ j=0}A_j-\sum_{i\circ j=1}A_j\),下面来证明,\(FC_i=FA_i\cdot FB_i\)

\[\begin{aligned} FC_i&=(\sum_{i\circ j=0}A_j-\sum_{i\circ j=1}A_j)(\sum_{i\circ j=0}B_j-\sum_{i\circ j=1}B_j)\\ &=(\sum_{i\circ j=0}\sum_{i\circ k=0}A_jB_k+\sum_{i\circ j=1}\sum_{i\circ k=1}A_jB_k)-(\sum_{i\circ j=1}\sum_{i\circ k=0}A_jB_k+\sum_{i\circ j=0}\sum_{i\circ k=1}A_jB_k)\\ \end{aligned} \]

然后我们又知道 \((j\oplus k)\circ i=(j\circ i)\oplus (k\circ i)\),所以上式可以变形

\[\begin{aligned} FC_i&=\sum_{i\circ(j\oplus k)=0}A_jB_k-\sum_{i\circ(j\oplus k)=1}A_jB_k\\ &=\sum_{i\circ t=0}\sum_{j\oplus k=t}A_jB_k-\sum_{i\circ t=1}\sum_{j\oplus k=t}A_jB_k\\ &=\sum_{i\circ j=0}C_j-\sum_{i\circ j=1}C_j\\ \end{aligned} \]

所以这个是对的!

现在我们要来由原序列算出 \(F\)。依旧记 \(f_{x,i}\) 表示仅考虑二进制下仅有后 \(i\) 位与 \(x\) 不同的所有下标对当前位置的 \(F\) 的贡献。

枚举每一位,若该位为 \(0\),则与当前位为 \(1\) 的数进行与运算时会使得该位由 \(1\) 变为 \(0\),贡献需要取负,\(f_{x,i}=f_{x,i-1}-f_{x\oplus 2^i,i-1}\)

若该位为 \(1\),则与当前位为 \(0\) 的数进行与运算时不会改变他的 popcount,直接加。\(f_{x,i}=f_{x,i-1}+f_{x\oplus 2^i,i-1}\)

上面这个过程其实比较抽象,我们来举个例子。

现在原数组 0,1,2,3 位置上放的数就是 0,1,2,3。初始时,\(f_{i,-1}=i\) (请先容许我用 \(-1\) 表示完全相同)。
考虑 \(f_{0,1}\) 的计算,\(0\) 的第 \(1\) 位为 \(0\) ,属于第一种情况。\(0\oplus2^1=2\),而下标 \(2\) 上原本的“和他的最大差异位至多是第 \(0\) 位的” 下标集合,也即 \(2,3\),是被算在 \(f_{2,0}\) 内的。
此时我们丢给 \(0\) 的下标集,第 \(1\) 位都与 \(2\) 相同,都是 \(1\),与 \(0\) 取交后这一位就没了,所以 \(\mathrm{popcount}-=1\) ,奇偶性改变。所以算 \(f_{0,1}\) 我们要减掉 \(f_{2,0}\)

来到代码部分。我们注意到 \(j\)\(j\oplus 2^i\) 要同时计算。更改一下 SOSdp 代码即可。下述代码取自洛谷板子,然后 \(cap=2^n\)

for(int i=0;i<n;i++)
    for(int j=0;j<cap;j++)
        if(j&(1<<i))
        {
            int t=fa[j];
            fa[j]=(fa[j]+fa[j^(1<<i)])%mod;
            fa[j^(1<<i)]=(fa[j^(1<<i)]-t+mod)%mod;
        }

求逆变换,也即我们需要通过 fc[j]+fc[j^(1<<i)],-fc[j]+fc[j^(1<<i)] 还原出两者。这个应该挺简单的。代码中的 i2\(2\) 在模意义下的逆元。

for(int i=n-1;i>=0;i--)
    for(int j=0;j<cap;j++)
        if(j&(1<<i))
        {
            int t=fc[j],tt=fc[j^(1<<i)];
            fc[j]=1LL*(t-tt+mod)*i2%mod;
            fc[j^(1<<i)]=1LL*(t+tt)*i2%mod;
        }

下面给出洛谷板子代码qwq

code
#include<cstdio>
using namespace std;
const int mn=131080,mod=998244353;
const int i2=499122177;
int n,cap;
int a[mn],b[mn],c[mn];
int fa[mn],fb[mn],fc[mn];
int main()
{
    scanf("%d",&n);
    cap=1<<n;
    for(int i=0;i<cap;i++)scanf("%d",&a[i]);
    for(int i=0;i<cap;i++)scanf("%d",&b[i]);
    
    // OR
    for(int i=0;i<cap;i++){fa[i]=a[i];fb[i]=b[i];}
    for(int i=0;i<n;i++)
        for(int j=0;j<cap;j++)
            if(j&(1<<i))
            {
                fa[j]=(fa[j]+fa[j^(1<<i)])%mod;
                fb[j]=(fb[j]+fb[j^(1<<i)])%mod;
            }
    for(int i=0;i<cap;i++)fc[i]=1LL*fa[i]*fb[i]%mod;
    for(int i=n-1;i>=0;i--)
        for(int j=0;j<cap;j++)
            if(j&(1<<i))
                fc[j]=(fc[j]-fc[j^(1<<i)]+mod)%mod;
    for(int i=0;i<cap;i++)printf("%d ",fc[i]);
    puts("");

    // AND
    for(int i=0;i<cap;i++){fa[i]=a[i];fb[i]=b[i];}
    for(int i=0;i<n;i++)
        for(int j=0;j<cap;j++)
            if(!(j&(1<<i)))
            {
                fa[j]=(fa[j]+fa[j^(1<<i)])%mod;
                fb[j]=(fb[j]+fb[j^(1<<i)])%mod;
            }
    for(int i=0;i<cap;i++)fc[i]=1LL*fa[i]*fb[i]%mod;
    for(int i=n-1;i>=0;i--)
        for(int j=0;j<cap;j++)
            if(!(j&(1<<i)))
                fc[j]=(fc[j]-fc[j^(1<<i)]+mod)%mod;
    for(int i=0;i<cap;i++)printf("%d ",fc[i]);
    puts("");

    // XOR
    for(int i=0;i<cap;i++){fa[i]=a[i];fb[i]=b[i];}
    for(int i=0;i<n;i++)
        for(int j=0;j<cap;j++)
            if(j&(1<<i))
            {
                int t=fa[j];
                fa[j]=(fa[j]+fa[j^(1<<i)])%mod;
                fa[j^(1<<i)]=(fa[j^(1<<i)]-t+mod)%mod;
                t=fb[j];
                fb[j]=(fb[j]+fb[j^(1<<i)])%mod;
                fb[j^(1<<i)]=(fb[j^(1<<i)]-t+mod)%mod;
            }
    for(int i=0;i<cap;i++)fc[i]=1LL*fa[i]*fb[i]%mod;
    for(int i=n-1;i>=0;i--)
        for(int j=0;j<cap;j++)
            if(j&(1<<i))
            {
                int t=fc[j],tt=fc[j^(1<<i)];
                fc[j]=1LL*(t-tt+mod)*i2%mod;
                fc[j^(1<<i)]=1LL*(t+tt)*i2%mod;
            }
    for(int i=0;i<cap;i++)printf("%d ",fc[i]);
    puts("");
}
posted @ 2025-03-15 15:31  ikusiad  阅读(129)  评论(0)    收藏  举报