ProjectEuler Problem 53: Combinatoric selections

ProjectEuler Problem 53: Combinatoric selections

Jul 14, 2021
Coding
ProjectEuler

Problem 53: Combinatoric selections #

https://projecteuler.net/problem=53

分析 #

组合 C(n,r) 表示从 n 个数中选择 r 个数选择方案的总数。 当 1 ≤ n ≤ 1000时, 统计有多少个 C(n,r) 的值超过了 1 百万。

思路很简单直接,难点是如果采用非python的实现,计算阶乘时存在大数问题。所以需要对 C(n,r) 的计算做优化,避免大数问题。

C(n,r) = n! / r!(n-r)!,计算过程中,可以先做约分,再计算最终结果。假设 max = max(r,n-r). 可以先约去 max!。 min = min(r,n-r).

   n!        (约去max)     (max+1)(max+2)...(n-2)(n-1)n
-----------     =     ------------------------------------
 max!*min!                 1*2*3...*(min-2)*(min-1)*min

然后再对分子的每个乘数,依次和分母中的所有乘数做约分。因为C(n,r) 最终必然是整数,所以约分到最后,分母中所有乘数都可以被约分成1,直接计算分子的乘积即可。并且,计算分子乘积的过程中,为了避免大数问题,我们不需要计算最终的结果,只要计算判断 > 1000000 即可。

方法 通过约分,避免大数 #

C++ #

#include <iostream>
#include <vector>

#define MAX_C 1000000

// 辗转相除法 求最大公约数
int gcd(int a, int b){
    int r=1;        // 余数,赋初值为1 
    while(r != 0){  // 如果a<b,亦无需颠倒ab,在计算中商0余除数本身,在下次运算中自可颠倒回来 
        r = a % b;
        a = b;
        b = r;
    }
    return a;
}

// 判断 C(n,r) 是否大于 MAX_C
bool C(int n, int r)
{
    // 获取 r 和 n-r 中较大的数和较小的数,保存在 max,min 中
    int max = r, min = n-r; 
    if(r < n-r){
        max = n-r;
        min = r;
    }

    //C(n,r) = n! / (max! * min!)

    // 分别统计分子和分母的所有乘数

    std::vector<int> numerator; // 保存分子的所有乘数 (max+1)(max+2)...(n-2)(n-1)n
    for(int i = max + 1; i <= n; i++){
        numerator.push_back(i);
    }

    std::vector<int> denominator; // 保存分母的所有乘数 1*2*3...*(min-2)*(min-1)*min
    for(int i = 1; i <= min; i++){
        denominator.push_back(i);
    }

    // 对分子分母做约分
    
    // 从分子的最后一个乘数(因为最后一个乘数最大)开始,分别对分母的所有乘数做约分
    for(int i = numerator.size() - 1; i >= 0; i--){
        for(int j = denominator.size() -1; j >= 0; j--){
            if(numerator[i] == 1 ) break;    // 分子的乘数无需再化简了
            if(denominator[j] == 1) continue; // 分母的乘数无法再化简了,继续判断分母的下一个乘数

            // 找到 numerator[i] 和 denominator[j] 的最大公约数,然后做约分
            int g = gcd(numerator[i], denominator[j]);
            //printf("i=%d, j=%d, %d, %d, %d\n", i, j, numerator[i], denominator[j], g);
            numerator[i] /= g;
            denominator[j] /= g;
        }
    }

    // 因为C(n,r) 最终必然是整数,所以约分到最后,分母中所有乘数应该都是 1
    // 所有直接计算分子的乘积即可

    int mul = 1;
    for(int i = 0; i < numerator.size(); i++){
        mul *= numerator[i];

        if(mul > MAX_C) return true;
    }

    return false;
}

int main()
{
    int cnt = 0;
    for(int n = 1; n <= 100; n++){
        for(int r = 1; r <= n; r++){
            if (C(n,r)) {
                cnt ++;
            }
        }
    }

    printf("cnt=%d\n", cnt);
    return 0;
}

Golang #

package main

import "fmt"

const MAX_C = 1000000

// 辗转相除法 求最大公约数
func gcd(a, b int) int {
	r := 1       // 余数,赋初值为1
	for r != 0 { // 如果a<b,亦无需颠倒ab,在计算中商0余除数本身,在下次运算中自可颠倒回来
		r = a % b
		a = b
		b = r
	}
	return a // 此时b的值已经在a中了,所以输出的a就是最大公约数
}

// 判断 C(n,r) 是否大于 MAX_C
func C(n, r int) bool {

	// 获取 r 和 n-r 中较大的数和较小的数,保存在 max,min 中
	max, min := r, n-r
	if r < n-r {
		max = n - r
		min = r
	}

	// C(n,r) = n! / (max! * min!)

	// 分别统计分子和分母的所有乘数

	var numerator []int // 保存分子的所有乘数 (max+1)(max+2)...(n-2)(n-1)n
	for i := max + 1; i <= n; i++ {
		numerator = append(numerator, i)
	}

	var denominator []int // 保存分母的所有乘数 1*2*3...*(min-2)*(min-1)*min
	for i := 1; i <= min; i++ {
		denominator = append(denominator, i)
	}

	// 对分子分母做约分

	// 从分子的最后一个乘数(因为最后一个乘数最大)开始,分别对分母的所有乘数做约分
	for i := len(numerator) - 1; i >= 0; i-- {
		for j := len(denominator) - 1; j >= 0; j-- {
			if numerator[i] == 1 { // 分子的乘数无需再化简了
				break
			}

			if denominator[j] == 1 { // 分母的乘数无法再化简了,继续判断分母的下一个乘数
				continue
			}

			// 找到 numerator[i] 和 denominator[j] 的最大公约数,然后做约分
			g := gcd(numerator[i], denominator[j])
			numerator[i] /= g
			denominator[j] /= g
		}
	}

	// 因为C(n,r) 最终必然是整数,所以约分到最后,分母中所有乘数应该都是 1
	// 所有直接计算分子的乘积即可

	mul := 1
	for i := 0; i < len(numerator); i++ {
		mul *= numerator[i]

		if mul > MAX_C {
			return true
		}
	}

	return false
}

func main() {

	cnt := 0
	for n := 1; n <= 100; n++ {
		for r := 1; r <= n; r++ {
			if C(n, r) {
				cnt++
			}
		}
	}

	fmt.Printf("cnt=%d\n", cnt)
}

Python #

可以复用上面的方法,也可以直接计算,因为python处理大数很方便。

def factorial(n):
    mul = 1
    for i in range(1,n+1):
        mul *= i
    return mul

def C(n,r):
    return factorial(n) / (factorial(r) * factorial(n-r))

cnt = 0
for n in range(1,101):
    for r in range(1, n+1):
        if C(n,r) > 1000000:
            cnt += 1
print(cnt)

答案 #

4075