计数 dp 部分例题(六~十部分)

六、转化求和顺序(線形和への分解)

例题1

题意

有一个长为 \(n\) 的数组 \(a\)。求在 \(a\) 中选择 \(k\) 个数的所有方案中,每次选择的所有数的中位数的和。\(n\le 10^5,k\) 为偶数。

解法

\(B=\frac k2-1\)。排序后,设在最中间的两个数为 \(a_i,a_j\),则 \(\frac 12(a_i+a_j)\) 在所有中位数里出现了 \(\binom{i-1}B\binom{n-j-1}B\) 次,则答案为 \(\frac 12\sum_{i=1}^n\sum_{j=i+1}^n(a_i+a_j)\binom{i-1}B\binom{n-j-1}B\),化简可以变成 \(\frac 12\sum_{i=1}^n\binom{i-1}B\left(a_i\left(\sum_{j=i+1}^n\binom{n-j-1}B\right)+\sum_{j=i+1}^n\binom{n-j-1}Ba_j\right)\),后缀和优化计算即可。

例题2

\(\sum_{x=0}^n\sum_{y=0}^m x{\,{\rm{XOR}}\,}y\)\(n,m\le 10^9\)

解法

考虑将每个 \(\rm{XOR}\) 的每一位拆开,然后合并每一位。设 \(B_{x,j,0/1}\) 为满足 \(\le x\) 且第 \(j\) 位为 \(0/1\) 的数的数量(显然可以 \(O(1)\) 计算),则第 \(j\) 位造成的贡献为 \(2^j(B_{n,j,0}B_{m,j,1}+B_{n,j,1}B_{m,j,0})\)

例题3:Many Easy Problems

给定一棵大小为 \(n\) 的无根树,定义 \(f(i)\),对于所有大小为 \(i\) 的点集,求出能够包含它的最小连通块大小之和。对于 \(i=1 \sim n\) 的所有 \(i\),求出 \(f(i)\bmod 924844033\)\(n\le 2\times 10^5\)。(\(924844033=2^{21}\times 441+1\),原根为 \(5\)

解法

考虑将“能够包含每个大小为 \(i\) 的点集的最小连通块大小之和”在每个点而非每个连通块处计算贡献,则其可以转化为“对于每个点,求作为包含某个大小为 \(i\) 的点集的最小连通块中包含了该点的连通块数量”。

设以某个点 \(u\) 为根时,其的每棵子树内大小分别为 \(siz_{u,1},siz_{u,2},\cdots,siz_{u,s_u}\),则某个最小连通块内不包含 \(u\) 的充要条件是内部每个点都在 \(u\) 儿子的子树内,\(u\)\(f(i)\) 的贡献即为 \(\binom ni-\sum_{j=1}^{s_u}\binom{siz_{u,j}}i\)\(f(i)\) 即为 \(n\binom ni-\sum_{u=1}^n\sum_{j=1}^{s_u}\binom{siz_{u,j}}i\)。设 \(c_i=\sum_{u=1}^n\sum_{j=1}^{s_u}[siz_{u,j}=i]\),则 \(f(i)=n\binom ni-\sum_{j=1}^nc_j\binom ji\)。将 \(\binom ji\) 拆成 \(\frac{j!}{i!(j-i)!}\) 的形式,令 \(F_j=(n-j)!\),则 \(\sum_{j=1}^nc_j\binom ji=\sum_{j=1}^n(c_jj!)F_{n+i-j}\),就是标准的卷积形式了(差卷积)。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=200010;
const int maxd=maxn<<1;
const int maxp=524300;
const int md=924844033,G=5;
int n,i,j,k,u,v,t,p,le,lm;
int h[maxn],c[maxp],d[maxp];
int r[maxp],fac[maxn],inv[maxn];
struct edge{int to,nxt;}E[maxd];
inline int Pow(int d,int z){
    int r=1;
    do{
        if(z&1) r=(1LL*r*d)%md;
        d=(1LL*d*d)%md;
    }while(z>>=1);
    return r;
}
int dfs(int p,int f){
    int lp,to,sz=1,nw;
    for(lp=h[p];lp;lp=E[lp].nxt){
        to=E[lp].to; if(to==f) continue;
        nw=dfs(to,p); sz+=nw; ++c[nw];
    }
    ++c[n-sz]; return sz;
}
inline void Add(int &x,int y){x-=((x+=y)>=md)*md;} 
void DFT(int *f){
    for(i=1;i<t;++i) if(i<r[i]) swap(f[i],f[r[i]]);
    for(i=1,le=2;le<=t;i=le,le<<=1){
        v=Pow(G,(md-1)/le);
        for(j=0;j<t;j+=le){
            for(k=0,u=1;k<i;++k,u=(1LL*u*v)%md){
                p=(1LL*f[i+j+k]*u)%md;
                Add(f[i+j+k]=f[j+k],md-p);
                Add(f[j+k],p);     
            }
        }
    }
} 
inline int C(int x,int y){return ((1LL*fac[y]*inv[x])%md*inv[y-x])%md;}
int main(){
    fac[0]=1;
    for(i=1;i<maxn;++i) fac[i]=(1LL*fac[i-1]*i)%md;
    inv[maxn-1]=Pow(fac[maxn-1],md-2);
    for(i=maxn-1;i;--i) inv[i-1]=(1LL*inv[i]*i)%md;
    scanf("%d",&n);
    for(i=1;i<n;++i){
        scanf("%d%d",&u,&v);
        E[++t]={u,h[v]}; h[v]=t;
        E[++t]={v,h[u]}; h[u]=t;
    }
    dfs(1,0); c[0]=0; d[0]=fac[n];
    for(i=1;i<=n;++i){
        d[i]=inv[n-i];
        c[i]=(1LL*c[i]*fac[i])%md;
    }
    for(u=n<<1,t=1;t<u;t<<=1); 
    for(le=t>>1,i=0;i<t;++i)
        r[i]=(r[i>>1]>>1)+(i&1)*le;
    DFT(c); DFT(d);
    for(i=0;i<t;++i) c[i]=(1LL*c[i]*d[i])%md;
    DFT(c); reverse(c+1,c+t); u=Pow(t,md-2);
    for(i=1,j=n+1;i<=n;++i,++j){
        v=((1LL*c[j]*u)%md*inv[i])%md; 
        Add(v=md-v,(1LL*n*C(i,n))%md);
        printf("%d\n",v);
    } 
    return 0;
}

七、置换群相关知识(部分群のテクニック)

置换群相关知识可以用于解决某些不可重计数问题。然而自认为后面的两个题的解法和置换群相关知识联系不大,需要者可以自行看 OI Wiki。不过这里的题目都是和“如何交换两个可交换的位置以尽量少影响其他位置/找出能够表示其他所有操作的单位操作”的思路有关。

例题1:Reverse Grid

题意

有一个 \(H\times W\) 的矩阵 \(a\),可以进行若干次操作,每次选择一行或一列翻转,求最后能够生成的本质不同的矩阵有多少种。\(H,W\le 200,\) 矩阵内的元素均为小写字母。

解法

考虑某个 \(a_{i,j}\) 只会变到 \(a_{H-i+1,j},a_{i,W-j+1},a_{H-i+1,W-j+1}\) 的位置。(对于 \(H,W\) 为奇数的情况,可以直接乘上中间能否变化的方案数,则其他元素都有四种可能出现的位置)所以这四个元素可以看成一个四元组整体处理。设四个位置的数分别为 \(a,b,c,d\),则可以通过下面的方式使得只有 \(a,b,c,d\) 之间的顺序进行变化(箭头表示行/列内其他元素的顺序):

同理其他任意三个元素也可以按照这样的方式变化,打表可以发现这些元素出现的顺序有 \(12\) 种。同时可以发现将同在一行/一列的元素调换位置可得所有 \(4!\) 种方式中另外 \(12\) 种出现方式,意味着某行被翻转将导致若干个四元组能够变换的位置同时变化。注意如果某个四元组内部出现了相同的数,则它们一定可以在某次变化后出现在同一行,它们在被翻转某行/某列后能够变换出的顺序一样,可以把对应的贡献先乘上;否则在翻转某行/某列后对应能够变换出的顺序会变化。

考虑将每一行和每一列看成点,然后对于某个四元组 \(\{a_{i,j},a_{H-i+1,j},a_{i,W-j+1},a_{H-i+1,W-j+1}\}(2i\le H,2j\le W)\),在第 \(i\) 行和第 \(j\) 列之间连边。在翻转某行/某列之后对应出边的四元组状态也会改变,所以可以对于某个连通块 \(S\) 求出某棵生成树,然后对于树上 \(2^{|S|-1}\) 种状态,自顶向下确定某行/某列是否翻转(会有两种等效的方式),进一步确定其他所有边的状态(只会有一种)。时间复杂度为 \(O(HW)\)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=210;
const int maxt=maxn<<1;
const int md=1000000007; 
int n,m,i,j,k,p,w,a,b,ans=1,c[4];
int fa[maxt],siz[maxt];
char s[maxn][maxn];
int Find(int x){
    if(x==fa[x]) return x;
    return fa[x]=Find(fa[x]);
}
inline void Merge(int x,int y){
    x=Find(x); y=Find(y);
    if(x==y) return;
    if(siz[x]>siz[y]) swap(x,y);
    fa[x]=y; siz[y]+=siz[x]; siz[x]=1;
}
int main(){
    scanf("%d%d",&n,&m);
    for(i=1;i<=n;++i) scanf("%s",s[i]+1);
    a=(n>>1)+1; b=(m>>1)+1;
    if(n&1){
        for(i=1;s[a][i]==s[a][m-i+1]&&i<=m;++i);
        if(i<=m) ans<<=1;
    }
    if(m&1){
        for(i=1;s[i][b]==s[n-i+1][b]&&i<=n;++i);
        if(i<=n) ans<<=1;
    }
    for(i=1,j=a+b-2;i<=j;++i) fa[i]=i,siz[i]=1;
    for(i=1;i<a;++i){
        for(j=1;j<b;++j){
            char t[4]={s[i][j],s[i][m-j+1],
                       s[n-i+1][j],s[n-i+1][m-j+1]};
            sort(t,t+4); w=24;
            for(k=c[p=0]=1;k<4;++k)
                w/=(++c[p+=(t[k]!=t[k-1])]);
            if(p==3) Merge(i,a+j-1),w=12;
            ans=(1LL*ans*w)%md;
            memset(c,0,(p+1)<<2);
        }
    }
    for(i=1,j=a+b-2;i<=j;++i)
        for(w=siz[i];--w;ans-=((ans<<=1)>=md)*md); 
    printf("%d\n",ans);
}

例题2:Rotate 3x3

题意

有一个 \(3\times n\) 的网格,第 \(i\) 行第 \(j\) 列的数为 \(3(j-1)+i\)。可以进行若干次操作,每次将某个 \(3\times 3\) 的子矩阵旋转 \(180^{\rm o}\),求能否经过若干次操作得出某个 \(3\times n\) 矩阵。\(n\le 10^5\)

解法

显然每一列的数不会改变,且已知每一列被翻转的次数的奇偶性,则可以将每一列压成一个数,可以用正负号表示对应的列是否翻转。此时每次操作即为取三个相邻的数一起取反,然后交换两边的数。

显然奇数/偶数位置内部才能进行变换,然后对于某两个数交换位置时,我们可以选择中间的任意一个奇偶性不同的数变号。证明考虑对于两个数 \(i,j\) 和中间奇偶性不同的数 \(k\),可以先将 \(i\) 移动到 \(k-1\) 位置(会使得中间一段数全部变号,且从 \(i+1\) 开始每两个数交换一次;最后整体向左平移),再将 \(j\) 移动到 \(k+1\) 位置(同理,但是整体向右平移);对 \(i,k,j\) 进行操作;最后把 \(i,j\) 移回 \(j,i\) 原来的位置。这种操作覆盖了给出的操作,所以可以表示所有操作。同理可以在不改变其他数的情况下将任意两个奇偶性相同的数变号。端点位置也可以变号,考虑让其变成非端点位置变号再移回去即可。

综上,可以设计出任意一个方案使得每个数到达对应位置,然后判断奇数/偶数位置的负数个数是否均为偶数即可。设计方案时,令 \(r_i\) 为第 \(i\) 行的数的绝对值,然后考虑一张 \(n\) 个点的有向图,每个 \(i\)\(r_i\) 连一条边(显然有向图由若干个简单环组成);则每次对于某个 \(i\ne r_i\)\(i\)\(r_i\)\(r_{r_i}\) 交换位置时,对应在有向图上就是将 \(r_i\) 移出所在的简单环,所以在不超过 \(n\) 次操作后一定可以将每个数换到原来的位置。最后注意在每次操作后维护奇数/偶数位置的负数个数。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=100010;
int n,i,j,x,y,z; bool c[2];
int a[maxn][3],r[maxn];
int main(){
    scanf("%d",&n); c[0]=c[1]=1;
    for(i=0;i<3;++i)
        for(j=1;j<=n;++j)
            scanf("%d",a[j]+i);
    for(i=1;i<=n;++i){
        x=a[i][0]; 
        y=a[i][1]; 
        z=a[i][2];
        j=y/3+1;
        if(y%3!=2||(j^i)&1||
        !((y==x+1&&z==y+1)||
          (y==x-1&&z==y-1))){
            printf("No\n");
            return 0; 
        }
        r[i]=j; c[i&1]^=(y<x);
    }
    for(i=1;i<=n;++i){
        x=!(i&1);
        while(j=r[i],i!=j){
            swap(r[i],r[j]);
            c[x]^=1;
        }
    }
    if(c[0]&&c[1]) printf("Yes\n");
    else printf("No\n");
    return 0;
}

八、递归定义的运用(再帰的な定義の利用)

在分形相关的题目中,可以将 \(K\) 级分形的对应内容拆成 \(K-1\) 级分形的内容以 DP 计算。

例题:Chaotic Polygons

题意

\(n\) 阶谢尔宾斯基三角形(如下)内简单闭合回路数量。\(n\le 10^5\)

解法

考虑该三角形内的回路有两种:

  • 只经过其中一个 \(n-1\) 阶三角形内的。
  • 同时经过三个 \(n-1\) 阶三角形并绕回来的。

\(S_n\)\(n\) 阶三角形的答案,而 \(A_n\) 为从某个顶点到另一个顶点的简单路径数量,则 \(S_n=3S_{n-1}+A_{n-1}^3\)。同时从某个顶点到相邻顶点的简单路径也有两种:

  • 只经过两个 \(n-1\) 阶三角形的。
  • 同时经过三个 \(n-1\) 阶三角形的。

此时 \(A_n=A_{n-1}^3+A_{n-1}^2\)。注意简单回路的限制,路径上不能有环,所以需要减去成环(经过某个顶点两次)的方案数。设 \(B_n\) 为从某个顶点到另一个顶点 且经过第三个顶点 的简单路径数,则要经过某个顶点两次时只能选择在两个 \(n-1\) 阶三角形内强制经过第三个顶点,此时 \(A_n=A_{n-1}^3+A_{n-1}^2-A_{n-1}B_{n-1}^2\),同理 \(B_n=B_{n-1}A_{n-1}^2-B_{n-1}^3\)

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int md=1000000007;
int n,i,s=1,a=2,b,c=1,d;
int main(){
    scanf("%d",&n); 
    for(i=1;i<=n;++i){
        b=(1LL*a*a)%md;
        d=md-(1LL*c*c+md-b)%md;
        s=(1LL*b*a+3LL*s)%md;
        a=(1LL*a*d+b)%md;
        c=(1LL*c*d)%md;
    }
    printf("%d\n",s);
}

九、数位 dp(桁 DP について)

例题1:Zig-Zag Numbers

题意

求满足下列条件的十进制数 \(x\) 的个数模 \(10000\)

  • \(x\ge A\)\(x\le B\)
  • \(x\)\(M\) 的倍数。
  • \(x\) 在十进制表示下,如果位数不为 \(1\),则不能有相邻两位相同,且如果第 \(i+2\) 位大于第 \(i+1\) 位则需要第 \(i+1\) 位小于第 \(i\) 位。

\(A,B\le 10^{500},M\le 500\)

解法

套路题,记 \(dp_{i,j,0/1,r,0/1}\) 为填好了前 \(i\) 位,第 \(i\) 位为 \(j\),模 \(M\) 的值为 \(r\),是否大于 \(i-1\) 位,当前所填后缀是否大于 \(A-1/B\) 的当前后缀的数的个数即可。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=510;
const int md=10000;
int n,m,i,j,k,p,u,v,c,f,g,ans;
int dp[2][maxn][10][2][2];
char a[maxn],b[maxn];
inline void Add(int &x,int y){x-=((x+=y)>=md)*md;}
int Solve(char *x){
    n=strlen(x+1); 
    if(!n) return 0;
    auto X=dp[0],Y=dp[1]; int ret=0;
    memset(dp,0,sizeof(dp));
    c=x[1]^'0';
    for(j=0;j<10;++j) X[j%m][j][j>c][0]=1;
    for(j=1;j<10;++j) ret+=X[0][j][0][0];
    if(n==1) return ret;
    for(j=1;j<10;++j) ret+=X[0][j][1][0];
    c=x[2]^'0';
    for(j=u=0;j<10;++j){
        f=j>c; g=j>=c;
        for(k=0;k<j;++k){
            v=(u+k)%m; p=k%m;
            Add(Y[v][j][f][1],X[p][k][0][0]);
            Add(Y[v][j][g][1],X[p][k][1][0]);
        }
        for(++k;k<10;++k){
            v=(u+k)%m; p=k%m;
            Add(Y[v][j][f][0],X[p][k][0][0]);
            Add(Y[v][j][g][0],X[p][k][1][0]);
        }
        u+=10; 
    }
    for(j=1;j<10;++j) ret+=Y[0][j][0][0]+Y[0][j][0][1];
    if(n==2) return ret;
    for(j=1;j<10;++j) ret+=Y[0][j][1][0]+Y[0][j][1][1];
    swap(X,Y);
    for(i=3,v=100%m;i<=n;++i){
        memset(Y,0,sizeof(dp[0]));
        c=x[i]^'0';
        for(p=0;p<m;++p){
            for(j=0,u=p;j<10;++j){
                f=j>c; g=j>=c;
                for(k=0;k<j;++k){
                    Add(Y[u][j][f][1],X[p][k][0][0]);
                    Add(Y[u][j][g][1],X[p][k][1][0]);
                }
                for(++k;k<10;++k){
                    Add(Y[u][j][f][0],X[p][k][0][1]);
                    Add(Y[u][j][g][0],X[p][k][1][1]);
                }
                u-=((u+=v)>=m)*m;
            }
        }
        for(j=1;j<10;++j){
            Add(ret,Y[0][j][0][0]);
            Add(ret,Y[0][j][0][1]);
            if(i!=n){
                Add(ret,Y[0][j][1][0]);
                Add(ret,Y[0][j][1][1]);
            }
        }
        v=(10*v)%m; swap(X,Y);
    }
    return ret;
}
int main(){
    scanf("%s%s%d",a+1,b+1,&m); 
    reverse(a+1,a+strlen(a+1)+1);
    reverse(b+1,b+strlen(b+1)+1); 
    for(i=1;a[i]=='0';++i) a[i]='9'; 
    --a[i]; if(a[i]=='0') a[i]=0; 
    Add(ans=md-Solve(a),Solve(b));
    printf("%d\n",ans); return 0;
}

例题2

题意

\(n\) 种硬币,第 \(i\) 种硬币为 \(a_i\) 元。现在每种硬币都有无穷个,求使用这些硬币凑出 \(X\) 元的方案数。\(n\le 20,a_i\le 30,X\le 10^{18}\)

解法

考虑在每次不只多增加一个硬币而是 \(2^k\) 个(或者是对硬币数量二进制拆分)。此时对于每个 \(k\),每种硬币只能有选择 \(2^k\) 个或者不选择两个选项(可以使用背包处理对应的方案);而在选择后需要当前硬币面额总和和 \(X\) 的后 \(k+1\) 位相等。此时可以对 \(X\) 的每一位使用数位 dp 进行转移,转移时需要维护当前面额除以 \(2^k\) 的商方便找出合法的 dp 值。

十、dp 优化(偏实现而非思想)(高速化のテクニック)

1. 前缀和优化(累積和の利用)

2. 数据结构优化(データ構造の利用)

3. 利用之前的数组(配列の使いまわし)

例题:Division into Two

题意

有一个长为 \(n\) 的单增序列 \(a\)。求将其划分为两个子序列 \(x,y\),满足 \(x\) 中任意相邻两项之差不小于 \(A\)\(y\) 中任意相邻两数之差不小于 \(B\) 的方案数。\(n\le 10^5\)

解法

\(dp_{i,j,0/1}\) 为考虑了前 \(i\) 个数,且 \(a_i\) 不在的子序列末尾为 \(a_j\)\(a_i\) 是否在 \(y\) 子序列的方案数。转移有:

\[dp_{i+1,i,0}=dp_{i,0,1}+\sum_{a_{i+1}-a_j\ge A}dp_{i,j,1}\\dp_{i+1,i,1}=dp_{i,0,0}+\sum_{a_{i+1}-a_j\ge B}dp_{i,j,0}\\dp_{i+1,j,0}=[a_{i+1}-a_i\ge A]dp_{i,j,0}\\dp_{i+1,j,1}=[a_{i+1}-a_i\ge B]dp_{i,j,1} \]

考虑在枚举到 \(a_i\) 时,只会更新 \(dp_{i,i-1}\) 的值为 \(dp_{i-1}\) 的某个前缀和,且如果 \(a_{i+1}-a_i\) 过小则新的 \(dp_{i,0}\sim dp_{i,i-2}\) 会赋为 \(0\),否则新的 \(dp_{i,0}\sim dp_{i,i-2}\) 可以直接赋值为 \(dp_{i-1,0}\sim dp_{i,i-2}\)。此时某个 \(dp_{i,j}\) 被赋为 \(0\)\(\forall k>i,dp_{k,j}\) 都会为 \(0\)。此时在计算 \(dp_i\) 时可以直接使用 \(dp_{i-1}\),维护最后一次被赋为 \(0\) 的对应右端点即可。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=100010;
const int md=1000000007;
int n,i,s0,s1,l0,l1,r0=-1,r1=-1;
int d0[maxn],d1[maxn],t0[maxn],t1[maxn];
long long a,b,w,s[maxn]; 
inline void Add(int &x,int y){x-=((x+=y)>=md)*md;}
int main(){
    scanf("%d%lld%lld%lld",&n,&a,&b,s+1); 
    s0=s1=t0[0]=t1[0]=1;
    for(i=1;i<n;++i){
        scanf("%lld",&w); s[i+1]=w; 
        while(w-s[l0+1]>=b) ++l0;
        while(w-s[l1+1]>=a) ++l1;
        if(l1>r1) Add(d0[i],t1[min(l1,i-1)]);
        if(l0>r0) Add(d1[i],t0[min(l0,i-1)]);
        if(w-s[i]<a) r0=i-1,s0=0;
        if(w-s[i]<b) r1=i-1,s1=0;
        Add(s0,d0[i]); Add(s1,d1[i]);
        t0[i]=s0; t1[i]=s1;
    }
    Add(s0,s1); printf("%d\n",s0);
    return 0;
}

4. FFT(高速フーリエ変換)

板子

点此查看代码
void DFT(int *f){
    for(i=1;i<t;++i) if(i<r[i]) swap(f[i],f[r[i]]);
    for(i=1,le=2;le<=t;i=le,le<<=1){
        v=Pow(G,(md-1)/le);
        for(j=0;j<t;j+=le){
            for(k=0,u=1;k<i;++k,u=(1LL*u*v)%md){
                p=(1LL*f[i+j+k]*u)%md;
                Add(f[i+j+k]=f[j+k],md-p);
                Add(f[j+k],p);     
            }
        }
    }
} 

属于需要背的知识。原根表。常见的原根形如 \(998244353\) 原根为 \(3\)\(924844033\) 原根为 \(5\)

5. 高维前缀和/FMT(高速ゼータ変換)

计算 \(f_S=\sum_{T\in S}g_T\) 时,考虑分开处理每一位的贡献,设 \(dp_{i,S}\)\(\sum_{j}f_j\)\(j\) 满足第 \(i\) 位以上部分 \(S,j\) 相等,第 \(i\) 位以下部分 \(j\in S\),则有 \(dp_{i,S}\gets dp_{i-1,S}\);转移时如果 \(S\)\(i\) 位为 \(1\),则需要加上 \(dp_{i,S\mathop{\rm xor} 2^i}\) 部分,表示此时的 \(j\) 可以取 \(i\) 位为 \(0\)。最后目标 dp 值即为 \(f\) 值。

6. FWT(And と Add の畳み込み)

此处用了一种较为清奇(?)的思路。

(1) 或卷积(求 \(C_i=\sum_{j\mathop{\rm or}k=i}A_jB_k\)

考虑多项式乘法的过程:我们会先对序列进行 DFT,然后相乘,最后进行 IDFT。此时可以使用类似的方式,用某种序列表示需要操作的序列。

\(o(A)_i=\sum_{x\mathop{\rm or}i=i}A_x\),则 \(o(C)_i=\sum_{(j\mathop{\rm or}k)\mathop{\rm or}i=i}A_jB_k\)。而 \(x\mathop{\rm or}i=i\) 等效于 \(x\)\(i\) 的子集,所以 \((j\mathop{\rm or}k)\mathop{\rm or}i\) 当且仅当 \(j,k\) 均是 \(i\) 的子集,也就是说 \(C_i=A_iB_i\)。求 \(o(A/B)\) 可以使用上述的方法,而从 \(o(C)\) 变回 \(C\) 时,考虑上述 dp 过程的逆过程:在从 \(dp_i\) 推到 \(dp_{i-1}\) 时,如果某个 \(S\) 的第 \(i\) 位为 \(1\)\(dp_{i,S}=dp_{i-1,S}+dp_{i-1,S\mathop{\rm xor}2^i}=dp_{i-1,S}+dp_{i,S\mathop{\rm xor}2^i}\),否则 \(dp_{i,S}=dp_{i-1,S}\),直接 dp 即可。

(2) 与卷积(求 \(C_i=\sum_{j\mathop{\rm and}k=i}A_jB_k\)

仍然可以考虑上述的过程,设 \(a(A)_i=\sum_{x\mathop{\rm and}i=i}A_x\),则同样有 \((j\mathop{\rm and}k)\mathop{\rm and}i=i\) 当且仅当 \(j\mathop{\rm and}i=k\mathop{\rm and}i=i\),所以仍然有 \(a(C)_i=a(A)_ia(B)_i\)。求 \(a(A)\) 时仍然可以使用上面 dp 的思路,但是转移时有 \(dp_{i,S}=dp_{i-1,S}+[S\mathop{\rm and}2^i=0]dp_{i-1,S\mathop{\rm or}2^i}\)。从 \(a(C)\) 推回 \(C\) 同理。

(3) 异或卷积(求 \(C_i=\sum_{j\mathop{\rm xor}k=i}A_jB_k\)

考虑异或的性质,令 \(c(x)\)\(x\) 在二进制表示下的位数,可以发现 \((c(j)+c(k)-c(j\mathop{\rm xor}k))\bmod 2=0\),同理有 \((c(j\mathop{\rm and}i)+c(k\mathop{\rm and}i)-c((j\mathop{\rm xor}k)\mathop{\rm and}i))\bmod 2=0\)。令 \(x_p(A)_i=\sum_{c(j\mathop{\rm and}i)\bmod 2=p}A_j\),则 \(x_p(C)_i=\sum_{j,k\in\{0,1\}}[j\mathop{\rm xor}k=p]x_j(A)_ix_k(B)_i\)\(x_{0/1}\) 也可以使用上述的 dp 方式计算。不过有更简洁的方式计算:设 \(x(A)_i=x_0(A)_i-x_1(A)_i\),则 \(x(C)_i=x(A)_ix(B)_i\)。同或卷积可以直接将求得的 \(C\) 翻转即可。

(4) 子集卷积(求 \(C_i=\sum_{j\mathop{\rm or}k=i,j\mathop{\rm and}k=0}A_jB_k\)

考虑第二个性质等效于 \(c(j)+c(k)=c(i)\)。可以把所有下标按照 \(c\) 的不同值分开处理,设 \(o_p(A)_i\)\(\sum_{c(j)=p,j\mathop{\rm and}i=i}A_j\),则 \(o_p(C)_i=\sum_{j+k=p}o_j(A)_io_k(B)_i\)(暴力计算即可)。

7. 简单剪枝(簡単な枝刈り)

posted @ 2023-01-14 21:15  Fran-Cen  阅读(56)  评论(0编辑  收藏  举报