1. 整除的尾数问题解析
1.1 问题重述与理解
这个问题描述了一个有趣的数学场景:已知一个整数的前几位数字a(0<a<10000),但不知道它的最后两位数字。这个整数能够被另一个整数b(10<b<100)整除。我们的任务是找出所有可能的最后两位数字。
举个例子,假设a=123,b=25。那么我们需要找出所有可能的两位数x(00到99),使得12300+x(即123*100+x)能被25整除。在这个例子中,12300/25=492余0,所以x可以是00;12325/25=493,所以x可以是25;12350/25=494,所以x可以是50;12375/25=495,所以x可以是75。因此,满足条件的尾数是00,25,50,75。
1.2 数学原理分析
这个问题的核心在于模运算。我们需要找到所有x∈[0,99],使得:
(a * 100 + x) ≡ 0 mod b
这可以转化为:
x ≡ -a*100 mod b
因为x的范围是0到99,所以我们需要找到所有在这个范围内满足上述同余关系的x值。
1.3 算法实现详解
1.3.1 暴力枚举法
最直观的方法是遍历所有可能的x值(0到99),检查哪些满足条件:
cpp复制for(int i=0; i<100; i++) {
if((a*100 + i) % b == 0) {
// i是一个有效的尾数
}
}
这种方法简单直接,因为最多只需要检查100个可能的x值,对于现代计算机来说计算量可以忽略不计。
1.3.2 数学优化方法
我们可以通过数学方法减少计算量。首先计算a*100 mod b的值,然后找到所有x满足:
x ≡ (-a*100) mod b
且0 ≤ x < 100
具体步骤:
- 计算remainder = (a * 100) % b
- 第一个解x0 = (b - remainder) % b
- 所有解为x0, x0+b, x0+2b,...,直到x < 100
这种方法理论上更高效,但对于b的范围(10到100)和x的范围(0到99),性能提升不明显。
1.4 代码实现与优化
原始代码使用了vector来存储结果,这是合理的。但我们可以做一些小优化:
- 预先计算a*100,避免在循环中重复计算
- 使用reserve预分配vector空间,避免多次扩容
- 输出时使用更简洁的方式处理空格
优化后的代码:
cpp复制#include <iostream>
#include <vector>
using namespace std;
int main() {
int T;
cin >> T;
while(T--) {
int a, b;
cin >> a >> b;
vector<int> res;
res.reserve(100/b + 1); // 预分配空间
int base = a * 100;
for(int i = 0; i < 100; i++) {
if((base + i) % b == 0) {
res.push_back(i);
}
}
// 输出结果
for(size_t i = 0; i < res.size(); i++) {
if(i != 0) cout << " ";
cout << res[i];
}
cout << endl;
}
return 0;
}
1.5 常见问题与调试技巧
- 边界条件处理:特别注意a=0或b=100的情况(虽然题目限制了范围)
- 输出格式:确保最后一个数字后面没有空格,否则可能会被判错
- 性能考虑:虽然本题数据量小,但养成优化习惯很重要
- 变量溢出:a100可能超过int范围吗?在本题中a<10000,10000100=1e6,而int通常能表示到2e9左右,所以安全
提示:在编程竞赛中,输出格式往往比算法本身更容易出错。建议专门编写一个函数来处理输出格式,确保符合题目要求。
2. 回文质数问题解析
2.1 问题理解与定义
回文质数是指同时满足两个条件的数:
- 是质数(只能被1和自身整除)
- 是回文数(正读反读都一样)
题目要求在给定范围[a,b]内找出所有这样的数。
2.2 算法设计思路
2.2.1 直接判断法
对于区间内的每个数,依次判断:
- 是否是回文数
- 是否是质数
这种方法简单直接,但对于大范围数据(如b=100,000)可能效率不高。
2.2.2 预生成质数法
可以先使用筛法(如埃拉托斯特尼筛法)生成所有质数,然后从中筛选回文数。这种方法对于多次查询更高效。
2.3 代码实现细节
原始代码实现了直接判断法,我们可以做一些优化:
- 质数判断优化:只需检查到√n即可
- 回文数判断优化:可以转换为字符串判断
优化后的质数判断函数:
cpp复制bool isPrime(int n) {
if(n <= 1) return false;
if(n == 2) return true;
if(n % 2 == 0) return false;
for(int i = 3; i * i <= n; i += 2) {
if(n % i == 0) return false;
}
return true;
}
回文数判断的字符串方法:
cpp复制bool isPalindrome(int n) {
string s = to_string(n);
string r(s.rbegin(), s.rend());
return s == r;
}
2.4 性能分析与优化
对于b=100,000,直接判断法需要:
- 回文数判断:每个数平均约5次比较(因为最多是5位数)
- 质数判断:最坏情况下约√100000≈316次取模运算
总操作量约为100,000*(5+316)=32,100,000次操作,现代CPU可以在瞬间完成。
但如果范围更大(如到1e8),就需要更优化的算法了。
2.5 数学性质与观察
有趣的是,除了11以外,所有偶数位数的回文数都能被11整除,因此不可能是质数。这意味着我们可以跳过所有偶数位数的检查(除了11)。
利用这个性质可以显著减少检查次数:
cpp复制bool isPalindromePrime(int n) {
if(n == 11) return true;
string s = to_string(n);
if(s.size() % 2 == 0) return false; // 跳过偶数位数
return isPalindrome(n) && isPrime(n);
}
3. 汽水瓶问题解析
3.1 问题理解与递归关系
这是一个典型的递归问题,规则如下:
- 3个空瓶可以换1瓶汽水
- 喝完后又得到1个空瓶
- 当剩下2个空瓶时,可以借1瓶,喝完后再还3个空瓶
递推关系可以表示为:
f(n) = n/3 + f(n%3 + n/3)
边界条件:
f(1) = 0
f(2) = 1
3.2 迭代解法实现
原始代码使用了迭代法,这是正确的。我们可以更清晰地表达这个逻辑:
cpp复制int maxBottles(int n) {
int total = 0;
while(n >= 3) {
int exchanged = n / 3;
total += exchanged;
n = n % 3 + exchanged;
}
if(n == 2) {
total++;
}
return total;
}
3.3 数学公式解法
经过分析,可以发现最大汽水瓶数其实就是n/2的整数部分。因为最终每瓶汽水实际上消耗2个空瓶(自己喝掉的1个和换新汽水需要的2个)。
因此可以直接计算:
cpp复制int maxBottles(int n) {
return n / 2;
}
这个发现展示了从实际问题中抽象出数学模型的重要性。
3.4 测试用例验证
让我们验证几个测试用例:
- n=10: 10/2=5,与题目描述一致
- n=3: 3/2=1,实际可以换1瓶
- n=2: 2/2=1,可以借1瓶
- n=1: 1/2=0,不能换
这个简单的公式完美匹配所有情况。
4. 阶乘最后的非零位问题
4.1 问题理解与难点分析
计算n!的最后非零位数字的挑战在于:
- 阶乘增长极快,直接计算会溢出
- 我们需要的是最后非零位,所以需要去掉所有的10因子(即2和5的因子对)
4.2 算法设计思路
核心思路:
- 计算1到n中所有数的2和5的因子个数
- 去掉成对的2和5因子(每个对产生一个10,即末尾的0)
- 计算剩余数的乘积,保留最后几位防止溢出
- 乘以剩余的2因子(因为2的因子通常比5多)
- 取最后一位
4.3 代码实现细节
原始代码已经很好地实现了这个逻辑。我们可以做一些小的改进:
- 使用更小的模数(如100而不是1000),因为最后我们只需要个位数
- 提前终止循环:当res变为0时可以提前结束
优化后的代码:
cpp复制int lastNonZeroDigit(int n) {
int count2 = 0, count5 = 0;
int res = 1;
for(int i = 1; i <= n; i++) {
int x = i;
// 移除所有2和5因子
while(x % 2 == 0) { x /= 2; count2++; }
while(x % 5 == 0) { x /= 5; count5++; }
res *= x;
// 移除末尾的0并截断
while(res % 10 == 0) res /= 10;
res %= 100; // 只需要保留最后两位即可
}
// 应用剩余的2因子
int extra2 = count2 - count5;
for(int i = 0; i < extra2; i++) {
res *= 2;
while(res % 10 == 0) res /= 10;
res %= 100;
}
return res % 10;
}
4.4 数学性质与优化
观察到一个性质:对于n≥15,2的因子总是比5多很多。因此我们可以预先计算一些结果或寻找模式。
另一个优化是注意到我们只需要最后非零位,所以在乘法时可以忽略更高位的影响。
5. 算菜价问题解析
5.1 问题理解与输入输出
这个问题要求计算多组菜品的总价,并将结果四舍五入到小数点后一位(即精确到角)。
输入格式需要注意:
- 第一行是测试组数
- 每组数据第一行是菜种数
- 随后每行是菜名、数量、单价
5.2 浮点数处理技巧
关键点:
- 使用double类型存储金额
- 使用
中的fixed和setprecision控制输出 - 注意四舍五入是自动进行的
5.3 代码实现与优化
原始代码已经正确。可以添加一些错误处理:
cpp复制#include <iostream>
#include <iomanip>
using namespace std;
int main() {
int group;
cin >> group;
while(group--) {
int num;
cin >> num;
double total = 0.0;
while(num--) {
string name;
double quantity, price;
cin >> name >> quantity >> price;
if(quantity < 0 || price < 0) {
cerr << "Error: Negative value detected" << endl;
return 1;
}
total += quantity * price;
}
cout << fixed << setprecision(1) << total << endl;
}
return 0;
}
5.4 常见问题与注意事项
- 浮点数精度问题:在金融计算中,通常建议使用整数表示分或用decimal类型
- 输入验证:应该检查数量和单价是否为负数
- 输出格式:确保总是输出一位小数,如3应输出为3.0
6. 求最晚和最早日期问题
6.1 问题理解与数据结构
需要比较多个日期,找出最早和最晚的。使用结构体存储年月日很合适。
6.2 比较算法设计
日期比较的规则:
- 先比较年份
- 年份相同比较月份
- 月份相同比较日
原始代码中的islater和isearlier函数实现了这个逻辑。
6.3 代码优化与改进
可以重载运算符使代码更直观:
cpp复制struct Date {
int y, m, d;
bool operator<(const Date& other) const {
if(y != other.y) return y < other.y;
if(m != other.m) return m < other.m;
return d < other.d;
}
};
这样可以直接使用std::min_element和std::max_element算法:
cpp复制vector<Date> dates(N);
for(auto& date : dates) {
cin >> date.y >> date.m >> date.d;
}
auto min_date = *min_element(dates.begin(), dates.end());
auto max_date = *max_element(dates.begin(), dates.end());
6.4 边界条件与验证
需要注意:
- 日期的有效性验证(如2月30日)
- 空输入处理
- 所有日期相同的情况
7. 素数判断问题
7.1 素数判断基础算法
最基本的素数判断是试除法,检查2到n-1是否有能整除n的数。
7.2 优化方法
- 只需检查到√n
- 跳过偶数(除了2)
- 预生成素数表
优化后的isPrime函数:
cpp复制bool isPrime(int n) {
if(n <= 1) return false;
if(n <= 3) return true;
if(n % 2 == 0 || n % 3 == 0) return false;
for(int i = 5; i * i <= n; i += 6) {
if(n % i == 0 || n % (i + 2) == 0) {
return false;
}
}
return true;
}
7.3 性能比较
对于大数判断,更高效的算法有:
- Miller-Rabin概率测试
- AKS素数测试
但在编程竞赛中,优化后的试除法通常足够。
8. 综合总结与经验分享
在实际编程竞赛中,除了算法正确性外,还需要注意:
- 输入输出效率:对于大数据量,使用快速的IO方法(如C的stdio或关闭C++的同步)
- 边界条件:总是测试最小、最大和特殊值
- 代码简洁性:良好的代码结构可以减少错误
- 数学洞察:寻找问题背后的数学规律往往能大幅简化问题
经验之谈:在解决"汽水瓶"问题时,我最初使用了复杂的递归,直到发现简单的n/2规律。这提醒我,在编码前应该多思考问题的数学本质。
对于想要提高算法能力的开发者,建议:
- 系统学习数论基础知识
- 练习经典算法问题
- 参加编程竞赛积累经验
- 学习阅读和分析他人的优秀代码
