您的位置:首页 > 其它

[数论][题目]K尾相等数

2008-05-09 14:51 399 查看
题目描述:
从键盘输入一个自然数K(K>1),若存在自然数M和N(M>N),使得K^M和K^N均大于或等于1000,且他们末尾三位数相等,则称M和N是一对“K尾相等数”。编写一程序,输出M+N值最小的K尾相等数。
测试输入数据:
2

测试输出数据:
120

题目分析:
解题:
无论拿到什么样的题目,把题目读懂是第一件必须做到的事情。然后是分析题目中的关键词语以及重点内容。
就上面的题目而言,我们必须注意到下面的要点:
1.四个要点量:K,M,N,1000,且K,M,N是自然数
2.K是M,N的底数,M,N是K的幂。
3.K^M 和 K^N都必须大于1000
4.K^M 和 K^N 末尾三位相等。
5.要输出的的是M+N的值
6.M>N(两个数不相等…人家不会让小聪明成功的,K^M当然和它自己相等)
分析
1.上面的要点中,1,2,3,5,6都是辅助条件,关键是弄清楚第4点。末尾三位相等是什么意思?比如 123456 末尾三位是 ‘456’ 而 45678 末尾三位则是‘678’。显然(虽然我也不喜欢在书上看到这个字眼,但是这里确实是很显然……),这里指的是10进制数的末尾3位,如何取末尾3位的数字?很显然( =.=! )将这个数字对1000求模就可以了。
2.仔细思考后,我们可以注意到,任何数对1000求模只有1000种可能(0~999),所以我们将K^Power 中的Power从1(为什么不是0?因为K^0=1<1000,而我们不考虑小于1000的情况,题解第3点)到1001逐个求值,总有相等的两个数字。因为结果只有1000种可能,但是有1001次求值,哪怕前1000次所求结果都不一样,最后一次的值必然与前面1000种其中的一种相等(相关知识:抽屉原理1)。

流程:
根据上面的理解和分析,解决流程大致如下:
1.获得K的大小
2.逐个求K^Power (Power= 1,2,3…….1001)的结果并记录下来
3.若在记录的时候发现之前该值已经出现过,那么上次记录该值时的Power值和本次尝试记录时的Power值就分别是N和M了
4.输出M+N

数据结构与设计技巧:
1.其实这个程序不需要什么数据结构,但是有一个技巧还是得提到一下,K^Power末尾的三位数字只可能是0~999中的一种,而我们如何记录哪一种结果曾经出现过?最好的方法是使用一个一维数组,维数刚好是1000,分别记录0~999每种情况出现时Power的值。存取的时候可以利用末尾三位数本身的值作为该数组的索引来进行(相关知识:重复计数2
2.其实我们也不需要逐个求K^Power的值,那样的运算效率太低了。考虑到 K^Power = K^(Power-1),所以,利用这一性质,我们可以求出K^1的值,然后据此推出K^2到K^1001的结果就行了。
3. 当K或者Power比较大的时候它们相乘可能导致数据溢出,这就需要参考我的另一篇文章里的对大数进行求模运算的相关算法了,总之结果就是求M*N%R,当M*N可能溢出时,应该使用 ((M%R)*(N%R))%R 来计算。

程序代码:
#include <iostream>
#include <memory>

using namespace std;

int Record[1000]; // 用以记录的一维数组

int KTail( int K )
{
memset( Record, 0, sizeof(Record) ); // 清除之前记录的内容
int Product = 1; // 记录逐次相乘的积,至于为什么从1开始,
// 请自行观察体会
bool bTakeRecord = false; // 需要一个标记,用以判断是否>=1000
if ( K>=1000 )
{
bTakeRecord = true;
K=K%1000; // 在数论基础知识里曾证明过(M*N)%R = (M%R *N%R)%R
} // 这样做的原因是避免后面进行乘运算时数据溢出

for ( int Power = 1; Power<=1001; ++ Power) // Power从1到1001
{
Product *= K; // Product = (K^Power-1)%1000 * K
if ( bTakeRecord || Product>=1000 ) // 必须符合题目的条件 K^Power>=1000
{
bTakeRecord = true; // 考虑到有可能之前是false,再赋值
Product = Product%1000; // 去结果的末尾三位!!!!

if ( Record[Product]==0 ) // 这个结果之前没出现过,记录结果出现时的幂值
Record[Product] = Power; // 记录之
else
return Power + Record[Product]; // 啊哈!出现过,那么返回我们的结果
}
}

return -1; // 这行其实可以不写,因为不可能到这里,但是为了不让编译器提示编译警告
// 信息,还是加上吧。
}

int main()
{
int K;
while( (cin>>K).good() ) // 输入K值,并判断输入是否正常
{
cout<<KTail(K)<<endl; // 输出结果
}

return 0;
}

测试数据:
25
125
1000
1234
111111
1000003
123454321

测试数据结果:
7
6
3
56
52
102
27

相关知识:
1.抽屉原理:
桌上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,有的抽屉可以放一个,有的可以放两个,有的可以放五个,但最终我们会发现至少我们可以找到一个抽屉里面至少放两个苹果。这一现象就是我们所说的抽屉原理

2.重复计数:
记得曾经遇到过这样的一道笔试题:从文件里读入数据,每行都是一个独立的数字N(N>=0 且 N<=65535),且N出现的次数小于256次,要求设计一个算法,用高效的方式将0到65535出现的次数进行由大到小的排序。

这个题目的关键是如何记录每个数字出现的次数,有人可能会想到Hashtable,甚至自建数据结构如:
class Node
{
unsigned short int Num;
unsigned char Count;
};

然后再用一个vector<Node> 来保存他们,并且在每次读入一个N的时候,在vector中搜索Num=N 的元素,将其Count加1。若没有出现过,则新加入一个Node。
方法是对的,但是效率实在是非常的低,原因在于每读入一个N都必须进行一次搜索,哪怕在插入元素时已经考虑到按Num大小顺序排列,这样搜索元素时可以使用2分查找,但是效率依然不乐观,使用Hashtable情况是一样的,只是内部搜索操作被隐藏了。
最好的方法其实是很简单的,我们只要建一个普通的unsigned char数组Times[65535]就可以了。将N作为该数组的存取索引,当N出现一次时,将Timers
加1。这样,输入结束时我们就可以根据数组每个元素的值,知道对应的N出现过多少次。当然,考虑到后面我们还得排序和输出,我们也可以使用上面定义的Node结构来定义数组,这样在进行排序之后我们就才能知道对应的是哪个N。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: