【题解】P5283 [十二省联考 2019] 异或粽子(字典树 Trie,优先队列)

【题解】P5283 [十二省联考 2019] 异或粽子

很好的优先队列+可持久化字典树练手题!

题目链接

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;
}
posted @ 2022-06-22 14:56  向日葵Reta  阅读(50)  评论(0编辑  收藏  举报