Problem 74 : Digit factorial chains
The number 145 is well known for the property that the sum of the factorial of its digits is equal to 145:
1! + 4! + 5! = 1 + 24 + 120 = 145
Perhaps less well known is 169, in that it produces the longest chain of numbers that link back to 169; it turns out that there are only three such loops that exist:
169 → 363601 → 1454 → 169
871 → 45361 → 871
872 → 45362 → 872
It is not difficult to prove that EVERY starting number will eventually get stuck in a loop. For example,
69 → 363600 → 1454 → 169 → 363601 (→ 1454)
78 → 45360 → 871 → 45361 (→ 871)
540 → 145 (→ 145)
Starting with 69 produces a chain of five non-repeating terms, but the longest non-repeating chain with a starting number below one million is sixty terms.
How many chains, with a starting number below one million, contain exactly sixty non-repeating terms?
1. 欧拉项目第74道题 数字阶乘链(序列)
数145有众所周知的特性:它的各位上数字的阶乘之和等于145:
1! + 4! + 5! = 1 + 24 + 120 = 145
也许不太为人所知的是169,因为它产生了连接回到169的最长的数字链;原来只有三个这样的循环存在:
169 → 363601 → 1454 → 169
871 → 45361 → 871
872 → 45362 → 872
起始数是69,产生一个有五个不重复项的链(序列),但是起始数小于一百万,产生的最长的不重复的链(序列)有60项。
69 → 363600 → 1454 → 169 → 363601 (→ 1454)
78 → 45360 → 871 → 45361 (→ 871)
540 → 145 (→ 145)
起始数低于一百万,正好包含60个不重复的项,有多少个链(序列)呢?
2. 求解分析
这道题常规方法并不难,但比较耗时间。常规方法就是从69一直到一百万逐一计算每个数开始的链(序列) 是否有60个项,把有60个项的链(序列)的个数累加,就是求解答案。
在计算每一个链(序列)的时候,把各个项放进集合里,如果有重复的项出现,链(序列)产生的过程结束,再判断集合里是否有60个不重复的项。
因为每个项都是一个数的阶乘的和,所以,我们把数字0到9的阶乘预先算出来,保存在数组中,可以避免重复计算0到9的阶乘。
常规方法肯定可以计算出正确的解。
后来,我做了一些分析,所有的有60项不重复的链(序列)有什么特点呢?主要是第一项有何特点?结果发现:在小于106的起始数,只要数的各位阶乘之和是367945或者367954,也就是说第一项是367945或者367954,那么就能产生60个不重复的链(序列)。
当起始数小于107或108时,也有同样的规律存在,但是第一项的集合不同,需要计算出来。
起始数小于106: 链(序列)的第一项的集合为 {367945,367954}
起始数小于 107: 链(序列)的第一项的集合为 {1479 1497 367945 367954 373944 735964}
起始数小于108: 链(序列)的第一项的集合为 {1479 1497 367945 367954 373944 379443 379465 732249 732294 735964 1129734}
3. C++ 代码实现
按照求解分析的描述,C++用代码实现了普通常规方法,也用C++11代码实现了特殊规律的方法。
C++ 代码 (打开编译开关 CPP_11 时使用特殊规律的方法, 否则,使用常规方法)
#include <iostream>
#include <set>
#include <vector>
#include <cmath>
#include <cassert>
#include <ctime>
#define UNIT_TEST // print first term in chains
#define CPP_11 // take first terms' signatures method to find chains
using namespace std;
class PE0074
{
private:
static const int m_numOfTerms = 60; // sixty non-repeating terms
int m_FactorialsArray[10]; // 0..9
#ifdef CPP_11
set<int> firstTermsSignatures = { 367945, 367954 }; // first terms below 10^6
#else
set<int> firstTermsSignatures; // first terms' set
#endif
bool checkFirstTermInChain(int number);
public:
int getSumOfDigitFactorial(int n);
int getNumOfNonRepeatingTermsInChain(int number);
int getNumOfSixtyNonRepeatingTermsChains(int max_n);
void printFirstTermsSet();
PE0074();
};
PE0074::PE0074()
{
m_FactorialsArray[0] = 1;
for (int i = 1; i < 10; i++)
{
m_FactorialsArray[i] = i * m_FactorialsArray[i - 1];
}
}
int PE0074::getSumOfDigitFactorial(int n)
{
int sum = 0;
while (n > 0)
{
sum += m_FactorialsArray[n % 10];
n /= 10;
}
return sum;
}
int PE0074::getNumOfNonRepeatingTermsInChain(int number)
{
set<int> dfChain;
dfChain.insert(number);
int numOfTerms, sof = number;;
for (numOfTerms = 1; numOfTerms < m_numOfTerms; numOfTerms++)
{
sof = getSumOfDigitFactorial(sof);
dfChain.insert(sof);
if (numOfTerms == dfChain.size())
{
break; // sof is a repeating term
}
}
if (m_numOfTerms == numOfTerms)
{
firstTermsSignatures.insert(getSumOfDigitFactorial(number));
}
return numOfTerms;
}
bool PE0074::checkFirstTermInChain(int number)
{
int sof = getSumOfDigitFactorial(number);
for (auto firstTerm : firstTermsSignatures)
{
if (sof == firstTerm)
{
return true;
}
}
return false;
}
int PE0074::getNumOfSixtyNonRepeatingTermsChains(int max_n)
{
int numOfChains = 0;
for (int number = 69; number <= max_n; number++)
{
// find chains containing exactly sixty non-repeating digit factorial terms
#ifdef CPP_11
if (true == checkFirstTermInChain(number))
#else
if (m_numOfTerms == getNumOfNonRepeatingTermsInChain(number))
#endif
{
numOfChains++;
}
}
return numOfChains;
}
void PE0074::printFirstTermsSet()
{
set<int>::iterator iter;
cout << "The first terms in the chains : ";
for (iter = firstTermsSignatures.begin(); iter != firstTermsSignatures.end(); iter++)
{
cout << *iter << " ";
}
cout << endl;
}
int main()
{
clock_t start = clock();
PE0074 pe0074;
assert(145 == pe0074.getSumOfDigitFactorial(145));
assert(3 == pe0074.getNumOfNonRepeatingTermsInChain(169));
int max_d = (int)pow(10, 6.0);
int numOfChains = pe0074.getNumOfSixtyNonRepeatingTermsChains(max_d);
cout << numOfChains << " chains, with a starting number below " << max_d << " contain" << endl;
cout << "exactly sixty non-repeating digit factorial terms chains." << endl;
#ifdef UNIT_TEST
pe0074.printFirstTermsSet();
#endif
clock_t finish = clock();
double duration = (double)(finish - start) / CLOCKS_PER_SEC;
cout << "C++ application running time: " << duration << " seconds" << endl;
return 0;
}
4. Python 代码实现
按照求解分析的描述,Python用代码实现了普通常规方法,也用Python代码实现了特殊规律的方法。两种方法的运行时间相差太多了,两种方法都需要知道,各有特点。
Python 代码
import time
class PE0074(object):
def __init__(self):
self.m_FactorialsArray = [1] * 10
for i in range(2, 10):
self.m_FactorialsArray[i] = i*self.m_FactorialsArray[i-1]
self.firstTerms_set = set()
def getSumOfDigitFactorial(self, n):
if 0 == n: return 1
total = 0
while n > 0:
total, n = total + self.m_FactorialsArray[n % 10], n//10
return total
def getNumOfNonRepeatingTermsInChain(self, number):
dfChains, sof = set(), number
dfChains.add(sof)
for i in range(1 , 60):
sof = self.getSumOfDigitFactorial(sof);
dfChains.add(sof)
numOfTerms = len(dfChains)
if i == numOfTerms:
break # num is a repeating term
if numOfTerms == 60:
self.firstTerms_set.add(self.getSumOfDigitFactorial(number))
return numOfTerms
def getNumOfSixtyNonRepeatingTermsChains(self, max_n):
numOfChains = 0
for number in range(69, max_n+1):
if 60 == self.getNumOfNonRepeatingTermsInChain(number):
numOfChains += 1 # contain exactly sixty non-repeating digit factorial Chains
return numOfChains
def main():
start = time.process_time()
pe0074 = PE0074()
assert 145 == pe0074.getSumOfDigitFactorial(145)
assert 3 == pe0074.getNumOfNonRepeatingTermsInChain(169)
numOfChains = pe0074.getNumOfSixtyNonRepeatingTermsChains(10**6)
print(numOfChains, "chains, with a starting number below one million")
print("contain exactly sixty non-repeating digit factorial Chains.")
print('The set of the first terms in the chains :', pe0074.firstTerms_set)
end = time.process_time()
print('CPU processing time :', end-start)
if __name__ == '__main__':
main()
Python代码 II
import time
class PE0074(object):
def __init__(self):
self.m_FactorialsArray = [1] * 10
for i in range(2, 10):
self.m_FactorialsArray[i] = i*self.m_FactorialsArray[i-1]
def getSumOfDigitFactorial(self, n):
total = 0
while n > 0:
total, n = total + self.m_FactorialsArray[n % 10], n // 10
return total
def getNumOfSixtyNonRepeatingTermsChains(self, max_n):
firstNumberSignatures = [367945, 367954]
numOfChains = len([n for n in range(69, max_n+1) \
if self.getSumOfDigitFactorial(n) in firstNumberSignatures])
return numOfChains
def main():
start = time.process_time()
pe0074 = PE0074()
assert 145 == pe0074.getSumOfDigitFactorial(145)
numOfChains = pe0074.getNumOfSixtyNonRepeatingTermsChains(10**6)
print(numOfChains, "chains, with a starting number below one million")
print("contain exactly sixty non-repeating digit factorial Chains.")
end = time.process_time()
print("CPU processing time :", end-start)
if __name__ == '__main__':
main()