首页 > 其他分享 >Luogu P3369 【模板】普通平衡树 01Tire树解法

Luogu P3369 【模板】普通平衡树 01Tire树解法

时间:2023-08-20 09:00:28浏览次数:41  
标签:01Tire 数字 int Luogu list 查询 P3369 ic

题目传送门

闲话:Luogu总共105篇题解中只有4篇01Tire树解法,虽说是非正解但未免也太少了些(貌似也不少?)……总之01Tire树的效率并不低,这道题用01Tire是很轻松的。

Q:这题为什么可以用01Tire树解?

能否解决一个问题,无非于三个点:可行性,空间,时间。

本题要求维护一个有序数集,能进行元素修改和查询。

而01Tire树通过数字的二进制01串储存,根据这条特性,01Tire树就可以作为维护有序数集的天然容器。

可行性上,01Tire树可解,至于空间和时间,等到代码部分结束后再讨论。

话不多说,接下来就进入01tire树解法详解

首先看到数据范围 \(|x| \le 10^7\) ,意识到数据中可能有负数,然而01Tire只能维护非零整数,于是使用最简单粗暴的方法:给所有数字加上 1e7 ,在输出时再减去即可。

这样数据范围就变成了 $ x \le 2 * 10^7 $ ,实际上根据01Tire建树的方法,这样只会增加一层,因此不用担心时空爆炸。

由于01Tire树所需空间比较大,因此我习惯于提前预估一下01Tire的深度,编译 log2(2e7) = 24.2 于是取 25 作为01Tire的深度。

至此,我们可以写出来基本的插入删除操作了。

//插入操作
void insert( int x ){
	int u = 0 , ic ;
	for( reg i = 24 ; i+1 ; i-- ){
		ic = ( x >> i ) % 2;
		if( !tire[u][ic] ) tire[u][ic] = ++tot;
		u = tire[u][ic];
	}
	word[u]++;
}
//删除操作
void delete_( int x ){
	int u = 0 , ic ;
	for( reg i = 24 ; i+1 ; i-- ){
		ic = ( x >> i ) % 2;
		u = tire[u][ic];
	}
	word[u]--;
}

其中 word[i] 表示以 \(i\) 结尾的数字个数。

查询 \(x\) 在当前数集中的排名

我们将所有儿子按照 0 儿子在左,1 儿子在右画图,最终会发现:对于所有的叶子节点,从左到右,代表的数字依次增大,这也是我在前面说“01Tire树是天然的有序数集容器”的原因。

这下,我们只需要检查 \(x\) 数字左边的数字个数就可以了。

注意到,如果有一个数字小于 \(x\) ,那么这个数字和 \(x\) 路径中一定存在一个公共节点,使得 \(x\) 路径向右遍历,这个数字向左遍历。

也可以理解为,倘若 \(x\) 在这个节点右转,那么在这个节点左转的所有数字一定比 \(x\) 小,因为这个数字必然在 \(x\) 的左侧。

于是我们就可以通过这个得到查询排名的方法了:遍历 \(x\) 的路径,对于路径中 \(1\) 的节点,统计这个 \(1\) 节点的兄弟 \(0\) 节点下的数字个数
最终加上1得到 \(x\) 的排名。

//查询某数的排名
int search_top( int x ){
	int u = 0 , ic ; 
	int tp = 1 ;
	for( reg i = 24 ; i+1 ; i-- ){
		ic = ( x >> i ) % 2 ;
		if( ic ) tp += list[ tire[u][0] ];
		//如果当前儿子是1,那么加上0儿子的数字个数 
		u = tire[u][ic];
	}
	return tp;
}

其中 list[i] 表示 \(i\) 节点下的数字个数。

维护 list 数组的方法也很简单,加入操作时在每一个经过的节点上进行 list[u]++ 操作,删除时进行 list[u]-- 操作即可。

查询排名为 \(x\) 的数字。

我们从源点开始,带着我们要查询的排名 \(x\) ,我们遇到了左儿子 \(l\) ,左儿子下有 list[l] 个数字,右儿子 \(r\) ,有 list[r] 个数字。

如果有 $ x \le list[l] $ 那么证明我们要查询的数字必然在左子树中,进入左子树即可。

如果有 $ x > list[l] $ 那么我们要查询的数字在右子树中,进入右子树。

然而我们还有两个重要的事情要做:

  1. 因为进入了右子树,意味着我们在这一位选择了 \(1\) ,所以应该在记录路径的数字上加入 ( 1 << i )

  2. 因为接下来我们要在右子树中查询,不考虑左子树,所以应当对排名 \(x\) 减去左子树的数字个数,接下来的操作是 “查询右子树的第 x - list[l] 名 ”。

遍历到底,我们就成功获得了排名 \(x\) 的数字,不要忘记减去 1e7 。

//查询排名为 x 的某数
int top_search( int x ){
	int u = 0 , lf , rg ;
	int as = 0 ;
	for( reg i = 24 ; i+1 ; i-- ){
		lf = tire[u][0];
		rg = tire[u][1];
		//lf 和 rg是两个子节点的坐标
		if( list[lf] >= x && lf ) u = lf ;
		//左子树直接进入即可 
		else if( rg ){
			x -= list[lf];
			as += ( 1 << i );
			//减去左子树数字个数,记录1 
			u = rg;
		}
	}
	return as-1e7;
	//减去1e7恢复原来的数字 
}

最后两个操作,查询小于(大于) \(x\) 的第一个数字。

按照我们之前的画图方法,可以看出来:

小于 \(x\) 的第一个数字,实际上就是排名为 \(x\) 排名-1 的数字,因此可以有最简单的操作方法:

  • 插入 \(x\) → 查询 \(x\) 的排名 → 查询排名-1的数字并输出 → 删除 \(x\)

为了防止 \(x\) 不在当前集合中,我们先插入,查询后再删除,保证查询时 \(x\) 一定存在。

按照这样的思路,我们也可以做到查询大于 \(x\) 的第一个数字了:

  • 插入 \(x\) → 查询 \(x\) 的排名 → 查询排名+word[x]的数字并输出 → 删除 \(x\)

为了方便理解,这里word[x]暂时表示 \(x\) 这个数字的个数,代码中还是保持此前的定义。

特别的,由于我们需要调用当前数字个数,需要把 insert 函数中的根节点拿出来作为全局变量,或者直接将 insert 函数改成 int 型函数返回根节点。

//查询小于x的第一个数字
void search_before( int x ){
	
	insert(x);
	int top = search_top(x)-1;
	printf("%d\n",top_search(top));
	delete_(x);
	
}
//查询大于x的第一个数字
void search_after( int x ){
	
	insert(x);
	int top = search_top(x) + word[u_];
	printf("%d\n",top_search(top));
	delete_(x);
	
}

其中 u_ 是我们在 insert(x) 操作中维护的根节点。

终于完成了所有操作。

下面是对于时间和空间的分析。

先说结论:对于这道题,时间和空间是完全足够的。

对于时间

每一次操作,因为本质上都是遍历树,所以每次操作的复杂度是 \(O(logx)\) ,总共有 \(n\) 次操作,所以总时间复杂度为 \(O(nlogx)\) ,足以通过本题。

对于空间

01Tire树解法的一大特点就是空间换时间,与其他的算法相比,虽然思维简单许多,然而空间却是其他算法的几倍,当然,这道题所用的空间和128M的内存限制相比简直是九牛一毛,因此完全不用担心空间问题,如果实在担心空间问题的话,也有一些方法能够优化一点空间,在这里不展开了。

解析写了这么多行,完整代码我就不直接放在题解里了,我放在了云剪切板里,这里是传送门:https://www.luogu.com.cn/paste/x80142m0

代码实现的过程中有一些细节,具体可以看我代码中的注释~

希望看完这篇题解的你能够对这种非正解有所了解~

题解中如果出现问题,请私信我,我会及时进行修正。

标签:01Tire,数字,int,Luogu,list,查询,P3369,ic
From: https://www.cnblogs.com/kscn/p/17643566.html

相关文章

  • 牛的旅行 luoguP1522 多余的换行造成的影响
    牛的旅行#include<bits/stdc++.h>usingnamespacestd;intread(){intf=1,x=0;charc=getchar();while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}while(c>='0'&&c<='9'){......
  • Luogu P9510 『STA - R3』高维立方体 题解
    题目传送门没见过这玩意,写个题解记下。题目大意周知斐波那契数列定义为:\[\operatorname{fib}(n)=\left\{\begin{aligned}1&&n\le2\\\operatorname{fib}(n-1)+\operatorname{fib}(n-2)&&n>2\end{aligned}\right.\]然后定义(\(n\le0\)函数值为\(0\))\[f(n)......
  • [Luogu P8716] 回文日期 题解
    STEP1:分析题目大意:给定一个8位数的日期,请你计算该日期之后下一个回文日期和下一个ABABBABA型的回文日期各是哪一天。这一题一眼看出是P2010的升级版,所以要先考虑到超时问题,因为如果一天一天地枚举,时间复杂度会非常高,所以我们不能直接枚举。因为题目只要"回文",所以我们只......
  • LuoguP1717 钓鱼
    题面题目分析动态规划。\(\bullet\)设计状态。思考:我从哪里来?从上一个湖过来。我到哪里去?到下一个湖去\(or\)继续在这个湖钓鱼。设\(dp[pos][tim]\)为前\(pos\)个湖花费了\(tim\)分钟所能钓的最大的鱼数量。\(\bullet\)转移状态。(为了方便计算,我们将题目中的数据......
  • [刷题笔记] Luogu P1725 琪露诺
    ProblemDescription若当前在\(pos\)位置,每次可以在\([pos+l,pos+r]\)区间内任选一个点跳。每跳到一个地方就可以获得这个地方的值,最后跳到位置\(pos\geqn\)即为结束,求如何跳才能使结束的时候获得的值最大?Analysis我们发现所有操作都是在一条链上完成的,显然若再次跳到这个位......
  • 题解 LuoguP3306 [SDOI2013] 随机数生成器
    题目链接:【LuoguP3306】。前置知识OI-Wiki:快速幂,扩展欧几里得算法(exgcd),BabyStep,GiantStep算法。题意很清楚,不说。分析当\(t=x\)时答案很明显为\(1\),即在第一天就可以读到。当\(t\neqx\)时当\(a=0\)时观察一下规律:\[x_1\equivx_1\pmod{p}\]\[x_2\equivb......
  • [刷题笔记] Luogu P1280 尼克的任务
    ProblemAnalysis首先,如果一个时间只有一个任务开始,则她必须做。如果一个时间有多个任务开始,她可以选一个去做。我们发现这样的决策是取决于后面的空暇时间,而不是前面。所以在dp的时候需要从后往前搜时间(当然如果从前往后可以跑记搜)考虑转移,如果一个时间有多个任务开始,则选一个......
  • luogu P4200 千山鸟飞绝 题解 【一维数组套平衡树】
    目录题目解题思路code题目题目链接解题思路首先,此题有明显的插入、删除、查找,所以必须要使用平衡树。考虑如何使用平衡树维护每个鸟的状态。发现很不方便,因为鸟的位置改变,整个平衡树的值都要修改。考虑针对每个节点开一颗平衡树,这样就有\(3e4\times3e4\)棵树。这显然太多了......
  • luogu P7352 炉心融解
    记\(f_S\)为所有人以当前信息推断出\(S\)这种情况是否合法,\(g_S\)表示假如真实情况是\(S\),应该有哪些人喊出来了。每一轮中,通过告诉你的\(k\)条消息可以推断出哪些集合不合法,将其\(f_S\)赋为\(0\),然后根据新的\(f_S\),有些人可能可以据此喊了,所以根据新的\(f_S\)更......
  • 【题解】Luogu[P9504] 『MGOI』Simple Round I C. 魔法禁林
    Link这题我们发现如果直接去枚举生命和法力值显然是不行的,又看到说最小的生命值,不禁想到最短路,但是怎么跑?我们令经过一条边之前魔力值为\(k\),那么该边的边权为\(\lfloor\dfrac{w}{k}\rfloor\),于是我们讲题目转化为了边权为\(\lfloor\dfrac{w}{k}\rfloor\)时\(s\)到\(t\)......