引言
视频封面:
今天在castelu大佬评论区看到这样一条评论:
我们不禁要问:能否仅根据BMI反推身高和体重?答案是肯定的,但有没有更优雅的做法?
本文 52pojie: www.52pojie.cn/thread-2132…
本文 博客园: www.cnblogs.com/hans77/arti…
本文 juejin: juejin.cn/post/769633…
作者:hans774882968以及hans774882968以及hans774882968以及hans77
建模前的约定
我们知道BMI的计算公式:
为了方便数学建模,不妨先结合现实生活进行一些约定:
- 体重的单位是kg,所以精确到1位小数,比如114.5kg
- BMI使用的身高的单位是米,所以身高精确到3位小数,比如1.919米
- 有了前两条约定,B显然是有理数,不妨设为
初步思路
进行上述约定后,凭Hans多年的算法竞赛经验(金(dǎ)牌(tiě)水平),一瞬间就想到,约定一个身高上限(因为很难见到3米以上的人类),便能直接枚举范围内所有可能的身高,用公式 看何时能得到一位小数的体重即可。
虽然能解决问题,但局限性有点强啊……有没有更好的方法呢?有的。
Hans的想法:质因数分解
回顾BMI的计算公式,令 ,则W和H都为正整数,代入BMI公式:
这里同学们不妨思考一下:为什么不变形为 等于xxx,而是这样变形呢?
也就是说,我们希望找到最小的正整数H,使得 是 的倍数,这就是道数论题了。为了方便,不妨设BMI已经是最简分数,即P和Q互素,于是P对倍数关系没有影响,问题转化为:
求最小的正整数H,使得 是 的倍数
这时,等式的分子只剩H,足够纯净,我们便不难想到:考虑 的质因数分解
为了让 包含 的所有质因数, 至少要包含 个 ,因此,最小的
对D质因数分解后立刻就能求出H,进而求出W。数据范围不太大的情况下,直接用试除法对D质因数分解即可,时间复杂度 。最后,我们把P和Q互素的假设去掉,1式只需稍作修改:
因为 和 互素,所以只需要把上面假设的D改成
完整代码:
import math
from fractions import Fraction
def prime_factors(n):
"""对整数 n 进行质因数分解,返回字典 {素数: 幂次}"""
factors = {}
while n % 2 == 0:
factors[2] = factors.get(2, 0) + 1
n //= 2
d = 3
while d * d <= n:
while (n % d) == 0:
factors[d] = factors.get(d, 0) + 1
n //= d
d += 2
if n > 1:
factors[n] = factors.get(n, 0) + 1
return factors
def find_exact_hw(bmi_value):
"""
根据精确的有理数 BMI,构造出最小的一组
身高精确到3位小数、体重精确到1位小数的 (h, w)
"""
if isinstance(bmi_value, float):
bmi_frac = Fraction(bmi_value).limit_denominator(10**9)
else:
bmi_frac = Fraction(bmi_value)
P = bmi_frac.numerator
Q = bmi_frac.denominator
if P <= 0 or Q <= 0:
raise ValueError("BMI 必须是正有理数")
# 1. 计算有效分母 D = 100000 * Q / gcd(P, 100000 * Q)
denom = 100000 * Q
g = math.gcd(P, denom)
D = denom // g
# 2. 对 D 进行质因数分解
factors = prime_factors(D)
# 3. 构造最小的 H,使得 H^2 是 D 的倍数
H = 1
for p, alpha in factors.items():
# 幂次向上取整:ceil(alpha / 2)
power = (alpha + 1) // 2
H *= (p ** power)
# 4. 计算对应的 W = P * H^2 / (100000 * Q)
# 因为 H 的构造保证了 H^2 是 D 的倍数,这里必定能整除
W = (P * H * H) // (100000 * Q)
# 还原为3位小数的身高和1位小数的体重
h = H / 1000.0
w = W / 10.0
return h, w, H, W
if __name__ == "__main__":
test_cases = [
25, # 整数
18.5, # 常见 BMI
"3.14159", # 字符串输入,避免浮点误差
Fraction(1, 3), # 分数 1/3
Fraction(22, 7), # 分数 22/7
Fraction(560000, 29241), # 56kg 1.71m
Fraction(565000, 29241), # 56.5kg 1.71m
Fraction(65000, 3249), # 58.5kg 1.71m
Fraction(30625, 1681), # 49kg 1.64m
Fraction(5952000, 104329), # 595.2kg 3.23m
Fraction(28625000, 1846881), # 114.5kg 2.718m
114.514,
810.1919,
]
for bmi in test_cases:
h, w, H, W = find_exact_hw(bmi)
print(f"目标 BMI: {bmi} | 最小解: h={h:<5}米, w={w:<5}千克 | 验证: {w} / {h}^2 = {w / (h**2):.5f}")
输出:
目标 BMI: 25 | 最小解: h=0.2 米, w=1.0 千克 | 验证: 1.0 / 0.2^2 = 25.00000
目标 BMI: 18.5 | 最小解: h=1.0 米, w=18.5 千克 | 验证: 18.5 / 1.0^2 = 18.50000
目标 BMI: 3.14159 | 最小解: h=100.0米, w=31415.9千克 | 验证: 31415.9 / 100.0^2 = 3.14159
目标 BMI: 1/3 | 最小解: h=3.0 米, w=3.0 千克 | 验证: 3.0 / 3.0^2 = 0.33333
目标 BMI: 22/7 | 最小解: h=3.5 米, w=38.5 千克 | 验证: 38.5 / 3.5^2 = 3.14286
目标 BMI: 560000/29241 | 最小解: h=0.855米, w=14.0 千克 | 验证: 14.0 / 0.855^2 = 19.15119
目标 BMI: 565000/29241 | 最小解: h=1.71 米, w=56.5 千克 | 验证: 56.5 / 1.71^2 = 19.32218
目标 BMI: 65000/3249 | 最小解: h=0.57 米, w=6.5 千克 | 验证: 6.5 / 0.57^2 = 20.00616
目标 BMI: 30625/1681 | 最小解: h=1.64 米, w=49.0 千克 | 验证: 49.0 / 1.64^2 = 18.21832
目标 BMI: 5952000/104329 | 最小解: h=1.615米, w=148.8千克 | 验证: 148.8 / 1.615^2 = 57.05029
目标 BMI: 28625000/1846881 | 最小解: h=2.718米, w=114.5千克 | 验证: 114.5 / 2.718^2 = 15.49910
目标 BMI: 114.514 | 最小解: h=10.0 米, w=11451.4千克 | 验证: 11451.4 / 10.0^2 = 114.51400
目标 BMI: 810.1919 | 最小解: h=100.0米, w=8101919.0千克 | 验证: 8101919.0 / 100.0^2 = 810.19190
最后公布前面思考题的答案:
- 为什么不变形为 等于xxx,而是这样变形呢?因为验证一个分式是完全平方数虽然可行(同学们不妨动手试试),但相比于验证一个分式是正整数,更为困难