用 Rust 刷 Codewars 之 Color choice
问题描述
以下为本人简单翻译的描述
组合数: 举个例子, 从 52 个卡牌中抽取 5 个卡牌, 会有 2,598,960 种情况, 这个数就是组合数.
在数学中, 从 n 个元素中取出的 x 个组合被称为 n 和 x 的二项式系数, 或者称为组合数 C(n, x)
. n 个数计算第 x 个数 大小的公式:
m = n! / ( x! * (n - x)!)
.
其中 !
是阶乘运算符.
现在假设你是一个着名的海报设计师和画家. 你被要求提供 6 张具有相同设计的海报, 每张海报用 2 种颜色. 海报必须有不同的颜色组合, 你有 4 种可供选择的颜色: 红色, 蓝色, 黄色, 绿色. 那么你可以为每张海报选择多少种颜色?
答案是 2, 因为组合数 C(4, 2) = 6
. 组合将是: {红, 蓝}, {红, 黄}, {红, 绿}, {蓝, 黄}, {蓝, 绿}, {黄, 绿 }.
现在同样的问题, 但是你要提供 35 张海报, 有 7 种颜色可用. 每张海报有多少种颜色? 如果你用上述公式计算 C(7, 2)
, 会得到 21. 但是 35 张海报如果只有 21 种组合是不够用的. 如果你采取 C(7, 5)
, 你同样会得到21. 幸运的是, 如果你采用 C(7, 3)
或者 C(7, 4)
, 正好是 35, 所以每个海报将有 3 种或 5 种不同颜色的组合. 这种情况你会采取 3 种颜色, 因为它更便宜.
问题如下:
知道 m (海报的数量), 知道 n (可用颜色的总数), 找出一个 x (每个海报的颜色数量, 使得每个海报具有唯一的颜色组合, 并且组合的数量与海报的数量是完全相同的).
换句话说, 对于给定的 m, n, 我们必须找到满足公式 C(n, x) = m ★
的 x(m >= 0且 n > 0)
. 如果 x 有多个解, 则给出最小的 x. 当给出的 m 没有 x 满足等式 ★
时, 返回 -1.
例子:
checkchoose(6, 4) --> 2
checkchoose(4, 4) --> 1
checkchoose(4, 2) --> -1
checkchoose(35, 7) --> 3
checkchoose(36, 7) --> -1
a = 47129212243960
checkchoose(a, 50) --> 20
checkchoose(a + 1, 50) --> -1
clojure: Use big integers in the calculation of C(n, k)!
题解 1
看到这题的时候感觉很简单, 一个阶乘公式, 一个组合数公式, 然后在 main 函数中组合起来就行了.
代码如下:
fn check_choose(m: u64, n: u64) -> i64 {
let mut result: i64 = -1;
for x in 1..n {
if get_choices(n, x) == m {
result = x as i64;
break;
}
}
result
}
fn get_choices (n: u64, x: u64) -> u64 {
factorial(n) / factorial(x) / factorial(n - x)
}
fn factorial(number: u64) -> u64 {
fn fac_iter(nth: u64, product: u64) -> u64 {
if nth <= 1 {
product
} else {
fac_iter(nth - 1, nth * product)
}
}
fac_iter(number, 1)
}
很简单嘛, 连阶乘我都优化了下递归. 复制粘贴, 跑下例子, 没问题. 提交! 然后就报 Overflow 了. ಥ_ಥ
题解 2
自己测试一下, 发现数字小的时候没问题, 大了之后乘法就会溢出.
这个简单, 改成用大数就行了, 然后发现 u64
在标准库里是最大的, 但是 crates 上有 bigint, 问题的提示也这么说. 我就改成了下边的代码(只列出了 factorial).
fn factorial(number: u64) -> BigUint {
let number = number.to_biguint().unwrap();
fn fac_iter(nth: BigUint, product: BigUint) -> BigUint {
if nth <= One::one() {
product
} else {
fac_iter(nth.clone().sub(1.to_biguint().unwrap()), nth * product)
}
}
fac_iter(number, One::one())
}
提交测试, 发现用不了第三方库, 想想也知道. ಥ_ಥ
题解 3
既然用不了第三方库, 那容我来优化一下 factorial
函数.
本来想实现 factorial
返回一个惰性值, 然后在 get_choices
函数里怎么计算不会溢出. 然后发现自己想错了, 直接优化 get_choices
, 不要用 factorial
函数就行了. 几个加减乘除, u64
不会溢出.
代码如下 (未列出 check_choose
):
fn get_choices (n: u64, x: u64) -> u64 {
if x == 1 {
n
} else {
get_choices(n, x - 1) * (n - x + 1) / x
}
}
递归实现了, 复制粘贴, 测试通过, 提交!
题解 4
上边用递归实现了, 但是本着完美主义的心, 必须优化一下啊.
下边是优化后的代码, check_choose
也稍优化.
最终代码:
fn check_choose(m: u64, n: u64) -> i64 {
let mut result: i64 = -1;
let h = (n + 2) / 2;
for x in 1..h {
if get_choices(n, x) == m {
result = x as i64;
break;
}
}
result
}
fn get_choices(n: u64, x: u64) -> u64 {
fn get_choices_iter(n:u64, x: u64, c: u64, r: u64) -> u64 {
let r = r * (n - c + 1) / c;
if c < x {
get_choices_iter(n, x, c + 1, r)
} else {
r
}
}
get_choices_iter(n, x, 1, 1)
}
总结
上边 get_choices
依然不完美, get_choices_iter
参数使用了外部变量 n, x. 总感觉可以去掉, 写成 closure.
我使用匿名函数时发现没法递归了, 暂时不知道该怎么办了.
代码其实就是一点一点优化过来的, 有些书上告诉我们要想好再写代码, 有些书上又警告我们不要过度设计, 但是这个度在哪里 只能靠多谢代码来锻炼了, 增加一些经验, 更容易发现问题的关键所在.