计算正整数中1的数目

这是从编程之美上看到的一道题,简述题目内容如下:
给定正整数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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值