高维前缀和

作用

求解一个集合的所有子集/超集信息的和。

前缀和

一般来说,会这样写前缀和。

对于一维的前缀和是: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;
}
posted @ 2025-09-01 19:54  咚咚的锵  阅读(114)  评论(0)    收藏  举报