GESP3级2026年3月份编程题超详解
·
针对“二进制回文串”问题,我们需要计算在区间 $[1, n]$ 内,有多少个正整数的二进制表示(不含前导零)是回文串。题目给定的数据范围是 $1 \leq n \leq 10^5$,这要求算法的时间复杂度应控制在 $O(n \log n)$ 或更优。
问题解构与算法分析
问题的核心在于判断一个正整数是否为二进制回文数。其标准流程为:
- 转换为二进制字符串:将整数 $x$ 转换为其二进制表示,并去除可能的前导零(在标准
toBinaryString等方法中,非零整数转换结果本身不含前导零)。 - 回文判定:检查该二进制字符串是否与其反转字符串相同。
暴力枚举法是最直观的解法:遍历 $1$ 到 $n$ 的每个整数,执行上述两步判断,统计回文数个数。对于每个数,二进制转换和回文判定的时间复杂度为 $O(\log n)$,总复杂度为 $O(n \log n)$,在 $n=10^5$ 的约束下完全可行。
算法流程:
- 读取输入的正整数 $n$。
- 初始化计数器
count = 0。 - 循环
i从1到n:- 将
i转换为二进制字符串binStr。 - 判断
binStr是否等于其反转字符串reversedStr。 - 若相等,则
count++。
- 将
- 输出
count。
复杂度分析:
- 时间复杂度:$O(n \log n)$。遍历 $n$ 个数,每个数的二进制位数约为 $\log_2 n$,转换和比较操作与之线性相关。
- 空间复杂度:$O(\log n)$。主要开销在于存储每个数的二进制字符串。
代码实现与详细注释
以下是 C++ 语言的实现代码,包含两种常见方法:使用标准库函数进行回文判断,以及手动进行回文判断。
#include <iostream>
#include <bitset>
#include <algorithm>
#include <string>
using namespace std;
// 方法一:使用标准库函数判断回文
int countBinaryPalindromes_method1(int n) {
int count = 0;
for (int i = 1; i <= n; ++i) {
// 将整数转换为二进制字符串(bitset会自动处理前导零)
string binStr = bitset<32>(i).to_string(); // 使用足够大的位数,如32
// 去除前导零:找到第一个'1'的位置
size_t firstOne = binStr.find('1');
if (firstOne != string::npos) {
binStr = binStr.substr(firstOne);
} else {
// 如果全为0(即i=0的情况,但i从1开始,所以不会进入此分支)
binStr = "0";
}
// 判断是否为回文:比较字符串与其反转
string reversedStr = binStr;
reverse(reversedStr.begin(), reversedStr.end());
if (binStr == reversedStr) {
++count;
}
}
return count;
}
// 方法二:手动判断回文(更高效,避免字符串拷贝和反转)
int countBinaryPalindromes_method2(int n) {
int count = 0;
for (int i = 1; i <= n; ++i) {
// 获取二进制表示
string binStr;
int temp = i;
while (temp > 0) {
binStr.push_back((temp & 1) ? '1' : '0');
temp >>= 1;
}
// 此时binStr是逆序的(最低位在前),需要反转一次得到正序
reverse(binStr.begin(), binStr.end());
// 手动判断回文
bool isPalindrome = true;
int left = 0, right = binStr.size() - 1;
while (left < right) {
if (binStr[left] != binStr[right]) {
isPalindrome = false;
break;
}
++left;
--right;
}
if (isPalindrome) {
++count;
}
}
return count;
}
int main() {
int n;
cin >> n;
// 两种方法任选其一
// int result = countBinaryPalindromes_method1(n);
int result = countBinaryPalindromes_method2(n);
cout << result << endl;
return 0;
}
代码要点解析:
- 二进制转换:
- 方法一:使用
bitset<32>(i).to_string()将整数转换为固定32位的二进制字符串,然后通过find('1')和substr去除前导零。这种方法简单直观,但会产生额外的字符串操作开销。 - 方法二:通过循环和位运算(
temp & 1和temp >>= 1)手动构建二进制字符串。注意,这样得到的字符串是逆序的(最低位在前),因此需要调用reverse一次得到正序。这种方法避免了查找前导零的步骤,通常更高效。
- 方法一:使用
- 回文判断:
- 方法一:使用
reverse函数生成反转字符串,然后直接比较。代码简洁,但需要创建反转字符串的副本。 - 方法二:使用双指针法,从字符串两端向中间遍历比较。只需一次遍历,空间复杂度为 $O(1)$,是更优的选择。
- 方法一:使用
- 效率考量:对于 $n=10^5$,两种方法均能轻松通过。方法二在字符串操作上更节省,推荐在实际竞赛中使用。
运行示例与场景扩展
以样例输入 n = 15 为例,程序的执行过程如下:
- 输入
15。 - 遍历
i从1到15:i=1:二进制"1",是回文。i=2:二进制"10",不是回文。i=3:二进制"11",是回文。i=4:二进制"100",不是回文。i=5:二进制"101",是回文。i=6:二进制"110",不是回文。i=7:二进制"111",是回文。i=8:二进制"1000",不是回文。i=9:二进制"1001",是回文。i=10:二进制"1010",不是回文。i=11:二进制"1011",不是回文。i=12:二进制"1100",不是回文。i=13:二进制"1101",不是回文。i=14:二进制"1110",不是回文。i=15:二进制"1111",是回文。
- 统计得到回文数个数为
6,输出结果与样例一致。
为了更深入地理解二进制回文数的分布,我们可以分析其数学特性。二进制回文数与对称性紧密相关。对于一个 $k$ 位的二进制数,如果它是回文数,那么其二进制串的前 $\lceil k/2 \rceil$ 位决定了整个数。例如,所有3位二进制回文数为: 101(5)、 111(7)。所有4位二进制回文数为: 1001(9)、 1111(15)。这种特性可以用于构造法生成所有不超过 $n$ 的二进制回文数,从而将时间复杂度降低到 $O(\sqrt{n} \log n)$ 或更好,但这超出了本题暴力法的范畴。
算法思想总结与扩展
本题是数制转换与字符串处理的结合,主要考查以下能力:
- 进制转换:熟练掌握整数到二进制字符串的转换方法。
- 回文判断:掌握字符串回文判断的多种实现(反转比较、双指针法)。
- 边界处理:注意二进制表示不含前导零的要求。
扩展思考:
- 更大数据范围:如果 $n$ 增大到 $10^9$ 甚至更大,暴力枚举将不可行。此时需要利用二进制回文数的构造规律进行计数。例如,可以按二进制位数分组,对于长度为 $len$ 的二进制回文数,其前一半($\lceil len/2 \rceil$ 位)可以任意取(但不能全为0),从而计算出该长度下的回文数个数,再累加所有长度不超过 $\lfloor \log_2 n \rfloor + 1$ 且对应数值不超过 $n$ 的回文数。
- 其他进制回文:问题可以推广到判断任意进制(如十进制、八进制、十六进制)下的回文数。只需将转换基数从2改为目标进制即可。
- 同时是多种进制回文的数:寻找同时是二进制和十进制回文的数(如1, 3, 5, 7, 9, 33, 99等)。这需要多一层进制转换和回文判断。
通过解决此题,可以巩固循环、位运算、字符串操作等基础编程技能,并为处理更复杂的数论与字符串结合问题打下基础。
更多推荐

所有评论(0)