前言:再论“拨”取数位上的数
在我曾经的博客中提到过,“水仙花数”类型的题目,精髓就在于“拨数”。关于“拨数”的内容,在往期的文章中已经详细讨论过,本文便不再赘述。我们将“拨数”作为一个大家已经掌握了的知识储备,再来讨论水仙花数系列的习题。
C语言编程必会技能:“拨”取数位上的数
三位水仙花数,即其个位、十位、百位数字的立方和等于该数本身。这是我们常见的水仙花数题,它的讨论范围限定在3位数。事实上,水仙花数可以不止有3位数,但它们的运算规则是相同的:
一个n位数,其各位数字的n次方之和恰好等于该数本身。于是就有:
三位的水仙花数共有4个:153,370,371,407;
四位的四叶玫瑰数共有3个:1634,8208,9474;
五位的五角星数共有3个:54748,92727,93084;
六位的六合数只有1个:548834;
七位的北斗七星数共有4个:1741725,4210818,9800817,9926315;
八位的八仙花数共有3个:24678050,24678051,88593477
我们干脆将所有具有该特点的数成为水仙花数。
本文讨论的重点在于,如何判断一个任意数位的水仙花数。我们再次巩固和运用“拨数”大法来解决问题,文末附上了水仙花数的变种:
把任意的数字,从中间拆分成两个数字,如果所有拆分后的乘积之和等于该数本身。
希望本文能让诸位读者对数位拨取类习题有更深刻的理解。
一、引例:三位水仙花数
1. 题干
输出所有水仙花数。
水仙花数是一个3位数,各位数字的立方之和等于他本身,例如:153= 13+53+33。
2. 思路
实现3位水仙花数是一件很容易构思的事情。先把三位数中个位、十位、百位上的数字取下来,然后进行立方求和运算,最后判断是否与原数相等即可。
3位水仙花数数位确定,且幂仅是三次方(幂数较少),实现起来并不复杂。大致分为两步:
1. 取数字
2. 计算并判断是否相等。
3. 代码
#include<iostream> int main(){ int flowerN,ge,shi,bai; for(flowerN = 100; flowerN < 1000; flowerN++){ //拨取数位上的数 ge = flowerN%10; shi = flowerN/10%10; bai = flowerN/100%10; if(flowerN == ge*ge*ge + shi*shi*shi + bai*bai*bai){ printf("%d ",flowerN); } } return 0; }
二、推广:N位水仙花数
1. 题干
求出0~100000之间的所有“水仙花数”并输出。
“水仙花数”是指一个n位数,其各位数字的n次方之和确好等于该数本身,如:153=1^3+5^3+3^3,则153是一个“水仙花数”。
2. 思路
有些同学在这道题卡住,很大的一个原因是,当具象的“3”变成抽象的“n”时,比较难以想象该如何让代码做到普适。当感觉一团乱麻的时候,我们依旧可以考虑用上次讲到的分析问题“三板斧”(举例、模拟(必要时画图)、找规律)进行考虑。
碳基肥宅:分析破题“三板斧”
有了计算3位水仙花数的经验(3位水仙花数正是一个特例,很好地帮助我们完成了“举例”这第一板“斧”),我们可以直接将问题切割为三个大步骤,再逐步求精:
1. 一共有多少个数位?这决定了一共有多少个数要拨取并相加,以及相应的幂数。
--求位数
2.求数的n次方。
3.累加:求每个数位上的数的n次方之和。这需要对叠加的算法熟悉。
以上,我们将一个大问题分割成了几个小的功能模块。在编程时,我们只需要照着这些小的功能模块逐步完善求精即可。
详细说明上述思路不是没有意义的。我们需要有意识地在编程分析问题时“慢下来”,分割大问题,完善小问题,才能保证编程的顺利。相比起拿到题目就从头开始干,想到哪里写到哪里,一运行bug肆虐,我认为这样有规划地写代码,确实是更好的选择。
3. 代码
#include<stdio.h> #include<math.h> //求出整数的位数n -- 拨取数位上的数 int digits(int num){ int n = 1; while(num/10){ n++; num /= 10; //这一句注意不要遗漏!!否则就是死循环 } return n; } //判断是不是水仙花数,是返回1,不是返回0 int isNumber(int num){ int n = digits(num); //调用函数,获取数位 int sum = 0; int tmp = num; //把num的值先保存起来,便于后续比较 while(num){ sum += pow(num%10,n); //可以类比叠加的算法 num /= 10; } if(sum == tmp){ return 1; }else{ return 0; } } int main(){ for(int num = 0; num <= 100000; num++){ if(isNumber(num)){ printf("%d\n",num); } } return 0; }
说明
1. while循环中,千万不要忘记num /= 10这一步骤。这是循环变量的调整步骤,没有这一步程序会死循环。由于while(num / 10)和num /= 10这一步有些相像,num /= 10这一步就很容易漏写。
2. int tmp = num;这一步很关键。它是将num的值暂时保存到tmp中,最后比较时,也是将sum与tmp比。因为num同时作为循环变量,它是不断变化调整的。当while执行结束后,num早已不是我们最初输入的那个值。最后sum要和“该数本身比较”,num早已不是“该数本身”。因此我们需要再创建一个变量,拷贝出num最初的值备用。
这一步非常隐蔽,很容易直接按照预先想好的逻辑上手而将它忽略。这样程序当然会出bug。我第一次编写代码时,正是遇到了这个问题,看了很久调试后才发现问题所在。一不小心,就花费了很多的时间。
三、拓展:水仙花数的变种 -- 灵活“拨”取数字
1. 题干
牛客网习题链接
BC38 变种水仙花
2. 思路
三板斧的使用
举例
这道题相比上面的题目,难度未必有所提升。我们直接以五位数12345为例,对这个情况进行模拟。
模拟
num:12345。以下示意程序运行时达到的效果:
- 1234 5
12345/10 * 12345%10
- 123 45
12345/100 * 12345%100
- 12 345
12345/1000 * 12345%1000
- 1 2345
12345/10000 * 12345%10000
找规律
我们发现,同一组内,除数与模数始终相等,且从10开始变化到10000.我们将该数设定为循环变量i,i从10变化到10000,且每次增长10倍。
而被分隔成的左右两个数,一个是num%i的结果,一个是num/i的结果。
因此,将规律整合成为如下代码即可:
3. 代码
#include<stdio.h> /* 12345 1234 5 12345/10 * 12345%10 123 45 12345/100 * 12345%100 12 345 12345/1000 * 12345%1000 1 2345 12345/10000 * 12345%10000 */ int isLily(int num){ int tmp = num; int sum = 0; for(int i = 10; i <= 10000; i*=10){ sum += (num/i)*(num%i); } if(sum == tmp){ return 1; } return 0; } int main(){ for(int i = 10000; i < 100000; i++){ if(isLily(i)){ printf("%d ",i); } } return 0; }
四、总结
1. 编写代码时可以考虑先分隔功能模块,再逐步求精。
2. 水仙花数从3位到n位的推广:找相似。