P6025题解
CDFLS_mao_zx · · 题解
P6025 题解
upd 23.2.1:修好了链接。
提供一个复杂度为的
这道题相当不错,考察了线段树和位运算的理解。
题意
-
一颗常规方式构建的线段树,求大小为
1,2,3,\cdots n 的线段树分别占用的最大空间,即最大下标。 -
输出答案的异或和。
-
n\leq 10^{15}
构建代码为:
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]
}
原题意是求
分析
40PTS
首先,线段树建立的过程,就是从根往下走的过程,往左走,向当前编号的二进制表示后写一个
考虑线段树的最大下标在何处取到,由于右儿子下标相对较大,一个比较显然的想法是一直走右儿子,但是这样是错误的,显然的反例为
这个例子中,
但是,如果某个节点处左右儿子高度相同,那么很显然应该走右儿子,因为当前这一步决定的位数是最高位。
所以走左儿子,当且仅当左右儿子高度不同,下面分析线段树的高度和长度的关系。
首先,子树构建情况只和长度有关,所以我们只关心长度而非左右端点,然后,长度为
容易发现,常规构建方式构建出的子树,其左儿子的大小不小于右儿子,且差值至多为 1,因此,如果出现了左右儿子高度不同,一定是左儿子比右儿子高,并且只能是一种情况,即长度的二进制表示为
所以我们对于一个
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() 为内置函数,参数为 unsigned, int 也可以使用但不能为负数。
如果希望对 long long 使用,应改为 _builtin_popcountll()。
对每个数暴力求解即可。
100PTS
我们需要观察求解一个数的过程,不妨考虑二进制数
手动模拟一下上述
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
容易发现,在出现
在出现
而最后
不难发现,一个固定的
注意如果为
所以我们有了一个比较显然的想法,即枚举二进制下最高位和次高位的位置,统计能取到这两个位置的数的数量,并异或上对应的权值。
这样的复杂度是
对于二进制下位数小于
这样就只用考虑二进制下位数和
总复杂度
可以参考我博客中的一些线段树的总结,会不定期更新。
从ZKW线段树看线段树的性质
参考代码
#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;
}