【算法】【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核心思路
- 按位分析:将数字 n 按位(个位、十位、百位...)拆分,分别计算每一位上 “1” 出现的次数,最后累加总和。
- 位拆分定义:对于第 i 位(从右数,个位为第 0 位,位因子为 10ⁱ):
higher
:当前位左侧的高位部分;curr
:当前位的数字;lower
:当前位右侧的低位部分;digit
:当前位的位因子(如个位为 1,十位为 10)。
- 分情况计算:根据当前位数字
curr
与 1 的大小关系,计算该位上 “1” 出现的次数:- 若
curr < 1
:次数 =higher * digit
; - 若
curr == 1
:次数 =higher * digit + lower + 1
; - 若
curr > 1
:次数 =(higher + 1) * digit
- 若
2.2示例解析(输入 n=13)
-
个位分析(digit=1):
higher = 13 // 10 = 1
,curr = 3
,lower = 0
;curr > 1
,次数 =(1 + 1) * 1 = 2
(个位为 1 的数:1、11)。
-
十位分析(digit=10):
higher = 13 // 100 = 0
,curr = 1
,lower = 3
;curr == 1
,次数 =0 * 10 + 3 + 1 = 4
(十位为 1 的数:10、11、12、13)。
-
总和: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=100
,digit*10=1000
higher = 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=100
,digit*10=1000
higher = 3145 // 1000 = 3
(千位的 3)curr = (3145 // 100) % 10 = 1
(百位的数字)lower = 3145 % 100 = 45
(百位右侧的低位:45) -
当前位要为 “1”(即百位 = 1),需分两种子情况:
-
higher' < higher
(即 0~2):数字形式为[0~2] 1 [lower']
(如 0100~0199、1100~1199、2100~2199),均 ≤ 3145,合法。数量 =higher × digit = 3 × 100 = 300
。 -
higher' == higher
(即 3):数字形式为3 1 [lower']
,需满足整体 ≤ 3145。此时lower'
最大只能取 45(原数的低位),即0~45
,共lower + 1
种可能(46 种)。
-
-
总数 = 300 + 46 = 346
情况 3:curr > 1
示例:n=3245
(百位是 2)
-
正确拆分:
digit=100
,digit*10=1000
higher = 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 为例,看循环如何工作:
-
第一次循环(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。
-
第二次循环(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。
-
第三次循环(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
,直接遍历每个数字逐位统计会超时,因此需要采用 数位统计法,通过数学分析高效计算每个数码在每一位(个位、十位、百位等)的出现次数。
核心思路
-
前缀和转化:区间
[a, b]
中数码d
的出现次数 =count(b, d) - count(a-1, d)
,其中count(x, d)
表示 0 到 x 中数码 d 出现的总次数。 -
数位拆分与计算:对于
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
- 若
-
特殊处理数码 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()
-
count_digits(x, d)
函数:核心函数,用于计算 0 到 x 中数码 d 的总出现次数。通过遍历每一位(从个位到最高位),拆分高位、当前位、低位,根据当前位与 d 的大小关系计算次数,特别处理了数码 0 以避免前导 0 的误统计。 -
main
函数:读取输入的a
和b
,通过count_digits(b, d) - count_digits(a-1, d)
计算每个数码在[a, b]
中的出现次数,最后格式化输出结果。 -
示例验证(输入 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
,与示例输出一致。
- 对于数码 0:
复杂度分析
- 时间复杂度:O (log₁₀ max (a, b)),仅需遍历数字的每一位(10¹² 最多 13 位),与区间长度无关。
- 空间复杂度:O (1),仅使用常数个变量存储中间结果。
该方案高效处理大规模输入,完全满足题目要求。
一、为何 1e12
规模下暴力法不可行?
- 若
b - a = 1e12
(如a=1
,b=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=1
,x=1e12
为例)
x=1000000000000
(13 位数,首位为 1,其余为 0),计算 0~1e12 中 1 的出现次数:
-
处理万亿位(digit=1e12):
higher=0
,curr=1
,lower=0
;curr==1
,次数 =0×1e12 + 0+1 = 1
(对应数字1e12
)。
-
处理千亿位到个位(digit=1e11 ~ 1):
- 以十亿位(digit=1e10)为例:
higher=10
,curr=0
,lower=0
; curr < 1
,次数 =10 × 1e10 = 1e11
;- 其余 11 位计算逻辑类似,累加后总次数为
1 + 12×1e11 = 120000000001
。
- 以十亿位(digit=1e10)为例:
五、方案优势总结
指标 | 暴力法(不可行) | 数位统计法(可行) |
---|---|---|
时间复杂度 | 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=3045
,higher=3
,即千位的 3);curr
:百位的数字(如n=3045
,curr=0
);lower
:百位右侧的低位(如n=3045
,lower=45
,即十位 + 个位的 45);digit=100
:百位的位因子。
例子 1:curr==0
(对应公式 res += (higher - 1) * digit + lower + 1
)
场景:n=3045
,统计百位为 0的次数(d=0
,digit=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=0
,digit=100
)
1. 位拆分结果
higher=3
(千位),curr=2
(百位 > 0),lower=45
(低位),digit=100
。
2. 为什么无需 lower+1
?
当 curr>0
时,百位为 0 的数字需满足:高位取 0~higher
,百位固定为 0,低位取 0~99
(digit=100
种可能)。但由于 0 不能作为首位,需排除高位为 0 时的无效数字(0000~0099
)。
- 高位取
0
:0000~0099
(无效,不计入); - 高位取
1~3
:1000~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~1099
、2000~2099
、3000~3099
,合计 300 个,与公式结果一致。
d=0
特殊处理的本质
情况 | 公式 | 核心逻辑(排除前导 0) |
---|---|---|
curr == 0 | (higher-1)*digit + lower+1 | 有效高位取 1~higher-1 (共 higher-1 组,每组 digit 个);高位取 higher 时,低位受 lower 限制(lower+1 个)。 |
curr > 0 | higher * digit | 有效高位取 1~higher (共 higher 组,每组 digit 个),因 curr>0 时低位可任意取,无需额外限制。 |
higher == 0 | 跳过 | 高位为 0 时,所有含当前位 0 的数字均为 “前导 0”(如 00xx ),无有效统计意义。 |
(curr > 0
时)
以 n=3245
(百位 curr=2>0
,higher=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
组。