[十二省联考 2019] 异或粽子

这篇博客介绍了如何优化算法来解决一个数组问题,即给定一个数组和一个整数k,求不同子区间的异或和之和的最大值。通过前缀异或和的计算,将问题转化为寻找特定数量的最大异或值点对。博主提出了一种初始的O(n^2 log n)解决方案,并进一步优化为O(n log^2 n),利用0/1 trie数据结构动态维护最大异或值,并使用优先队列实现。最终实现了一个更高效的算法来找到答案。

题目大意

  给一个长度为 nnn 的数组 {an}\{ a_n \}{an} 并给定一个整数 kkk, 求由 kkk 个不同的子区间的异或和之和的最大值。

算法分析

  先做一个前缀的异或和:

si=⨁i=1nsi s_i = \bigoplus_{i = 1}^n s_i si=i=1nsi

  类似于前缀和我们就有:

⨁i=lrai=sr−sl−1 \bigoplus_{i = l}^ra_i = s_r - s_{l - 1} i=lrai=srsl1

  那么问题就转化成了求 kkk 对不同的点对 (x,y)(x, y)(x,y),其中 x≠yx \neq yx=y,最大化:

∑i=1k(sxi⨁syi) \sum_{i = 1}^k (s_{x_i} \bigoplus s_{y_i}) i=1k(sxisyi)

  于是很好象到一种 O(n2log⁡n)O(n^2\log n)O(n2logn) 的做法,枚举所有点对 (x,y)(x, y)(x,y),然后求出异或值,再扔到一个堆里面,最后从堆顶取 kkk 个数出来然后再加起来就完了。

#include<bits/stdc++.h>
using namespace std;
#define in read()
#define MAXN 1010
#define int long long

inline int read(){
	int x = 0; char c = getchar();
	while(c < '0' or c > '9') c = getchar();
	while('0' <= c and c <= '9'){
		x = x * 10 + c - '0'; c = getchar();
	}
	return x;
}

int a[MAXN] = { 0 };
int s[MAXN] = { 0 };
priority_queue<int> q;
int n = 0; int k = 0;

signed main(){
	n = in; k = in; int ans = 0;
	for(int i = 1; i <= n; i++) a[i] = in;
	for(int i = 1; i <= n; i++) s[i] = (s[i - 1] ^ a[i]);
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= i; j++)
			q.push(s[i] ^ s[j - 1]);
	for(int i = 1; i <= k; i++){
		ans += q.top(); q.pop();
	} cout << ans << '\n';
	return 0;
}

  这样就能有 60pts60pts60pts 的高分。

  但是这个算法仍然不够优秀,我们考虑优化这个算法。首先我们发现找点对这件事情就是处理三角形上的最大,我们考虑将三角形扩大两倍变成正方形然后算 2k2k2k 个点对的最大值,最后的答案再除以一个 222 就是我们要的答案。然后我们又发现对角线上的点对,也就是 (i,i)(i, i)(i,i) 对答案的贡献都是 000,所以现在我们也不需要考虑 x≠yx \neq yx=y 这个条件了。

  这样之后就很好处理了,首先我们显然能用一个 0/1  trie0/1 \; trie0/1trieO(log⁡si)O(\log s_i)O(logsi) 的时间内求出在 sss 中与 sis_isi 异或的第 kkk 大值:

void build(int x) {
    int p = 0;
    for(int i = 31; i >= 0; i--){
        int ch = (x >> i) & 1; size[p]++;
        if(!trie[p][ch]) trie[p][ch] = ++tot;
        p = trie[p][ch];
    }
    size[p]++;
}

int query(int x, int rk){
	int p = 0; int ans = 0;
	for(int i = 31; i >= 0; i--){
		int ch = (x >> i) & 1;
		if(!trie[p][ch ^ 1]) p = trie[p][ch];                                         // 如果没有相反的直接跑 
		else if(rk <= size[trie[p][ch ^ 1]]) p = trie[p][ch ^ 1], ans |= 1ll << i;    // 相反的子树大小大于等于当前值 
		else rk -= size[trie[p][ch ^ 1]], p = trie[p][ch];
	}
	return ans;
}

  然后我们维护一个堆,首先把 sis_isi 初始在 sss 中的异或的最大值插入堆中,然后每次取出堆顶,并累积答案,如果我们取出的是 sis_isisss 中的第 kkk 大,那么我们就把 k+1k + 1k+1 大加入堆中,这样就能保证次大的那个数一定在堆里面。这样操作 2k2k2k 次就能得到答案了。

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define in read()
#define MAXN 20002000

inline int read(){
	int x = 0; char c = getchar();
	while(c < '0' or c > '9') c = getchar();
	while('0' <= c and c <= '9'){
		x = x * 10 + c - '0'; c = getchar();
	}
	return x;
}

int a[MAXN] = { 0 };
int s[MAXN] = { 0 };
int n = 0; int k = 0;

int tot = 0;
int trie[MAXN][5] = { 0 };
int size[MAXN] = { 0 };

void build(int x) {
    int p = 0;
    for(int i = 31; i >= 0; i--){
        int ch = (x >> i) & 1; size[p]++;
        if(!trie[p][ch]) trie[p][ch] = ++tot;
        p = trie[p][ch];
    }
    size[p]++;
}

int query(int x, int rk){
	int p = 0; int ans = 0;
	for(int i = 31; i >= 0; i--){
		int ch = (x >> i) & 1;
		if(!trie[p][ch ^ 1]) p = trie[p][ch];                                       // 如果没有相反的直接跑 
		else if(rk <= size[trie[p][ch ^ 1]]) p = trie[p][ch ^ 1], ans |= 1ll << i;    // 相反的子树大小大于等于当前值 
		else rk -= size[trie[p][ch ^ 1]], p = trie[p][ch];
	}
	return ans;
}

struct Tnode{
	int dat;
	int id, rk;
	bool operator < (const Tnode &rhs) const { return dat < rhs.dat; }
};
priority_queue<Tnode> q;

signed main(){
	n = in; k = in; k <<= 1; int ans = 1;
	for(int i = 1; i <= n; i++) a[i] = in;
	for(int i = 1; i <= n; i++) s[i] = s[i - 1] ^ a[i];
	for(int i = 0; i <= n; i++) build(s[i]);                                 // 注意有 0
	for(int i = 0; i <= n; i++) q.push((Tnode){ query(s[i], 1), i, 1 });     // 0
	for(int i = 1; i <= k; i++){
		Tnode t = q.top(); ans += t.dat; q.pop();
		if(t.rk < n) q.push((Tnode){ query(s[t.id], t.rk + 1), t.id, t.rk + 1 });
	}
	cout << (ans >> 1) << '\n';
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值