F. Interesting Function - 题解
比赛与标签
比赛: Codeforces Round 725 (Div. 3) 标签: binary search, dp, math, number theory 难度: *1500
题目大意喵~
主人,下午好呀!这道题是这样的喵~
我们有两个整数 l 和 r,然后我们要不停地给 l 加 1,直到它变成 r 为止。在这个过程中,每一次加 1,我们都要观察一下,有多少个数字位发生了变化。
举个栗子🌰:
- 如果
l = 909,加 1 之后变成910。个位的9变成了0,十位的0变成了1,所以有2个数字位改变了。 - 如果
l = 9,加 1 之后变成10。个位的9变成了0,还多出了一个十位的1,这也算2个数字位改变了。 - 如果
l = 489999,加 1 之后变成490000,有5个数字位改变了呢!
我们的任务就是,把从 l 增加到 r 的过程中,每一次加法所导致的数字位变化数量全部加起来,得到一个总和。这就是我们要输出的答案啦,喵~
解题思路大揭秘!
这道题看起来是要我们模拟从 l 到 r 的每一次加法,然后数变化,但 r 最大可以到 10^9,直接模拟肯定会超时的说!所以,我们需要找到一个更聪明的办法,喵~
换个角度看问题
题目要求的是一个区间 [l, r-1] 上每个数 +1 后的变化总和。这种区间求和的问题,通常可以转换成“前缀和”的思想来解决,呐。
我们可以定义一个函数 F(n),它表示从 1 开始,一直加到 n,这个过程中总共发生了多少次数字位变化。也就是 (0->1) + (1->2) + ... + ((n-1)->n) 的总变化数。
如果有了这个神奇的 F(n) 函数,那从 l 到 r 的总变化数就可以表示为: 总变化数(l, r) = F(r) - F(l)
为什么呢?因为 F(r) 是从 1 到 r 的总变化,F(l) 是从 1 到 l 的总变化。两者相减,剩下的不就是从 l+1 到 r 的变化了嘛,这正好对应了 l 到 r-1 这些数字加 1 之后的变化总和,完美!
如何计算 F(n) 呢?
现在的问题就变成了如何高效地计算 F(n)。我们不一个一个地数,而是从“贡献”的角度来思考,喵~
我们来分析一下,从 1 到 n,每个数位(个位、十位、百位...)总共变化了多少次。
个位 (10^0): 每次加 1,个位都会变(除非是
9->0这种进位)。但其实我们可以认为,从0到n,每次加 1 都导致了个位的变化。比如1->2,2->3...9->0。所以,从1到n,个位总共变化了n次。十位 (10^1): 十位什么时候会变呢?只有在个位从
9变成0的时候,才会发生进位,从而引起十位的变化。这发生在9->10,19->20,29->30... 也就是说,每过 10 个数,十位就会变一次。那么从1到n,十位总共变化了多少次呢?就是n里面包含了多少个10,也就是floor(n / 10)次!百位 (10^2): 同理,百位只在
99->100,199->200... 的时候变化。从1到n,百位总共变化了floor(n / 100)次。更高位: 我们可以推广这个规律!对于
10^k这个数位,它在从1到n的过程中,总共变化了floor(n / 10^k)次。
所以,F(n) 的值就是所有数位变化次数的总和! F(n) = floor(n/1) + floor(n/10) + floor(n/100) + floor(n/1000) + ...F(n) = n + n/10 + n/100 + n/1000 + ... (在程序里用整除就可以啦)
这个公式是不是超级优雅的说!我们只需要一个简单的循环就可以算出 F(n),然后用 F(r) - F(l) 就能得到最终答案了!
代码实现喵~
#include <iostream>
// 这个函数用来计算 F(n),也就是从 1 一直加到 n,总共的数字位变化次数,喵~
//
// 题目要求的是从 l 到 r-1 每次加 1 的变化总和。
// 我们可以用前缀和的思想,把它变成 F(r) - F(l)。
//
// F(n) 的计算公式是基于“贡献法”推导出来的:
// - 个位 (10^0) 的变化次数:每次加1都变,共 n 次。
// - 十位 (10^1) 的变化次数:每逢10变一次,共 floor(n/10) 次。
// - 百位 (10^2) 的变化次数:每逢100变一次,共 floor(n/100) 次。
// - ...
// - 10^k 位的变化次数:共 floor(n/10^k) 次。
//
// 所以 F(n) = n + n/10 + n/100 + ...
// 下面的循环就是这个公式的巧妙实现!
long long F(int n) {
long long total = 0;
// 循环计算 n + n/10 + n/100 + ...
// 每次循环,我们加上当前的 n,然后把 n 除以 10
// 这就相当于依次加上了 n/1, n/10, n/100 ... 的值
while (n > 0) {
total += n;
n /= 10;
}
return total;
}
void solve() {
int l, r;
std::cin >> l >> r;
// 根据我们的推导,从 l 到 r 的总变化数就是 F(r) - F(l)
std::cout << F(r) - F(l) << "\n";
}
int main() {
// 加速输入输出,让程序跑得更快一点~
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}复杂度分析的说
时间复杂度: O(log r) 的说。 对于每个测试用例,我们调用了两次
F函数。F(n)函数中的循环次数取决于n的位数,也就是log10(n)。所以计算F(r)和F(l)的时间复杂度是O(log r)。如果有T个测试用例,总时间复杂度就是O(T * log r),非常快喵!空间复杂度: O(1) 的说。 我们在计算过程中只用了几个变量来存储
l,r和中间结果,没有使用额外的数组或者数据结构,所以空间消耗是常数级别的。
知识点与总结
这道题虽然标签里有 dp 和 binary search,但最优雅的解法其实是纯数学的,喵~
前缀和/差分思想: 这是解决区间问题的经典利器!将对一个区间
[l, r]的查询,转化为对两个端点F(r)和F(l)的查询,大大简化了问题。贡献法: 当直接模拟或计算很复杂时,可以换个角度,去计算每个基本元素(在这里是每个数位)对总答案的贡献。这种思维方式在很多组合计数和数学题中都非常有用!
优雅的实现:
while (n > 0) { total += n; n /= 10; }这个循环是计算n + n/10 + n/100 + ...的一个非常简洁的写法,值得学习和记忆呐!
希望这篇题解能帮助到主人哦!继续加油,享受解题的乐趣吧,喵~ (ฅ'ω'ฅ)