【题解】P5283 [十二省联考 2019] 异或粽子(字典树 Trie,优先队列)
【题解】P5283 [十二省联考 2019] 异或粽子
很好的优先队列+可持久化字典树练手题!
题目链接
题意概述
给定长度为 \(n\) 的序列 \(a_i\)。一个区间 \([l,r](1 \le l \le r \le n)\) 的价值为从 \(a_l\) 到 \(a_r\) 之间的每个数字进行的异或值。求这个序列中价值最大的 \(k\) 个区间的价值和。
思路分析
从问题入手。
首先求价值最大的 \(k\) 个区间的价值和,这一看就可以用优先队列(大根堆)来维护。
然后考虑如何快速的求出一个区间 \([l,r]\) 的价值。
由于异或也满足前缀和的性质,所以设 \(S_i\) 表示从 1 到 \(i\) 的异或和,则 \([l,r]\) 的价值就是 \(S_r \oplus S_{l-1}\)。
那么如何找到价值最大的 \(k\) 个区间呢?
首先我们可以预处理出来每一个 \(S_i\)(就像预处理前缀和那样),然后对于每一个 \(S_i\) 找到对应的 \(S_j\) 将其价值放入优先队列中。每次从优先队列中取出堆顶,累加进答案。
若当前取出的是 \(S_i\) 和对应的第 \(t\) 大值,则将和 \(S_i\) 对应的第 \(t+1\) 大值放入优先队列中。直到优先队列被取出堆顶 \(k\) 次。
但是有一个问题:我们在查找对于 \(S_i\) 第 \(t\) 大值时,是在整个所有区间的价值中查找的。假如在查找与 \(S_i\) 对应的值时查找到了 \(S_j\),那么在查找 \(S_j\) 的时候又会查一遍 \(S_i\)。也就是说,同一个区间的价值 \(S_i \oplus S_j\) 可能会入队两次,这样就会导致答案算重。
那么怎么解决呢?
或许我们会说,对于一个 \(i\),在查找的时候只查找 \(j>i\) 的 \(S_j\),但是这样显然并不好维护。
有一个方法就是,由于 \(S_i\) 和 \(S_j\) 互为对偶,所以我们可以继续这样查找,但是这次变成了让优先队列被取出栈顶 \(2k\) 次,那么答案就是总取出栈顶 \(2k\) 次的和除以 2。
现在问题就转化为:
已知一个序列 \(S_i\),求对于每一个 \(S_i\),序列中与其异或值第 \(t\) 大的值是多少?
先不考虑第 \(t\) 大,如果求的是最大。那么我们可以用类似于最长异或路径 - 洛谷中的额方法来维护,即:
建立一棵 01Trie,对于每一个 \(S_i\),将其插入 01Trie,对于其二进制下的每一位在 01Trie 中尽量寻找和这一位不相同的,若找不到不相同的,则找相同的,一路走到叶子节点,找到的就是字典树上与其异或路径最大的点。
现在考虑第 \(t\) 大。
可以模仿在学习可持久化线段树(主席树)时我们查找树上第 \(k\) 大的值时的做法:
在字典树插入过程中,记录每个节点的数字数量。
在查询的时候,根据优先往 0 走还是往 1 走,以及左右子节点数量和 \(t\) 来决定下一步的方向。
代码实现如下:
int find(int x,int num)
{
int now=0,ret=0;
for(int i=31;i>=0;i--)
{
int k=(x>>i)&1;
if(vis[son[now][k^1]]>=num)//如果 k^1 中的值够用。
{
now=son[now][k^1];
//ret=(ret<<1)+(k^1);
ret|=(1ll<<i);
}
else
{
num-=vis[son[now][k^1]];//需要减去 k^1 的大小。
now=son[now][k];
//ret=(ret<<1)+k;
}
}
return ret;
}
求解步骤
-
前缀异或和求出 \(S_i\);
-
将所有的 \(S_i\) 插入 01Trie(注意:\(S_0\) 也要插入);
-
将所有与 \(S_i\) 异或值最大的 \(S_j\) 加入优先队列(S_0 对应值同样也要加入);
-
for(i→1 到 2k) 每次执行以下步骤:
-
取出优先队列大根堆堆顶,累加进答案;
-
查找堆顶对应的 \(S_i\) 第 \(t+1\) 大值加入到优先队列中。
-
-
输出 \(ans/2\)。
易错点
-
由于选择的区间有可能是 [1,i],所以需要插入 \(S_0\) 到 01Trie 中。
-
在字典树的 find/querry 操作时:
-
当 k^1 的值不够用时,应该执行以下操作:
num-=vis[son[now][k^1]];//需要减去 k^1 的大小。 now=son[now][k];
这两句的顺序不能互换,若互换,则对应 \(num\) 减去的是新的 \(now\) 对应的 vis[son[now][k^1]]。
因为这玩意挂了一下午。。。 -
更新 ret 的值应该是下面代码,而我以前写成了下面注释中的代码:
if(vis[son[now][k^1]]>=num)//如果 k^1 中的值够用。 { now=son[now][k^1]; //ret=(ret<<1)+(k^1); ret|=(1ll<<i); } else { num-=vis[son[now][k^1]];//需要减去 k^1 的大小。 now=son[now][k]; //ret=(ret<<1)+k; }
因为 ret 表示的是 \(S_i \oplus S_j\) 的值,而非 \(S_j\) 的值;
-
-
勿忘开 longlong!!!
经验
-
在遇到要求前 \(k\) 大/前 \(k\) 小类似的问题上,要想到优先队列;
-
字典树上求第 \(k\) 大可以建立可持久化字典树,记录每个节点的数字数量;
-
有些题目中可以善于化”有序“为”无序“,比如此题中把求”有序“情况下前 \(k\) 个价值最大转化成了求”无序“状态下前 \(2k\) 个价值最大。
但需要考虑这样做的正确性,比如本题中正确性保证于 \(S_i\) 和 \(S_j\) 互为对偶。
代码实现
//luoguP5283
#include<iostream>
#include<cstdio>
#include<queue>
#define int long long
using namespace std;
const int maxn=5e5+100;
int sum[maxn],son[maxn*32][2],vis[maxn*32],tot;
int ans;
struct Node
{
int w,id,num;
bool operator<(const Node &t)const
{
return w<t.w;
}
}d[maxn];
priority_queue<Node>q;
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
return x*f;
}
void ins(int x)
{
int now=0;
for(int i=31;i>=0;i--)
{
int k=(x>>i)&1;
if(!son[now][k])son[now][k]=++tot;
now=son[now][k];vis[now]++;
}
//vis[now]++;
}
int find(int x,int num)
{
int now=0,ret=0;
for(int i=31;i>=0;i--)
{
int k=(x>>i)&1;
if(vis[son[now][k^1]]>=num)
{
now=son[now][k^1];
//ret=(ret<<1)+(k^1);
ret|=(1ll<<i);
}
else
{
num-=vis[son[now][k^1]];
now=son[now][k];
//ret=(ret<<1)+k;
}
}
return ret;
}
signed main()
{
// freopen("data.in.txt","r",stdin);
// freopen("data.out","w",stdout);
int n,k;
n=read();k=read();
for(int i=1;i<=n;i++)
{
int x=read();
sum[i]=sum[i-1]^x;
}
for(int i=0;i<=n;i++)ins(sum[i]);
for(int i=0;i<=n;i++)
{
d[i].w=find(sum[i],1);
d[i].id=i;d[i].num=1;
q.push(d[i]);
}
k<<=1;
for(int i=1;i<=k;i++)
{
Node tmp=q.top();
q.pop();
ans+=tmp.w;
d[tmp.id].num++;
d[tmp.id].w=find(sum[tmp.id],d[tmp.id].num);
q.push(d[tmp.id]);
}
cout<<(ans>>1ll)<<'\n';
return 0;
}