ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

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

2022-06-22 15:06:03  阅读:170  来源: 互联网

标签:队列 Trie 题解 ret son int 异或 now 联考


【题解】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;
}

标签:队列,Trie,题解,ret,son,int,异或,now,联考
来源: https://www.cnblogs.com/xrkforces/p/luogu-P5283.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有