Problem 738: Counting Ordered Factorisations
View on Project EulerProject Euler Problem 738 Solution
EulerSolve provides an optimized solution for Project Euler Problem 738, Counting Ordered Factorisations, with C++, Python, Java, and a step-by-step mathematical explanation.
Problem Summary For fixed integers \(N\) and \(K\), the task is to count all factorisations whose product is at most \(N\) and whose total length is at most \(K\), with the factors written in nondecreasing order. Factors equal to \(1\) are allowed, so each factorisation consists of some leading ones followed by a nondecreasing block of factors at least \(2\). The final total is taken modulo \(10^9+7\). Mathematical Approach It is useful to separate the trivial factors \(1\) from the genuinely multiplicative part. For integers \(L \ge 1\), \(m \ge 2\), and \(p \ge 0\), define $$C(L,m,p)=\#\left\{(a_1,\dots,a_p)\in \mathbb{Z}_{\ge 2}^p : m \le a_1 \le \cdots \le a_p,\ \prod_{i=1}^{p} a_i \le L\right\}.$$ When \(m=2\), this counts the core factorisations with exactly \(p\) nontrivial factors. Step 1: Bound the Number of Nontrivial Factors Every nontrivial factor is at least \(2\). Therefore any factorisation with \(p\) such factors has product at least \(2^p\). If \(2^p>N\), no such factorisation can contribute. Hence the largest relevant value of \(p\) is $$P=\left\lfloor \log_2 N \right\rfloor.$$ This explains why the implementations only need to compute counts up to that depth. Step 2: Derive the Recurrence Fix \(p \ge 2\), and choose the first factor \(a_1=x\). Because the sequence is nondecreasing, every remaining factor is at least \(x\)....
Detailed mathematical approach
Problem Summary
For fixed integers \(N\) and \(K\), the task is to count all factorisations whose product is at most \(N\) and whose total length is at most \(K\), with the factors written in nondecreasing order. Factors equal to \(1\) are allowed, so each factorisation consists of some leading ones followed by a nondecreasing block of factors at least \(2\). The final total is taken modulo \(10^9+7\).
Mathematical Approach
It is useful to separate the trivial factors \(1\) from the genuinely multiplicative part. For integers \(L \ge 1\), \(m \ge 2\), and \(p \ge 0\), define
$$C(L,m,p)=\#\left\{(a_1,\dots,a_p)\in \mathbb{Z}_{\ge 2}^p : m \le a_1 \le \cdots \le a_p,\ \prod_{i=1}^{p} a_i \le L\right\}.$$
When \(m=2\), this counts the core factorisations with exactly \(p\) nontrivial factors.
Step 1: Bound the Number of Nontrivial Factors
Every nontrivial factor is at least \(2\). Therefore any factorisation with \(p\) such factors has product at least \(2^p\). If \(2^p>N\), no such factorisation can contribute.
Hence the largest relevant value of \(p\) is
$$P=\left\lfloor \log_2 N \right\rfloor.$$
This explains why the implementations only need to compute counts up to that depth.
Step 2: Derive the Recurrence
Fix \(p \ge 2\), and choose the first factor \(a_1=x\). Because the sequence is nondecreasing, every remaining factor is at least \(x\). Thus the remaining product is at least \(x^{p-1}\), and the whole product is at least \(x^p\). So we must have
$$x^p \le L,$$
which gives the sharp upper bound
$$x \le \left\lfloor L^{1/p} \right\rfloor.$$
After fixing \(x\), the remaining \(p-1\) factors must still be nondecreasing, each at least \(x\), and their product must be at most \(\lfloor L/x \rfloor\). Therefore
$$C(L,m,p)=\sum_{x=m}^{\left\lfloor L^{1/p} \right\rfloor} C\!\left(\left\lfloor \frac{L}{x} \right\rfloor, x, p-1\right).$$
The base cases are immediate:
$$C(L,m,0)=1,$$
because the empty tail contributes one valid completion, and
$$C(L,m,1)=\max\!\bigl(0,\,L-m+1\bigr),$$
because a single remaining factor can be any integer from \(m\) up to \(L\).
Step 3: Why This Counts Each Core Factorisation Exactly Once
The condition \(m \le a_1 \le \cdots \le a_p\) fixes a canonical order, so no permutation duplicates appear. Every valid core factorisation has one and only one first factor \(x\), and once \(x\) is chosen, the rest of the sequence is described by the smaller problem with limit \(\lfloor L/x \rfloor\), lower bound \(x\), and one fewer position.
Thus the recursion is exact, not heuristic. Two different branches cannot represent the same factorisation, and every valid factorisation belongs to one branch.
Step 4: Reintroduce the Factors Equal to \(1\)
Let
$$A_p=C(N,2,p)$$
for \(1 \le p \le P\). This counts the nondecreasing factorizations whose nontrivial part has exactly \(p\) factors, all at least \(2\).
If such a core factorisation has length \(p\), then for any total length \(t\) with \(p \le t \le K\), it extends uniquely to
$$(\underbrace{1,\dots,1}_{t-p\text{ times}},a_1,\dots,a_p),$$
because in nondecreasing order all ones must appear at the front. Therefore each core factorisation contributes exactly
$$K-p+1$$
times to the final answer.
There is also the purely trivial family consisting only of ones. For each length \(t=1,2,\dots,K\), the factorisation \((1,\dots,1)\) has product \(1\le N\), so these contribute another \(K\) in total.
Step 5: Final Formula
Combining the previous steps gives
$$D(N,K)=K+\sum_{p=1}^{\min(P,K)} A_p\,(K-p+1)\pmod{10^9+7},$$
where \(P=\lfloor \log_2 N \rfloor\) and \(A_p=C(N,2,p)\). This is exactly the quantity accumulated by the implementations.
Worked Example: \(N=10,\ K=10\)
Here \(P=\lfloor \log_2 10 \rfloor=3\), because \(2^3 \le 10 \lt 2^4\).
For \(p=1\), the nontrivial core factorisations are simply \((2),(3),\dots,(10)\), so
$$A_1=9.$$
For \(p=2\), the valid nondecreasing pairs are
$$ (2,2),\ (2,3),\ (2,4),\ (2,5),\ (3,3), $$
so
$$A_2=5.$$
For \(p=3\), only
$$ (2,2,2) $$
works, hence
$$A_3=1.$$
No longer core factorisation is possible. Therefore
$$D(10,10)=10 + 9 \cdot 10 + 5 \cdot 9 + 1 \cdot 8 = 153,$$
which matches the built-in checkpoint.
How the Code Works
The C++, Python, and Java implementations first determine the maximum relevant depth \(P\) by repeatedly doubling until the product would exceed \(N\). They then evaluate \(C(N,2,p)\) for each \(p=1,2,\dots,P\) using the same memoized recursion described above.
Each cached state is determined by three mathematical parameters: the remaining product limit, the minimum allowed next factor, and the number of factors still to place. An exact integer \(p\)-th root is used to compute \(\lfloor L^{1/p} \rfloor\) safely, so the loop bounds remain correct even near perfect powers.
After all core counts are known, the implementation adds the \(K\) all-one factorizations, applies the multiplicity \(K-p+1\) to each core count, and performs every arithmetic step modulo \(10^9+7\).
Complexity Analysis
Let \(\mathcal{S}\) be the set of distinct states reached by the memoized recursion. A state with parameters \((L,m,p)\) performs
$$\max\!\left(0,\left\lfloor L^{1/p} \right\rfloor - m + 1\right)$$
transitions. Therefore the total running time is
$$O\!\left(\sum_{(L,m,p)\in\mathcal{S}} \max\!\left(0,\left\lfloor L^{1/p} \right\rfloor - m + 1\right)\right).$$
The recursion depth is at most \(\lfloor \log_2 N \rfloor\), because every nontrivial factor is at least \(2\). Memory usage is \(O(|\mathcal{S}|)\) for the memo table. In practice, many branches collapse to the same subproblem, so memoization removes a large amount of repeated work.
Footnotes and References
- Problem page: https://projecteuler.net/problem=738
- Multiplicative partitions: MathWorld - Multiplicative Partition
- Integer \(n\)-th root: Wikipedia - Integer nth root
- Dynamic programming: Wikipedia - Dynamic programming
- Memoization: Wikipedia - Memoization
Problem 738 source code
C++
#include <cassert>
#include <cmath>
#include <cstdint>
#include <iostream>
#include <unordered_map>
#include <vector>
namespace {
using u64 = std::uint64_t;
using u128 = __uint128_t;
constexpr u64 kMod = 1'000'000'007ULL;
bool power_leq(const u64 base, const int exp, const u64 limit) {
u128 p = 1;
for (int i = 0; i < exp; ++i) {
p *= static_cast<u128>(base);
if (p > static_cast<u128>(limit)) {
return false;
}
}
return true;
}
u64 int_root(const u64 n, const int k) {
long double guess = std::powl(static_cast<long double>(n), 1.0L / static_cast<long double>(k));
u64 r = static_cast<u64>(guess);
if (r < 1ULL) {
r = 1ULL;
}
while (power_leq(r + 1ULL, k, n)) {
++r;
}
while (!power_leq(r, k, n)) {
--r;
}
return r;
}
struct Key {
u64 limit;
u64 min_factor;
std::uint16_t parts;
bool operator==(const Key& o) const {
return limit == o.limit && min_factor == o.min_factor && parts == o.parts;
}
};
struct KeyHash {
std::size_t operator()(const Key& k) const {
std::size_t h1 = std::hash<u64>{}(k.limit);
std::size_t h2 = std::hash<u64>{}(k.min_factor);
std::size_t h3 = std::hash<std::uint16_t>{}(k.parts);
return h1 ^ (h2 + 0x9e3779b97f4a7c15ULL + (h1 << 6U) + (h1 >> 2U)) ^
(h3 + 0x9e3779b97f4a7c15ULL + (h2 << 6U) + (h2 >> 2U));
}
};
u64 count_nondecreasing(const u64 limit,
const u64 min_factor,
const int parts,
std::unordered_map<Key, u64, KeyHash>& memo) {
if (parts == 0) {
return 1ULL;
}
if (parts == 1) {
if (limit < min_factor) {
return 0ULL;
}
return limit - min_factor + 1ULL;
}
const Key key{limit, min_factor, static_cast<std::uint16_t>(parts)};
const auto it = memo.find(key);
if (it != memo.end()) {
return it->second;
}
const u64 r = int_root(limit, parts);
if (r < min_factor) {
memo.emplace(key, 0ULL);
return 0ULL;
}
u64 total = 0ULL;
for (u64 x = min_factor; x <= r; ++x) {
total += count_nondecreasing(limit / x, x, parts - 1, memo);
}
memo.emplace(key, total);
return total;
}
u64 D(const u64 N, const u64 K) {
int max_parts = 0;
for (u64 p = 1ULL; p <= N; p <<= 1U) {
++max_parts;
}
--max_parts;
std::vector<u64> counts(static_cast<std::size_t>(max_parts + 1), 0ULL);
counts[0] = 1ULL;
std::unordered_map<Key, u64, KeyHash> memo;
memo.reserve(1 << 20);
for (int m = 1; m <= max_parts; ++m) {
counts[static_cast<std::size_t>(m)] = count_nondecreasing(N, 2ULL, m, memo);
}
u64 answer = K % kMod;
for (int m = 1; m <= max_parts; ++m) {
if (static_cast<u64>(m) > K) {
break;
}
const u64 ways = counts[static_cast<std::size_t>(m)] % kMod;
const u64 multiplicity = (K - static_cast<u64>(m) + 1ULL) % kMod;
answer = (answer + static_cast<u64>((static_cast<u128>(ways) * multiplicity) % kMod)) % kMod;
}
return answer;
}
} // namespace
int main() {
assert(D(10ULL, 10ULL) == 153ULL);
assert(D(100ULL, 100ULL) == 35'384ULL);
std::cout << D(10'000'000'000ULL, 10'000'000'000ULL) << '\n';
return 0;
}
Python
import math
def solve():
MOD = 1000000007
N = K = 10000000000
def int_root(n, k):
r = max(1, int(n ** (1.0/k)))
while True:
try:
if (r+1)**k <= n: r += 1
else: break
except: break
while r**k > n: r -= 1
return r
max_parts = 0
p = 1
while p <= N: max_parts += 1; p <<= 1
max_parts -= 1
memo = {}
def count_nd(limit, min_f, parts):
if parts == 0: return 1
if parts == 1:
return max(0, limit - min_f + 1) if limit >= min_f else 0
key = (limit, min_f, parts)
if key in memo: return memo[key]
r = int_root(limit, parts)
if r < min_f: memo[key] = 0; return 0
total = 0
for x in range(min_f, r + 1):
total += count_nd(limit // x, x, parts - 1)
memo[key] = total; return total
counts = [0] * (max_parts + 1); counts[0] = 1
for m in range(1, max_parts + 1):
counts[m] = count_nd(N, 2, m)
answer = K % MOD
for m in range(1, max_parts + 1):
if m > K: break
ways = counts[m] % MOD
mult = (K - m + 1) % MOD
answer = (answer + ways * mult) % MOD
return str(answer)
if __name__ == '__main__':
print(solve())
Java
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class Euler738 {
static final long kMod = 1000000007L;
static boolean powerLeq(long base, int exp, long limit) {
long p = 1;
for (int i = 0; i < exp; ++i) {
if (Long.MAX_VALUE / base < p)
return false;
p *= base;
if (p > limit)
return false;
}
return true;
}
static long intRoot(long n, int k) {
if (n == 0)
return 0;
double guess = Math.pow((double) n, 1.0 / k);
long r = (long) guess;
if (r < 1L)
r = 1L;
while (powerLeq(r + 1, k, n)) {
r++;
}
while (!powerLeq(r, k, n)) {
r--;
}
return r;
}
static class Key {
long limit, minFactor;
int parts;
Key(long limit, long minFactor, int parts) {
this.limit = limit;
this.minFactor = minFactor;
this.parts = parts;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Key key = (Key) o;
return limit == key.limit && minFactor == key.minFactor && parts == key.parts;
}
@Override
public int hashCode() {
return Objects.hash(limit, minFactor, parts);
}
}
static long countNondecreasing(long limit, long minFactor, int parts, Map<Key, Long> memo) {
if (parts == 0)
return 1L;
if (parts == 1) {
if (limit < minFactor)
return 0L;
return limit - minFactor + 1L;
}
Key key = new Key(limit, minFactor, parts);
Long cached = memo.get(key);
if (cached != null)
return cached;
long r = intRoot(limit, parts);
if (r < minFactor) {
memo.put(key, 0L);
return 0L;
}
long total = 0L;
for (long x = minFactor; x <= r; ++x) {
total += countNondecreasing(limit / x, x, parts - 1, memo);
}
memo.put(key, total);
return total;
}
public static String solve() {
long N = 10000000000L;
long K = 10000000000L;
int maxParts = 0;
for (long p = 1; p <= N; p <<= 1) {
maxParts++;
}
maxParts--;
long[] counts = new long[maxParts + 1];
counts[0] = 1L;
Map<Key, Long> memo = new HashMap<>(1 << 20);
for (int m = 1; m <= maxParts; ++m) {
counts[m] = countNondecreasing(N, 2L, m, memo);
}
long answer = K % kMod;
for (int m = 1; m <= maxParts; ++m) {
if (m > K)
break;
long ways = counts[m] % kMod;
long multiplicity = (K - m + 1L) % kMod;
answer = (answer + ways * multiplicity) % kMod;
}
return Long.toString(answer);
}
public static void main(String[] args) {
System.out.println(solve());
}
}