之前看过一道题:有m盏灯,编号分别为1,2,3,...,m,每拉一次灯的开关,灯的亮灭状态就发生一次变化。这m盏灯初始状态都是亮着的,有m个人去拉灯,第1个人把所有的灯都拉一次,第2个人把编号是2的倍数的灯拉都拉一次,第3个人把编号是3的倍数的灯都拉一次,...,第m个人把编号是m的灯都拉一次。问:最终哪些编号的灯是灭的?
以下是解决这种问题的几种方法。
目录
一、人工按步骤操作
二、编程求解方法
1、编程方法一:最基本编程求解思路
2、编程方法二:数理分析+枚举法
3、编程方法三:求m中最大平方根法
一、人工按步骤操作
如果m的数值比较小,可以用手动的方法一个个去试一下,也是可以做出来的,如下表所示。
灯1 | 灯2 | 灯3 | 灯4 | 灯5 | 灯6 | 灯7 | 灯8 | 灯9 | 灯10 | |
初始 | 亮 | 亮 | 亮 | 亮 | 亮 | 亮 | 亮 | 亮 | 亮 | 亮 |
拉1次 | 灭 | 灭 | 灭 | 灭 | 灭 | 灭 | 灭 | 灭 | 灭 | 灭 |
拉2次 | 灭 | 亮 | 灭 | 亮 | 灭 | 亮 | 灭 | 亮 | 灭 | 亮 |
拉3次 | 灭 | 亮 | 亮 | 亮 | 灭 | 灭 | 灭 | 亮 | 亮 | 亮 |
拉4次 | 灭 | 亮 | 亮 | 灭 | 灭 | 灭 | 灭 | 灭 | 亮 | 亮 |
拉5次 | 灭 | 亮 | 亮 | 灭 | 亮 | 灭 | 灭 | 灭 | 亮 | 灭 |
拉6次 | 灭 | 亮 | 亮 | 灭 | 亮 | 亮 | 灭 | 灭 | 亮 | 灭 |
拉7次 | 灭 | 亮 | 亮 | 灭 | 亮 | 亮 | 亮 | 灭 | 亮 | 灭 |
拉8次 | 灭 | 亮 | 亮 | 灭 | 亮 | 亮 | 亮 | 亮 | 亮 | 灭 |
拉9次 | 灭 | 亮 | 亮 | 灭 | 亮 | 亮 | 亮 | 亮 | 灭 | 灭 |
拉10次 | 灭 | 亮 | 亮 | 灭 | 亮 | 亮 | 亮 | 亮 | 灭 | 亮 |
但这种方法效率太低了,而且容易出错,如果m值很大,用这种手动的方法就更不可行了。
二、编程求解方法
1、编程方法一:最基本编程求解思路
可以用编程的方法来解决,这个问题如果用c++语言编程的思路如下:
第一步:声明一个整形变量m,由键盘输入m的具体值。
第二步:声明一个有m+1个元素的布尔型数组,其中脚标为1到m的元素用于存储m盏灯的亮灭状态,亮为1,灭为0。
第三步:用for循环求解当所有人都拉过一次灯后,灯的状态。这一步是关键,灯的序号用i表示,人的序号用j表示,从i=1开始到m结束,循环求解j=1开始到m结束,i是否是j的倍数,如果是则序号为i的灯状态翻转一次。
第四部:把状态为灭的灯的序号输出。
具体代码如下:
#include <iostream>
using namespace std;
int main()
{
int m; //声明m盏灯
cin>>m; //键盘输入m的具体数值
bool lamp_status[m+1]; //声明存储灯状态的布尔型变量数组,1为亮,0为灭
for(int i=1;i<=m;i++) //循环求解i盏灯的状态
{
lamp_status[i]=1; //灯的初始状态为亮
for(int j=1;j<=m;j++) //循环求解第j个人拉灯之后,灯的状态变化
{
if(i%j==0) //如果i是j的倍数,则灯的状态进行翻转
lamp_status[i]=!lamp_status[i];
}
if(lamp_status[i]==0) //如果灯的状态为灭则输出灯的序号
{
cout<<i<<endl;
}
}
return 0;
}
运行程序,当m=10时,结果如下:
1
4
9
当m=100时,结果如下:
1
4
9
16
25
36
49
64
81
100
运算结果与题目给出的答案是一致的,编程求解的算法成功。
2、编程方法二:数理分析+枚举法
但本文目的不仅在此。
通过观察编程方法一运行的结果发现,灯状态为灭的序号都是完全平方数!为了排除巧合,把m的值再加大到1000,10000,得到的所有结果仍然是完全平方数。也就是说,这不是偶然的现象,而是一个必然的规律。
那么这个规律是什么呢?我们从数学角度来分析一下。
用i来表示从1到m的某盏灯的编号,j表示从1到m的某个拉灯的顺序号。
第i盏灯的最终亮灭状态是由灯的状态翻转次数决定的,当翻转次数是奇数次,那么灯的状态与初始状态相反,为灭;当翻转次数为偶数次,灯的状态与初始状态相同,为亮。
第j个人拉灯时,当i是j的倍数,或者说j是i的因数时,第i盏灯状态才发生翻转。而j的最大值和i的最大值都为m,那么第i盏灯翻转的次数等于i的因数的个数。
所以求解第i盏灯的状态问题,就转化为求解i这个自然数含有因数的个数是奇数还是偶数的问题。那么一个自然数,它的因数的个数有什么规律呢?这个就跟这个自然数是不是完全平方数有关,我们知道一个自然数总可以表示成另外两个自然数相乘的形式,这两个自然数就是它的因数。
比如:1=1×1,2=1×2;4=1×4=2×2;5=1×5;6=1×6=2×3,7=1×7,8=1×8=2×4,12=1×12=2×6=3×4,36=1×36=2×18=3×12=4×9=6×6...。
通过观察发现,一个自然数的因数都是成对出现的,当存在某对因数相等的时候,这个自然数就是完全平方数,也就是完全平方数的因数的个数是奇数。而非完全平方数,不存在某对因数相等的情况,所以因数的个数为偶数。
至此,判断第i盏灯状态是否是亮灭的时候,就转化为看i是否为完全平方数的问题了,当i为完全平方数,其因数个数为奇数个,第i盏灯状态与初始状态相反,为灭;当i不是完全平方数时,其因数个数为偶数个,第i盏灯状态与初始状态相同,为亮。所以,这个问题实际就是求解从1到m的自然数中一共有哪几个完全平方数。
这个问题怎么用c++语言来求呢?
思路如下:可以从1到m,一个一个判断i是否是完全平方数,如果是就枚举出来。判断的方法是先求i的平方根,然后取整,再把取整后的平方根进行求平方运算。如果i是完全平方数,其平方根是整数,取整后不变,对取整后的平方根进行平方运算,结果和i相等。如果i不为完全平方数,那么其平方根不为整数,取整后再进行平方运算,结果会小于i。
根据以上思路,程序代码如下:
#include <iostream>
#include <math.h>
using namespace std;
int main()
{
int m; //声明m盏灯
cin>>m; //键盘输入m的具体数值
for(int i=1;i<=m;i++) //循环求解i盏灯的状态
{
int squarRoot=sqrt(i); //求i的平方根并取整
if(squarRoot*squarRoot==i) //如果取整后的平方根的平方与i相等,说明i的平方根为整数,i是完全平方数
cout<<i<<endl;
}
return 0;
}
运行的结果和编程方法一的结果是一样的。
3、编程方法三:求m中最大平方根法
通过观察发现,自然数m以内的完全平方数,虽然不是连续的自然数,但这些完全平方数的平方根是连续的自然数,如下表所示。
完全平方数 | 1 | 4 | 9 | 16 | 25 | 36 | 49 | 64 | 81 | 100 | ... |
对应平方根 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ... |
所以,当我们知道m的具体值时,m的平方根的整数部分为,那么就是m以内最大完全平方数对应的那个最大平方根,{1,2,...,}就是所有的完全平方数对应的平方根,知道了这些平方根,那么再反过来求出完全平方数的集合为{}。
根据以上思路的编程代码如下:
#include <iostream>
#include <math.h>
using namespace std;
int main()
{
int m; //声明m盏灯
cin>>m; //键盘输入m的具体数值
int MaxsquarRoot=sqrt(m); //求平方数为m的最大整数平方根,那么小于等于这个最大整数平方根的所有自然数的平方都是小于m的完全平方数。
for(int i=1;i<=MaxsquarRoot;i++) //循环求解i盏灯的状态
{
cout<<i*i<<endl;
}
return 0;
}
对比三种编程方法,从时间复杂度上来说,方法一的时间复杂度为,方法二为,而方法三为,方法三完胜!方法三不但时间复杂度低,而且适合人工求解,真是个好方法。
(全文结束)