Problem 290: Digital Signature
View on Project EulerProject Euler Problem 290 Solution
EulerSolve provides an optimized solution for Project Euler Problem 290, Digital Signature, with C++, Python, Java, and a step-by-step mathematical explanation.
Problem Summary We must count all decimal strings of length \(d=18\), equivalently all integers \(0 \le n \lt 10^{18}\), such that $$s(n)=s(137n),$$ where \(s(x)\) denotes the base-10 digit sum. Direct enumeration over \(10^{18}\) candidates is impossible, so we need to process the multiplication structurally, one digit at a time. Mathematical Approach 1. Multiply from right to left Write $$n=\sum_{i=0}^{d-1} a_i 10^i,\qquad a_i\in\{0,\dots,9\}.$$ When we multiply by \(m=137\), the \(i\)-th output digit depends only on the current input digit \(a_i\) and the incoming carry \(c_i\): $$t_i=137a_i+c_i,\qquad b_i=t_i\bmod 10,\qquad c_{i+1}=\left\lfloor \frac{t_i}{10}\right\rfloor.$$ Thus the low \(d\) digits of \(137n\) are produced digit by digit, and whatever remains above position \(d-1\) is exactly the final carry \(c_d\). 2. The whole past collapses to carry plus digit-sum difference After processing the first \(i\) digits, define $$\Delta_i=\sum_{j=0}^{i-1} a_j-\sum_{j=0}^{i-1} b_j.$$ This is the difference between the digit sum already contributed by \(n\) and the digit sum already contributed by the low part of \(137n\). Once \((c_i,\Delta_i)\) is known, the future no longer depends on earlier digits individually....
Detailed mathematical approach
Problem Summary
We must count all decimal strings of length \(d=18\), equivalently all integers \(0 \le n \lt 10^{18}\), such that
$$s(n)=s(137n),$$
where \(s(x)\) denotes the base-10 digit sum. Direct enumeration over \(10^{18}\) candidates is impossible, so we need to process the multiplication structurally, one digit at a time.
Mathematical Approach
1. Multiply from right to left
Write
$$n=\sum_{i=0}^{d-1} a_i 10^i,\qquad a_i\in\{0,\dots,9\}.$$
When we multiply by \(m=137\), the \(i\)-th output digit depends only on the current input digit \(a_i\) and the incoming carry \(c_i\):
$$t_i=137a_i+c_i,\qquad b_i=t_i\bmod 10,\qquad c_{i+1}=\left\lfloor \frac{t_i}{10}\right\rfloor.$$
Thus the low \(d\) digits of \(137n\) are produced digit by digit, and whatever remains above position \(d-1\) is exactly the final carry \(c_d\).
2. The whole past collapses to carry plus digit-sum difference
After processing the first \(i\) digits, define
$$\Delta_i=\sum_{j=0}^{i-1} a_j-\sum_{j=0}^{i-1} b_j.$$
This is the difference between the digit sum already contributed by \(n\) and the digit sum already contributed by the low part of \(137n\). Once \((c_i,\Delta_i)\) is known, the future no longer depends on earlier digits individually. That is why the dynamic-programming state can be just
$$\text{state}_i=(c_i,\Delta_i).$$
If the next digit chosen is \(a_i\), then the produced output digit is \(b_i\), so the update is simply
$$\Delta_{i+1}=\Delta_i+a_i-b_i.$$
3. Why the final condition is \(\Delta_d=s(c_d)\)
After all \(d\) input digits have been processed, the product has the form
$$137n=\sum_{i=0}^{d-1} b_i10^i+c_d10^d.$$
The first term contributes digit sum \(\sum_{i=0}^{d-1} b_i\), and the untouched carry tail contributes \(s(c_d)\). Therefore
$$s(137n)=\sum_{i=0}^{d-1} b_i+s(c_d).$$
Since also \(s(n)=\sum_{i=0}^{d-1} a_i\), the target condition \(s(n)=s(137n)\) is equivalent to
$$\sum_{i=0}^{d-1} a_i-\sum_{i=0}^{d-1} b_i=s(c_d),$$
that is, precisely
$$\boxed{\Delta_d=s(c_d).}$$
This is the key point: we never need the full product, only the final carry and the running digit-sum difference.
4. Why the state space stays small
The carry is tightly bounded. If \(c_i\le 136\), then
$$c_{i+1}\le \left\lfloor\frac{137\cdot 9+136}{10}\right\rfloor=136.$$
Because \(c_0=0\), induction gives
$$0\le c_i\le 136\qquad \text{for all }i.$$
The difference \(\Delta_i\) also grows only linearly, since each step changes it by \(a_i-b_i\in[-9,9]\). So the reachable state space is tiny compared with \(10^{18}\) possibilities. The implementation uses a safe offset when packing \((carry,diff)\) into a hash-map key.
5. Worked example: \(n=9\)
This is the smallest nonzero solution. We have
$$137\cdot 9=1233,\qquad s(9)=9,\qquad s(1233)=1+2+3+3=9.$$
The DP sees this as one processed digit:
$$t_0=137\cdot 9+0=1233,\qquad b_0=3,\qquad c_1=123.$$
Hence
$$\Delta_1=9-3=6,\qquad s(c_1)=s(123)=6.$$
So the acceptance condition \(\Delta_1=s(c_1)\) holds exactly.
6. Small checkpoints
The code verifies the DP against brute force on smaller sizes:
$$\text{solve}(4,137)=306,\qquad \text{solve}(5,137)=2902.$$
After those checks, the program computes the required case
$$\text{solve}(18,137)=20444710234716473.$$
How the Code Works
The implementation stores a hash map from packed keys \((carry,diff)\) to counts. For each of the \(18\) positions, it tries all \(10\) possible next digits, computes the new output digit and carry, updates the difference, and accumulates the new count. At the end it sums exactly those states for which diff == digit_sum(carry). A brute-force routine is included only for the small validation cases.
Complexity Analysis
If \(S\) is the number of reachable \((carry,diff)\) states, then each layer tries \(10\) digits, so the running time is
$$O(d\cdot S\cdot 10),$$
with memory \(O(S)\). Here \(d=18\), and \(S\) is small because the carry is bounded by \(136\) and the difference range is narrow. This is exponentially smaller than testing all \(10^{18}\) candidates one by one.
Further Reading
- Problem page: https://projecteuler.net/problem=290
- Digit DP overview: https://cp-algorithms.com/dynamic_programming/intro-to-dp.html
- Positional notation and carries: https://en.wikipedia.org/wiki/Positional_notation
Problem 290 source code
C++
#include <cstdint>
#include <iostream>
#include <string>
#include <unordered_map>
namespace {
using u64 = std::uint64_t;
struct Options {
int digits = 18;
int multiplier = 137;
bool run_checkpoints = true;
};
bool parse_int_after_prefix(const std::string& arg, const std::string& prefix, int& value) {
if (arg.rfind(prefix, 0U) != 0U) {
return false;
}
const std::string tail = arg.substr(prefix.size());
if (tail.empty()) {
return false;
}
int parsed = 0;
for (char c : tail) {
if (c < '0' || c > '9') {
return false;
}
parsed = parsed * 10 + static_cast<int>(c - '0');
}
value = parsed;
return true;
}
bool parse_arguments(int argc, char** argv, Options& options) {
for (int i = 1; i < argc; ++i) {
const std::string arg(argv[i]);
if (arg == "--skip-checkpoints") {
options.run_checkpoints = false;
continue;
}
if (parse_int_after_prefix(arg, "--digits=", options.digits) ||
parse_int_after_prefix(arg, "--multiplier=", options.multiplier)) {
continue;
}
std::cerr << "Unknown argument: " << arg << '\n';
return false;
}
return options.digits >= 1 && options.multiplier >= 1;
}
int digit_sum(u64 x) {
int s = 0;
while (x > 0ULL) {
s += static_cast<int>(x % 10ULL);
x /= 10ULL;
}
return s;
}
u64 solve(const int digits, const int multiplier) {
// key = carry * 1000 + (diff + offset)
// diff = sum(processed digits of n) - sum(processed low digits of multiplier*n)
static constexpr int kOffset = 500;
std::unordered_map<int, u64> dp;
dp.reserve(100000);
dp[0 * 1000 + kOffset] = 1ULL;
for (int pos = 0; pos < digits; ++pos) {
std::unordered_map<int, u64> next;
next.reserve(dp.size() * 8U);
for (const auto& [key, ways] : dp) {
const int carry = key / 1000;
const int diff = (key % 1000) - kOffset;
for (int d = 0; d <= 9; ++d) {
const int t = multiplier * d + carry;
const int out_digit = t % 10;
const int next_carry = t / 10;
const int next_diff = diff + d - out_digit;
const int next_key = next_carry * 1000 + (next_diff + kOffset);
next[next_key] += ways;
}
}
dp.swap(next);
}
u64 answer = 0ULL;
for (const auto& [key, ways] : dp) {
const int carry = key / 1000;
const int diff = (key % 1000) - kOffset;
if (diff == digit_sum(static_cast<u64>(carry))) {
answer += ways;
}
}
return answer;
}
u64 brute_small(const int digits, const int multiplier) {
u64 upper = 1ULL;
for (int i = 0; i < digits; ++i) {
upper *= 10ULL;
}
u64 count = 0ULL;
for (u64 n = 0; n < upper; ++n) {
if (digit_sum(n) == digit_sum(static_cast<u64>(multiplier) * n)) {
++count;
}
}
return count;
}
bool run_checkpoints() {
if (solve(4, 137) != brute_small(4, 137)) {
std::cerr << "Checkpoint failed for digits=4 multiplier=137" << '\n';
return false;
}
if (solve(5, 137) != brute_small(5, 137)) {
std::cerr << "Checkpoint failed for digits=5 multiplier=137" << '\n';
return false;
}
return true;
}
} // namespace
int main(int argc, char** argv) {
Options options;
if (!parse_arguments(argc, argv, options)) {
return 1;
}
if (options.run_checkpoints && !run_checkpoints()) {
return 2;
}
std::cout << solve(options.digits, options.multiplier) << '\n';
return 0;
}
Python
def digit_sum(x):
s = 0
while x > 0:
s += x % 10
x //= 10
return s
def solve(digits=18, multiplier=137):
kOffset = 500
dp = {0 * 1000 + kOffset: 1}
for _ in range(digits):
nxt = {}
for key, ways in dp.items():
carry = key // 1000
diff = (key % 1000) - kOffset
for d in range(10):
t = multiplier * d + carry
out_digit = t % 10
next_carry = t // 10
next_diff = diff + d - out_digit
next_key = next_carry * 1000 + (next_diff + kOffset)
nxt[next_key] = nxt.get(next_key, 0) + ways
dp = nxt
answer = 0
for key, ways in dp.items():
carry = key // 1000
diff = (key % 1000) - kOffset
if diff == digit_sum(carry):
answer += ways
return str(answer)
if __name__ == '__main__':
print(solve())
Java
import java.util.HashMap;
import java.util.Map;
public class Euler290 {
static int digitSum(long x) {
int s = 0;
while (x > 0) {
s += (int) (x % 10);
x /= 10;
}
return s;
}
public static String solve() {
int digits = 18;
int multiplier = 137;
int kOffset = 500;
Map<Integer, Long> dp = new HashMap<>();
dp.put(0 * 1000 + kOffset, 1L);
for (int pos = 0; pos < digits; ++pos) {
Map<Integer, Long> next = new HashMap<>((int) (dp.size() * 8 / 0.75f) + 1);
for (Map.Entry<Integer, Long> entry : dp.entrySet()) {
int key = entry.getKey();
long ways = entry.getValue();
int carry = key / 1000;
int diff = (key % 1000) - kOffset;
for (int d = 0; d <= 9; ++d) {
int t = multiplier * d + carry;
int outDigit = t % 10;
int nextCarry = t / 10;
int nextDiff = diff + d - outDigit;
int nextKey = nextCarry * 1000 + (nextDiff + kOffset);
next.put(nextKey, next.getOrDefault(nextKey, 0L) + ways);
}
}
dp = next;
}
long answer = 0L;
for (Map.Entry<Integer, Long> entry : dp.entrySet()) {
int key = entry.getKey();
long ways = entry.getValue();
int carry = key / 1000;
int diff = (key % 1000) - kOffset;
if (diff == digitSum(carry)) {
answer += ways;
}
}
return String.valueOf(answer);
}
public static void main(String[] args) {
System.out.println(solve());
}
}