高维前缀和 发表于 2026-04-29 更新于 2026-09-17
济南
算法 动态规划 高维前缀和 HeJie 2026-04-29 2026-09-17 高维前缀和是对一维、二维前缀和的多维数据结构的推广。
核心求解思想:逐维前缀和
在了解高维前缀和之前,需要先学习逐维前缀和的实现。
我们一般用容斥原理来构建二维前缀和,但还有一种基础的做法,是逐维前缀和。它的做法是,先对一行或者一列做前缀和,然后再对另一维度做前缀和,这样,可以得到二维前缀和。
对于这种逐一维度做前缀和的做法,我们叫它逐维前缀和。推广一下,可以发现,这个做法也可以应对更多维度的数据结构。
概述
对于 维的数组 ,大小为 ,其前缀和 定义为
高维前缀和常用于解决二进制中的问题,大概因为二进制的维度(位)比较多,但是大小(两种选择:0
和 1)却比较小,从时间以及空间复杂度来说最利于做成算法题吧。
在算法竞赛中,高维前缀和常用来解决的问题是 子集和(sum over
subsets, SOS) 问题。
就是指,用高维前缀和来得到二进制数的子集之和,例如,二进制数
11,其子集有 00 01
10 11, 求得这几个子集的贡献之和。
这时我们发现,这似乎和动态规划中的状压DP有点像,没错,其实这个问题是状压DP的一种,所以这种问题也叫
SOS DP 。
因此,高维前缀和是解决特定状压DP问题的技巧。
例题
给定一个长度为 ( )的数组 。对于数组中的每一个元素 ,你需要在这个数组中找到另一个元素
,使得 。如果找不到,输出
-1。数据范围 (约 22 个二进制位)。
这是一道高维前缀和的模板题,题目要求找到与其互异的元素(任意一个),其实可以看作,找到其补集的子集。
也就是对于数字 x,找到其补集 ((1 << 22) - 1) ^ x
的子集元素,这道题中,我们可以定义一个高维前缀和,求子集元素中的最大值。
这样的话,初始化dp数组为 ,录入所有元素,让 。
接下来,做逐维前缀和,先枚举一个维度,然后依次将这个维度的大的值(这一位为1)对小的(这一位为0)做前缀和,也就是找其它维度相同,只有此维度不同的做前缀最大值。这部分代码也是高维前缀和的模板代码。
for (int i = 0 ; i < 22 ; i ++) { for (int j = 0 ; j < (1 << 22 ); j ++) { if (j >> i & 1 ) { dp[j] = max (dp[j], dp[j ^ (1 << i)]); } } }
做完前缀和之后,dp数组表示的就是一个数的子集元素最大是多少,只需要枚举每个元素,然后输出其补集的dp值就可以了。
#include <bits/stdc++.h> using namespace std;int main () { ios::sync_with_stdio (false ); cin.tie (nullptr ); int n; cin >> n; vector<int > a (n) , dp (1 << 22 , -1 ) ; for (auto &i : a) { cin >> i; dp[i] = i; } for (int i = 0 ; i < 22 ; i ++) { for (int j = 0 ; j < (1 << 22 ); j ++) { if (j >> i & 1 ) { dp[j] = max (dp[j], dp[j ^ (1 << i)]); } } } for (auto &i : a) { cout << dp[(1 << 22 ) - 1 ^ i] << ' ' ; } cout << '\n' ; return 0 ; }
给定一个大小为 的数组 。对于所有的 从 到 ,求出最大的 (满足 且 )。
这道题和上一道做法是一样的,只是需要维护的不再是子集中最大的一个元素,而是最大的两个元素。
因此我们只要改变一下存储和做前缀和的方式,就可以得到答案。注意,本题要找的是小于等于
的所以,对集合也从小到大做一个前缀和。
#include <bits/stdc++.h> using namespace std;int main () { ios::sync_with_stdio (false ); cin.tie (nullptr ); int n; cin >> n; vector<int > a (1 << n) ; vector<array<int , 2>> dp (1 << n, array<int , 2 >{}); for (int i = 0 ; i < (1 << n); i ++) { cin >> a[i]; dp[i][0 ] = a[i]; } for (int i = 0 ; i < n; i ++) { for (int j = 0 ; j < (1 << n); j ++) { if (j >> i & 1 ) { for (int k = 0 ; k < 2 ; k ++) { if (dp[j ^ (1 << i)][k] > dp[j][1 ]) { dp[j][1 ] = dp[j ^ (1 << i)][k]; if (dp[j][0 ] < dp[j][1 ]) swap (dp[j][0 ], dp[j][1 ]); } } } } } int mx = 0 ; for (int i = 1 ; i < (1 << n); i ++) { mx = max (mx, dp[i][0 ] + dp[i][1 ]); cout << mx << '\n' ; } return 0 ; }
给定
个数的数组。对于数组中的每个数 ,求出数组中有多少个数 满足:
(即 是 的子集)
(即 是 的超集)
(即
和 至少有一个二进制位同为 1)
对于这几种问题,都可以进行转化。
第一个问题,显然可以用高维前缀和维护子集的个数。
第二个问题,有两种做法,既可以维护一个后缀和得到答案,也可以用容斥原理,总数减去子集的个数之后,加上y等于x的个数。
第三个问题,可以用容斥原理,答案是x的补集的子集元素个数。
知道这些后,只需要维护一个子集个数的高维前缀和,以及记录每个元素的个数,可以得出答案。
给定一个长度为 的数组 ( )。求出有多少个非空子序列,使得子序列中所有元素的按位与(AND)结果为
0 。
这是一道结合了容斥原理的题目,涉及了一个容斥原理的经典套路。
想要得出每个二进制数不重合的的组合方式显然不太现实,我们想到用容斥原理。用所有的组合方式减去有重合的组合方式。
我们可以用高维后缀和,得到至少有这些位为 的元素有多少个,如果有 个元素,则可以选择的方式有 种。
接下来,我们用这个式子来得到答案:
有 个 的 方 案 数 至 少 有 个 的 方 案 数 至 少 有 个 的 方 案 数 至 少 有 个 的 方 案 数 至 少 有 个 的 方 案 数
于是,对于每个高维后缀和得到的超集数量,代表的就是相互按位与之后,至少这些位为
的个数。对 的数量进行计数,如果 的个数为偶数( ),则答案加上这些方案数,如果是奇数,则答案减去这些方案数。
#include <bits/stdc++.h> using namespace std;typedef long long ll;const ll mod = 1e9 + 7 ;const int N = 1 << 20 ;int main () { ios::sync_with_stdio (false ); cin.tie (nullptr ); int n; cin >> n; vector<int > dp (N) , f (n + 1 ) , a (n) ; for (auto &i : a) { cin >> i; dp[i] ++; } for (int i = 0 ; i < 20 ; i ++) { for (int j = N - 1 ; ~j; j --) { if (j >> i & 1 ) dp[j ^ (1 << i)] += dp[j]; } } f[0 ] = 1 ; for (int i = 1 ; i <= n; i ++) { f[i] = f[i - 1 ] * 2 % mod; } ll ans = 0 ; for (int i = 0 ; i < N; i ++) { if (__builtin_popcount(i) & 1 ) ans -= f[dp[i]]; else ans += f[dp[i]]; ans = (ans + mod) % mod; } cout << ans << '\n' ; return 0 ; }