排列
如果我们想知道在不重复的情况下在某个大小 n 的集合中有多少可能的排序(即,从可用池中删除所选元素), 的 factorial n(或 n!)给出了答案:
double factorial(int n)
{
if (n <= 0)
return 1;
else
return n * factorial(n - 1);
}
注意:这也有一个迭代解,甚至可以使用 gamma 函数来近似:
std::round(std::tgamma(n + 1)); // where n >= 0
问题集从所有 1s 开始。每次更改集合时,两个 1 被一个 2 替换。我们想找出 k 个项目(2s)可以排列成一组大小 n 的方式的数量。我们可以通过计算查询可能的排列数:
double permutation(int n, int k)
{
return factorial(n) / factorial(n - k);
}
但是,这并不是我们想要的结果。问题是,排列考虑排序,例如,序列 2,2,2 将被视为六个不同的变体。
组合
这些本质上是忽略排序的排列。由于顺序不再重要,因此许多排列是多余的。通过计算 k! 可以找到每个排列的冗余。将排列数除以该值得出组合数:
注意:这称为binomial coefficient,应该读作“n选择k。”
double combination(int n, int k)
{
return permutation(n, k) / factorial(k);
}
int solve(int n)
{
double result = 0;
if (n > 0) {
for ( int k = 0; k <= n; k += 1, n -= 1 )
result += combination(n, k);
}
return std::round(result);
}
这是一个通用的解决方案。例如,如果问题是要找出整数可以表示为 1s 和 3s 之和的方式数,我们只需要调整每次迭代时设置的大小 (n-2)。
斐波那契数列
使用斐波那契数的解决方案起作用的原因与它们与二项式系数的关系有关。二项式系数可以排列成Pascal's triangle,当存储为下三角矩阵时,可以使用 n 和 k 作为行/列索引来访问等于combination(n,k)的元素。
n 和 k 的模式随着它们在 solve 的生命周期内的变化,当被视为 2- 上的坐标时绘制对角线D 网格。沿着帕斯卡三角形的对角线求和的结果是一个斐波那契数。如果模式发生变化(例如,当找到 1s 和 3s 的总和时),情况将不再如此,此解决方案将失败。
有趣的是,斐波那契数可以在恒定时间内计算。这意味着我们可以通过找到第(n+1)个斐波那契数来在恒定时间内解决这个问题。
int fibonacci(int n)
{
constexpr double SQRT_5 = std::sqrt(5.0);
constexpr double GOLDEN_RATIO = (SQRT_5 + 1.0) / 2.0;
return std::round(std::pow(GOLDEN_RATIO, n) / SQRT_5);
}
int solve(int n)
{
if (n > 0)
return fibonacci(n + 1);
return 0;
}
最后一点,factorial 和 fibonacci 函数生成的数字可能非常大。因此,如果 n 很大,则可能需要大型数学库。