高维前缀和

我们知道一维前缀和的写法:SOSDP与高维前缀和 - 图1,那么对于二维前缀和呢?
显然,根据容斥原理,我们有
SOSDP与高维前缀和 - 图2%3Ds%7Bx_1%2Cy_1%7D-s%7Bx1%2Cy_1-1%7D-s%7Bx1%2Cy_1-1%7D%2Bs%7Bx1-1%2Cy_1-1%7D%0A#card=math&code=%5Coperatorname%7Bsum%7D%28x_1%2Cy_1%29%3Ds%7Bx1%2Cy_1%7D-s%7Bx1%2Cy_1-1%7D-s%7Bx1%2Cy_1-1%7D%2Bs%7Bx_1-1%2Cy_1-1%7D%0A&id=QI4Sv)
处理二维前缀和还可以这么写,但是三维,四维呢?这玩意搞起容斥岂不是逆天?
实际上,我们还有一个处理前缀和的方式:

  1. int a[N][N], s[N][N];
  2. for (int i = 1; i <= n; ++i)
  3. for (int j = 1; j <= n; ++j)
  4. s[i][j] = a[i][j];
  5. for (int i = 1; i <= n; ++i)
  6. for (int j = 1; j <= n; ++j)
  7. s[i][j] += s[i - 1][j];
  8. for (int i = 1; i <= n; ++i)
  9. for (int j = 1; j <= n; ++j)
  10. s[i][j] += s[i][j - 1];

这样处理是等价的(分维度处理前缀和,然后再合并),但是写起来更简单。

二进制与高维前缀和

我们来看一个例题

SOSDP与高维前缀和 - 图3 件物品,第 SOSDP与高维前缀和 - 图4 件物品的权值为 SOSDP与高维前缀和 - 图5,多个物品组成一个集合,集合的权值为集合中所有物品权值的异或和。现在有 SOSDP与高维前缀和 - 图6 次询问,每次询问给定一个集合 SOSDP与高维前缀和 - 图7,求出集合 SOSDP与高维前缀和 - 图8 的所有子集的权值和,以及其所有超集的权值和。 SOSDP与高维前缀和 - 图9

这个题目看起来跟高维前缀和没任何关系,不是吗?
我们假设有三个物品,编号分别为1,2,3,价值分别为A,B,C。我们不妨令:

SOSDP与高维前缀和 - 图10

我们好像发现,我们可以通过对应下标为0或者1来决定某件物品选还是不选,从而得到对应的某个状态的权值。我们可以每次询问暴力枚举,但是这个复杂度是 SOSDP与高维前缀和 - 图11#card=math&code=O%28m2%5En%29&id=A5vlU) 的。
那么,前缀和在这有啥含义呢?
我们做一次前缀和,有

SOSDP与高维前缀和 - 图12

某个状态的前缀和,其实就是该状态所有子集的权值之和!(不太看得明白的,可以二维模拟一下)
子集是前缀和,那么超集只要反着做一遍即可。
但显然,我们不可能开一个20维数组,能把手写麻。但是万幸的是,因为每一位只是0或者1,所以我们可以进行状态压缩,将其压进一个整数里面即可。若状态 SOSDP与高维前缀和 - 图13 是状态 SOSDP与高维前缀和 - 图14 的子集,那么有 SOSDP与高维前缀和 - 图15

状态压缩之后,稍微习惯一下,然后就可以当作普通的高维前缀和来做了。

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. #define LL long long
  4. const int N = 25, V = 1 << N;
  5. int n, m, a[N];
  6. LL pre[V], suf[V];
  7. int main()
  8. {
  9. //read
  10. scanf("%d%d", &n, &m);
  11. for (int i = 0; i < n; ++i)
  12. scanf("%d", &a[i]);
  13. //build:建立f数组
  14. for (int j = 0; j < (1 << n); ++j) {
  15. int sum = 0;
  16. for (int i = 0; i < n; ++i)
  17. if (j & (1 << i)) sum ^= a[i];
  18. pre[j] = suf[j] = sum;
  19. }
  20. //pre & suf
  21. for (int i = 0; i < n; ++i)
  22. for (int j = 0; j < (1 << n); ++j)
  23. if (j & (1 << i))
  24. pre[j] += pre[j ^ (1 << i)];
  25. else
  26. suf[j] += suf[j ^ (1 << i)];
  27. //query
  28. while (m--) {
  29. int k, v = 0;
  30. scanf("%d", &k);
  31. for (int i = 0, t; i < k; ++i) {
  32. scanf("%d", &t);
  33. v |= 1 << (t - 1);
  34. }
  35. printf("%lld %lld\n", pre[v], suf[v]);
  36. }
  37. return 0;
  38. }

SOSDP

感谢heyuhhh的博客:高维前缀和总结(sosdp)
SOSDP解决的是这样一类问题:

对于所有 SOSDP与高维前缀和 - 图16,求解 SOSDP与高维前缀和 - 图17

直接暴力枚举子集的话,复杂度是 SOSDP与高维前缀和 - 图18#card=math&code=O%282%5En%2A2%5En%29&id=PPDZo),也就是 SOSDP与高维前缀和 - 图19#card=math&code=O%284%5En%29&id=Jgk4i) (严格讲,均摊后的复杂度是 SOSDP与高维前缀和 - 图20#card=math&code=O%283%5En%29&id=Ty4UT),但是反正不够优就完事了)。如果进行高维前缀和,就可以将复杂度压进 SOSDP与高维前缀和 - 图21#card=math&code=O%28n2%5En%29&id=x7t44) 。
看起来有点玄学,其实就三行代码:

  1. for (int i = 0; i < n; ++i)
  2. for (int j = 0; j < (1 << n); ++j)
  3. if (j & (1 << i)) f[j] += a[j ^ (1 << i)];

(其实和上面的高维前缀和就是一个东西

我们来看一个改造版的题目(题目来源:ARC100E):

给定 SOSDP与高维前缀和 - 图22 个数,分别记为 SOSDP与高维前缀和 - 图23。 对于每个 SOSDP与高维前缀和 - 图24,求出 SOSDP与高维前缀和 - 图25 的最大值,且 SOSDP与高维前缀和 - 图26 SOSDP与高维前缀和 - 图27

如果我们将条件改一下,变成 SOSDP与高维前缀和 - 图28,算出的答案记为 SOSDP与高维前缀和 - 图29,那么原题目的答案就变成了 SOSDP与高维前缀和 - 图30 的前缀最大值了。但是,这个条件依然不太可做。
我们再改一下,变成 SOSDP与高维前缀和 - 图31,这样条件肯定比 SOSDP与高维前缀和 - 图32 强,但是比 SOSDP与高维前缀和 - 图33 要弱,所以我们可以根据这个来求,然后做一次前缀最大值即可。
我们需要找出状态 SOSDP与高维前缀和 - 图34 的两个值最大的子状态,如果暴力的话,这个复杂度显然有点高,所以我们可以魔改一下原来的SOSDP板子,将求和转化为求最大值和次大值即可。

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. const int N = 1 << 20;
  4. int n, a[N];
  5. int Max1[N], Max2[N];
  6. void update(int j, int k) {
  7. //更新最大值
  8. if (Max1[j] <= Max1[k])
  9. Max2[j] = Max1[j], Max1[j] = Max1[k];
  10. //更新次大值
  11. else if (Max2[j] <= Max1[k])
  12. Max2[j] = Max1[k];
  13. }
  14. int main()
  15. {
  16. //read
  17. scanf("%d", &n);
  18. for (int i = 0; i < (1 << n); ++i)
  19. scanf("%d", &a[i]);
  20. //init
  21. for (int i = 0; i < (1 << n); ++i)
  22. Max1[i] = a[i], Max2[i] = -1e9;
  23. //pre
  24. for (int i = 0; i < n; ++i)
  25. for (int j = 0; j < (1 << n); ++j)
  26. if (j & (1 << i)) update(j, j ^ (1 << i));
  27. //output
  28. int ans = 0;
  29. for (int i = 1; i < (1 << n); ++i) {
  30. ans = max(ans, Max1[i] + Max2[i]);
  31. printf("%d\n", ans);
  32. }
  33. return 0;
  34. }