洛谷P6025题解

洛谷P6025 题解

提供一个复杂度为的 \(O(\log n)\) 的,基于正常分析而非打表的做法,我认为这个做法比当前题解区的所有做法更加优美。

这道题相当不错,考察了线段树和位运算的理解。

题意

  • 一颗常规方式构建的线段树,求大小为 \(1,2,3,\cdots n\) 的线段树分别占用的最大空间,即最大下标。

  • 输出答案的异或和。

  • \(n\leq 10^{15}\)

构建代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void build(int k,int l,int r)
{
if(l==r)
{
//do something
//e.g. tree[k]=a[l]
return;
}
int mid=(l+r)>>1;
build(k<<1,l,mid);
build(k<<1|1,mid+1,r);
//do something
//e.g. tree[k]=tree[k<<1]+tree[k<<1|1]
}

原题意是求 \(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
2
3
4
5
int ask(int x,int now=1){
if(x==1)return now;
if(__builtin_popcount(x)==2&&x&1)return ask(x+1>>1,now*2);
return ask(x>>1,now*2+1);
}

其中 __builtin_popcount() 为内置函数,参数为 unsignedint 也可以使用但不能为负数。

如果希望对 long long 使用,应改为 _builtin_popcountll()

对每个数暴力求解即可。

100PTS

我们需要观察求解一个数的过程,不妨考虑二进制数 \(1001011\) 的计算过程。

手动模拟一下上述 \(\operatorname{ask()}\) 函数的调用过程,\(now\) 变量和 \(x\) 变量的值依次变为

1
2
3
4
5
6
7
8
now=00000001 x=1001011
now=00000011 x=0100101
now=00000111 x=0010010
now=00001111 x=0001001
now=00011110 x=0000101
now=00111100 x=0000011
now=01111000 x=0000010
now=11110001 x=0000001

容易发现,在出现 \(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)\)

可以参考我博客中的一些线段树的总结,会不定期更新。

从ZKW线段树看线段树的性质

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include<bits/stdc++.h>
#define y1 y3647
#define int long long
#define pii pair<int,int>
#define low(x) ((x)&-(x))
using namespace std;
template<typename _type>
inline void read(_type &x){
x=0;int f=1;char ch=getchar();
while(ch!=45&&(ch>'9'||ch<'0'))ch=getchar();
if(ch==45){f=-1,ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-48;ch=getchar();}x*=f;
}
template<typename _type1,typename _type2>void cmin(_type1 &a,const _type2 b){if(a>b)a=b;}
template<typename _type1,typename _type2>void cmax(_type1 &a,const _type2 b){if(a<b)a=b;}
int i,j,k,n,s,t,m,tp1,tp2;
int solve(int x){
if(x==0)return 0;
if(x==1)return 1;
//特判边界情况
int res=1,h=log2(x);
//最高位从 10 开始枚举,所以 res 初值应该设置为 1
//h 为二进制下 x 的位数
for(i=1;i<h;i++){
res^=(1ll<<i+1)-1;
res^=(1ll<<i+1)+1;
//注意左移操作的 1 是 long long 级别的数
//计算 2^k 和 2^k+1 的答案,其它位置没有贡献
}
res^=(1ll<<h+1)-1;
//计算 2^h 的答案
if(low(x)==x)return res;
//如果 x=2^h,无需进行下面的步骤
res^=(1ll<<h+1)+1;
if(x-1==1ll<<h||x&1)return res;
//如果 x=2^h+1 或者末尾为 1,那么之后取到的值个数一定是偶数,无需计算。
int now=1;
while(__builtin_popcountll(x)>2||(x&1)==0){
now=now<<1|1;
x>>=1;
}
//模拟求解的第一步,找到对应的次高位,在这之前往后写 1
while(x){
x>>=1;
now<<=1;
}
//模拟求解第二步,往后写 0
now|=1;
//记得左右还需要异或 1
return res^now;
}
signed main()
{
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
// freopen(".in","w",stdout);
int l,r;
read(l),read(r);
printf("%lld\n",solve(r)^solve(l-1));
return 0;
}