Task 10 统计从1到某个整数之间出现的1的次数
任务:给定一个十进制的正整数,写下从1开始,到N的所有整数,然后数一下其中出现“1”的个数。
要求: 写一个函数 f(N) ,返回1 到 N 之间出现的 “1”的个数。例如 f(12) = 5。
在32位整数范围内,满足条件的“f(N) =N”的最大的N是多少。
1.设计思想:因为上课很多同学都给出了一个一个数地求出所出现的1,最多每个数也就求5、6次,但是所给的整数很大的时候计算机会一下循环递归N次来计算1的次数,这样会导致效率非常低。我们都知道每个位数上都有一定的规律,每一位上出现1的次数都和其前一位和后一位以及当前位上的数字有关系,所以得通过大量的数据一位一位的进行分析,进而找到每个数的规律。
通过对1位数、2位数、3位数,,,进行分析统计,发现如果当前位上的数字为0,1,大于等于1时有不同的情况;则此位上出现的1的次数分别会由更高位数上、更低位或者当前位的数字决定,具体如下:
假设一个数为abcde
如果百位上数字c为0,百位上可能出现1的次数由更高位决定。比如:33033,则可以知道百位出现1的情况可能是:100~199,1100~1199,2100~2199,,.........,32100~32199,一共3300个。可以看出是由更高位数字(12)决定,并且等于更高位数字(ab)乘以 当前位数(100)。
如果百位上数字为1,百位上可能出现1的次数不仅受更高位影响还受低位影响。比如:33133,则可以知道百位受高位影响出现的情况是:100~199,1100~1199,2100~2199,,.........,32100~32199,一共3300个。和上面情况一样,并且等于更高位数字(ab)乘以 当前位数(100)。但同时它还受低位影响,百位出现1的情况是:33100~33133,一共134个,等于低位数字(cde)+1。
如果百位上数字大于1(2~9),则百位上出现1的情况仅由更高位决定,比如12213,则百位出现1的情况是:100~199,1100~1199,2100~2199,...........,11100~11199,12100~12199,一共有1300个,并且等于更高位数字+1(ab+1)乘以当前位数(100)。
除了百位上其他位数也都符合这个规律,所以只需要循环整数的位数次就可以求出最终的结果。
然后第二步实现的时候,开始以为只需要一个循环就行了,让计算机循环。不过由于基数太大所以会用很长时间
将要计算的范围划分为几个区间,然后对每个区间进行计算。比如说:
1到999,先将这些数划分为10个区间:1-99、100-199 … 900-999
由f(999) = 300可知,300以后的区间段可以不计算。当计算200时,可以先计算299,由于f(299)=160<200,200-299的区间可以都不必计算。对要计算的区间,再将它划分为10个区间,重复进行。这样划分的另一个好处是利用公式:f(10^n-1) = n * 10^(n-1),保存上次算得的f(n)直接计算下个数的f(n)。
2.源代码:
#include<iostream> using namespace std; int Count(int n){ int count = 0;//1的个数 int CurrentPosition = 1;//当前位 int LowerNum = 0;//低位数字 int CurrNum = 0;//当前位数字 int HigherNum = 0;//高位数字 while(n / CurrentPosition != 0) { LowerNum = n - (n / CurrentPosition) * CurrentPosition;//低位数字 CurrNum = (n / CurrentPosition) % 10;//当前位数字 HigherNum = n / (CurrentPosition * 10);//高位数字 if(CurrNum == 0)//如果为0,出现1的次数由高位决定 { count += HigherNum * CurrentPosition;//等于高位数字 * 当前位数 } else if(CurrNum == 1)//如果为1,出现1的次数由高位和低位决定 { count += HigherNum * CurrentPosition + LowerNum + 1;//高位数字 * 当前位数 + 低位数字 + 1 } else//如果大于1,出现1的次数由高位决定 { count += (HigherNum + 1) * CurrentPosition;//(高位数字+1)* 当前位数 } CurrentPosition *= 10;//前移一位 } return count; } void main() { int a; cout << "请输入一个正整数:"; cin >> a; cout << a; cout << "从1到该数字出现的1的次数为:" << Count(a) << endl; for (int i = 0; i < 4294967295 ; i++) { if( Count(i) == i) { cout << i << " "; } } }
改进之后:
#include<iostream> using namespace std; int Count(int n){ int count = 0;//1的个数 int CurrentPosition = 1;//当前位 int LowerNum = 0;//低位数字 int CurrNum = 0;//当前位数字 int HigherNum = 0;//高位数字 while(n / CurrentPosition != 0) { LowerNum = n - (n / CurrentPosition) * CurrentPosition;//低位数字 CurrNum = (n / CurrentPosition) % 10;//当前位数字 HigherNum = n / (CurrentPosition * 10);//高位数字 if(CurrNum == 0)//如果为0,出现1的次数由高位决定 { count += HigherNum * CurrentPosition;//等于高位数字 * 当前位数 } else if(CurrNum == 1)//如果为1,出现1的次数由高位和低位决定 { count += HigherNum * CurrentPosition + LowerNum + 1;//高位数字 * 当前位数 + 低位数字 + 1 } else//如果大于1,出现1的次数由高位决定 { count += (HigherNum + 1) * CurrentPosition;//(高位数字+1)* 当前位数 } CurrentPosition *= 10;//前移一位 } return count; } inline unsigned count_digits(unsigned long long num) { unsigned long long n = 1; unsigned ret = 0; while (n <= num) { n *= 10; ++ret; } return ret; } void get_nums() { unsigned long long x = 1e11 - 1, y; unsigned count = 0; unsigned idx = 0; while (true) { ++count; y = Count(x); if (x < y) { //x在1到10时,均不满足x<y,所以x>10,下面的k值肯定大于0 unsigned k = count_digits(x) - 1; x -= (y - x - 1)/k + 1; } else if (x > y) { x = y; } else { cout<< ++idx << ": " << x << endl; //break; --x; if (x == 0) break; } } } void main() { int a; cout << "请输入一个正整数:"; cin >> a; cout << a; cout << "从1到该数字出现的1的次数为:" << Count(a) << endl; cout << "整数与次数相同的有以下这些,最大值为第一个数:"; get_nums(); }
3.实验截图:
4.实验总结:
(1)这个题目跟之前的同样是数学题类型的程序,需要利用大量的来分析统计,从中得出规律,否则就失去了编程的高效性;
(2)而当完成第一步之后以为第二步很简单,其实不然。感觉当时一定是被成功的喜悦蒙蔽了双眼,只是看它一直在滚动数字,而且也得出了最后的结果,然而却没想到效率的问题,之才发现第二步的设计也包含了好多规律,所以一定要从头到尾保持清醒的头脑。