这是从编程之美上看到的一道题,简述题目内容如下:
给定正整数N,计算出从1到N的所有数字的十进制表示中出现1的次数,并找出能够满足f(N)==N的最大的N值。比如f(12) = 5, 因为存在的数字有:1,10,11,12,总共五个1。
1. 寻找1出现的次数
暴力方法
最简单的方法是将N个数全部遍历一遍,对每个数计算出其中1的个数,然后累加。具体代码如下:
func f(N uint) uint {
var count uint = 0
for i := uint(1); i <= N; i++ {
n := i
for n > 0 {
if n % 10 == 1 { count++ }
n /= 10
}
}
return count
}
这种方法效率非常低下,但是最不容易出错。
数学归纳方法
最高效的方法是先观察1出现的规律,然后通过数学公式来对逻辑进行优化。观察下表:
通过类推可以得到:对于最大的i位数,在其前面的数字中包含1的总数为 G(i) = i*(10^(i-1))
假设有一个数abcd,其千位数为a,所以我们可以将其拆解为两部分:0~a000-1, a000~abcd。
对于0a000-1,总共有a+1个0999,所以至少包含 aG(3)个1。但是考虑到a>=1, 如果a>1,则因为存在1000~1999这个区间,故而额外再加上1000,即aG(3)+1000。如果a=1,那么千位数为1的值总共应该有(bcd+1)个,即a*G(3) + bcd+1。
对于a000~abcd, 我们抽取出bcd,将之分解为0~b00-1, b00~bcd两部分,接下来计算方法同上。
总而言之,我们得到f(abcd)如下:
if a == 1:
f(abcd) = a *G(3) + bcd+1 + f(bcd)
else:
f(abcd) = a *G(3) + pow(10, 3)+ f(bcd)
转换为实际代码:
func f(N uint) uint {
if N < 10 { if N == 0 { return 0 } else { return 1} }
var firstVal uint = 0 // 最高位的值
var size uint = 0 // 十进制位数
n := N
for n > 0 {
firstVal = n
n /= 10
size++
}
base := pow(10, size-1) // 最小的size位数
if firstVal == 1 {
return f(N % base) + (N % base + 1) + (size-1) * pow(10, size-2)
}
return pow(10, size-1) + firstVal*(size-1) * pow(10, size-2) + f(N % base)
}
func pow(x uint, y uint) uint {
var res uint = 1
for y > 0 {
res *= x
y--
}
return res
}
2. 满足f(N) == N的最大N值
首先需要证明我们的函数f(N)和函数y=x相交。期望的图形如下:
从最上面的图片我们总结出:
f(9) = 1
f(99) = 20
f(999) = 300
f(9999) = 4000
…
通过归纳,不难得出 f(10^n - 1) = n * 10^(n-1)
使10^n - 1 <= n * 10^(n-1)成立的最小n为10。这意味着 10^9 - 1 <= N <= 10^10 - 1。
很明显这个N值很大,直接遍历上述范围所需要花费的时间也会很大。所以采取二分查找法,代码如下:
func getMaxN() uint {
max := pow(10, 10) - 1
min := pow(10, 9) - 1
for true {
mid := (max + min) / 2
val := f(mid)
if (val < mid) {
min = mid
} else if (val > mid) {
max = mid
} else {
return mid;
}
}
return 0
}
使f(N) == N的最大N值为1111111110