洛谷P6025题解
洛谷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 |
|