【题解】CF1151C Problem for Nazar(二分答案)

【题解】CF1151C Problem for Nazar

距离 CSP 剩下 10 天了,据说考前写题解可以增加 RP所以我来写一篇题解+水点贡献分

看题解区没有用二分答案来解决这道题的,我来提供一个二分做法。


题目链接

CF1151C Problem for Nazar

题意概述

设正奇数集合为 A\mathrm{A},正偶数集合为 B\mathrm{B},这两个集合是无限集。

在黑板上写了无数轮数,第 ii 轮写下了 2(i1)2^{(i-1)} 个数.

ii为奇数时,从集合 A\mathrm{A} 中向后取数,当 ii 为偶数时,从集合 B\mathrm{B} 中向后取数。

求黑板上第 ll 个数到第 rr 个数的和,模 1000000007\mathrm{1000000007}109+710^9+7

数据范围

1l,r10181 \le l,r \le 10^{18}

思路分析

首先看到这个数据范围,可以考虑到时间复杂度大概率是 logn\log n 这个级别的。

求的是下标为区间 [l,r][l,r] 的和,那我们很自然联想到差分:可以先把区间 [1,r][1,r] 的和求出来减去区间 [1,l1][1,l-1] 的和。

那么问题就转化为怎么求出前 xx 个数的和。

观察题目条件可以发现,黑板上的数无非就是一串奇数+一串偶数+一串奇数……这样排列的。

相邻的两串奇数之间一定是连续的,也就是假如这一串奇数的结尾是 kk,那么与它相邻的下一串奇数的第一个数一定是 k+2k+2

相邻的两串偶数之间也同理。

注意:这里的相邻的两串奇数与相邻的两串数含义不同,每相邻的两串奇数之间隔着一串偶数。

那么我们能否把前 xx 个数的和转化为所有的奇数串之和加上所有的偶数串之和呢?

答案是肯定的。

考虑最终形成的序列一定是这样的:

202^0 个奇数+212^1 个偶数+222^2 个奇数+232^3 个偶数+ \cdots +2(i1)2^{(i-1)} 个不知道是奇数还是偶数的一串数+某个数 kkxx

很显然第 ii 串数的奇偶性取决于 ii 的奇偶性,并与 ii 的奇偶性相同。

这里的 kk 是第 i1i-1 串数的最后一个数 +2。

那么我们其实可以考虑把前 ii 串数的和求出来,然后再单独把从 kkxx 的和求出来然后相加。

那么我们首先要求出这个 ii

因为前 ii 个串的长度和为 20+21+22++2(i1)=2i12^0+2^1+2^2+\cdots+2^{(i-1)}=2^i-1,所以我们可以对于每个 xx 直接二分求出这个 ii 是多少。

具体代码如下:

int now=0,ret=0;
for(int step=(1ll<<60);step>=1;step>>=1)
{
	if(now+step<=60&&(1ll<<(now+step))-1<x)now+=step;
}

(这里采用的是倍增二分)

最后得到的这个 nownow 就是我们所说的 ii

由于前面已经提到过,相邻的两串奇数/偶数之间都是连续的。

那我们可以直接将前 ii 串数的和分成奇数列和偶数列来求。

显然,对于前 ii 串数,有 i/2i/2 串偶数列,有 ii/2i-i/2 串是奇数列。

那么我们可以预处理出来奇数列和偶数列单独的和。

sum1isum1_i 表示整个序列中前 ii 串奇数列中有多少个数,sum2isum2_i 表示整个序列中前 ii 串偶数列中有多少个数,预处理如下:

void init()
{
	int cnt1=0,cnt2=0;
	for(int i=1;i<=60;i++)//之所以是 60,因为 2^60-1 已经大于 1e18 了,所以最多有 60 串数。
	{
		if(i&1)
		{
			cnt1++;
			sum1[cnt1]=sum1[cnt1-1]+(1ll<<(i-1));
		}
		else
		{
			cnt2++;
			sum2[cnt2]=sum2[cnt2-1]+(1ll<<(i-1));
		}
	}
}

那么对于前 ii 串数,有 sum2[i/2]sum2[i/2] 个偶数,sum1[ii/2]sum1[i-i/2] 个奇数。

接下来要利用一些等差数列的知识:

  • 等差数列第 nn 项为 a1+nda_1+nda1a_1 表示首项,dd 表示公差)。

  • 等差数列前 nn 项的和为 (a1+an)n2\frac{(a_1+a_n)*n}{2}

那么第 sum2[i/2]sum2[i/2] 个偶数就为 sum2[i/2]×2sum2[i/2] \times 2,第 sum1[ii/2]sum1[i-i/2] 个奇数就为 sum1[ii/2]×21sum1[i-i/2] \times 2-1

所以前 ii 串数中偶数列之和为 s1=(2+sum2[i/2]×2)×sum2[i/2]2s_1=\frac{(2+sum2[i/2]\times 2) \times sum2[i/2]}{2},奇数列之和为 s2=(1+sum1[ii/2]21)×sum1[ii/2]2s2=\frac{(1+sum1[i-i/2]*2-1) \times sum1[i-i/2]}{2}

然后对于最后 kkxx 的那一段和,可以先把 kk 求出来:当 ii 为奇数时,k=sum2[i/2]×2+2k=sum2[i/2]\times 2+2,当 ii 为偶数时,k=sum1[ii/2]×2+2k=sum1[i-i/2] \times 2+2。然后再把 xx 求出来:当 ii 为奇数时,k=sum2[i/2]×2+(x2i+1)×2k=sum2[i/2] \times 2+(x-2^i+1)\times 2,当 ii 为偶数时,k=sum1[ii/2]×2+(x2i+1)×2k=sum1[i-i/2] \times 2+(x-2^i+1)\times 2

然后从 kkxx 也可以直接等差数列求和,设结果为 ss

那么最后答案就是 s1+s2+ss1+s2+s

易错点

别忘了及时取模以及负数保护!!!防止爆 long long!我在这挂了 2h……老毛病又犯了。。

代码实现

//CF1151C
#include<cstdio>
#include<iostream>
#define int long long
//#define debug
using namespace std;
const int maxn=105;
const int mod=1e9+7;
int sum1[maxn],sum2[maxn],n;

inline int read()
{
	int x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=x*10+ch-48;ch=getchar();}
	return x*f;
}

void init()
{
	int cnt1=0,cnt2=0;
	for(int i=1;i<=60;i++)
	{
		if(i&1)
		{
			cnt1++;
			sum1[cnt1]=sum1[cnt1-1]+(1ll<<(i-1));
		}
		else
		{
			cnt2++;
			sum2[cnt2]=sum2[cnt2-1]+(1ll<<(i-1));
		}
	}
}

int work(int x)
{
	int now=0,ret=0;
	for(int step=(1ll<<60);step>=1;step>>=1)
	{
		if(now+step<=60&&(1ll<<(now+step))-1<x)now+=step;
	}
	int L=now;
	int re=x-(1ll<<L)+1;
	int tt=L>>1;
	int num=sum2[tt];
	int tot1=num*2;
	int s1=(2+tot1)/2%mod*(num%mod)%mod;
	int kk=L-tt;
	num=sum1[kk];
	int tot2=num*2-1;
	int s2=(1+tot2)/2%mod*(num%mod)%mod;
	if(L&1)
	{
		(ret=(s1+s2)%mod+(tot1+2+tot1+re*2)/2%mod*(re%mod)%mod)%=mod;
	}
	else
	{
		(ret=(s1+s2)%mod+(tot2+2+tot2+re*2)/2%mod*(re%mod)%mod)%=mod;
	}
	return ret;
}

signed main()
{
	init();
	int l,r;
	l=read();r=read();
	cout<<(work(r)-work(l-1)+mod)%mod<<'\n';
	return 0;
}
posted @ 2022-10-18 15:22  向日葵Reta  阅读(30)  评论(0编辑  收藏  举报