KM and M - 题解
标签与难度
标签: 数学, 数论, 类欧几里得算法, 位运算, 模运算, __int128 难度: 2300
题目大意喵~
主人你好呀!这道题是让我们计算一个看起来有点复杂的求和式,喵~
具体来说,给定两个正整数 和 ,我们需要计算下面这个式子的值:
其中 & 代表按位与(Bitwise AND)运算。最后的结果需要对 取模。
和 的值可能会非常大,所以直接从 1 到 循环计算肯定会超时哦,我们需要找到更聪明的办法,呐!
解题思路分析
看到这个题目,一个直接的想法就是写一个循环,从 到 ,一步步累加 (k * M) & M 的值。但是 可以很大很大(比如到 ),这样的 暴力解法肯定是不行的啦,会等到猫咪的胡子都白了也算不完的,喵~
既然有位运算 &,这通常是一个强烈的信号,提示我们可以把问题拆解到二进制的每一个位上进行考虑!
我们要求的和是 。 一个数 可以表示为它所有二进制位的权值之和,也就是 ,其中 是 在二进制表示下第 位的值(0 或 1)。
那么我们的总和 也可以这样拆分:
S = \sum_{k=1}^{N} \left( \sum_{i=0}^{\text{max_bits}} \text{bit}_i((kM) \& M) \cdot 2^i \right)
交换一下求和的顺序,对我们分析问题更有利哦:
S = \sum_{i=0}^{\text{max_bits}} 2^i \cdot \left( \sum_{k=1}^{N} \text{bit}_i((kM) \& M) \right)
现在,我们来分析一下里面的 。 根据按位与的性质, 的第 位为 1,当且仅当 的第 位为 1 并且 的第 位为 1。
- 如果 的第 位是 0,那么无论 的第 位是什么, 的第 位永远是 0。这一位对总和的贡献就是 0。
- 如果 的第 位是 1,那么 的第 位就完全取决于 的第 位。此时,。
所以,我们只需要对那些 的第 位为 1 的位进行计算!问题就转化为了,对于每个使得 的 ,计算出 的值,然后乘以 累加到最终答案里。
那么,如何高效地计算 呢? 这里有一个非常关键的小技巧,喵~ 一个数 的第 位的值,可以用下取整函数来表示:
这个公式的原理是, 算的是 中包含的 的倍数的数量,而 算的是 中包含的 的倍数的数量(乘以2)。两者相减,正好就分离出了第 位的系数,也就是 0 或 1。就像猫咪从一堆毛线球里,只挑出特定颜色的那一颗,喵~
把它代入我们的求和式中:
看呐!问题现在变成了如何计算形如 的和式。在这里, 或 。
锵锵~ 这时候就要请出我们的秘密武器——类欧几里得算法 (Floor Sum Algorithm) 啦!它专门解决这种带下取整的求和问题,非常厉害的说!
一个计算 的类欧几里得函数 floor_sum(a, b, c, n) 通常通过递归实现:
- 基本情况: 如果 ,那么 是个常数,总和就是 。
- 规约: 如果 或 ,我们可以把 对 取模来简化问题。
求和后,后两项是等差数列求和和常数求和,可以 计算,问题规模被减小。
- 递归转换: 如果 且 ,这是最核心的一步。令 ,可以证明(证明过程比较复杂,涉及到几何计数),原式可以转换为一个与原问题结构相同,但参数互换的新问题:
这样 的角色互换,就像欧几里得算法一样,参数规模会不断减小,最终达到基本情况。
我们的求和是从 到 的,而类欧模板通常是从 到 。不过没关系,因为当 时,,所以 。我们可以直接调用 floor_sum(M, 0, c, N)。
总结一下我们的完整步骤:
- 初始化总答案
ans = 0。 - 遍历二进制位 从 0 到 60 左右(因为 在
long long范围内)。 - 检查 的第 位是否为 1。
- 如果是 1,就利用类欧几里得算法计算:
- 第 位的贡献次数为 。
- 将 累加到
ans中,注意全程取模。 - 最后输出
ans就好啦!
注意哦,在类欧几里得算法的计算过程中,像 a * n 这样的乘积可能会超过 long long 的范围,所以我们需要使用 __int128 来保证计算的精度,喵~
代码实现
这是我根据上面的思路,精心重构的一份代码哦,希望能帮助到你,喵~
#include <iostream>
// 定义一些常量和类型别名,让代码更清晰~
using int128 = __int128;
using ll = long long;
const int MOD = 1e9 + 7;
// 快速幂函数,用来计算模逆元
ll power(ll base, ll exp) {
ll res = 1;
base %= MOD;
while (exp > 0) {
if (exp % 2 == 1) res = (int128)res * base % MOD;
base = (int128)base * base % MOD;
exp /= 2;
}
return res;
}
// 计算 a 在模 MOD 下的逆元
ll modInverse(ll n) {
return power(n, MOD - 2);
}
const ll INV2 = modInverse(2);
// 类欧几里得算法核心函数,喵~
// 计算 sum_{k=0 to n} floor((a*k + b) / c)
ll floor_sum(ll a, ll b, ll c, ll n) {
if (a == 0) {
// a=0, 项为常数 floor(b/c)
return (n + 1) % MOD * (b / c % MOD) % MOD;
}
if (a >= c || b >= c) {
// 规约 a 和 b
ll a_mod_c = a % c;
ll b_mod_c = b % c;
ll a_div_c = a / c;
ll b_div_c = b / c;
// 递归计算简化后的部分
ll res = floor_sum(a_mod_c, b_mod_c, c, n);
// 加上被分离出来的部分
// 等差数列求和: sum_{k=0 to n} k = n(n+1)/2
ll n_mod = n % MOD;
ll sum_k_term = (a_div_c % MOD) * (n_mod * (n_mod + 1) % MOD) % MOD * INV2 % MOD;
// 常数项求和: sum_{k=0 to n} 1 = n+1
ll const_term = (b_div_c % MOD) * ((n + 1) % MOD) % MOD;
res = (res + sum_k_term) % MOD;
res = (res + const_term) % MOD;
return res;
}
// 核心递归转换步骤
int128 m_128 = (int128)a * n + b;
ll m = m_128 / c;
ll res = (n % MOD) * (m % MOD) % MOD;
ll recursive_part = floor_sum(c, c - b - 1, a, m - 1);
// 注意减法可能产生负数,要加上 MOD
return (res - recursive_part + MOD) % MOD;
}
int main() {
// 为了更快的输入输出,喵~
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
ll n, m;
std::cin >> n >> m;
ll total_sum = 0;
// 遍历M的二进制位,从第0位到第60位
for (int i = 0; i <= 60; ++i) {
// 检查M的第i位是否为1
if ((m >> i) & 1) {
int128 c1 = (int128)1 << i;
int128 c2 = (int128)1 << (i + 1);
// 计算 sum floor(k*m / 2^i)
ll sum1 = floor_sum(m, 0, c1, n);
// 计算 sum floor(k*m / 2^(i+1))
ll sum2 = floor_sum(m, 0, c2, n);
// 第i位的贡献次数 = sum1 - 2 * sum2
ll count = (sum1 - 2 * sum2 % MOD + 2 * MOD) % MOD;
// 第i位的总贡献值
ll bit_contribution = (count * (c1 % MOD)) % MOD;
total_sum = (total_sum + bit_contribution) % MOD;
}
}
std::cout << total_sum << std::endl;
return 0;
}复杂度分析
时间复杂度: 主循环遍历了大约 60 个二进制位,这是一个常数。在循环内部,我们调用了类欧几里得函数 floor_sum(a, b, c, n)。该函数的复杂度与欧几里得算法(求最大公约数)类似,递归深度为 。在我们的调用中, 或 。所以单次调用的复杂度是 。总时间复杂度是 。由于 的最大值是常数 60,并且我们通常认为 也和位数相关,所以整体复杂度可以近似看作是 ,在 为 long long 范围时,这非常快。
空间复杂度: 空间主要消耗在
floor_sum函数的递归调用栈上。递归的深度是 ,所以空间复杂度与时间复杂度中的对数部分成正比。
知识点总结
- 位运算分解: 遇到涉及位运算
&,|,^的求和问题,一个非常强大的思想是按位独立计算贡献。将整体的求和问题分解为对每一位的贡献求和。 - 提取特定位: 公式 是一个从数值中分离出特定二进制位的精妙技巧,值得牢记于心!
- 类欧几里得算法: 这是解决一类特定形式的求和问题的模板算法,即 。它的核心思想是通过类似欧几里得算法的递归和规约,不断减小问题规模。
__int128的使用: 在处理可能超过long long()范围的整数运算时,__int128是一个非常有用的工具,可以避免溢出导致的错误。喵~ 记得要用__int128哦,不然数字太大会溢出,就像小猫咪的碗装不下太多小鱼干一样!
希望这篇题解能帮助你理解这道有趣的题目!继续加油哦,主人!喵~