這是從編程之美上看到的一道題,簡述題目內容如下:
給定正整數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