从张量分解理解集合幂级数卷积
emm,虽说是给我自己写的,但是我应该写的详细一点,要保证这个东西能让以后的我看懂。
但是不想太耗费时间,写这个公式很多有点难受。
问题
\(n\) 维,每一维度都有 \(k\) 种值,总共有 \(k^n\) 类数。
做卷积 \(: C \rightarrow A \times B\) ,其中 \(c_i = \sum a_i b_j W(i, j, k)\) 。
满足 \(n\) 维是指,每一维度独立,满足 \(W(i,j,k) = \prod_{x} w(i_x, j_x, k_x)\) 。
比较通俗的理解就是乘法分配律,\(n\) 维独立就是乘法对于加法拥有分配律,贡献矩阵可以拆成 \(()^n\) 的形式。
原理
两个大小为 \(n\) 的普通卷积可以理解成执行了 \(n^3\) 次形如 \(( \dots ) ( \dots ) \rightarrow c_x\) 的东西,就是一个 \(A\) 中的线性组合点乘上 \(B\) 中的一个线性组合贡献给 \(c_x\) 。
那么这个显然就是暴力的模拟,这是很浪费的,这个做法是合并一些状态,用尽可能少的上边的形式来做。
做法
回到原问题,假定 \(n = 1\) 。
贡献 \(w\) 是一个 \(k \times k \times k\) 的张量,那么根据上边的原理,我们希望拆成 \(m\) 个 \(k \times k\) 秩为1的矩阵,且使得 \(m\) 尽可能小的。
而且可以通过 \(m\) 个矩阵将贡献张量表示出来。
秩为 1 是指可以通过高斯消元找到一个行向量 \(\vec{v}\) ,使得矩阵中的每一行都可以表示成 \(s_i \vec{v}\) 。
例如:异或的贡献就是(使用两个切片来表示):\(\begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix}\) , \(\begin{bmatrix} 0 & 1 \\ 1 & 0 \end{bmatrix}\) 。
它可以由 \(\begin{bmatrix} 1 & 1 \\ 1 & 1 \end{bmatrix}\) , \(\begin{bmatrix} 1 & -1 \\ -1 & 1 \end{bmatrix}\) 的线性组合表示出来,且这两个矩阵都是秩为 1 的。
一维问题就可以模拟这个东西做到 \(O(mk)\) 的复杂度。
\(n\) 维问题就是把 \(k^n\) 种数变成 \(m^k\) 个数,这个可以使用高维前缀和,就是以一种顺序枚举每一个维,然后期间维护在枚举过的维答案正确,没有枚举过的维全部相等的数。
可以做到 \(\sum_{i=0}^{n-1} m^{i}k^{n-i - 1} (mk)\) 的复杂度。
当 \(m \not= k\) 的时候,通常是 \(m > k\) 可以做到 \(O(m^n)\) 。
当 \(m = k\) 的时候,就是熟悉的 \(O(k^nn)\) 。
一个比较有意思的事情是,当 \(k=2\) 的时候,如果是异或卷积,可以拆成 \(k \times k = 4\) 个秩为 1 的矩阵,就是每一次只有矩阵的一个位置有值,这样做的复杂度就是 \(O(4^n)\) ,也就是暴力卷积。
如果是子集卷积,发现有一种情况贡献为 \(0\) 所以只用拆成 \(3\) 个这样的矩阵,然后做就是 \(O(3^n)\) ,也就是枚举子集的复杂度。
所以这个方法的难点就在于如何张量分解?当然这个问题好像是不可做的,所以只能手玩或者爆搜,但是不一定要做到非常优秀的 \(m\) 。
一些想法是只去 搜/考虑 值为 \(1, 0, -1\) 的矩阵。以及可以尝试先放一个全一的矩阵,或者观察贡献张量切片之间的差之类的。
练习
这道题非常困难,这里只尝试部分分2。
这里就不写了题解了,只用做到 \(m = 4\) 复杂度就可以接受了,构造方法可以看题解,应该比较容易手玩出来。
只用做到 \(m = 4\) 复杂度就可以接受了,这个比上边这个题的构造方法还要简单。
给一下如何变换的代码:
const ll K = 3;
const ll M = 4;
const ll S = 16777216;
ll g[S];
ll w[M][K] = {{1, 1, 1}, {0, 1, 0}, {0, 0, 1}, {1, 0, 0}};
ll iw[K][M] = {{1, -1, 0, 0}, {1, 0, -1, 0}, {1, 0, 0, -1}};
void trans (ll *f, ll n) {
ll len = std::pow(K, n);
for (ll o = M, k = 1, tim = 0; tim < n; tim++, o *= M, k *= M) {
len = len / K * M;
for (ll i = 0; i < len; i += o) {
ll _i = i / M * K;
for (ll j = 0; j < k; j++)
for (ll y = 0, v = 0; y < M; y++, v += k) {
g[i + j + v] = 0;
for (ll x = 0, u = 0; x < K; x++, u += k)
g[i + j + v] = g[i + j + v] + f[_i + j + u] * w[y][x];
}
}
for (ll i = 0; i < len; i++) std::swap(f[i], g[i]);
}
}
void itrans (ll *f, ll n) {
ll len = std::pow(M, n);
for (ll o = K, k = 1, tim = 0; tim < n; tim++, o *= K, k *= K) {
len = len / M * K;
for (ll i = 0; i < len; i += o) {
ll _i = i / K * M;
for (ll j = 0; j < k; j++)
for (ll y = 0, v = 0; y < K; y++, v += k) {
g[i + j + v] = 0;
for (ll x = 0, u = 0; x < M; x++, u += k)
g[i + j + v] = g[i + j + v] + f[_i + j + u] * iw[y][x];
}
}
for (ll i = 0; i < len; i++) std::swap(f[i], g[i]);
}
}
尝试把交换写成指针,但是失败了,指针低手写不对指针🤷♂️。
劣处
虽然来说相比于原来的卷积,我们的目标比较明确,问题似乎比较简单,但是张量分解构造仍然非常困难。
最简单的例子 \(n\) 维 \(k\) 进制循环卷积,这个要是构造矩阵就困难的多得多。
目前认为这个东西通过推式子,利用单位根反演?就是可以用一个式子表示出 \([k \mid i]\) ,然后移项分离可以拿下。
扩展?
这部分不知道听懂了没有,我们可以借助占位符 \(x\) 来时间更可能的张量分解。
经典的例子就是子集卷积,通过占位符,我们就可以只取对应项的系数来做到更好的构造,劣处就是复杂度更高了,因为每一次乘法都是多项式乘法。
例如:子集卷积的贡献就是(使用两个切片来表示):\(\begin{bmatrix} 1 & 0 \\ 0 & 0 \end{bmatrix}\) , \(\begin{bmatrix} 0 & 1 \\ 1 & 0 \end{bmatrix}\) 。
它可以由 \(\begin{bmatrix} 1 & 0 \\ 0 & 0 \end{bmatrix}\) , \(\begin{bmatrix} 1 & x \\ x & x^2 \end{bmatrix}\) 的线性组合表示出来,且这两个矩阵都是秩为 1 的。
这样看起来构造更加复杂了😕。

浙公网安备 33010602011771号