VitrelosTia
2025-01-06 17:32:51
前言:暑假的时候听学长讲过一次高维前缀和,当时听得一头雾水,后面查阅资料在网上也没要找到能让我看懂的文章(我好菜 qwq)。最近进行学习之后决定来浅谈高维前缀和。
所谓高维前缀和,就是很多维前缀和。
看到我们常写的二维前缀和,相信大家通常会使用这样的写法。
for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++)
s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];
这是一个容斥的想法。但大家有没有考虑过,这里的维度是 3 的时候,你要怎么写?此时你要设计 8 个容斥系数,当维数来到
我们有一个想法,就是一维一维地做前缀和,那我们只要一维一维递推即可。实现非常容易,以下是一个三维的例子。
for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) for (int k = 1; k <= n; k++)
s[i][j][k] += s[i - 1][j][k];
for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) for (int k = 1; k <= n; k++)
s[i][j][k] += s[i][j - 1][k];
for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) for (int k = 1; k <= n; k++)
s[i][j][k] += s[i][j][k - 1];
但是一个问题还是没有解决,如果要做十维前缀和,难道要开十维数组吗?我们联想到可以使用状态压缩来把它压成一维。在上面这个例子中,我们设计一个状态压缩函数 f 就可以把数组压到一维。
int f(int i, int j, int k) { return i * (n + 1) * (n + 1) + j * (n + 1) + k; }
for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) for (int k = 1; k <= n; k++)
s[f(i, j, k)] += s[f(i - 1, j, k)];
for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) for (int k = 1; k <= n; k++)
s[f(i, j, k)] += s[f(i, j - 1, k)];
for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) for (int k = 1; k <= n; k++)
s[f(i, j, k)] += s[f(i, j, k - 1)];
我们显然可以把这几个循环搞到一起,并且可以直接枚举压缩后的状态。比较普适性的,当维数为
for (int o = 0; o < k; o++)
for (int i = 0; i < m; i++)
if (g(i, o) > 0) s[i] += s[i - h(n, o)]
// g(i, o) 可以提取状态 i 的第 o 位
// h(n, o) 可以表示状态第 o 位的 "1"
这就是高维前缀和的实现方法,时间复杂度
题意:求满足
考虑对于每一位作为一维做前缀和,就是六维前缀和。考虑不进位就是每一位都满足和 s[99999 - a[i]]
即可。注意
const int N = 1e6 + 5, M = 999999;
int n, a[N], s[N];
signed main() {
ios::sync_with_stdio(false); cin.tie(nullptr);
cin >> n;
int ans = 0;
for (int i = 1; i <= n; i++) {
cin >> a[i]; s[a[i]]++;
bool flag = false;
for (int j = 1; j <= M; j *= 10) flag |= ((a[i] / j % 10) >= 5);
ans -= !flag;
}
for (int j = 1; j <= M; j *= 10) for (int i = 1; i <= M; i++)
if (i / j % 10 > 0) s[i] += s[i - j];
for (int i = 1; i <= n; i++) ans += s[M - a[i]];
cout << ans / 2;
return 0;
}
以下规定对于二进制数
在一些问题中,我们会遇到要维护所有子集信息的问题(二进制意义下的)。我们考虑到在高维前缀和中,维数为
for (int j = 0; j < K; j++) for (int i = 0; i < (1 << K); i++)
if (i >> j & 1) f[i] += f[i ^ (1 << j)];
这样我们对于二进制下的每一位都做了前缀和,显然也就维护了所有子集的信息。同时我们也可以维护所有超集的信息。
for (int j = 0; j < K; j++) for (int i = (1 << K) - 1; i >= 0; i--)
if (!(i >> j & 1)) f[i] += f[i ^ (1 << j)]
也可以通过 dp 的方式来理解,注意这里的转移顺序,每次从一位有 1 的转移到 0,所以要从大到小转移。这种写法也叫 sosdp(子集 dp,实际上也可以解决超集的问题),实际上就是高维前缀和的想法。时间复杂度
题意:对于所有
对于每一位或起来都是 0,那对于一位,
const int N = 1e6 + 5, K = 23;
int n, a[N], b[N], f[1 << K];
signed main() {
ios::sync_with_stdio(false); cin.tie(nullptr);
cin >> n; memset(f, -1, sizeof f);
for (int i = 1; i <= n; i++) {
cin >> a[i]; f[a[i]] = a[i];
for (int j = 0; j < K; j++) b[i] += (!(a[i] >> j & 1)) << j;
}
for (int j = 0; j < K; j++) for (int i = 0; i < (1 << K); i++)
if (i >> j & 1 && f[i ^ (1 << j)] != -1) f[i] = f[i ^ (1 << j)];
for (int i = 1; i <= n; i++) cout << f[b[i]] << ' ';
return 0;
}
后记:sosdp 是高维前缀和最广泛的应用,遇到与二进制有关的问题,可以想想能不能转化成子集/超集相关的问题。FMT(快速莫比乌斯变换)运用的就是这个想法,大家可以去了解一下(我太菜了我不会 qwq)。