西格玛与罗比特(PTA)的一种思路分析(初稿)

题目为:

给定一个整数n,计算\sum_{i=1}^{n}{i\cdot \text{lowbit}\left(i \right )},其中\text{lowbit}\left(i \right )为i的最低位1所在的位置(注意和常规的lowbit定义不同),例如\text{lowbit}\left(4\right )=3,因为4=\left ( 100 \right )_2​​。

输入格式:

第一行一个数 T 表示 T 组测试数据。

接下来有 T 行,每行一个 n 。

(1≤T≤100000,1≤n≤10^{18}​​)

输出格式:

输出 T 行,每行一个数,第 i 行的数表示第 i 组测试的答案,由于答案可能很大,请将答案对 10^9+7取模。

输入样例:

5

1

2

3

5

输出样例:

1

5

8

20

25

分析:

在一般的解题过程,比如比赛或日常训练赛中,这种题型大概率是先枚举找规律。但是作者把这道题划分在数论范围内,让我得以思考,为什么作者要塞进数论中。

事实上,如果按照一般找规律的思路,我们没有必要非常深入地去分析规律是什么,我们只需要找到部分规律以至于可以把题在有限时间和空间条件下解出即可。

同时事实上,这个确实有一个类似数论的性质,但既不是模性质也不是什么整数定理,而是二进制模式本身具备的性质。我单纯着重分析这部分的内容。

注意到,\text{lowbit}\left(2^k\right )=k+1,而且\text{lowbit}\left(n\right )序列服从\text{lowbit}\{2 n\}=\text{lowbit}\{n\}\#\#\text{lowbit}\{n-1\}\#\#\log_2{n}+2其中\#\#表示数组连接符,这个关系的意思是lowbit序列的前2n项为lowbit的前n项复制n-1项到后面,并再添加一个\log_2{n}+2元素到最末尾。前面一个性质只是也许会体现出可以将n进行二进制展开而利用的结果,而后面一个性质则揭示可以进行二进制展开的属性。

但真正要表现这个属性的前提需要进行求和,但是求和对于原式有什么作用呢?这里不妨先对所需计算求和表达式进行一种整理,这种整理的技术叫做Abel变换(错位相减的扩展版本):

\sum_{i=1}^n{a_i b_i}=a_{n+1}B_n-\sum_{i=1}^n{\Delta a_i B_i}

其中,B_i=\sum _{k=1}^i{b_k}\Delta a_i = a_{i+1}-a_i,小写改大写一般表示序列的前n项和,而三角表示差分符。这个公式是Abel变换的特殊形式,而更复杂的形式或者一般形式不打算展开,因为可以通过该结论做差完成。

注意,和计算机的顺序不一样的是,该公式针对于数列(即高中所学过的从1开始的序列)

有了该公式后,先分析题目,可以得到:

\sum_{i=1}^{n}{i\cdot \text{lowbit}\left(i \right )}=\left(n+1\right)\sum _{i=1}^n{\text{lowbit}\left(i \right )}-\sum _{i=1}^n{\sum _{k=1}^i{\text{lowbit}\left(k \right )}}

这意味着需要先计算\sum _{i=1}^n{\text{lowbit}\left(i \right )}

这里就可以考虑利用\text{lowbit}\{2 n\}=\text{lowbit}\{n\}\#\#\text{lowbit}\{n-1\}\#\#\log_2{n}+2的性质,假设需要求\text{sum}\[\text{lowbit}\{2 n\}\](这个表示整个序列的和),那么实际上就是在求

\text{sum}\[\text{lowbit}\{n\}\#\#\text{lowbit}\{n-1\}\#\#\log_2{n}+2\] =\text{sum}\[\text{lowbit}\{n\}\]+\text{sum}\[\text{lowbit}\{n-1\}\]+\log_2{n}+2

可以考虑补充一个数以至于有2\times \text{sum}\[\text{lowbit}\{n\}\]+\log_2{n}+2-\log_2{n}-1

注意到这里可以化解到\text{sum}\[\text{lowbit}\{2 n\}\]=2\times \text{sum}\[\text{lowbit}\{n\}\]+1这么简单的形式。

这就意味着\text{sum}\[\text{lowbit}\{2^n\}\]=2^{n+1}-1了。

而对于\text{lowbit}\{2 n\}=\text{lowbit}\{n\}\#\#\text{lowbit}\{n-1\}\#\#\log_2{n}+2

这个表达式的构造非常重要,因为在不满足2^n项的lowbit序列中,其中大于2^{n-1}的部分项都是前面的复制!这意味着对其求和等于两部分的和,而一部分的和是一个子问题!即:

\text{sum}\[\text{lowbit}\{2^n+a\}\]=\text{sum}\[\text{lowbit}\{2^n\}\]+\text{sum}\[\text{lowbit}\{a\}\]0\leq a< 2^n,也意味着sum[lowbit{0}]=0)

这个性质十分重要,进一步揭示lowbit这种形式可以考虑直接对n进行二进制展开再计算,而每个计算的主要部分都只是\text{sum}\[\text{lowbit}\{2^n\}\],意味着对其求和的原O\left(N\right)计算化为O\left(\log_2{N}\right)的计算量

这个性质我简单阐述为二进制分解定理。这个定理的需求就是存在这种\text{lowbit}\{2 n\}=\text{lowbit}\{n\}\#\#\text{lowbit}\{n-1\}\#\#\log_2{n}+2复制性的条件,其实后面还可以看见不只是简单的复制,有规律的变换都可以利用变换的规律把O\left(N\right)计算化为O\left(\log_2{N}\right)的计算量。

然后观察n二进制展开并于位对应的\text{sum}\[\text{lowbit}\{2^n\}\]=2^{n+1}-1相加就有\text{sum}\[\text{lowbit}\{N\}\]=2^{n_1+1}-1+...+2^{n_m+1}-1=2n-Countbits(n)

即任何的前n项lowbit相加只为两倍n减去n中二进制为1的个数,这是由于2^{n_1}+...+2^{n_m}=N\Rightarrow 2^{n_1+1}+...+2^{n_m+1}=2N以及-1个数等于二进制为1的个数。

所以又有

\sum_{i=1}^{n}{i\cdot \text{lowbit}\left(i \right )}=\left(n+1\right)\left(2n-Countbits(n)\right)-\sum _{i=1}^n{2i-Countbits(i)}=\left(n+1\right)\left(2n-Countbits(n)\right)-n\left(n+1\right)+\sum _{i=1}^n{Countbits(i)}

可进一步化成\left(n+1\right)\left(n-Countbits(n)\right)+\sum _{i=1}^n{Countbits(i)}

而观察Countbits(n)的性质,它也有类似的二进制分解性:Countbits\{2^{n+1}\}=Countbits\{2^{n}\}\#\#Countbits\{2^{n}-1\}+1\#\#1

注意,每个花括号的表示前N项的项数,而对其运算则表示全部的元素都进行一个这样的运算,比如这里加1是表示所有项都加1。

这里不同于lowbit的是,这里不是直接复制,是之前的元素经过一个同样规则的运算之后再得到的。

考虑求和:

\sum Countbits\{2^{n+1}\}=\sum Countbits\{2^{n}\}+\sum Countbits\{2^{n}-1\}+2^n

补项有:

\sum Countbits\{2^{n+1}\}=2\sum Countbits\{2^{n}\}+2^n-1

这由于有2^n的项,无法很简单地计算出该结果,但是可以通过动态规划的思路,直接计算并储存!

unsigned long long _scb2k[65] = {1, 2};
void scb2k_init() {
    for (int i = 1; i < 64; ++i) {
        _scb2k[i + 1] = ((_scb2k[i] << 1) + (1ull << i) - 1) % mod;
    }
}

根据二进制分解定理,当具备这种分解性时,只需要用二进制展开取位置为1的项就可以用于计算:

\sum Countbits\{2^{n}+a\}=\sum Countbits\{2^{n}\}+\sum Countbits\{a\}+a

这是由于大于2^n,且小于2^{n+1}的项为前面的值加一。故剩余量a是指剩余长度,而加一的量就是指相应的长度量。

因此,\sum Countbits\{n\}的代码就可以表示为:(数学结果和计算机有位差,会有\pm 1的情况,很正常)

unsigned long long scb(unsigned long long n) {
    unsigned long long pow_iter = 0x8000000000000000ull;
    int pow_index = 64;
    unsigned long long ret = 0;
    while (pow_index > 0) {
        if (n & pow_iter) {
            ret += (_scb2k[pow_index - 1] + (n -= pow_iter)) % mod;
        }
        if (!n) break;
        pow_iter >>= 1;
        --pow_index;
    }
    return ret;
}

其实还可以顺着从最小位开始,具体代码请读者自行操作了。

而注意到,最终的计算结果还差一个关于countbits的计算,这里用一个非常著名的O\left(\log_2\log_2N\right)的算法:

int count_bits(unsigned long long x) {
    x = (x & 0x5555555555555555ull) +
        ((x & 0xaaaaaaaaaaaaaaaaull) >> 1);
    x = (x & 0x3333333333333333ull) +
        ((x & 0xccccccccccccccccull) >> 2);
    x = (x & 0x0f0f0f0f0f0f0f0full) +
        ((x & 0xf0f0f0f0f0f0f0f0ull) >> 4);
    x = (x & 0x00ff00ff00ff00ffull) +
        ((x & 0xff00ff00ff00ff00ull) >> 8);
    x = (x & 0x0000ffff0000ffffull) +
        ((x & 0xffff0000ffff0000ull) >> 16);
    x = (x & 0x00000000ffffffffull) +
        ((x & 0xffffffff00000000ull) >> 32);
    return x;
}

因此最终的计算结果就可以表示为:整个计算过程只有O\left(\log_2{N}\right)的复杂度,会比for历遍好不少。这都是由于lowbit及其和的具备的二进制分解性的功劳。

unsigned long long func(unsigned long long n) {
    __uint128_t k = n;
    return ((k + 1) * (k - count_bits(n)) + scb(n)) % mod;
}

用128位是为了不想写一堆容易错的内部mod,这只是从理论上进行一定程度的优化,虽然说是为了解这道题,但是主要是分析出一种二进制分解性的东西,具体可能有很多方面的应用,这道题本身可能还有更微妙的计算方法,具体不做展开。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值