洛谷P6025 题解
提供一个复杂度为的 $O(\log n)$ 的,基于正常分析而非打表的做法,我认为这个做法比当前题解区的所有做法更加优美。
这道题相当不错,考察了线段树和位运算的理解。
题意
一颗常规方式构建的线段树,求大小为 $1,2,3,\cdots n$ 的线段树分别占用的最大空间,即最大下标。
输出答案的异或和。
- $n\leq 10^{15}$
构建代码为:
1 | void build(int k,int l,int r) |
原题意是求 $f(l,r)$,但很容易转化为求 $f(1,n)$,直接求 $f(1,l-1)\bigoplus f(1,r)$ 即可。
分析
40PTS
首先,线段树建立的过程,就是从根往下走的过程,往左走,向当前编号的二进制表示后写一个 $0$,往右走,就写一个 $1$。
考虑线段树的最大下标在何处取到,由于右儿子下标相对较大,一个比较显然的想法是一直走右儿子,但是这样是错误的,显然的反例为 $n=3$,如下图所示。
这个例子中,$[1,3]$ 节点的左儿子比右儿子多一层,所以出现了走左儿子最优的情况。
但是,如果某个节点处左右儿子高度相同,那么很显然应该走右儿子,因为当前这一步决定的位数是最高位。
所以走左儿子,当且仅当左右儿子高度不同,下面分析线段树的高度和长度的关系。
首先,子树构建情况只和长度有关,所以我们只关心长度而非左右端点,然后,长度为 $2^k$ 的节点,构建出的树为一颗完全二叉树,其高度为 $k$。 此时,如果点数继续增大,那么高度为 $k$ 的树就无法容下这么多节点,高度会变为 $k+1$,所以,长度为 $(2^k,2^{k+1}]$ 的节点,构建出子树的高度为 ${k+1}$。
容易发现,常规构建方式构建出的子树,其左儿子的大小不小于右儿子,且差值至多为 1,因此,如果出现了左右儿子高度不同,一定是左儿子比右儿子高,并且只能是一种情况,即长度的二进制表示为 $100001$。即除了最高位和最低位其余位均为 $0$,只有这样才会出现左右儿子落在不同区间的情况。
所以我们对于一个 $n$,很容易给出一个 $O(\log n)$ 的计算方式,即:
1 | int ask(int x,int now=1){ |
其中 __builtin_popcount()
为内置函数,参数为 unsigned
, int
也可以使用但不能为负数。
如果希望对 long long
使用,应改为 _builtin_popcountll()
。
对每个数暴力求解即可。
100PTS
我们需要观察求解一个数的过程,不妨考虑二进制数 $1001011$ 的计算过程。
手动模拟一下上述 $\operatorname{ask()}$ 函数的调用过程,$now$ 变量和 $x$ 变量的值依次变为
1 | now=00000001 x=1001011 |
容易发现,在出现 $1001$ 这样的情况之前,我们将 $now$ 左移一位并在后面写上了 $1$,同时将 $x$ 右移一位。
在出现 $1001$ 之后,我们一直在往 $now$ 后面写 $0$,而 $x$ 的变化则是先右移一位并将最后一位改为 $1$。
而最后 $x$ 从 $10$ 变为 $1$ 时,我们在 $now$ 末尾添加了一个 $1$。
不难发现,一个固定的 $x$,我们最终得到的占用下标最大值,仅仅和它二进制表示下的最高位和第二高的为 $1$ 的位有关,因为在这之前我们一定会添加 $1$,而在这之后一定会往后添加 $0$ 并在最后添加一个 $1$。
注意如果为 $2^k$ 这样不存在次高位的数,我们需要特判。
所以我们有了一个比较显然的想法,即枚举二进制下最高位和次高位的位置,统计能取到这两个位置的数的数量,并异或上对应的权值。
这样的复杂度是 $O(\log^2n)$ 的,我们考虑继续优化。
对于二进制下位数小于 $n$ 的二进制下位数的所有数,实际上会有贡献的只有两个值,分别是 $2^k$ 和 $2^k+1$。因为如果次高位不为第一位,那么能取到该值的数的个数一定为偶数个(低于次高位的位置可以任意填)。剩下这两个值的贡献都是可以 $O(1)$ 计算的。
这样就只用考虑二进制下位数和 $n$ 相同的数,同样的道理,如果次高位高于 $1$,那么没有贡献,但需要注意的是,如果次高位取到了上界,例如 $10110$ 中,次高位取到了第三位的 $1$,那么由于必须小于 $n$,实际上只有奇数个值可以取到,这样会带来一定的贡献,所以我们需要计算这种情况的答案,复杂度为 $O(\log n)$。
总复杂度$O(\log n)$。
可以参考我博客中的一些线段树的总结,会不定期更新。
参考代码
1 |
|