简单子集运算
【参考】
对于集合 \(U = \{1,2,\cdots,n\}\), 有下标系统 \(A = \{X\mid X\subseteq U\}\),和定义在 \(A\) 上的函数:\(f:A\to \mathcal R\)。(\(\mathcal R\) 大概是某个很牛逼的代数系统,有交换律什么的,应该直接默认是实数域就行了)
本文讨论对于这个函数 \(f\) 的变换。
用状压模拟 \(f\) 的下标系统:
n = read();
for (int I = 0; I < (1 << n); ++ i) f[i] = read();
子集和
\[\hat f(I) = \sum_{T\subseteq I} f(T)
\]
求这个 \(\hat f\)。
从另一个角度看待 \(f\) 的状压表示, 二进制的每一位看成多维数组的一个维度的某个数,那么这个子集和就是个高维前缀和。
插入
高维前缀和的算法:
对于一个高维点 \((x_1,x_2,\cdots,x_n)\), 其高维前缀和是对 \((y_1\le x_1,y_2\le x_2,\cdots,y_n\le x_n)\) 这些点值的求和。
将这些点值按照最后一维分类,将整个 n 维超立方体分成了 \(|\{k\mid k\le x_n\}|\) 个 n - 1 维的超立方体, 看上去就十分显然了。
代码
for (int i = 0; i < n; ++ i)
for (int s = 0; s < (1 << n); ++ s)
if ((s >> i) & 1) f[s] += f[s ^ (1 << i)];
子集和的逆变换
就是给你 \(\hat f\), 求 \(f\)。
有了前面的铺垫,这个东西就很简单了,就是对每一维从大到小扫过去差分,不过在这里一个维只有两个点值所以看起来就很简洁。
代码
for (int i = 0; i < n; ++ i)
for (int s = 0; s < (1 << n); ++ i)
if ((s >> i) & 1) f[s] -= f[s ^ (1 << i)];
超集和
\[f(I)\zeta = \sum_{I\subseteq T} f(T)
\]
给 f 求 fζ。
就是把 1 看成 0, 0 看成 1 了而已, 在一般的有限高维下标系统里, 可以直接看成把偏序关系反过来做子集和。
代码
for (int i = 0; i < n; ++ i)
for (int s = 0; s < (1 << n); ++ s)
if (! ((s >> i) & 1)) f[s] += f[s ^ (1 << i)];
超集和逆变换
不多说了,代码
for (int i = 0; i < n; ++ i)
for (int s = 0; s < (1 << n); ++ s)
if (! ((s >> i) & 1)) f[s] -= f[s ^ (1 << i)];
补集和
这个东西是啥啊,原文没说我不知道。