链式舍入与直接舍入不同的数恰好落在每个位数下 [44...45, 49...9] 的区间中,逐段计数即可。
OJ: luogu
题目 ID: P11449
难度:普及-
标签:数学模拟usacopython
日期: 2026-07-11 12:32
同题说明
洛谷 P11449 与 USACO 1443 是同一道题,完整题目解析请见:
题意
给定正整数
- 直接舍入到
:看从右往左第 位数字 ,若 则加 ,然后把最低 位全部置 。 - 链式舍入到
:先舍入到 ,再舍入到 ,……,最后舍入到 。
思路
先看暴力模拟,帮助理解两种舍入的区别:
# brute.py:小数据暴力解,模拟 Bessie 舍入和 Elsie 链式舍入,逐个数检查。
import sys
from functools import reduce
# ==========================================
# 原始写法:按题目描述,提取数位,直观易懂
# ==========================================
def round_to_original(a, b):
"""Bessie 舍入原始版本:将 a 四舍五入到最接近的 10^b。"""
# 提取第 b 位数字(从右往左)
digit = (a // (10 ** (b - 1))) % 10
if digit >= 5:
a += 10 ** b
# 把最低的 b 位全部置 0
a = (a // (10 ** b)) * (10 ** b)
return a
def chain_round_original(a, b):
"""Elsie 链式舍入原始版本:先舍入到 10^1,再 10^2,...,直到 10^b。"""
for i in range(1, b + 1):
a = round_to_original(a, i)
return a
# ==========================================
# Pythonic 写法:数学简化 + 高阶函数
# ==========================================
def round_to_pythonic(a, b):
"""Bessie 舍入:加半取整法
(a + 10^b // 2) // 10^b * 10^b
"""
p = 10 ** b
return (a + p // 2) // p * p
def chain_round_A(a, b):
"""Elsie 链式舍入(方案 A):递推维护幂次"""
p = 10
for _ in range(b):
a = (a + p // 2) // p * p
p *= 10
return a
def chain_round_B(a, b):
"""Elsie 链式舍入(方案 B):函数式 reduce 写法"""
return reduce(round_to_pythonic, range(1, b + 1), a)
def get_P(x):
"""P = 满足 10^P >= x 的最小正整数。"""
p = 0
v = 1
while v < x:
p += 1
v *= 10
return p
def solve():
data = sys.stdin.buffer.read().split()
T = int(data[0])
out = []
for i in range(1, T + 1):
N = int(data[i])
ans = 0
for x in range(2, N + 1):
P = get_P(x)
# 在这里,我们可以自由切换原始写法或 Pythonic 写法进行验证
if round_to_original(x, P) != chain_round_original(x, P):
ans += 1
out.append(str(ans))
sys.stdout.write('\n'.join(out) + '\n')
if __name__ == '__main__':
solve()这个暴力对每个
关键观察
手动枚举小数据可以发现规律:
| 不同的 |
个数 | |
|---|---|---|
| 2 | 5 | |
| 3 | 55 | |
| 4 | 555 |
规律:对于
个数为
为什么是这个区间
- 直接舍入到
只看第 位(最高位)。最高位 则结果为 ,否则为 。 - 链式舍入会从低位向高位逐级进位。当最高位恰好是
时,低位的连锁进位可能把它推成 ,使链式结果变成 ,而直接舍入仍是 。 - 最高位
时,即使低位全部进位也推不到 ;最高位 时,直接和链式都得到 。所以只有最高位 时才可能不同。 - 低位需要满足"链式舍入能产生进位"的条件,这恰好是一个递归结构:
位低位数 时才会进位。
最终做法
对每个
其中
Python 知识
本题用到的 Python 模式与优化:
-
原汁原味的数位提取: 提取第
b位数字的常见写法是(a // (10 ** (b - 1))) % 10。 将尾部归零的常见写法是(a // (10 ** b)) * (10 ** b)。这在理解题意时最直观(见brute.py中的round_to_original)。 -
Pythonic 舍入:加半取整法 Python 的自带
round()使用的是“银行家舍入法”(遇到 5 向偶数舍入),不符合本题意。可以用数学运算代替复杂的“提取数位”逻辑: 要对进位并把低位置零,只需: (a + p // 2) // p * p(其中p = 10**b)。 这种写法利用了//整除不会产生浮点误差的特性,是处理向下/向上取整和进位的最佳范式。 -
递推优化或函数式编程(
reduce) 在brute.py的链式舍入中,如果要在循环中多次计算次幂,用p *= 10递推维护代替10**b会更快(方案 A)。如果追求极致代码简短,可以使用高阶函数reduce将链式调用浓缩为一行(方案 B)。 -
sys.stdin.buffer.read().split():一次读入全部输入并按空白切分,比逐行input()快很多。参见 Python 竞赛输入输出与字符串处理 中"按 token 读取整份输入"一节。 -
'\n'.join(out):先把所有答案攒到列表,最后一次性拼接输出,避免多次print的开销。
C++ → Python 对照:
| C++ | Python |
|---|---|
scanf / cin |
sys.stdin.buffer.read().split() |
printf / cout |
sys.stdout.write('\n'.join(...)) |
long long |
int(自动大整数) |
模仿清单:
# 1. 一次读入全部 token
data = sys.stdin.buffer.read().split()
# 2. 攒答案再一次性输出
out = []
out.append(str(ans))
sys.stdout.write('\n'.join(out) + '\n')
# 3. 直观写法:提取数位与归零
def round_to_original(a, b):
digit = (a // (10 ** (b - 1))) % 10
if digit >= 5: a += 10 ** b
return (a // (10 ** b)) * (10 ** b)
# 4. Pythonic 进位舍入(加半取整,规避自带 round 的坑)
def round_to_pythonic(a, b):
p = 10 ** b
return (a + p // 2) // p * p
# 5. 链式操作(函数式写法)
from functools import reduce
def chain_round(a, b):
return reduce(round_to_pythonic, range(1, b + 1), a)代码
import sys
def main():
LU = []
for d in range(2, 11):
L = 4 * 10 * (10**(d-1) - 1) // 9 + 5
U = 5 * 10**(d-1) - 1
LU.append((L, U))
data = sys.stdin.buffer.read().split()
T = int(data[0])
out = []
for i in range(1, T + 1):
N = int(data[i])
ans = 0
for L, U in LU:
if N >= L:
ans += min(U, N) - L + 1
else:
break
out.append(str(ans))
sys.stdout.write('\n'.join(out) + '\n')
main()复杂度
- 时间:预计算
,每个查询 ,总计 - 空间:
存查找表 + 存输出
总结
这道题的核心是发现"链式进位"的递归结构:只有最高位为