题目描述
一个正整数 X
被称为 好整数,当且仅当它满足以下条件:
- 存在一对正整数
(a, b)
使得X = 2^a * b^2
。
例如,400
是一个好整数,因为 400 = 2^2 * 10^2
(这里 a=2, b=10)。
另一个例子,18 = 2^1 * 3^2
(这里 a=1, b=3)。
72 = 2^3 * 3^2
(这里 a=3, b=3)。72
也可以写成 2^1 * 6^2
(这里 a=1, b=6),只需要存在 至少一对 (a, b)
即可。
给定一个正整数 N
,找出在 1
到 N
(包含两端)之间有多少个好整数。
样例
输入:
20
输出:
5
说明:在 1 到 20 之间有 5 个好整数:
2 = 2^1 * 1^2
(a=1, b=1)
4 = 2^2 * 1^2
(a=2, b=1)
8 = 2^3 * 1^2
(a=3, b=1)
16 = 2^4 * 1^2
(a=4, b=1)
* 18 = 2^1 * 3^2
(a=1, b=3)
输入:
400
输出:
24
输入:
1234567890
输出:
42413
注意:输入 N
可能很大,需要使用 64 位整数类型。
算法 (枚举 + 数学)
O(logN×logN) 或 O(logN) 取决于求平方根的复杂度
思路分析
-
理解好整数的结构:
一个数X
是好整数,意味着X = 2^a * b^2
,其中a >= 1
且b >= 1
。 -
唯一表示:
直接统计满足条件的(a, b)
对可能会重复计算同一个X
。例如72 = 2^1 * 6^2 = 2^3 * 3^2
。我们需要找到一种方法来唯一地表示或生成每一个好整数X
。
考虑X
的质因数分解。如果X
是好整数,X = 2^a * b^2
。设b
的质因数分解为b = 2^k * m
,其中m
是b
的奇数部分(k >= 0
,m >= 1
且m
为奇数)。
那么X = 2^a * (2^k * m)^2 = 2^a * 2^{2k} * m^2 = 2^{a+2k} * m^2
。
令A = a + 2k
。因为a >= 1
且k >= 0
,所以A >= 1
。
令B = m
。因为b >= 1
,所以m >= 1
,且m
是奇数。
所以,任何一个好整数X
都可以表示为X = 2^A * B^2
的形式,其中A >= 1
,B >= 1
且B
是 奇数。
这种表示是唯一的吗?假设X = 2^A * B^2 = 2^C * D^2
,其中A, C >= 1
且B, D >= 1
都是奇数。根据算术基本定理(唯一质因数分解),比较等式两边的质因数分解,奇数部分的平方必须相等,即B^2 = D^2
,因为B, D
都是正数,所以B = D
。同样,2 的幂次也必须相等,即A = C
。
因此,每个好整数X
都可以 唯一地 表示为X = 2^A * B^2
,其中A >= 1
,B >= 1
且B
为奇数。 -
计数策略:
我们的目标是计算满足1 <= X <= N
的好整数X
的数量。这等价于计算满足以下条件的 唯一 对(A, B)
的数量:A >= 1
B >= 1
且B
是奇数2^A * B^2 <= N
-
迭代变量选择:
直接计算满足条件的(A, B)
对仍然有些困难。我们可以尝试固定一个变量,然后计算另一个变量的可能性。- 固定 B (奇数): 对于一个固定的奇数
B >= 1
,我们需要2^A <= N / B^2
且A >= 1
。设K = N / B^2
。我们需要2^A <= K
且A >= 1
。满足条件的A
的个数是max(0, floor(log2(K)))
。我们需要对所有奇数B
求和。B
的范围是多少?因为2^A >= 2^1 = 2
,所以2 * B^2 <= N
,即B^2 <= N/2
,B <= sqrt(N/2)
。迭代B
到sqrt(N)
级别(最高10^9
)还是太慢。 - 固定 A: 对于一个固定的
A >= 1
,我们需要B^2 <= N / 2^A
,B >= 1
且B
是奇数。设K = N / 2^A
。我们需要B^2 <= K
,B >= 1
且B
是奇数。- 首先,
B
的最大可能值是m = floor(sqrt(K))
。 - 我们需要计算在
[1, m]
这个范围内有多少个奇数B
。 - 奇数序列是
1, 3, 5, ...
。第k
个奇数是2k-1
。 - 我们需要
2k-1 <= m
,即2k <= m+1
,所以k <= (m+1)/2
。 - 因此,
[1, m]
中奇数的个数是floor((m+1)/2)
。在 C++ 中,使用整数除法(m + 1) / 2
即可得到这个结果。 - 我们需要对所有可能的
A
值求和。A
的范围是多少?因为B^2 >= 1^2 = 1
,所以2^A * 1 <= N
,即2^A <= N
。所以A
的最大值是floor(log2(N))
。 - 由于
N <= 10^18
,log2(N)
大约是 60。所以A
的取值范围很小,从 1 到 大约 60。 - 这个方法可行!
- 首先,
- 固定 B (奇数): 对于一个固定的奇数
-
算法流程:
- 初始化总数
sum = 0
。 - 循环变量
A
从 1 开始。 - 在循环中,计算
pow2 = 2^A
。注意使用 64 位整数 (long long
) 防止溢出。1LL << A
是计算2^A
的常用方法。 - 检查
pow2
是否已经大于N
。或者更稳健地,计算k = N / pow2
(整数除法)。如果k == 0
,说明2^A > N
,对于当前及更大的A
,不可能找到满足条件的B
,可以提前结束循环。 - 计算
m = floor(sqrt(k))
。我们需要一个高效的整数平方根函数。二分查找是标准方法。floor_sqrt(k)
函数实现的就是这个。它找到最大的整数l
使得l * l <= k
。 - 计算
[1, m]
范围内的奇数个数:num_odd_b = (m + 1) / 2
。 - 将
num_odd_b
加到sum
上。 - 增加
A
,继续循环,直到k
变为 0。 - 循环结束后,
sum
就是答案。
- 初始化总数
-
整数平方根 (
floor_sqrt
):
函数floor_sqrt(k)
使用二分查找来计算floor(sqrt(k))
。- 设置查找范围
[l, r]
,初始为[0, 1e9 + 1]
(因为k <= N <= 10^18
,sqrt(k) <= 10^9
,所以10^9 + 1
是一个安全的上界)。 - 当
l < r
时,计算中间值mid = l + (r - l + 1) / 2
(这种写法防止l+r
溢出,并且倾向于取右中位数,配合l=mid
更新,避免死循环)。 - 检查
mid * mid <= k
是否成立。同样需要注意mid * mid
可能溢出long long
。更安全的方法是检查mid <= k / mid
(需要确保mid > 0
,不过在我们的二分实现中mid
总是非负,如果mid=0
不等式也成立)。 - 如果
mid <= k / mid
(即mid^2 <= k
),说明mid
可能就是答案,或者答案比mid
更大,所以更新下界l = mid
。 - 否则(即
mid^2 > k
),说明mid
太大了,更新上界r = mid - 1
。 - 循环结束时(
l == r
),l
就是最大的满足l*l <= k
的整数。
- 设置查找范围
时间复杂度
- 外层循环的次数取决于
A
的范围,最多约为log2(N)
次,大约 60 次。 - 循环内部的主要操作是计算
pow2
(O(1) 位运算),整数除法(O(1)),和整数平方根floor_sqrt
。 floor_sqrt
使用二分查找,其范围最大是10^9
,所以需要log(10^9)
大约 30 次迭代。每次迭代是 O(1) 的计算。- 因此,总时间复杂度大约是
O(log N * log(sqrt(N))) = O((log N)^2)
。如果认为log(sqrt(N))
相对log N
是常数级的(因为它们只差一个常数因子 1/2),也可以认为是O(log N)
次高精度运算。在这个问题规模下,60 * 30
次操作是非常快的。
C++ 代码
#include <bits/stdc++.h> // 引入所有标准库
using namespace std;
using i64 = long long; // 定义 i64 为 long long 的别名
// 计算 floor(sqrt(k)),即小于等于 sqrt(k) 的最大整数
// 使用二分查找实现
i64 floor_sqrt(i64 k) {
i64 l = 0, r = 1e9 + 1; // 初始化二分查找的左右边界 [l, r]
// sqrt(10^18) = 10^9, 所以 10^9 + 1 是安全的上界
while (l < r) {
// 计算中间值 mid,倾向于取右中位数,避免 l = mid 时死循环
i64 mid = l + (r - l + 1) / 2;
// 检查 mid*mid <= k 是否成立
// 为了防止 mid*mid 溢出 i64,使用除法判断: mid <= k / mid
// 需要注意 mid=0 的情况,但这里 mid >= l >= 0,且 mid=0 时 mid <= k/mid 总成立
if (mid <= k / mid) {
// 如果 mid*mid <= k,说明 mid 可能是答案,或者答案更大
// 所以将下界更新为 mid
l = mid;
} else {
// 如果 mid*mid > k,说明 mid 太大了
// 将上界更新为 mid - 1
r = mid - 1;
}
}
// 循环结束时 l == r,l 就是 floor(sqrt(k))
return l;
}
int main() {
// 关闭 C++ 标准 IO 流与 C stdio 的同步,提高 cin/cout 速度
ios::sync_with_stdio(false);
// 解除 cin 和 cout 的绑定,进一步提速
cin.tie(nullptr);
i64 n; // 输入 N (可能高达 10^18)
cin >> n;
i64 sum = 0; // 用于累计好整数的数量
// 循环遍历唯一表示 X = 2^A * B^2 中的指数 A
// A 从 1 开始。A 最大约为 60 (log2(10^18))
for (int i = 1; i <= 60; i ++ ) { // 循环到 60 足够了
// 计算 2^A (即 2^i),使用 1LL 确保是 long long 类型
i64 pow2 = (1LL << i);
// 如果 pow2 已经大于 n,或者 n / pow2 < 1 (即 pow2 > n),则后续的 i 更大,
// 对应的 B^2 必须小于 1,不可能存在 B >= 1。
// 所以可以提前结束循环。
// 计算 k = N / (2^A)
i64 k = n / pow2;
if (k == 0) { // 等价于 pow2 > n
break;
}
// 我们需要找到满足 B^2 <= k 且 B 为奇数的 B 的个数
// 先找到满足 B^2 <= k 的最大 B,即 m = floor(sqrt(k))
i64 m = floor_sqrt(k);
// 计算 [1, m] 中有多少个奇数
// 奇数个数为 floor((m+1)/2),用整数除法计算 (m + 1) / 2
i64 num = (m + 1) / 2;
// 将找到的奇数 B 的个数累加到总数 sum
sum += num;
}
// 输出最终结果
cout << sum << "\n";
return 0; // 程序正常结束
}