• 从零开始的简单集合幂级数


    集合幂级数 学习笔记

    0 集合幂级数与高维前后缀和

    定义集合幂级数为形如 \(\sum_{i\sube U} a_ix^i\) 的幂级数。即对于每一个子集,我们都有一个值 \(a_i\),就可以有一个形式幂级数。这样我们就能定义对于此类幂级数的各类卷积和各类复合。

    \(a\) 的高维前缀和 \(A\)

    \[A_i=\sum_{j\subseteq i} a_j \]

    这个可以通过 \(O(2^nn)\) 的方法得到。我们也可以用同样的复杂度用 \(A\) 反求出 \(a\)

    不止有前缀和,同样的,我们可以得到一个高维后缀和。对于 \(a\) 的高维后缀和 \(A\) 我们有

    \[A_i=\sum_{i\sube j} a_j \]

    1 或/与卷积与 FMT

    这样求出集合幂级数的高维前缀和的变换为 FMT,逆变换则为 IFMT。

    我们闲来无事,把两个前缀和乘起来

    \[A_iB_i=\sum_{j\sube i}\sum_{k\sube i}a_jb_k=\sum_{j\cup k\sube i} a_jb_k=\sum_{x\sube i} \sum_{j\cup k=x} a_jb_k \]

    若设后面那部分为 \(c_i\),我们就可以发现

    \[A_iB_i=\sum_{x\sube i}c_i=C_i \]

    这个 \(c_i\) 正是 \(a_i\)\(b_i\)或卷积

    对于 \(a_i\)\(b_i\),其或卷积为

    \[c_i=\sum_{j\cup k=i} a_jb_k \]

    于是我们就有了 \(O(2^nn)\) 的求出 \(a_i\)\(b_i\) 的或卷积的办法。

    同样的,我们也有与卷积。设 \(d_i\)\(a_i\)\(b_i\)与卷积,则

    \[d_i=\sum_{j\cap k=i} a_jb_k \]

    同或卷积相似,\(d_i\) 的后缀和是 \(a_i\)\(b_i\) 各自的后缀和的点积。

    void fmt_or(int *a,int c) {
    	rep(i,0,n-1) rep(j,0,s) if(j&(1<<i)) a[j]+=c*a[j^(1<<i)];
    }
    void conv_or(int *a,int *b,int *c) {
        fmt_or(a,1), fmt_or(b,1);
        rep(i,0,s) c[i]=a[i]*b[i];
        fmt_or(c,-1);
    }
    void fmt_and(int *a,int c) {
    	rep(i,0,n-1) rep(j,0,s) if(j&(1<<i)) a[j^(1<<i)]+=c*a[j];
    }
    void conv_and(int *a,int *b,int *c) {
        fmt_and(a,1), fmt_and(b,1);
        rep(i,0,s) c[i]=a[i]*b[i];
        fmt_and(c,-1);
    }
    

    2 异或卷积与 FWT

    异或卷积的定义和上面几乎相同,只不过把与/或换成了异或。

    定义一个算子 \(\operatorname{FWT}(a)\),得到的结果也为集合幂级数

    \[\operatorname{FWT}(a)_i=\sum_{j\sube U} (-1)^{\mid i \cap j \mid} a_j \]

    \(c\)\(a\)\(b\) 异或卷积的结果,那么有 \(\operatorname{FWT}(c)_i=\operatorname{FWT}(a)_i\cdot \operatorname{FWT}(b)_i\)

    \[\begin{aligned} \operatorname{FWT}(c)_i &= \sum_{j} (-1)^{|i\and j|}c_j\\ &=\sum_j (-1)^{|i\and j|} \sum_{k,l} [k\oplus l=j] a_kb_l\\ &=\sum_{k,l} (-1)^{|(k\oplus l)\and i|} a_kb_l\\ &=\sum_{k} (-1)^{|k\and i|}a_k \sum_{l} (-1)^{|l\and i|}b_l\\ &=\operatorname{FWT}(a)_i\cdot \operatorname{FWT}(b)_i \end{aligned} \]

    现在我们希望能用较低的复杂度计算 \(\operatorname{FWT}(a)\)。考虑每一位给结果带来的更新。对于第 \(i\) 位和不包含第 \(i\) 位的集合 \(s\),我们发现有一个类似蝴蝶变换的操作:令 \(x=a_s, y=a_{s+2^i}\),则有新的 \(a_s=x+y\)\(a_{s+2^i}=x-y\)

    于是我们只需要枚举这一位,然后枚举所有不包含 \(i\) 的集合,做一次如上变换即可。代码和 FFT 十分的相似(因为也可以用 FFT 的分治思想去理解这种变换过程)。

    注意逆变换的过程相当于 \(a_s=\frac{x+y}{2}\)\(a_{s+2^i}=\frac{x-y}{2}\)

    void fwt_xor(int *a,int c) {
    	for(int i=1;i<=s;i<<=1) for(int j=0;j<=s;j+=(i<<1)) rep(k,j,j+i-1) {
    		int x=a[k], y=a[k+i];
    		a[k]=c*(x+y)%mod, a[k+i]=c*(x-y)%mod;
    	}
        rep(i,0,s) a[i]=(a[i]%mod+mod)%mod;
    }
    void conv_xor(int *a,int *b,int *c) {
    	fwt_xor(a,1), fwt_xor(b,1);
        rep(i,0,s) c[i]=a[i]*b[i]%mod;
        fwt_xor(c,(mod+1)/2);
    }
    

    3 子集卷积

    有时候我们需要求类似以下的式子

    \[c_i=\sum_{j\or k=i\\j\and k=\emptyset} a_{j}b_{k} \]

    即将 \(i\) 恰好拆分成两个没有相同元素的子集 \(j,k\),然后做卷积。这就是子集卷积

    考虑将所有子集按照集合元素个数进行分类。设 \(P_x\) 表示对于 \(a\),元素个数为 \(x\) 的所有集合的形式幂级数。

    \[P_{x,i}= \begin{cases} a_i & |i|=x\\ 0 & |i|\neq x \end{cases} \]

    也设 \(Q_x\) 为对于 \(b\) 的这样的元素个数为 \(x\) 的所有集合的形式幂级数,\(R_x\) 为对于 \(c\) 的这样的形式幂级数。

    那么我们要做的就是对于所有 \(x,y\),把 \(P_x\)\(Q_y\) 的或卷积加到 \(R\) 上。

    我们可以预处理出所有 \(P_x\)\(Q_x\)\(\operatorname{FMT}\),这样就能做到复杂度 \(O(2^nn^2)\)

    LOJ 上模板题的代码:https://loj.ac/s/1373726


    以下内容需要指数级生成函数的前置知识,不然会较难看懂。

    4 集合幂级数 EXP

    有时我们要处理类似以下的式子

    \[c_i=\sum_{j_1\or j_2\or j_3 \or\dots\or j_k=i\\|j_1|+|j_2|+\dots+|j_k|=|i|} \prod_l a_{j_l} \]

    即对集合幂级数做一个类似 exp 的操作。

    我们考虑将所有子集按照其最高位分类,形成 \(n\) 组,而每一组中最多选择一个出来,这样就能转换成和普通子集卷积比较类似的问题。设 \(p_i\) 表示最高位为 \(i\) 的组。

    我们动态维护集合幂级数 \(g\) 表示(从低位开始往高位合并)前 \(i-1\) 组合并出来的结果,并且现在需要合并上第 \(i\) 组。我们发现这个合并过程其实就是一个子集卷积,并且此时 \(g\)\(p_i\) 都只有 \(i\) 位,意味着单次合并复杂度是 \(O(2^ii^2)\) 的。

    总复杂度为 \(O(\sum 2^ii^2)=O(2^nn^2)\)

    和多项式 exp 一样,我们需要注意除掉 \(i!\)

    5 多项式复合集合幂级数

    EI 在 CF 上发的原 blog:https://codeforces.com/blog/entry/92183

    image-20220207211433065

    没关系,本来就学不会 binary search,学一学这东西也无妨。

    是根据 xtq 翻讲的版本学的,和原版难免有偏差,但是 xtq 讲的真心非常好(。


    需要计算 \(c=\sum_i f_ia^i\)

    和 exp 类似,我们考虑按最高位分组。设 \(F_x\) 表示最高位为 \(x\) 的组的集合幂级数。

    我们还是采取从低位到高位的合并方法。不过不同于上面的 exp,这里特殊的地方在于每个 \(f_i\)\(a\) 卷了几次的要求是不一样的。\(f_i\) 的贡献必须限定在卷了 \(i\) 次的东西上。

    \(G_{i,j}\) 表示,目前考虑前 \(i\) 组(即 \(0\)\(i-1\) 位),并且还需要再卷 \(j\) 次的结果。初始值有 \(G_{0,j}=f_j\),答案为 \(G_{n,0}\)

    转移比较简单,\(G_{i,j}\) 可以贡献给 \(G_{i+1,j}\)\(i\) 这一位不卷),\(G_{i+1,j-1}\)\(i\) 这位卷一次),具体而言有

    \[\begin{aligned} G_{i+1,j} &\leftarrow G_{i,j}\\ G_{i+1,j-1} &\leftarrow G_{i,j} * F_{i} \end{aligned} \]

    乍一看这复杂度很不对,但实际上我们梳理一下有用的 \(G_{i,j}\) 满足 \(j\le n-i\),所以实际上复杂度为 \(O(\sum (n-k)O(k^22^k))=O(n^22^n)\),因为 \(\sum k\times 2^{-k}\) 是收敛的。

    有了这个神奇的工具,我们就能用 \(O(n^22^n)\) 算许多有趣的东西了。

    LOJ154 集合划分计数

    这个划分可以想到类似 EGF 的东西。

    大小为 \(k\) 的划分相当于一个 \(F^k\)。所以我们实际上要求的是

    \[[x^{S}]\sum_{i}[i\le k]\frac{F^i}{i!} \]

    我们用 \([x^i]G=[i\le k]\frac{1}{i!}\) 去复合即可。

    namespace SetP {
    	void fmt(vi &a,int n,int c) { //FMT/IFMT
    		int s=(1<<n)-1;
    		for(int i=1;i<=s;i<<=1) jmp(j,0,s,i<<1) rep(k,j,j+i-1)
    			a[k+i]=(1ll*a[k+i]+c*a[k]+mod)%mod;
    	}
    	vector<vi> trans(vi &a,int n) { //按popcount分组
    		int s=(1<<n)-1;
    		vector<vi>r; r.resize(n+1);
    		rep(i,0,n) r[i].resize(s+1);
    		rep(i,0,s) r[popc[i]][i]=a[i];
    		return r;
    	}
    	vi itrans(vector<vi> &a,int n) { //分组重新变成集合幂级数
    		int s=(1<<n)-1;
    		vi r; r.resize(s+1);
    		rep(i,0,s) r[i]=a[popc[i]][i];
    		return r;
    	}
    	vector<vi> conv(vector<vi> &a,vector<vi>&b,int n) { //分组的卷积
    		int s=(1<<n)-1;
    		vector<vi>r; r.resize(n+1);
    		rep(i,0,n) r[i].resize(s+1);
    		rep(i,0,n) rep(j,0,n-i) rep(k,0,s)
    			r[i+j][k]=(r[i+j][k]+1ll*a[i][k]*b[j][k])%mod;
    		return r;
    	}
    	vi comp(vi &a,vi &b,int n) { //多项式a复合集合幂级数b
    		int s=(1<<n)-1;
    		vector<vi> lst(n+1),cur(n+1),tb(n+1);
    		rep(i,0,n) lst[i].resize(s+1), cur[i].resize(s+1), tb[i].resize(s+1);
    		rep(i,0,n) lst[i][0]=a[i];
    		rep(i,0,n-1) {
    			vi af; af.resize(s+1);
    			int s=(1<<i)-1, t=(1<<i+1)-1;
    			rep(j,0,s) af[(s+1)|j]=b[(s+1)|j]; //处理出F
    			vector<vi> pf=trans(af,i+1);
    			rep(j,0,i) fmt(pf[j],i+1,1);
    			rep(j,1,n-i) {
    				vector<vi> plst=trans(lst[j],i+1);
    
    				rep(k,0,i+1) fmt(plst[k],i+1,1);
    				vector<vi> cvr=conv(plst,pf,i+1);
    				
    				rep(k,0,i+1) fmt(cvr[k],i+1,-1);
    				cur[j-1]=itrans(cvr,i+1); //第二种转移
    				rep(k,0,t) cur[j-1][k]=(cur[j-1][k]+lst[j-1][k])%mod; //第一种转移
    			}
    			rep(j,0,n-i-1) rep(k,0,t) lst[j][k]=cur[j][k];
    		}
    		return cur[0];
    	}
    }
    

  • 相关阅读:
    1.python全栈之路:python基础
    21、指定元素置后——数组
    20、泰勒展开式
    19、显示表达式的运算形式
    1、模拟蚂蚁借呗—利息计算
    05、C语言——循环结构
    04、C语言——选择结构
    03、C语言——顺序结构
    02、C语言——C语言组成与数据
    07、C语言——函数
  • 原文地址:https://www.cnblogs.com/TetrisCandy/p/15870078.html
Copyright © 2020-2023  润新知