高维前缀和学习笔记

First Post:

Last Update:

Word Count:
1.5k

Read Time:
7 min

引入

首先来看这样一个问题:

,求 。

显然可以枚举子集做到 ,但高维前缀和可以做到 。

那么什么是高维前缀和呢?

我们先来看一些低维的前缀和,例如二维。

在求二维前缀和时,我们通常用容斥的方法来求,即

但事实上,我们还可以把二维前缀和分解成两次一维前缀和,分别对每一维求前缀和。

1
2
3
4
5
6
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
a[i][j] += a[i - 1][j];
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
a[i][j] += a[i][j - 1];

高维前缀和

高维前缀和的思路是类似的。

我们可以把求集合 的子集和的过程看成求一个一共 维,每一维大小为 2 的前缀和。

1
2
3
4
5
for(re int i=0;i<n;++i){
for(re int s=0;s<(1<<n);++s){
if((s>>i)&1) f[s]+=f[s^(1<<i)];
}
}

代码中的 表示集合 的子集和。

阅读代码不难发现,我们事实上就是在干一件事:对每一个子集的每一维分别求前缀和。

因为是对每一维分别求,所以一定是先枚举维再枚举子集。

事实上,高维前缀和不止能维护前缀和,它可以维护任何半群信息。

所以在遇到需要维护一个集合的所有子集的所有子集的半群信息时,都可以用高维前缀和做。

例题

[ARC100E] Or Plus Max

下文定义 为按位或。

首先把 的限制转化掉,所以考虑对每个 分别计算。

设 ,那么 。

但这个 依旧不好算,我们考虑拓宽限制。

设 ,那么 。

这为什么是对的? 不一定等于 啊?

这是因为 是 的必要条件,是 的充分条件。

所以 的所有元素 一定完全包含 的所有元素,且不包含 的任何元素,但可能会包含 的元素。

但这不影响,因为我们的答案是前缀 ,所以统计到的答案依旧是对的。

那么现在问题变成了 ,求 。

不难发现,我们的 一定是最大和次大相加得到,所以只需要对于所有 ,维护子集最大和次大即可。

这显然可以高维前缀和维护。

复杂度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include"bits/stdc++.h"
#define re register
#define int long long
#define pii pair<int,int>
#define fi first
#define se second
using namespace std;
const int maxn=(1<<18)+10,inf=1e18;
int n,S;
pii f[maxn];
inline void upd(pii &a,pii b){
if(b.fi>a.fi) a.se=max(a.fi,b.se),a.fi=b.fi;
else a.se=max(a.se,b.fi);
}
signed main(){
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n;S=(1<<n)-1;
for(re int i=0;i<=S;++i) cin>>f[i].fi,f[i].se=-inf;
for(re int i=0;i<n;++i){
for(re int s=0;s<=S;++s){
if((s>>i)&1) upd(f[s],f[s^(1<<i)]);
}
}
int ans=0;
for(re int i=1;i<=S;++i) ans=max(ans,f[i].fi+f[i].se),cout<<ans<<'\n';
return 0;
}

Vowels

光题意就理解了半天···

一个非常直接的想法:对于每一个单词,我们都枚举超集,然后贡献到超集上。

这里的贡献注意不要算重,所以需要容斥一下。

具体的,我们对每一个子集都枚举超集,然后加贡献时带上容斥系数。

设 为单词长度, 为字符集大小,则总复杂度为

但这样每次都枚举超集太浪费了,能不能优化?

所以我们可以这样:对于每一个单词,先把贡献加到子集上,然后最后一块算贡献。

这样就可以只枚举一次了。

最后算贡献的时候,事实上就是求每个子集的子集和,高维前缀和即可。

复杂度为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include"bits/stdc++.h"
#define re register
#define int long long
using namespace std;
const int maxn=(1<<24)+10,m=24,S=(1<<24)-1;
int n;
int f[maxn];
signed main(){
#ifndef ONLINE_JUDGE
freopen("1.in","r",stdin);
freopen("1.out","w",stdout);
#endif
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n;char c;
for(re int i=1;i<=n;++i){
int t=0,cnt=0;
for(re int j=1;j<=3;++j) cin>>c,t|=(1<<(c-'a'));
for(re int s=t;s;s=(s-1)&t){
if(__builtin_popcountll(s)&1) ++f[s];
else --f[s];
}
}
for(re int i=0;i<m;++i){
for(re int s=0;s<=S;++s){
if((s>>i)&1) f[s]+=f[s^(1<<i)];
}
}
int ans=0;
for(re int s=0;s<=S;++s) ans^=(f[s]*f[s]);
cout<<ans;
return 0;
}

拓展

子集和能做,超集和呢?

事实上也很简单,子集和时我们是子集向自己贡献,那超集和只需要自己向子集贡献即可。

1
2
3
4
5
for(re int i=0;i<n;++i){
for(re int s=0;s<(1<<n);++s){
if((s>>i)&1) f[s^(1<<i)]+=f[s];
}
}

前缀和能做,差分呢?

只需要符号变一下就行了。

也不难理解,毕竟前缀和的逆运算就是差分。

1
2
3
4
5
for(re int i=0;i<n;++i){
for(re int s=0;s<(1<<n);++s){
if((s>>i)&1) f[s]-=f[s^(1<<i)];
}
}