背景题面
[EGOI2021] Number of Zeros / 零的个数
题目描述
圣诞老人已经在准备 2021 年圣诞节。他希望买正整数个礼物,使得可以平均分给所有不淘气的孩子。然而,他不知道具体有多少不淘气的孩子,只知道数量一定在 a 和 b 之间。他希望买最少的正整数个礼物,使得可以被任何 x∈{a,a+1,…,b} 个孩子平分。
他已经计算出这(可能很大的)礼物数量,但他不确定计算是否正确,希望你可以进行一些基本的正确性检查。你可以告诉他答案的后导零个数吗?
输入格式
一行,两个整数 a,b。
输出格式
一行,一个整数,表示答案的后导零个数。
样例 1
样例输入 1
1 6
样例输出 1
1
样例 2
样例输入 2
10 11
样例输出 2
1
提示
样例 1 解释
如果可能有 1 至 6 个不淘气的孩子,圣诞老人至少需要 60 个礼物(这是最小的能被 1,2,3,4,5,6 整除的正整数),而 60 有一个后导零。
样例 2 解释
如果可能有 10 或 11 个不淘气的孩子,圣诞老人会买 110 个礼物。
数据范围
对于全部数据,1 ≤ a ≤ b ≤ 10^18。
题意理解
首先,由题目中“任何 x∈{a,a+1,…,b} 个孩子平分”可知礼物总数一定是[a,b]区间内所有数的公倍数;再者,圣诞老人希望购买最少的正整数个礼物,即求[a,b]的最小公倍数lcm;最后,答案就是该最小公倍数的后导零个数。
上述思路就是该题的暴力求解思路,看似很水,(所以在考场中顿时兴奋了起来--终于有会做的题了\(^o^)/~),好巧不巧,一翻数据范围,10^18(老天,唯一有思路有把握的题还给我弄个大数据),所以暴力肯定是行不通的,否则会涉及高精度,问题就更复杂化了,时间效率也很差。
回到正题,该如何求出一个区间的最小公倍数的后导零个数呢?
既然不能直接求解,那有没有什么简便方法或数学公式呢?
答案是肯定的,我们先对后导零进行剖析(为什么会有0存在?),毋庸置疑,它一定是该最小公倍数中某两个因数的乘积,我们令q = lcm(a,a+1,...,b),显而易见,10 = 2 * 5,即 10 = 2^1 * 5^1.由此可推得 q = 2^n * 5^m * z ,(z为剩余的因数积),因为10中后缀0的个数为1,(min(1,1) = 1),所以就有 2^n * 5^m * z (q) 的后缀0的个数为 min(n,m)。
\(^o^)/~我们成功地将其转化为了求q中2的因数的个数和5的因数的个数的最小值了,(*^▽^*)
接下来就是代码实现了,我先给出代码,再对其细节部分做深刻剖析,以求深刻理解掌握。
Code:
#include <iostream>
#include <cstdio>
using namespace std;
typedef long long LL;
LL a,b;
int V(LL a,LL b,int k){
int cnt = 0;
while(a != b){
b /= k;
a /= k;
cnt ++;
}
return --cnt;
}
int main(){
cin >> a >> b;
a--;
cout << min(V(a,b,2),V(a,b,5));
return 0;
}
代码虽然简介,却暗藏玄机,(大佬勿喷)比如a为什么要自减1?cnt 返回时为什么也要自减1?(闲着无聊在装弄吗?o(* ̄︶ ̄*)o)。。。。。。
建议先自己模拟一遍,再继续学习。(自己理解时收获的才是自己)(重申:大佬勿喷)
ok,回到Code中来,下面我将会对该代码做深刻解析并进行相关优化,Let's begin!oiers.
代码深析
首先,a在[a,b]区间范围内(注意:左闭右闭),所以要先自减1,举个例子,当 a = 10 , b = 11 之时,若 a 不自减,带如 V 函数中将会造成 (10 / 2 == 11 / 2)&& (10 / 5 == 11 / 5)(int向下取整)结果两个都返回 0 ,而正确答案应为1.下面我将以一个约数图类比理解:
由上图我们不难发现这是一个 1~12 的约数图及其约数和,但现在我们只需约数即可。
若令 l = 6,r = 10,在[l,r]区间内,约数2的个数和应为3,但是 10/2 - 6/2 却等于 2 ,为什么会这样呢?不难发现,这张图的约数覆盖范围应是向右边的,例如对于 6 而言,它的约数是以其方格右边的竖线为界限的,所以有 3 个 2 在其旗下,而我们需要的是它左边的竖线,让它正下方的 2 归属到 [l,r] 区间内,于是我们发现这条竖线恰好就是 5 右边的竖线,([5/2] == 2,6/2 == 3也符合逻辑),为了不改变逻辑顺序,我们干脆直接让 6-- ,变成 5 ,避免不必要的麻烦。因此根据这个例子类比可得 a 要减减。
至于 cnt-- 嘛,自己手模一遍就知道了,当 b < k 时,会多处理一次,所以要自减,当然,也可以直接赋值 cnt 为 -1,简洁明了。
a != b 如果不好理解,那就反过来 b - a != 0,在根据上面的图进行理解会直白的多。
(本想都写的,只是时间太晚有点困了,若有时间日后会进行补充,请多见谅)
最后呈上优化过后的代码
#include <iostream>
using namespace std;
typedef long long LL;
LL a, b;
inline int V(LL a, LL b, int k) {
int cnt = 0;
while (b - a != 0) {
b /= k;
a /= k;
cnt ++;
}
return cnt - 1;
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> a >> b;
if (a < b / 2 + 1) {
a = b / 2 + 1;
}
a--;
cout << min(V(a, b, 2), V(a, b, 5));
return 0;
}
唯一有效的优化是将部分 a 变成了 b/2+1 ,这个不难理解,因为小于 b/2+1 的数都是后面的数的因数,可以剪枝。(本蒟蒻还是太菜了,誒,越写越无力,今天就到这儿吧┏(^0^)┛)