Calculate multinomial coefficient

烂漫一生 提交于 2019-12-04 15:43:49

I hope I'm not linkfarming here, but here is a process of work, to solve your problem :

  1. Naive implementations will always suffer from overflow errors. You have to be ready to exploit certain mathematical properties of the polynomial coefficient to reach a robust solution. Dave Barber does that in his library, where the recursive property is used (example for 4 numbers - the recursion stops when all branches are replaced by zero)

    multi (a, b, c, d) = multi (a − 1, b, c, d) + multi (a, b − 1, c, d) + multi (a, b, c − 1, d) + multi (a, b, c, d − 1)
    
  2. Based on the above, David Serrano Martínez shows how an implementation that provides overflow control can be divised. His code can be used as easily as

    unsigned long long result_2 = multinomial::multi<unsigned long long>({9, 8, 4});
    
  3. A third alternative would be to use (or learn from) libraries that are dedicated to combinatorics, like SUBSET. This is a bit more difficult code to read through due to dependencies and length, but invokation is as easy as

    int factors[4] = {1, 2, 3, 4};
    Maths::Combinatorics::Arithmetic::multinomial(4, factors)
    

You can calculate

by multiplying the numerator down from sum(ks) and dividing up in the denominator up from 1. The result as you progress will always be integers, because you divide by i only after you have first multiplied together i contiguous integers.
def multinomial(*ks):
    """ Computes the multinomial coefficient of the given coefficients
    >>> multinomial(3, 3)
    20
    >>> multinomial(2, 2, 2)
    90
    """
    result = 1
    numerator = sum(ks)
    ks = list(ks)       # These two lines are unnecessary optimizations
    ks.remove(max(ks))  # and can be removed
    for k in ks:
        for i in range(k):
            result *= numerator
            result //= i + 1
            numerator -= 1
    return result

I recently came across this problem, and my solution was to first map to log-space, do the work there, and then map back. This is helpful as we avoid the overflow issues in log-space, and also multiplications become sums which can be more efficient. It may also be useful to work directly with the log-space result.

The maths:

C(x1, ..., xn) = sum(x)! / (x1! * ... * xn!)

Therefore

ln C(x1, ..., xn) = ln sum(x)! - ln {(x1! * ... * xn!)}
                  = sum{k=1->sum(x)} ln k - sum(ln xi!)
                  = sum{k=1->sum(x)} ln k - sum(sum{j=1->xi} (ln j))

If any of the xi, or sum(x) are big (e.g. > 100), then we could actually just use Sterling's approximation:

ln x! ~= x * ln x - x

Which would give:

ln C(x1, ..., xn) ~= sum(x) * ln sum(x) - sum(x) - sum(xi * ln xi - xi)

Here's the code. It's helpful to first write a log factorial helper function.

#include <vector>
#include <algorithm>   // std::transform
#include <numeric>     // std::iota, std:: accumulate
#include <cmath>       // std::log
#include <type_traits> // std::enable_if_t, std::is_integral, std::is_floating_point

template <typename RealType, typename IntegerType,
          typename = std::enable_if_t<std::is_floating_point<RealType>::value>,
          typename = std::enable_if_t<std::is_integral<IntegerType>::value>>
RealType log_factorial(IntegerType x)
{
    if (x == 0 || x == 1) return 0;
    if (x == 2) return std::log(2); // can add more for efficiency

    if (x > 100) {
        return x * std::log(x) - x; // Stirling's approximation
    } else {
        std::vector<IntegerType> lx(x);
        std::iota(lx.begin(), lx.end(), 1);
        std::vector<RealType> tx(x);
        std::transform(lx.cbegin(), lx.cend(), tx.begin(),
                       [] (IntegerType a) { return std::log(static_cast<RealType>(a)); });
        return std::accumulate(tx.cbegin(), tx.cend(), RealType {});
    }
}

Then the log factorial function is simple:

template <typename RealType, typename IntegerType>
RealType log_multinomial_coefficient(std::initializer_list<IntegerType> il)
{
    std::vector<RealType> denoms(il.size());
    std::transform(il.begin(), il.end(), denoms.begin(), log_factorial<RealType, IntegerType>);
    return log_factorial<RealType>(std::accumulate(il.begin(), il.end(), IntegerType {})) -
            std::accumulate(denoms.cbegin(), denoms.cend(), RealType {});
}

And finally the multinomial coefficient method:

template <typename RealType, typename IntegerType>
IntegerType multinomial_coefficient(std::initializer_list<IntegerType> il)
{
    return static_cast<IntegerType>(std::exp(log_multinomial_coefficient<RealType, IntegerType>(std::move(il))));
}

e.g.

cout << multinomial_coefficient<double, long long>({6, 3, 3, 5}) << endl; // 114354240

For any inputs much greater than this we are going to overflow with built in types, but we can still obtain the log-space result, e.g.

cout << log_multinomial_coefficient<double>({6, 3, 11, 5, 10, 8}) << endl; // 65.1633
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!