【问题标题】:Number of occurrences of digit in numbers from 0 to n从 0 到 n 的数字中出现的数字的次数
【发布时间】:2022-10-30 08:31:00
【问题描述】:

给定一个数字 n,计算数字 0、2 和 4 的出现次数,包括 n。

示例 1:

n = 10
output: 4

示例 2:
n = 22
output: 11

我的代码:

n = 22

def count_digit(n):
    count = 0
    for i in range(n+1):
        if '2' in str(i):
            count += 1
        if '0' in str(i):
            count += 1
        if '4' in str(i):
            count += 1
    return count

count_digit(n)

代码输出:10

所需输出:11

约束:1 <= N <= 10^5

笔记:该解决方案不应导致outOfMemoryExceptionTime Limit Exceeded 用于大量数字。

【问题讨论】:

  • n 可以有多大,允许多少时间?
  • @KellyBundy 1 <= N <= 10^5 TLE 通常是 100ms

标签: python python-3.x list algorithm data-structures


【解决方案1】:

您可以像这样增加计数:

def count_digit(n):
    count = 0
    for i in range(n + 1):
        if '2' in str(i):
            count += str(i).count('2')
        if '0' in str(i):
            count += str(i).count('0')
        if '4' in str(i):
            count += str(i).count('4')
    return count

这样一来,像 22、44 等边缘情况就被覆盖了!

【讨论】:

    【解决方案2】:

    有些数字会重复所需的数字,例如 20 或 22,因此您必须加 2 而不是加 1

    >>> 
    >>> string = ','.join(map(str,range(23)))
    >>> 
    >>> string
    '0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22'
    >>> 
    >>> string.count('0') + string.count('2') + string.count('4')
    11
    >>> 
    
    
    
    n = 22
    
    def count_digit(n):
        count = 0
        for i in map(str,range(n+1)):
    
            count+=i.count('0')
            count+=i.count('2')
            count+=i.count('3')
        return count
    print(count_digit(n))
    

    那独奏很快: 它可以开发得更快:

    def count_digit(n):
        i=0
        count=0
        s='024'
        while i<n-1:
        
            j = 0
            for v in str(i):
                if v in s:
                    j+=1
    
            count+=3*j + (7*(j-1))
            i+=10
    
        for i in range(i,n+1,1):
            for v in str(i):
                if v in s:
                    count+=1
    
    
        return count
    

    【讨论】:

    • 20中没有任何重复。
    • 2 和 0 ....
    • 我写了一个新的快速解决方案
    • 2出现一次,0出现一次。两者都没有重复。 OP 的代码已经正确处理不同的数字,唯一的问题是重复特定数字时。
    • 两种解决方案都不准确。第一个是因为你数的是 3 而不是 4。第二个是因为……我不知道,还没读过 :-)。但是修复第一个并将这两种解决方案与 n=34567 进行比较,它们会给出不同的结果。
    【解决方案3】:

    TL;DR:如果你做对了,你可以将计数快一千倍n接近 10**5,并且由于更好的算法使用的时间与数字的位数成正比n,它可以很容易地处理偶数n对于 64 位整数来说太大了。


    像这样的谜题(“在从 x 到 y 的数字中,有多少......?”)经常出现这种情况,关键是找到一种方法来计算聚合计数,理想情况下在 O(1) 中,对于范围大。对于数字字符串表示的组合,一个方便的范围通常类似于所有数字的集合,其字符串表示为给定大小,可能带有特定前缀。换句话说,[prefix*10⁴, prefix*10⁴+9999] 形式的范围,其中下限中的 0 与上限中的 9 的数量以及乘数中 10 的指数相同。 (实际上通常使用半开范围更方便,其中下限包括在内,上限是排他性,所以上面的例子是[prefix*10⁴, (prefix+1)*10⁴)。)

    另请注意,如果问题是计算 [x, y) 的计数,而您只知道如何计算 [0, y),那么您只需进行两次计算,因为

    count [x, y) == count [0, y) - count [0, x)
    

    该恒等式是半开区间允许的简化之一。

    这可以很好地解决这个问题,因为很清楚一个数字有多少次d出现在给定前缀的所有 k 位后缀的集合中。 (在 10ķ后缀,每个数字与其他数字具有相同的频率;共有ķ×10ķ这 10 位中的数字ķ,并且由于所有数字都具有相同的计数,因此该计数必须是ķ×10ķ-1.) 然后你只需要添加前缀的位数,但前缀正好出现 10ķ次,并且每个人贡献相同的计数。

    所以你可以取一个像 72483 这样的数字,并将其分解为以下范围,这些范围大致对应于 72483 中的数字之和,加上一些包含较少数字的范围。

    • [0, 9]
    • [10, 99]
    • [100, 999]
    • [1000, 9999]
    • [10000, 19999]
    • [20000, 29999]
    • [30000, 39999]
    • [40000, 49999]
    • [50000, 59999]
    • [60000, 69999]
    • [70000, 70999]
    • [71000, 71999]
    • [72000, 72099]
    • [72100, 72199]
    • [72200, 72299]
    • [72300, 72399]
    • [72400, 72409]
    • [72410, 72419]
    • [72420, 72429]
    • [72430, 72439]
    • [72440, 72449]
    • [72450, 72459]
    • [72460, 72469]
    • [72470, 72479]
    • [72480, 72480]
    • [72481, 72481]
    • [72482, 72482]
    • [72483, 72483]

    然而,在下面的代码中,我使用了一个稍微不同的算法,结果证明它有点短。它考虑写出从 0 到 n 的所有数字(包括前导零)的矩形,然后计算每列的计数。连续整数矩形中的一列数字遵循简单的循环模式;通过从列的完全重复部分开始,可以轻松计算频率。完成重复后,其余数字按顺序排列,除最后一位外,每一位出现相同次数。通过在纸上画一个小例子可能最容易理解,但下面的代码也应该相当清楚(我希望)。

    这样做的一个问题是它计算实际不存在的前导零,因此需要通过减去前导零计数来纠正它。幸运的是,这个计数非常容易计算。如果您考虑一个以五位数字结尾的范围(它本身不能以零开头,因为如果它以零开头,它就不是真正的五位数字),那么您可以看到该范围包括:

    • 10000 个数字以零开头
    • 还有 1000 个数字,其中有第二个前导零
    • 还有 100 个数字,其中有第三个前导零
    • 还有 10 个数字,其中有第四个前导零 没有数字有五个前导零,因为我们这样写 0,而不是空字符串。

    加起来是 11110,很容易看出它是如何概括的。该值可以在没有循环的情况下计算,如 (10⁵ − 1) / 9 − 1。该校正在以下函数的末尾完成:

    def countd(m, s=(0,2,4)):
        if m < 0: return 0
        m += 1
        rv = 0
    
        rest = 0
        pos = 1
        while True:
            digit = m % 10
            m //= 10
            rv += m * pos * len(s)
            for d in s:
                if digit > d:
                    rv += pos
                elif digit == d:
                    rv += rest
            if m == 0:
                break
            rest += digit * pos
            pos *= 10
        if 0 in s:
            rv -= (10 * pos - 1) // 9 - 1
        return rv
    

    几乎可以肯定该代码可以收紧。我只是想把算法弄下来。但是,实际上,它的执行时间以微秒而不是毫秒为单位,即使对于更大的值n.

    这是凯利基准的更新;我删除了其他解决方案,因为它们的最后一个值花费了太长时间n

    Try it online!

    【讨论】:

    • 我发布了一个与您类似的答案,尽管可能没有那么雄辩。但是如果我们将n = 10 数为4,我认为n = 0 的答案应该是1。
    • @גלעדברקן,是的,这是一个很好的观点。我会解决的。
    • @גלעדברקן:我认为唯一的区别是前导零的处理;您的解决方案首先尝试不计算它们,而我的解决方案将它们与任何其他数字相同,然后在最后减去已知计数。也许还有其他一些细微的区别,但它们看起来和我很相似。在 Python 中执行此操作的最大优势是它可以更轻松地处理较大的 n 值。
    【解决方案4】:

    另一种蛮力,似乎更快:

    def count_digit(n):
        s = str(list(range(n+1)))
        return sum(map(s.count, '024'))
    

    n = 10**5 进行基准测试:

    result   time   solution
    
    115474  244 ms  original
    138895   51 ms  Kelly
    138895  225 ms  islam_abdelmoumen
    138895  356 ms  CodingDaveS
    

    代码(Try it online!):

    from timeit import default_timer as time
    
    def original(n):
        count = 0
        for i in range(n+1):
            if '2' in str(i):
                count += 1
            if '0' in str(i):
                count += 1
            if '4' in str(i):
                count += 1
        return count
    
    def Kelly(n):
        s = str(list(range(n+1)))
        return sum(map(s.count, '024'))
    
    def islam_abdelmoumen(n):
        count = 0
        for i in map(str,range(n+1)):
            count+=i.count('0')
            count+=i.count('2')
            count+=i.count('3')
        return count
    
    def CodingDaveS(n):
        count = 0
        for i in range(n + 1):
            if '2' in str(i):
                count += str(i).count('2')
            if '0' in str(i):
                count += str(i).count('0')
            if '4' in str(i):
                count += str(i).count('4')
        return count
    
    funcs = original, Kelly, islam_abdelmoumen, CodingDaveS
    
    print('result   time   solution')
    print()
    for _ in range(3):
        for f in funcs:
            t = time()
            print(f(10**5), ' %3d ms ' % ((time()-t)*1e3), f.__name__)
        print()
    

    【讨论】:

    • str(lst) 将所有列表元素转换为字符串是否正确?
    • @meallhour 是的。 (虽然这还不是全部,因为它还为整个列表构建了更大的字符串。)
    • 我写了一个新的快速解决方案看看
    • 我喜欢str(list(...)) 的那个技巧,而且它似乎确实符合 n=10**5 的时间限制。但是您可以在几微秒内完成计算。
    • @rici 是的,我相信我们可以。这就是为什么我马上称之为蛮力。我开始研究一个好的解决方案,但它很恶心,而且蛮力似乎足够好,或者至少比以前发布的其他人快,所以我认为无论如何都值得发布。你写的快吗?我很想看,可能没有足够的动力自己去做。
    【解决方案5】:

    我最终得到了与 rici 类似的答案,除了数字公式的措辞可能略有不同。每个位置的每个数字有多少个实例(“每列计数”,如 rici 所述),我们可以将其分为两部分,首先是p * floor(n / (10 * p)),其中p 是位置的 10 次方。例如,在位置 0(最右边),每十个数字就有一个 1。然而,计算 0 需要额外检查当前和下一个位置的人口。

    对于第一部分,我们仍然需要添加归因于除法其余部分的计数。例如,对于n = 6floor(6 / 10) = 0,但我们确实有一个 2 和一个 4。如果 n 中该位置的数字大于我们正在计算的数字,我们添加 p;或者,如果数字相同,我们将数字右侧的值加上 1(例如,对于n = 45,我们要计算 4 出现在位置 1 的 6 个实例:40、41、42, 43、44、45)。

    JavaScript 代码,与 rici 的即时比较全部从 1 到 600,000 的数字。 (如果我没记错的话,rici 的代码错误地为n = 0 返回了 0,而答案应该是 1。

    function countd(m, s = [0,2,4]) {
      if (m <= 0)
        return 0
      m += 1
      rv = 0
      rest = 0
      pos = 1
      while (true) {
        digit = m % 10
        m = Math.floor(m / 10)
        rv += m * pos * s.length
        for (d of s) {
          if (digit > d)
            rv += pos
          else if (digit == d)
            rv += rest
        }
        if (m == 0) {
          break
        }
        rest += digit * pos
        pos *= 10
      }
      if (s.includes(0)) {
        rv -= Math.floor((10 * pos - 1) / 9) - 1
      }
      return rv
    }
    
    function f(n, ds = [0, 2, 4]) {
      // Value on the right of position
      let curr = 0;
      let m = n;
      // 10 to the power of position
      let p = 1;
      let result = 1;
      
      while (m) {
        const digit = m % 10;
        m = Math.floor(m / 10);
        for (const d of ds) {
          if (d != 0 || n >= 11 * p) {
            result += p * Math.floor((n - (d ? 0 : 10 * p)) / (10 * p));
          }
          if (digit > d && (d != 0 || m > 0)) {
            result += p;
          } else if (digit == d) {
            result += curr + 1;
          }
        }
        curr += p * digit;
        p *= 10;
      }
      
      return result;
    }
    
    for (let n = 1; n <= 600000; n += 1) {
      const _f = f(n);
      const _countd = countd(n);
      if (_f != _countd) {
        console.log(`n: ${ n }`);
        console.log(_f, _countd);
        break;
      }
    }
    
    console.log("Done.");

    【讨论】:

    • 顺便说一句,我现在已经更正了我的初始检查。谢谢。在查看您的算法时,我注意到初始化 let result = 1,我猜这是基于需要计算值 0 中的数字。但是,只有当0 在目标集中时才应该这样做;如果使用目标集(例如)[1, 2, 4] 运行它,它会始终产生比我的值多 1 的值。
    • @rici 啊,好点!谢谢!
    【解决方案6】:

    使用 in 而不是多分支条件。

    def count_digit(n):
        s = '024'
        out = 0
        for cs in map(str, range(n+1)):
            for c in cs:
                if c in s:
                    o += 1
        return out
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-11-24
      • 2012-08-07
      • 2015-09-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-04-03
      相关资源
      最近更新 更多