EGOI零的个数--约数的深刻理解

背景题面

[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^)┛)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值