试题 算法训练 数的潜能

问题描述

  将一个数N分为多个正整数之和,即N=a1+a2+a3+…+ak,定义M=a1*a2*a3*…*ak为N的潜能。
  给定N,求它的潜能M。
  由于M可能过大,只需求M对5218取模的余数。

输入格式

  输入共一行,为一个正整数N。

输出格式

  输出共一行,为N的潜能M对5218取模的余数。

样例输入

10

样例输出

36

数据规模和约定

  1<=N<10^18

这道题有两个问题需要解决。

其一,如何分解这个数。此处我枚举了10以内的分解情况:

分解
111
222
333
42+22*2=4
52+32*3=6
63+33*3=9
72+2+32*2*3=12
82+3+32*3*3=18
93+3+33*3*3=27
102+2+3+32*2*3*3=36

这里不给证明地给出观察结果,任给一个数x>1,能够分解为x=x1+x2+x3···+xn(n为整数且n≥1)的形式,当且仅当任意因子xi≠1(1≤i≤n)且有最多的因子=3时,其乘积最大。(此时因子2不多于两个)

根据上述结论,我们可以预测11=2+3+3+3,积2*3*3*3=54为最大。

因此,对于任给的n,我们整除能得到因子3的个数。为了防止因子1出现,需要对3取余,若结果为1,则“返还”一个因子3凑成两个因子2。即为解。

至此,第一个问题已经得到解决。

注意到输入规模达到了10^18的量级,如果进行暴力连乘取余,也达到10^6的量级,很可能也时难以承受的,经过测试果不其然。

因此需要考虑如何减小这一运算规模。注意到输出结果要求对5218这样一个并不是很大的数取余。根据鸽巢原理可知,在不多于5219次操作中,必然会出现循环。此处我再详细解释一下鸽巢原理的应用,假设对3的连乘和取余操作达到5218次后,所有余数都已出现恰好一次,那么下一次必然与之前发生重复。重复意味着什么呢?一旦重复,所得到的值在进行相同的若干次操作后,必然沿着原来的“轨迹”再次回到这个值。此处考虑的是最坏情况需要5219次操作,事实上,很可能在某一步骤已经重复,一旦出现环,后续的运算就不再必要了。我们只需要计算出环的长度,令操作次数对这个长度取余就可以免去不必要的运算。

要注意,出现环时不一定会回到第一个元素,换句话说,可能有一条“尾巴”,这个尾巴的长度也应该考虑在内,实际操作时,需要将操作数与尾长比较,如果小于尾长,就来不及进入环了;如果大于尾长,则应该先减去尾长再对环长取余。

虽然代码实现时,我省略掉了得出这个结果的过程,不过此处有必要将具体细节说明:

声明一个整型数组,长为5218,初始化为0,数组下标即为余数标识,声明一个整型变量初始化为0用于记录路径长。开始时,访问1号下标,如果值为0,则将其标记为当前位次,此时是0。将路径长+1,并访问下一个结点。当访问到一个结点不为0时,其下标即为尾长,而路径长-尾长+1即为环长。

实际求解时,非常巧合的是第一个元素就是循环点,换句话说,整个过程都在一个环中,没有尾部,其长度为2608。得到这个长度,整个的判环操作就完成使命了,接下来只需声明一个长度为2608的整型数组依次迭代打表即可。

接下来就很容易了,根据前面的方法得到因子3的个数,就是操作数,将其对2608取余,直接从表中读取结果,这样一来,时间复杂度已经降低为常数级了。最后还有1~2个因子2需要处理,别忘了。

代码如下:

#include <iostream>
#include <stdlib.h> 
using namespace std;
int main(void){
	long long n,s,a,b=0;
	int *m=(int*)malloc(sizeof(int)*2608);
	for(int i=0,t=1;i<2608;i++){
		m[i]=t;
		t=(t*3)%5218;
	}
	cin>>n;if(n==1)n--;
	a=n/3;
	switch(n%3){
		case 1:if(a--)b=2;break;
		case 2:b=1;
	}
	s=m[a%2608];
	for(long long i=0;i<b;i++)s=(s*2)%5218; 
	cout<<s;
	return 0;
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值