当前位置: 首页 > news >正文

【算法】【Leetcode】【数学】统计1的个数 数位统计法

  1.题目链接 :

233. 数字 1 的个数 - 力扣(LeetCode)

力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

[ZJOI2010]COUNT 数字计数

2.问题分析 

本题要求计算区间 [a, b] 内所有整数中 0-9 每个数码出现的次数,核心难点在于处理 1≤a≤b≤10¹² 的超大数据范围 —— 暴力遍历每个数字逐位统计会因时间复杂度过高(O ((b-a+1)×log b))直接超时。

因此需要采用数位统计法,通过数学分析高效计算每一位上 “1” 出现的次数。

2.1核心思路

  1. 按位分析:将数字 n 按位(个位、十位、百位...)拆分,分别计算每一位上 “1” 出现的次数,最后累加总和。
  2. 位拆分定义:对于第 i 位(从右数,个位为第 0 位,位因子为 10ⁱ):
    • higher:当前位左侧的高位部分;
    • curr:当前位的数字;
    • lower:当前位右侧的低位部分;
    • digit:当前位的位因子(如个位为 1,十位为 10)。
  3. 分情况计算:根据当前位数字curr与 1 的大小关系,计算该位上 “1” 出现的次数:
    • curr < 1:次数 = higher * digit
    • curr == 1:次数 = higher * digit + lower + 1
    • curr > 1:次数 = (higher + 1) * digit

2.2示例解析(输入 n=13)

  1. 个位分析(digit=1)

    • higher = 13 // 10 = 1curr = 3lower = 0
    • curr > 1,次数 = (1 + 1) * 1 = 2(个位为 1 的数:1、11)。
  2. 十位分析(digit=10)

    • higher = 13 // 100 = 0curr = 1lower = 3
    • curr == 1,次数 = 0 * 10 + 3 + 1 = 4(十位为 1 的数:10、11、12、13)。
  3. 总和:2 + 4 = 6,与示例结果一致。

2.3复杂度分析

  • 时间复杂度:O (log₁₀ n),仅需遍历数字 n 的每一位(n=10¹² 最多 13 位);
  • 空间复杂度:O (1),仅使用常数个变量存储中间结果。

该方法高效适用于任意大小的 n,完全避免了暴力遍历的超时问题。

明确几个关键概念(以分析百位为例,位因子digit=100):

  • 设待分析的数字为n,拆分后:
    • higher当前位左侧的 “高位部分”(如n=56789,分析百位时,higher=56);
    • curr当前位的数字(如n=56789,分析百位时,curr=7);
    • lower当前位右侧的 “低位部分”(如n=56789,分析百位时,lower=89);
    • digit当前位的 “位因子”(百位为100,十位为10,个位为1)。

需要计算:在 1~n 中,当前位(如百位)为 “1” 的数字有多少个(每个这样的数字会贡献 1 次当前位的 “1”)

2.4分情况推导(以百位为例,digit=100

情况 1:curr < 1(即curr=0

示例n=3045(百位是 0)

  • 正确拆分:digit=100digit*10=1000higher = 3045 // 1000 = 3(仅千位的 3,是百位左侧的高位)curr = (3045 // 100) % 10 = 0(百位的数字)lower = 3045 % 100 = 45(百位右侧的低位:45)

  • 当前位要为 “1”(即百位 = 1),需满足:数字形式为 [higher'] 1 [lower'],且整体 ≤ 3045。

    • 高位higher'的取值范围:若higher' = higher(即 3),则数字为 3 1 [lower'](如 3100~3199),但原数是 3045,3100 > 3045,不合法。因此higher'只能取 0 ~ higher-1(即 0~2),共higher种可能。

    • 低位lower'的取值范围:百位固定为 1,低位可任意取(0~99),共digit种可能(100 种)。

  • 总数 = higher × digit = 3 × 100 = 300(这些数字是:0100~0199、1100~1199、2100~2199,均 ≤ 3045)

情况 2:curr == 1

示例n=3145(百位是 1)

  • 正确拆分:digit=100digit*10=1000higher = 3145 // 1000 = 3(千位的 3)curr = (3145 // 100) % 10 = 1(百位的数字)lower = 3145 % 100 = 45(百位右侧的低位:45)

  • 当前位要为 “1”(即百位 = 1),需分两种子情况:

    1. higher' < higher(即 0~2):数字形式为 [0~2] 1 [lower'](如 0100~0199、1100~1199、2100~2199),均 ≤ 3145,合法。数量 = higher × digit = 3 × 100 = 300

    2. higher' == higher(即 3):数字形式为 3 1 [lower'],需满足整体 ≤ 3145。此时lower'最大只能取 45(原数的低位),即0~45,共lower + 1种可能(46 种)。

  • 总数 = 300 + 46 = 346

情况 3:curr > 1

示例n=3245(百位是 2)

  • 正确拆分:digit=100digit*10=1000higher = 3245 // 1000 = 3(千位的 3)curr = (3245 // 100) % 10 = 2(百位的数字)lower = 3245 % 100 = 45(百位右侧的低位:45)

  • 当前位要为 “1”(即百位 = 1),需满足:数字形式为 [higher'] 1 [lower'],且整体 ≤ 3245。

    • 高位higher'的取值范围:即使higher' = higher(即 3),数字为 3 1 [lower'](3100~3199),也 ≤ 3245(合法)。因此higher'可取 0 ~ higher(即 0~3),共higher + 1种可能。

    • 低位lower'的取值范围:百位固定为 1,低位可任意取(0~99),共digit种可能(100 种)。

  • 总数 = (higher + 1) × digit = (3 + 1) × 100 = 400(这些数字是:0100~0199、1100~1199、2100~2199、3100~3199,均 ≤ 3245)

2.5while 循环 遍历 digit   位

分情况计算解决的是 “单个位” 的 1 出现次数,而 while 循环解决的是 “所有位” 的遍历与累加—— 两者是 “局部计算” 和 “全局汇总” 的关系,缺一不可。

一、先明确一个关键前提:数字有多个 “位”,每个位都可能出现 1

任何一个数(如 n=13、n=3245)都是由多个数位组成的(个位、十位、百位、千位……)。例如:

  • n=13 有两个位:个位(3)、十位(1);
  • n=3245 有四个位:个位(5)、十位(4)、百位(2)、千位(3)。

我们需要统计的是 “1 到 n 中所有 1 的总次数”,而这个总数 = 个位上 1 的次数 + 十位上 1 的次数 + 百位上 1 的次数 + …… + 最高位上 1 的次数

二、分情况计算:仅针对 “当前正在处理的某一个位”

之前的三种情况(curr<1、curr==1、curr>1),本质是计算 “单个位” 上 1 出现次数的方法。例如:

  • 当 digit=1(处理个位)时,分情况算出的是 “所有数中个位为 1 的总次数”;
  • 当 digit=10(处理十位)时,分情况算出的是 “所有数中十位为 1 的总次数”;
  • 当 digit=100(处理百位)时,分情况算出的是 “所有数中百位为 1 的总次数”。

如果没有 while 循环,你只能手动计算某一个位的 1 的次数,无法覆盖所有位。

三、while 循环:遍历所有 “位”,并累加每个位的结果

while 循环的核心作用是逐个处理数字的每一个位,从 “个位” 开始,逐步到 “十位”“百位”…… 直到 “最高位”(当 digit > n 时,更高位不存在,循环结束)。

以 n=13 为例,看循环如何工作:
  1. 第一次循环(digit=1,处理个位)

    • 拆分:higher=13//(1×10)=1,curr=(13//1)%10=3,lower=13%1=0;
    • curr>1,个位 1 的次数 =(1+1)×1=2(对应数字:1、11);
    • 累加结果:res=2。
  2. 第二次循环(digit=10,处理十位)

    • 拆分:higher=13//(10×10)=0,curr=(13//10)%10=1,lower=13%10=3;
    • curr==1,十位 1 的次数 = 0×10 + 3+1=4(对应数字:10、11、12、13);
    • 累加结果:res=2+4=6。
  3. 第三次循环(digit=100,判断 100>13,循环结束)

最终 res=6,即 1 到 13 中 1 的总次数 —— 这正是通过循环遍历 “个位” 和 “十位”,并累加两个位的结果得到的。

四、总结:分情况计算与循环的关系
逻辑模块作用举例(n=13)
分情况计算计算 “单个位” 上 1 的出现次数算个位:2 次;算十位:4 次
while 循环遍历 “所有位”,并累加每个位的结果遍历个位→十位,累加 2+4

python 代码

def count_ones(n: int) -> int:if n < 1:return 0  # 1到n无数字,返回0res = 0digit = 1  # 位因子,从个位开始(10^0)while digit <= n:# 拆分高位、当前位、低位higher = n // (digit * 10)  # 当前位左侧的高位部分curr = (n // digit) % 10    # 当前位的数字lower = n % digit           # 当前位右侧的低位部分# 根据当前位数字计算1出现的次数if curr < 1:# 当前位小于1,高位每取0~higher-1时,当前位可填1,低位任意res += higher * digitelif curr == 1:# 当前位等于1,高位0~higher-1时共higher*digit次,高位为higher时低位0~lower共lower+1次res += higher * digit + lower + 1else:# 当前位大于1,高位0~higher时,当前位可填1,低位任意res += (higher + 1) * digitdigit *= 10  # 移动到下一位(十位→百位→...)return res# 读取输入并输出结果
n = int(input())
print(count_ones(n))

ZJIO2010 题目分析 

链接:https://ac.nowcoder.com/acm/contest/385/1037 来源:牛客网 给定两个正整数a和b,求在[a,b]中的所有整数中,每个数码(digit)各出现了多少次。 输入描述: 输入文件中仅包含一行两个整数a、b,含义如上所述。 输出描述: 输出文件中包含一行10个整数,分别表示0-9在[a,b]中出现了多少次。

示例1

输入 :1 99

输出 :9 20 20 20 20 20 20 20 20 20

本题要求统计区间 [a, b] 内所有整数中 0-9 每个数码出现的总次数。由于 a 和 b 可能高达 10^12,直接遍历每个数字逐位统计会超时,因此需要采用 数位统计法,通过数学分析高效计算每个数码在每一位(个位、十位、百位等)的出现次数。

核心思路

  1. 前缀和转化:区间 [a, b] 中数码 d 的出现次数 = count(b, d) - count(a-1, d),其中 count(x, d) 表示 0 到 x 中数码 d 出现的总次数

  2. 数位拆分与计算:对于 count(x, d),将数字 x 按位拆分为 高位(higher)、当前位(curr)、低位(lower),结合位因子(digit,如个位为 1,十位为 10)分情况计算当前位上 d 的出现次数:

    • 若 curr < d:次数 = higher * digit
    • 若 curr == d:次数 = higher * digit + lower + 1
    • 若 curr > d:次数 = (higher + 1) * digit
  3. 特殊处理数码 0:0 不能作为数字的首位(避免统计 “0012” 这类无效数字中的前导 0),需修正高位计算逻辑。

     代码

    

def count_digits(x: int, d: int) -> int:"""计算 0 ~ x 中数码 d 出现的总次数"""if x < 0:return 0res = 0digit = 1  # 位因子:从个位开始(10^0)while digit <= x:higher = x // (digit * 10)  # 高位部分(当前位左侧)curr = (x // digit) % 10    # 当前位数字lower = x % digit           # 低位部分(当前位右侧)if d == 0:# 0 不能作为高位开头,需特殊处理if higher == 0:# 高位为 0 时,无有效的前导 0,跳过passelse:# 修正高位为 higher-1,避免统计前导 0if curr > 0:res += higher * digitelif curr == 0:res += (higher - 1) * digit + lower + 1else:# 非 0 数码的常规计算if curr < d:res += higher * digitelif curr == d:res += higher * digit + lower + 1else:res += (higher + 1) * digitdigit *= 10  # 处理下一位(十位→百位→...)return resdef main():import sys# 高效读取输入(适配大规模数据)a, b = map(int, sys.stdin.read().split())# 计算 0-9 每个数码在 [a, b] 中的出现次数result = []for d in range(10):cnt_b = count_digits(b, d)cnt_a_1 = count_digits(a - 1, d)result.append(str(cnt_b - cnt_a_1))# 按要求输出(空格分隔的 10 个整数)print(' '.join(result))if __name__ == "__main__":main()
  1. count_digits(x, d) 函数:核心函数,用于计算 0 到 x 中数码 d 的总出现次数。通过遍历每一位(从个位到最高位),拆分高位、当前位、低位,根据当前位与 d 的大小关系计算次数,特别处理了数码 0 以避免前导 0 的误统计。

  2. main 函数:读取输入的 a 和 b,通过 count_digits(b, d) - count_digits(a-1, d) 计算每个数码在 [a, b] 中的出现次数,最后格式化输出结果。

  3. 示例验证(输入 1 99)

    • 对于数码 0:count(99, 0) = 9(10,20,...,90),count(0, 0) = 1,结果为 9 - 1 = 9
    • 对于数码 1-9(以 1 为例):count(99, 1) = 20(个位 10 次:1,11,...,91;十位 10 次:10-19),count(0, 1) = 0,结果为 20 - 0 = 20,与示例输出一致。

复杂度分析

  • 时间复杂度:O (log₁₀ max (a, b)),仅需遍历数字的每一位(10¹² 最多 13 位),与区间长度无关。
  • 空间复杂度:O (1),仅使用常数个变量存储中间结果。

该方案高效处理大规模输入,完全满足题目要求。

一、为何 1e12 规模下暴力法不可行?

  • 若 b - a = 1e12(如 a=1b=1e12),暴力遍历需处理 1e12 个数字,即使每个数字仅需 1 纳秒(10⁻⁹ 秒),总耗时也需 1e12 × 1e-9 = 1000 秒(约 16 分钟),远超题目 1-2 秒的时间限制。
  • 数位统计法通过 按位拆解计算,无需遍历任何数字,仅需处理 13 位(1e12 是 13 位数:1000000000000),耗时可忽略不计。

二、1e12 规模下的核心适配点

数位统计法的核心逻辑(前缀和 + 位拆分)对 1e12 规模完全兼容,仅需注意以下细节:

1. 位因子的范围

位因子 digit 从 1(个位)逐步增长到 1e12(万亿位),循环次数仅 13 次(1→10→100→…→1e12),不会产生性能压力。

2. 大整数的处理

Python 原生支持任意大小的整数,无需担心 1e12 级别的数字溢出,计算 higher = x // (digit×10)curr = (x//digit)%10 等操作均能正常执行。

3. 数码 0 的特殊处理(关键适配)

对于 1e12 这样的大数字,前导 0 的误统计风险依然存在(如计算 0-999...999 中 0 的次数时),需严格执行 0 的特殊处理逻辑:

  • 当 d=0 时,高位 higher 需减 1(避免统计 00123 这类无效数字中的前导 0);
  • 仅当 higher > 0 时才计算当前位 0 的次数。

三、1e12 规模下的计算示例(以 d=1x=1e12 为例)

x=1000000000000(13 位数,首位为 1,其余为 0),计算 0~1e12 中 1 的出现次数:

  1. 处理万亿位(digit=1e12)

    • higher=0curr=1lower=0
    • curr==1,次数 = 0×1e12 + 0+1 = 1(对应数字 1e12)。
  2. 处理千亿位到个位(digit=1e11 ~ 1)

    • 以十亿位(digit=1e10)为例:higher=10curr=0lower=0
    • curr < 1,次数 = 10 × 1e10 = 1e11
    • 其余 11 位计算逻辑类似,累加后总次数为 1 + 12×1e11 = 120000000001

五、方案优势总结

指标暴力法(不可行)数位统计法(可行)
时间复杂度O((b-a) × log₁₀ b)O (log₁₀ b)(仅 13 次循环)
空间复杂度O(1)O(1)
适配规模≤1e6(30% 数据)≤1e18(完全适配)
核心优势逻辑简单极致高效,无规模限制

六、核心代码解  d==0 时候

if d == 0:# 0 不能作为高位开头,需特殊处理if higher == 0:# 高位为 0 时,无有效的前导 0,跳过passelse:# 修正高位为 higher-1,避免统计前导 0if curr > 0:res += higher * digitelif curr == 0:res += (higher - 1) * digit + lower + 1   举例说明

前提:d=0 的核心问题

非 0 数码(如 1-9)可以作为数字的首位,因此统计时高位可直接取 0~higher;但 0 不能作为首位,若按非 0 数码的逻辑计算,会误统计 “00xx”“0xxx” 等无效数字中的前导 0。因此需要修正高位的取值范围(通常减 1),避免前导 0。

定义复用

百位为例(digit=100),对任意数字 n 拆分:

  • higher:百位左侧的高位(如 n=3045higher=3,即千位的 3);
  • curr:百位的数字(如 n=3045curr=0);
  • lower:百位右侧的低位(如 n=3045lower=45,即十位 + 个位的 45);
  • digit=100:百位的位因子。

例子 1:curr==0(对应公式 res += (higher - 1) * digit + lower + 1

场景:n=3045,统计百位为 0的次数(d=0digit=100

1. 位拆分结果

higher=3(千位),curr=0(百位),lower=45(低位),digit=100

2. 为什么需要 higher-1

若按非 0 数码的逻辑(curr==d 时用 higher*digit + lower+1),会计算 3*100 +45+1=346,但其中包含了 “0000~0099”(前导 0,无效)、“1000~1099”(有效)、“2000~2099”(有效)、“3000~3045”(有效)—— 但 “0000~0099” 实际是 0~99,这些数字没有百位(或说百位是 “隐性 0”,不应计入),因此必须减去这部分无效统计。

修正方式:将高位 higher 减 1(3-1=2),表示高位只能取 0~2,但需进一步区分:

  • 高位取 0~1:对应数字 0000~0099(无效)、1000~1099(有效)—— 实际有效部分是 1000~1099(100 个)、2000~2099(100 个),共 2*100=200 个;
  • 高位取 2(即 higher-1=2 的最大值):对应数字 3000~3045(百位为 0,有效),共 45+1=46 个(lower+1)。
3. 公式计算与结果匹配

(higher-1)*digit + lower +1 = (3-1)*100 +45+1= 200+46=246

实际有效数字范围(百位为 0):

  • 1000~1099(100 个)、2000~2099(100 个)、3000~3045(46 个),合计 246 个,与公式结果一致。

例子 2:curr>0(对应公式 res += higher * digit

场景:n=3245,统计百位为 0的次数(d=0digit=100

1. 位拆分结果

higher=3(千位),curr=2(百位 > 0),lower=45(低位),digit=100

2. 为什么无需 lower+1

当 curr>0 时,百位为 0 的数字需满足:高位取 0~higher,百位固定为 0,低位取 0~99digit=100 种可能)。但由于 0 不能作为首位,需排除高位为 0 时的无效数字(0000~0099)。

  • 高位取 00000~0099(无效,不计入);
  • 高位取 1~31000~1099(100 个)、2000~2099(100 个)、3000~3099(100 个),共 3*100=300 个。

此时 higher=3,直接用 higher*digit=3*100=300 即可覆盖所有有效数字 —— 因为 curr>0 时,高位取 0~higher 中,仅高位 > 0 的部分有效,且恰好是 higher 组(每组 digit 个),无需额外加 lower+1(低位可任意取,不受 curr>0 限制)。

3. 结果验证

实际有效数字范围(百位为 0):1000~10992000~20993000~3099,合计 300 个,与公式结果一致。

d=0 特殊处理的本质

情况公式核心逻辑(排除前导 0)
curr == 0(higher-1)*digit + lower+1有效高位取 1~higher-1(共 higher-1 组,每组 digit 个);高位取 higher 时,低位受 lower 限制(lower+1 个)。
curr > 0higher * digit有效高位取 1~higher(共 higher 组,每组 digit 个),因 curr>0 时低位可任意取,无需额外限制。
higher == 0跳过高位为 0 时,所有含当前位 0 的数字均为 “前导 0”(如 00xx),无有效统计意义。

(curr > 0 时)

以 n=3245(百位 curr=2>0higher=3)为例:

  • 有效高位是 1~3(共 3 组),对应数字:1000~1099(100 个)、2000~2099(100 个)、3000~3099(100 个),合计 3×100=300 个,与 higher×digit 完全一致。
  • 若包含高位 = 0(0000~0099),这些数字实际是 0~99(无百位),其 “百位 0” 是前导 0,应排除 —— 因此有效高位必须从 1 开始,共 higher 组。
http://www.dtcms.com/a/394152.html

相关文章:

  • Kafka面试精讲 Day 21:Kafka Connect数据集成
  • MySQL 主从复制完整配置指南
  • 力扣每日一刷Day 23
  • LeetCode 53. 最大子数组和(四种解题思路)包含扩展返回最大和的数组
  • RTX 4090助力深度学习:从PyTorch到生产环境的完整实践指南——高效模型训练与优化策略
  • 23种设计模式之【桥接模式】-核心原理与 Java实践
  • LabVIEW手部运动机能实验
  • 669. 修剪二叉搜索树
  • 大QMT自动可转债申购
  • PolarCTF PWN 网络安全2023秋季个人挑战赛刷题
  • MySQL-day4_02(事务)
  • JUC(8)线程安全集合类
  • springboot中@EnableAsync有什么作用
  • Spark专题-第二部分:Spark SQL 入门(6)-算子介绍-Generate
  • C#练习题——Dictionary
  • Feign
  • SPA小说集之三《森林城市反甩锅战:ERP的权责边界》
  • Qt(模态对话框和非模态对话框)
  • 【无标题】物联网 frid卡控制
  • 【LLM LangChain】 模型绑定工具+调用工具(手动调用/LangGraph/AgentExecutor)+相关注意事项
  • 图神经网络(GNN)入门:用PyG库处理分子结构与社会网络
  • 【C++】编码表 STL简介:STL是什么,版本,六大组件,重要性以及学习方法总结
  • show_interrupts函数的进一步解析及irq_desc结构体
  • Kafka面试精讲 Day 19:JVM调优与内存管理
  • 10.vector容器
  • Linux系统介绍
  • MFC中的CMFCDynamicLayout类的介绍
  • UniScene 统一驾驶场景 | 生成语义占据 | 生成多视角视频 | 生成激光点云 CVPR2025
  • Git 简明教程:从原理到实战
  • 【设计模式】中介者模式