高维前缀和
作用
求解一个集合的所有子集/超集信息的和。
前缀和
一般来说,会这样写前缀和。
对于一维的前缀和是:sum[i] = sum[i - 1] + a[i];。
对于二维的前缀和是:sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + a[i][j];。
当要求解三维的前缀和信息时,就很麻烦了。
所以有另外一种求前缀和的方式。
对于二维前缀和有:
for ( int i = 1; i <= n; i ++)
for ( int j = 1; j <= m; j ++)
sum[i][j] = a[i][j];
for ( int i = 1; i <= n; i ++)
for ( int j = 1; j <= m; j ++)
sum[i][j] += sum[i - 1][j];
for ( int i = 1; i <= n; i ++)
for ( int j = 1; j <= m; j ++)
sum[i][j] += sum[i][j - 1];
这很好理解,先把横坐标的前缀和求出,再算出整体的前缀和。
按照这样,三维的前缀和就很好求了:
for ( int i = 1; i <= n; i ++)
for ( int j = 1; j <= m; j ++)
for ( int k = 1; k <= l; k ++)
sum[i][j][k] = a[i][j][k];
for ( int i = 1; i <= n; i ++)
for ( int j = 1; j <= m; j ++)
for ( int k = 1; k <= l; k ++)
sum[i][j][k] += sum[i - 1][j][k];
for ( int i = 1; i <= n; i ++)
for ( int j = 1; j <= m; j ++)
for ( int k = 1; k <= l; k ++)
sum[i][j][k] += sum[i][j - 1][k];
for ( int i = 1; i <= n; i ++)
for ( int j = 1; j <= m; j ++)
for ( int k = 1; k <= l; k ++)
sum[i][j][k] += sum[i][j][k - 1];
高维
好,现在考虑一个问题。
给出 \(n\) 个物品,对于这 \(n\) 个物品,给每个物品一个选或不选的状态,那么总共就有 \(2^n\) 种状态,每种状态对应一个集合,对于每一种集合 \(S\) 都有一个权值 \(a_S\)。
现在对于每一种集合 \(S\),求出它的所有子集权值之和。
如果只有 \(2\) 个物品,那么就只有四种集合,考虑用二维数组表示一下。
\(a_{0,0}\) 就表示什么都不选的权值,\(a_{1,0}\) 表示只选第一个物品的权值,\(a_{0,1}\) 表示只选第二个物品的权值,\(a_{1,1}\) 表示全选的权值。
记 \(sum_{0/1,0/1}\) 表示每种集合的子集权值之和。
那么就有:
\(sum_{0,0} = a_{0,0}\),\(sum_{1,0}=a_{0,0}+a_{1,0}\),\(sum_{0,1}=a_{0,0}+a_{0,1}\),\(sum_{1,1}=a_{0,0}+a_{1,0}+a_{0,1}+a_{1,1}\)。
可以发现,这就是一个二维前缀和。
那么对于 \(n\) 个物品的情况,参照上面所述的前缀和方式,加上状态压缩,就可以简单的求出。
for ( int S = 0; S < (1 << n); S ++) sum[S] = a[S];
for ( int i = 0; i < n; i ++) // 这里枚举的每个维度
for ( int S = 0; S < (1 << n); S ++)
if (S & (1 << i)) sum[S] += sum[S ^ (1 << i)];
这个不仅可以求子集和,还可以求子集最值和一些其他东西。
当然,也可以求超集信息。
for ( int S = 0; S < (1 << n); S ++) sum[S] = a[S];
for ( int i = 0; i < n; i ++)
for ( int S = 0; S < (1 << n); S ++)
if (! (S & (1 << i))) sum[S] += sum[S | (1 << i)];
题目
[ARC100E] Or Plus Max
这里的 \(A_i\),也就代表 \(i\) 这个集合的权值。
要选出 \(i|j\le k\),不好做,可以转化成 \(i|j=k\) 的情况,最后答案就是一个前缀的最大值。
对于 \(i|j=k\),它的必要条件是 \(i\cup j\subset k\),因为是取最大值,所以满足必要条件即可。
因为 \(i\cup j\subset k\),所以 \(i\)、\(j\) 都是 \(k\) 的一个子集。
那么转化成在 \(k\) 的子集中,选出一个权值最大和次大值。
直接高维前缀和即可。
代码
#include <bits/stdc++.h>
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = 18, M = 2e5 + 10, inf = 1e9, mod = 998244353;
int n;
int Mx[1 << N], Mxx[1 << N];
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n;
for ( int i = 0, x; i < (1 << n); i ++) cin >> Mx[i];
for ( int i = 0; i < n; i ++) {
for ( int S = 0; S < (1 << n); S ++) {
if (! (S & (1 << i))) continue ;
int mx = Mx[S ^ (1 << i)];
if (mx >= Mx[S]) Mxx[S] = Mx[S], Mx[S] = mx;
else if (mx > Mxx[S]) Mxx[S] = mx;
}
}
int ans = 0;
for ( int S = 1; S < (1 << n); S ++)
cout << (ans = max(ans, Mx[S] + Mxx[S])) << '\n';
return 0;
}
[COTS 2019] 疏散 Sklonište
因为关键点只有 \(18\) 个,所以可以对每个关键点求出它的最短路。
然后考虑二分答案,记当前的二分的答案为 \(lim\)。
现在把一个关键点 \(i\) 拆成 \(s_i\) 个点。
然后可以发现,如果每个点向它可以在 \(lim\) 内到达的关键点连边,那么就是一个二分图完备匹配。
考虑 Hall 定理,\(S\) 肯定是不能枚举的,考虑取枚举 \(N(S)\),但是这个 \(N(S)\) 还是很大。
考虑 \(N(S)\) 的本质,其实就是关键点集合 \(T\) 中的每个点 \(i\) 拆成 \(s_i\) 个点的集合。
所以只用枚举关键点集合 \(T\),就可以求出 \(|N(S)|\)。
对于一个关键点集合 \(T\),求出它最大的点集 \(S\),然后判断 \(|S|\le |N(S)|\) 即可。
\(T\) 的最大点集 \(S\) 的大小,其实就有多少个点满足它的邻域是 \(T\),这个用高维前缀和计算即可。
代码
#include <bits/stdc++.h>
#define int long long
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = 2e5 + 10, M = 2e5 + 10, inf = 1e18, mod = 998244353;
int n, m, k;
int a[N], s[N];
vector< pair< int, int> > G[N];
struct node {
int w, v;
bool operator < ( const node & rhs) const { return w > rhs.w; }
} ;
priority_queue< node> q;
int dis[18][N], vis[N];
void dij( int s) {
for ( int i = 1; i <= n; i ++)
dis[s][i] = inf, vis[i] = 0;
q.push({0, a[s]}), dis[s][a[s]] = 0;
while (q.size()) {
int u = q.top().v; q.pop();
if (vis[u]) continue;
vis[u] = 1;
for ( auto i : G[u]) {
int v = i.first;
if (dis[s][v] > dis[s][u] + i.second) dis[s][v] = dis[s][u] + i.second, q.push({dis[s][v], v});
}
}
}
int sum[1 << 17];
int chk( int lim) {
for ( int S = 0; S < (1 << k); S ++)
sum[S] = 0;
for ( int i = 1; i <= n; i ++) {
int S = 0;
for ( int j = 0; j < k; j ++)
if (dis[j + 1][i] <= lim) S |= (1 << j);
sum[S] ++;
}
for ( int i = 0; i < k; i ++)
for ( int S = 0; S < (1 << k); S ++)
if (S & (1 << i)) sum[S] += sum[S ^ (1 << i)];
for ( int S = 0; S < (1 << k); S ++) {
int Ns = 0;
for ( int j = 0; j < k; j ++)
if (S & (1 << j)) Ns += s[j + 1];
if (sum[S] > Ns) return 0;
}
return 1;
}
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> n >> m >> k;
for ( int i = 1; i <= m; i ++) {
int u, v, w; cin >> u >> v >> w;
G[u].push_back({v, w});
G[v].push_back({u, w});
}
for ( int i = 1; i <= k; i ++) cin >> a[i] >> s[i];
for ( int i = 1; i <= k; i ++)
dij(i);
int l = 0, r = inf, ans = inf;
while (l <= r) {
int mid = (l + r) >> 1;
if (chk(mid)) ans = mid, r = mid - 1;
else l = mid + 1;
}
cout << ans << '\n';
return 0;
}
New Divide
中文题面:

设 \(pre_i\) 表示 \([1,i]\) 的异或和。
那么转化一下就是对每个前缀 \([1,i]\) 找一个 \(k\),使得 \(pre_k+(pre_i \oplus pre_k)\) 的值最大。
直接求是困难的,考虑放在二进制下观察。
pre_i: 1 1 0 0 ...
pre_k: 1 0 1 0 ...
sum : 1 1 2 0 ...
可以发现,如果 \(pre_i\) 二进制下某一位为 \(1\),那么不论 \(pre_k\) 这一位是什么,贡献都是 \(1\);如果 \(pre_i\) 某一位为 \(0\),那么 \(pre_k\) 这一位为 \(1\),贡献为 \(2\),否则贡献为 \(0\)。
所以只用最大化 \(pre_i\) 为 \(0\),\(pre_k\) 为 \(1\) 的位数。
还是不好求,但是发现如果把 \(pre_i\) 按位取反,就可以转化成 \(pre_i\) 与 \(pre_k\) 的按位与最大。
现在问题变成了,给定一个 \(pre_i\) 记它取反后为 \(val\),要求在 \(pre_1,pre_2,\dots,pre_{i}\) 中选出一个数 \(x\),使得 \(x\vee val\) 最大。
这看上去可以用 Trie 求,但其实不行,因为当 \(val\) 的某一位为 \(0\) 时,走 \(0\) 或 \(1\) 的出边是不确定的。
还是考虑从高到低枚举 \(val\) 的每一位去贪,如果这一位为 \(1\),那么肯定是想在 \(pre_1,pre_2,\dots,pre_{i}\) 选一个这位同样为 \(1\) 的出来,这启发可以维护一个变量 \(now\),表示当前 \(x\) 的值是多少。
具体来说,如果 \(val\) 第 \(i\) 位为 \(1\),那么就看 \(pre_1,pre_2,\dots,pre_{i}\) 中是否有 \(now+2^i\) 的超集,若有就可以让 \(x\) 的第 \(i\) 位取 \(1\)。
这里就可以用高维前缀和了,\(mi_S\) 表示 \(S\) 的超集中编号最小的一个,初始 \(mi_{pre_i}=\min i\)。
只要满足 \(mi_{now+2^i}\le\) 当前前缀的编号,就可以让 \(x\) 第 \(i\) 位取 \(1\)。
最后算出答案即可。
代码
#include <bits/stdc++.h>
void Freopen() {
freopen("", "r", stdin);
freopen("", "w", stdout);
}
using namespace std;
const int N = (1 << 20), M = 2e5 + 10, inf = 1e9, mod = 998244353;
int n;
int a[N];
int mi[N];
signed main() {
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
memset(mi, 127, sizeof mi);
mi[0] = 0;
cin >> n;
for ( int i = 1; i <= n; i ++) cin >> a[i], a[i] ^= a[i - 1], mi[a[i]] = min(mi[a[i]], i);
for ( int i = 0; i < 20; i ++)
for ( int S = 0; S < N; S ++)
if (! (S & (1 << i))) mi[S] = min(mi[S], mi[S | (1 << i)]);
for ( int i = 1; i <= n; i ++) {
int val = (N - 1) ^ a[i], now = 0;
for ( int j = 19; j >= 0; j --)
if ((val & (1 << j)) && mi[now | (1 << j)] <= i) now |= (1 << j);
int res = ((val & a[mi[now]]) << 1) + a[i];
cout << res << ' ';
}
return 0;
}

浙公网安备 33010602011771号