首页 > 其他分享 >01trie专题

01trie专题

时间:2024-05-18 16:11:26浏览次数:17  
标签:专题 01trie int res -- 异或 ch bit

01trie特训2

题意:给定一个含有 n 个元素的数组 Ai,你可以选择两个不相交的子段。求出这两个子段内的数的异或和的差值的最大值。

Sol:考虑枚举两段的分界点,对于较短的两段来说可能会有多个分界点但这样我们求的是答案的超集,一定会包括答案的。对于一个分界点,我们先考虑如果要求以左边区间必须包含分界点,那么我们只需要异或前缀和后找一个位置和它异或最大或最小,这是经典的01trie的应用。再考虑右边,我们需要做后缀异或和,做跑一遍trie。注意实际上可以不以分界点为选取的子段的端点,我们需要需要维护异或后结果的前缀最大值,后缀最小值,剩下同理。

debug:我自己实现不好写的原因是没开第二个后缀异或和,在那一直用前缀和偏移位置,边界不好处理。

//trie树 + 前缀异或和 + 枚举
#include<bits/stdc++.h>
#define endl '\n'
#define deb(x) cout << #x << " = " << x << '\n';
#define INF 0x3f3f3f3f
using namespace std;
const int N = 2e5 + 10;
int a[N], ls[N], rs[N], lmax[N], lmin[N], rmax[N], rmin[N];
int n;
int ltre[1 << 22][3], rtre[1 << 22][3],  cnt;

void add(int x, int tre[][3]){
	int p = 0;	
	for(int i = 20; i >= 0; i --){
		int bit = (x >> i) & 1;
		if(tre[p][bit])
			p = tre[p][bit];
		else
		{
			tre[p][bit] = ++ cnt;
			p = tre[p][bit];
		}
	}
	return;
}

int query_max(int x, int tre[][3]){
	int res = 0, p = 0;
	for(int i = 20; i >= 0; i --){
		int bit = (x >> i) & 1;
		if(tre[p][!bit]){
			p = tre[p][!bit];
			res += (1 << i);
		}else{
			p = tre[p][bit];
		}
				
	}
	return res;	
}

int query_min(int x, int tre[][3]){
	int res = 0, p = 0;
	
	for(int i = 20; i >= 0; i --){
		int bit = (x >> i) & 1;
		if(tre[p][bit]){
			p = tre[p][bit];
		}else{
			p = tre[p][!bit];
			res += (1 << i);
		}	
	}
	return res;
}

void solve()
{
	cin >> n;
	for(int i = 1; i <= n; i ++){
		cin >> a[i];
	}

	for(int i = 0; i <= n + 1; i ++){
		lmax[i] = rmax[i] = 0;
		lmin[i] = rmin[i] = INF;
	}

    add(0, ltre);

	for(int i = 1; i <= n; i ++){
		ls[i] = ls[i - 1] ^ a[i];
		lmax[i] = max(lmax[i - 1], query_max(ls[i], ltre));

		lmin[i] = min(lmin[i - 1], query_min(ls[i], ltre));
		
		add(ls[i], ltre);
	}

	cnt = 0;
	add(0, rtre);
	for(int i = n; i >= 1; i --){
		rs[i] = rs[i + 1] ^ a[i];
		rmax[i] = max(rmax[i + 1], query_max(rs[i], rtre));
		rmin[i] = min(rmin[i + 1], query_min(rs[i], rtre));
		add(rs[i], rtre);
	}

	int ans = 0;
	for(int i = 1; i <= n - 1; i ++){
		ans = max(ans, max(lmax[i] - rmin[i + 1], rmax[i + 1] - lmin[i]));
	}
	cout << ans << endl;
}

signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	int t;
	t = 1;
	// cin >> t;
	while(t--)
	solve();
}

https://www.luogu.com.cn/problem/U109923给定一个长度为 \(n\) 的数列 \(a_1,a_2,...,a_n\),选定四个整数 \(l_1,r_1,l_2,r_2(1\le l_1\le r_1<l_2\le r_2\le n)\),则函数 \(\operatorname{F}(l_1,r_1,l_2,r_2)\) 的计算方式如下:

\[\operatorname{F}(l_1,r_1,l_2,r_2)=(a_{l_1}\oplus a_{l_1+1} \oplus \cdots \oplus a_{r_1})+(a_{l_2}\oplus a_{l_2+1} \oplus \cdots \oplus a_{r_2}) \]

对于所有符合条件的 \((l_1,r_1,l_2,r_2)\) ,求 \(\operatorname{F}(l_1,r_1,l_2,r_2)\) 的最大值。

Sol:上面的题和这个题非常像,这个维护和最大的非常直接,我们还是枚举分界点,固定端点求前缀异或和的单点最大值,再更新成前缀后缀最大值。后缀完全不需要特殊处理,直接对称的从后往前做异或和,并更新,没有顺序问题。

int a[N];
int ch[N*31][2], idx;

void insert(int x){
  int p=0;
  for(int i=30; i>=0; i--){
    int j=x>>i&1; //取出第i位
    if(!ch[p][j])ch[p][j]=++idx;
    p=ch[p][j];
  }
}
int query(int x){
  int p=0,res=0;
  for(int i=30; i>=0; i--){
    int j=x>>i&1;
    if(ch[p][!j]){
      res += 1<<i; //累加位权
      p=ch[p][!j];
    }
    else p=ch[p][j];
  }
  return res;
}
int l[N],r[N];
void solve(){
	cin>>n;
	for(int i=1;i<=n;i++)cin>>a[i];
	int sum=0;insert(sum);
	for(int i=1;i<=n;i++){
		sum^=a[i];
		insert(sum);
		l[i]=max(l[i-1],query(sum));
	}
	memset(ch,0,sizeof ch);
	idx=0;sum=0;
	insert(0);
	//本质上是后缀异或和之间找匹配最大
	for(int i=n;i>=1;i--){
		sum^=a[i];
		//insert(sum);//由于后缀和不能超出边界,所以我们要注意处理插入时机
		r[i]=max(r[i+1],query(sum));
		insert(sum);
	}
	int ans=0;
	for(int i=1;i<=n-1;i++){
		ans=max(ans,l[i]+r[i+1]);
	}
	cout<<ans<<endl;
}

有一个初始为空的可重集 \(S\)。现在有 \(Q\) 次操作,每次操作有 \(3\) 种类型,分别是:

  • \(1,p_i\),把 \(p_i\) 加入 \(S\)。

  • \(2,p_i\),将 \(p_i\) 从 \(S\) 中删除,保证在删除前 \(p_i\) 已经在 \(S\) 中。

  • \(3,p_i,l_i\),询问 \(S\) 中有多少个数按位异或上 \(p_i\) 的结果小于 \(l_i\)。

Choosing The Commander - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

Sol:对于前两个操作只需要开一个数组,每次在树上走的时候维护标记。对于第三个操作有点像在线段树二分上的感觉,都是计算一边的答案,然后递归到另一边。

具体来说,我们在\(l_{i}\)的某位为1的时候才有机会让某些数\(\oplus p_{i}\)使得其小于\(l_{i}\),所以我们要在每一个节点上维护当前左右儿子的节点数量。

int tot=0;
int t[N*31][2];
int num[N*31];
int l,ans;
void insert(int x){
	int u=0;
	for(int i=30;i>=0;i--){
		//cerr<<u<<" ";
		bool op=(x&(1<<i));
		if(!t[u][op])t[u][op]=++tot;
		u=t[u][op];
		num[u]++;
	}
	//cerr<<endl;
}
void del(int x){
	int u=0;
	for(int i=30;i>=0;i--){
		//cerr<<u<<" ";
		bool op=(x&(1<<i));
		u=t[u][op];
		num[u]--;
	}
	//cerr<<endl;
}
void ask(int x){
	int u=0; ans=0;
	for(int i=30;i>=0;i--){
		//cerr<<u<<" ";
		bool op=(x&(1<<i));
		bool pp=(l&(1<<i));
		if(pp==1){
//在当前子树节点下都是前面和x相同的
//现在L是1,是贡献的时机,前面一直不能超过他且也不能走相反方向
//当前x如果走与自己相同的部分,会出现0,这段子树的数都比k小,累加答案
//然后走与自己相反的路,异或起来是1保证高位到这位都与L相同,未来有潜力贡献
		ans+=num[t[u][op]];
		u=t[u][op^1]; 
		}
		else{
			//在没有出现机会之前,一直走的是x的路线,也就是说一旦后面
			//选择某一位和x不同做贡献,前面全部抑或掉一定小于L
			u=t[u][op];//这里求小于它的数,所以前面也要保证一样
			//由于为0的时候不贡献,所以为0的时候我们只需要让异或为0
			//也就是x走树上和自己节点相同的节点,这样异或起来是0
			//和L保证一致
		}
		if(!u)break;
	}
	//cerr<<endl;
}
void solve(){
	int q;cin>>q;
	while(q--){
		int op,p;cin>>op>>p;
		if(op==1)insert(p);
		else if(op==2)del(p);
		else {
			cin>>l;
			ask(p);
			cout<<ans<<endl;
		}
	}
      
}

Tokitsukaze 有一个长度为 \(n\) 的序列 \(a_1, a_2, \ldots, a_n\) 和一个整数 \(k\)。 https://ac.nowcoder.com/acm/contest/67742/C

她想知道有多少种序列 \(b_1, b_2, \ldots, b_m\),满足:

  • \(1 \leq b_i \leq n\)
  • \(b_{i-1} < b_i\) \((2 \leq i \leq m)\)
  • \(\min(a_{b_1}, a_{b_2}, \ldots, a_{b_m}) \oplus \max(a_{b_1}, a_{b_2}, \ldots, a_{b_m}) \leq k\)

有了前面的铺垫看这个题就觉得还行了,先给出官方题解

Sol:由于求的是子序列,套路的,我们可以对 \(a\) 进行排序。 排序后发现,当 \(max=a_i,min=a_j\) 时,中间数可以随便选,如果这组 min 与 max 满足条件,那么答案是 \(2^{i-j-1}\),特别的,当 \(i=j\) 时,答案是 \(1\)。 但这个做法既要枚举 min 又要枚举 max,是 \(O(n^2)\) 的。遇到这种情况,优化思路大多都是枚举一个,快速查询另一个。由于条件是异或,我们考虑 01字典树 (01 Trie)。把比当前枚举的 \(a_i\) 小的数全部插入进 Trie 里,这样每次就能在 \(O(\log n)\) 的时间复杂度内求出所有满足条件的 min 的信息。 此时又有新的问题:怎么用 Trie 维护答案信息呢? 对于一个 \(a_j\),它贡献的答案为 \(2^{i-j-1}\)。我们可以将这个式子拆掉: \(2^{i-j-1}=\dfrac{2^{i-1}}{2^{j}}\),于是 \(i\) 与 \(j\) 就分离了。所以我们可以把 \(v_i = \dfrac {1} {2^{i}}\) 插入 。当 \(max=a_i\) 时,在 Trie 中查询 \(\sum v_j\), \((a_j \oplus a_i \leq k)\),此时贡献为 \(1 + 2^{i-1} \cdot \sum v_j\)。 \(2^i\) 与 \(\dfrac {1}{2^i}\) 都可以预处理求出 (\(\dfrac {1}{2^i}\) 要用到逆元)。然后枚举 max,每次在 Trie 中查询,总时间复杂度为 \(O(n \log n)\)。

我的理解:每个节点维护记录的的不再是简单的数量,而是分离变量以后的贡献函数。

  • 注意全局变量与局部变量重名可能会出大问题,避免。
  • 多测数据下如何清空01trie,根据idx用到哪清理到哪。

debug:init函数中用到变量n,但n首先是变化的并且还没输入呢。所以应该直接给边界常量。

int qmi(int a,int b){
	int res=1;
	while(b){
		if(b&1)res=res*a%mod;
		a=a*a%mod;
		b>>=1;
	}
	return res;
}
int n, m;
int a[N];
int idx=0;
int pw[N];
int inv[N];
int ch[N*30][2];
int num[N*30];
void init(){
	pw[0]=1;
	pw[1]=2;
	inv[1]=qmi(2,mod-2);
	for(int i=2;i<=200000;i++){
		pw[i]=pw[i-1]*2%mod;
		inv[i]=inv[i-1]*inv[1]%mod;
		
	}
}


void insert(int x,int y){
	int p=0;
	for(int i=29;i>=0;i--){
		int u=(x>>i)&1;
		if(!ch[p][u])ch[p][u]=++idx;
		p=ch[p][u];
		num[p]=(num[p]+y)%mod;
	}
}
int query(int x){
	int res=0;
	int p=0;
	for(int i=29;i>=0;i--){
			int u=(x>>i)&1;
			int r=(m>>i)&1;
			if(r==1){
				res+=num[ch[p][u]];res%=mod;
				p=ch[p][!u];
			}
			else {
				p=ch[p][u];
			}
			if(p==0)break;
	}
	res+=num[p];
	res%=mod;
	return res;
}
void solve(){
   cin>>n>>m;
   
   for(int i=1;i<=n;i++)cin>>a[i];
   sort(a+1,a+1+n);
   int ans=0;
   //for(int i=1;i<=n;i++)cerr<<a[i]<<" ";cerr<<endl;
  // for(int i=1;i<=10;i++)cerr<<pw[i]<<" "<<inv[i]<<endl;
   for(int i=1;i<=n;i++){
   	 ans=(ans+1+pw[i-1]*query(a[i])%mod)%mod;
   	 insert(a[i],inv[i]);
   }
   cout<<ans<<endl;
  //每次结束用到哪就清空多少
  for(int i=0;i<=idx;i++)ch[i][1]=ch[i][0]=num[i]=0;
  idx=0;
  
}

标签:专题,01trie,int,res,--,异或,ch,bit
From: https://www.cnblogs.com/mathiter/p/18199402

相关文章

  • bitset专题
    bitsetbitset前身:普通状态压缩的优化以cf937G为例,对于邻接矩阵的由二维压缩到一维#include<bits/stdc++.h>usingi64=longlong;voidsolve(){intn;std::cin>>n;std::vector<std::string>g(n),w(n);for(inti=0;i<n;i++){......
  • 倍增专题
    倍增大专题[AHOI2008]紧急集合/聚会-洛谷题意:给定一棵树,多次查询费马点(bushi费马点的含义是:到三个点的距离之和最小Solution:考虑画图发现树上三点两两求lca,必然至少两个相同,然后我们只需要让费马点为另一个点就可以了,因为这一段路程只需要一个点走就最好了。//考虑到让......
  • Celery专题
    Celery专题【一】Celery介绍【二】Celery快速使用【三】Celery包结构【四】django中使用celery【五】使用django_celery_beat在admin后台配置计划任务【六】Celeryadmin监视任务【七】Flower监控celery任务【八】任务异常自动告警......
  • 【专题】2024小红书餐饮行业方法论报告合集PDF分享(附原数据表)
    原文链接:https://tecdat.cn/?p=36199原文出处:拓端数据部落公众号报告合集显示,消费者对餐饮的需求正从单一的口味体验转变为追求更高层次的情绪价值和文化价值。餐饮不仅是生活的小确幸,更成为社交、休闲和探索的重要场景。小红书凭借其真实、利他、生动、丰富的内容,成为餐饮消费......
  • 【专题】2024年中国即时配送行业研究报告合集PDF分享(附原数据表)
    原文链接:https://tecdat.cn/?p=36188原文出处:拓端数据部落公众号即时配送服务凭借其无与伦比的高效与便捷,已深深满足了当代社会对于速度和便捷性的双重追求。据权威报告揭示,即时配送行业规模已突破3400亿元,且预测至2028年,这一数值将飙升至超过8100亿元。阅读原文,获取专题报告合......
  • 【专题】2023年中国白酒行业消费白皮书报告PDF合集分享(附原数据表)
    原文链接:https://tecdat.cn/?p=34188原文出处:拓端数据部落公众号2023年中国白酒行业消费白皮书报告合集,总结了消费市场的两大传承和五大进化,以帮助白酒企业更好地理解消费者心理和供需变化,从而把握增长机会。两大传承包括争夺消费者的“第一口酒”以及品牌在消费决策中的关键作......
  • 当实时互动遇上新硬件:GIAC 全球互联网架构大会「新硬件」专题论坛
    今年,被广泛预见为AI技术关键转折点的年份,生成式AI热度不断攀升,应用落地加速深化。在这个过程中,为了适应日益复杂的业务需求,背后的架构也将迎来新一轮的革新。 而在这场技术变革的浪潮中,GIAC全球互联网架构大会无疑成为了引领风潮的灯塔。作为深圳乃至华南地区技术领导者和......
  • Go语言高并发与微服务实战专题精讲——远程过程调用 RPC——高性能的 gRPC
    远程过程调用RPC——高性能的gRPC gRPC,这一由Google推出的高性能、开源、通用RPC框架,凭借其众多引人注目的特性,已成为业界瞩目的焦点。它基于HTTP/2协议标准设计开发,并采用ProtocolBuffers作为默认的数据序列化协议,广泛支持多种编程语言。gRPC不仅简化了服务的精确定义,而且......
  • 【专题】2024中国医疗器械企业全球化发展报告合集PDF分享(附原数据表)
    原文链接:https://tecdat.cn/?p=36180原文出处:拓端数据部落公众号中国医疗器械企业在国内市场面临同质化竞争和研发能力薄弱等挑战,而海外市场则展现出巨大的增长潜力和性价比优势。因此,全球化布局对于中国医疗器械企业至关重要。该报告合集详细分析了这些市场的宏观经济、医疗体......
  • 中电金信:专题报告·商业银行对公数字化转型体系架构及实践拆解
    当今,数字化转型已然成为商业银行发展的关键动力,在这个数字时代,对公业务数字化转型更是势在必行。 基于此,中电金信发布《商业银行对公数字化转型专题报告》(简称《报告》),针对对公数字化转型进行了专题研究。报告对主要商业银行对公数字化转型进行了深入的业务调研和分析总结,从对......