Problem 346: Strong Repunits

View on Project Euler

Project Euler Problem 346 Solution

EulerSolve provides an optimized solution for Project Euler Problem 346, Strong Repunits, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary A repunit in base \(b\) is a number whose digits are all 1, such as \(111_b\) or \(11111_b\). Problem 346 asks for the sum of all strong repunits below a limit \(L\). A positive integer is called strong if it is a repunit in at least two bases \(b \gt 1\). The published sample below \(50\) is \(\{1,7,13,15,21,31,40,43\}\), so the goal is to generate exactly those values without scanning every integer below the limit. Mathematical Approach For base \(b \ge 2\) and length \(k \ge 1\), the repunit value is $$R_{b,k}=1+b+b^2+\cdots+b^{k-1}=\frac{b^k-1}{b-1}.$$ This is the finite geometric-series formula, and it shows immediately that for fixed \(b\), repunits grow strictly as \(k\) increases. Why Only Lengths \(k \ge 3\) Matter Every integer \(n \gt 2\) already has the trivial two-digit representation \(11_{n-1}\), because $$11_{n-1}=(n-1)+1=n.$$ So a number is a strong repunit precisely when it has at least one additional repunit representation with \(k \ge 3\). That lets us skip all length-2 cases and enumerate only nontrivial representations. The value \(1\) is added separately because Project Euler explicitly includes it in the sample set of strong repunits. Conversely, if \(n \gt 1\) is a strong repunit, then by definition it has some representation \(n=R_{b,k}\) with \(b \gt 1\) and \(k \ge 3\), so direct generation of all such pairs \((b,k)\) is complete....

Detailed mathematical approach

Problem Summary

A repunit in base \(b\) is a number whose digits are all 1, such as \(111_b\) or \(11111_b\). Problem 346 asks for the sum of all strong repunits below a limit \(L\). A positive integer is called strong if it is a repunit in at least two bases \(b \gt 1\). The published sample below \(50\) is \(\{1,7,13,15,21,31,40,43\}\), so the goal is to generate exactly those values without scanning every integer below the limit.

Mathematical Approach

For base \(b \ge 2\) and length \(k \ge 1\), the repunit value is

$$R_{b,k}=1+b+b^2+\cdots+b^{k-1}=\frac{b^k-1}{b-1}.$$

This is the finite geometric-series formula, and it shows immediately that for fixed \(b\), repunits grow strictly as \(k\) increases.

Why Only Lengths \(k \ge 3\) Matter

Every integer \(n \gt 2\) already has the trivial two-digit representation \(11_{n-1}\), because

$$11_{n-1}=(n-1)+1=n.$$

So a number is a strong repunit precisely when it has at least one additional repunit representation with \(k \ge 3\). That lets us skip all length-2 cases and enumerate only nontrivial representations. The value \(1\) is added separately because Project Euler explicitly includes it in the sample set of strong repunits.

Conversely, if \(n \gt 1\) is a strong repunit, then by definition it has some representation \(n=R_{b,k}\) with \(b \gt 1\) and \(k \ge 3\), so direct generation of all such pairs \((b,k)\) is complete.

Bounding the Base

For a fixed base \(b\), the smallest nontrivial repunit is the length-3 value

$$R_{b,3}=1+b+b^2.$$

If even this value is not below the limit, then every longer repunit in the same base is larger and can be ignored. Therefore we only need bases satisfying

$$1+b+b^2 \lt L.$$

Equivalently, \(b \le \left\lfloor\frac{\sqrt{4L-3}-1}{2}\right\rfloor\). In practice the code simply tests the inequality directly, which keeps the loop simple and safe.

Recurrence Used by the Code

After computing \(R_{b,3}\), the next repunit in the same base can be obtained without recomputing powers:

$$R_{b,k+1}=bR_{b,k}+1.$$

This follows immediately from the definition, since

$$b(1+b+\cdots+b^{k-1})+1=1+b+\cdots+b^k.$$

Operationally, multiplying by \(b\) shifts the base-\(b\) digits left, and adding 1 appends another trailing digit 1. This recurrence is the key reason the algorithm is fast.

Worked Example: \(L = 50\)

The admissible bases are those with \(1+b+b^2 \lt 50\), namely \(b=2,3,4,5,6\).

Base \(2\) gives \(7,15,31\). Base \(3\) gives \(13,40\). Base \(4\) gives \(21\). Base \(5\) gives \(31\) again. Base \(6\) gives \(43\). For \(b=7\), we already have \(1+7+49=57 \ge 50\), so the enumeration stops.

After deduplication and after inserting \(1\), the set is

$$\{1,7,13,15,21,31,40,43\},$$

exactly the sample from the statement. The duplicate \(31\) is a useful sanity check: \(31=11111_2=111_5\).

How the Code Works

The implementation first inserts \(1\) when \(L \gt 1\). Then it loops over bases starting from \(2\). The C++ and Java versions defensively stop if \(b^2\) could overflow, while the main structural stop condition is \(1+b+b^2 \ge L\).

For each base, the code starts with \(R_{b,3}\), repeatedly applies repunit = repunit * base + 1, and records every value below the limit. Before the multiplication, it checks whether repunit > (L-1)/base; if so, the next recurrence step would exceed the safe range, so the inner loop ends.

Because different \((b,k)\) pairs can yield the same number, the generated values must be deduplicated. The C++ version pushes candidates into a vector and later uses sort plus unique; the Python and Java versions use sets during generation and then sort the final collection before summing. The C++ file also includes checkpoints for the published examples: the list below \(50\) and the sum \(15864\) below \(1000\).

Complexity Analysis

The outer loop runs for only \(O(\sqrt{L})\) bases. For each fixed base \(b\), the recurrence produces about \(O(\log_b L)\) repunits before crossing the limit. Hence the total work is

$$\sum_{b=2}^{O(\sqrt{L})} O(\log_b L),$$

which is far smaller than testing all integers below \(L\). Memory usage is \(O(M)\), where \(M\) is the number of generated candidate values before or during deduplication.

Footnotes and References

  1. Problem page: https://projecteuler.net/problem=346
  2. Repunit numbers: Wikipedia - Repunit
  3. Geometric series: Wikipedia - Geometric series
  4. Positional notation and base representations: Wikipedia - Positional notation

Problem 346 source code

C++

#include <algorithm>
#include <cstdint>
#include <iostream>
#include <limits>
#include <string>
#include <vector>

namespace {

using u64 = std::uint64_t;

struct Options {
    u64 limit = 1'000'000'000'000ULL;
    bool run_checkpoints = true;
};

bool parse_u64_after_prefix(const std::string& arg, const std::string& prefix, u64& value) {
    if (arg.rfind(prefix, 0U) != 0U) {
        return false;
    }
    const std::string tail = arg.substr(prefix.size());
    if (tail.empty()) {
        return false;
    }
    u64 parsed = 0;
    for (char c : tail) {
        if (c < '0' || c > '9') {
            return false;
        }
        const u64 digit = static_cast<u64>(c - '0');
        if (parsed > (std::numeric_limits<u64>::max() - digit) / 10ULL) {
            return false;
        }
        parsed = parsed * 10ULL + digit;
    }
    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_u64_after_prefix(arg, "--limit=", options.limit)) {
            continue;
        }
        std::cerr << "Unknown argument: " << arg << '\n';
        return false;
    }
    return options.limit >= 2ULL;
}

std::vector<u64> strong_repunits_below(const u64 limit) {
    std::vector<u64> values;
    if (limit > 1ULL) {
        values.push_back(1ULL);
    }

    for (u64 base = 2ULL;; ++base) {
        if (base > (std::numeric_limits<u64>::max() - 1ULL) / base) {
            break;
        }
        const u64 square = base * base;
        if (1ULL + base + square >= limit) {
            break;
        }

        u64 repunit = 1ULL + base + square;  // Length 3.
        while (repunit < limit) {
            values.push_back(repunit);
            if (repunit > (limit - 1ULL) / base) {
                break;
            }
            repunit = repunit * base + 1ULL;
        }
    }

    std::sort(values.begin(), values.end());
    values.erase(std::unique(values.begin(), values.end()), values.end());
    return values;
}

u64 solve(const u64 limit) {
    const std::vector<u64> values = strong_repunits_below(limit);
    u64 sum = 0ULL;
    for (const u64 v : values) {
        sum += v;
    }
    return sum;
}

bool run_checkpoints() {
    const std::vector<u64> sample = strong_repunits_below(50ULL);
    const std::vector<u64> expected = {1ULL, 7ULL, 13ULL, 15ULL, 21ULL, 31ULL, 40ULL, 43ULL};
    if (sample != expected) {
        std::cerr << "Checkpoint failed for limit=50 strong repunits list" << '\n';
        return false;
    }
    if (solve(1000ULL) != 15864ULL) {
        std::cerr << "Checkpoint failed for limit=1000 sample sum" << '\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.limit) << '\n';
    return 0;
}

Python

def strong_repunits_below(limit):
    values = set()
    if limit > 1:
        values.add(1)
        
    base = 2
    while True:
        square = base * base
        if 1 + base + square >= limit:
            break
            
        repunit = 1 + base + square
        while repunit < limit:
            values.add(repunit)
            if repunit > (limit - 1) // base:
                break
            repunit = repunit * base + 1
            
        base += 1
        
    return sorted(list(values))

def solve():
    limit = 1000000000000
    values = strong_repunits_below(limit)
    ans = sum(values)
    return str(ans)

if __name__ == '__main__':
    print(solve())

Java

import java.util.*;

public class Euler346 {

    static List<Long> strongRepunitsBelow(long limit) {
        Set<Long> values = new HashSet<>();
        if (limit > 1) {
            values.add(1L);
        }

        for (long base = 2;; base++) {
            // Stop if base * base could overflow or exceed limit structurally
            if (base > Long.MAX_VALUE / base)
                break; // Won't happen for 10^12

            long square = base * base;
            if (1 + base + square >= limit) {
                break;
            }

            long repunit = 1 + base + square;
            while (repunit < limit) {
                values.add(repunit);
                if (repunit > (limit - 1) / base) {
                    break;
                }
                repunit = repunit * base + 1;
            }
        }

        List<Long> sortedValues = new ArrayList<>(values);
        Collections.sort(sortedValues);
        return sortedValues;
    }

    public static String solve() {
        long limit = 1000000000000L;
        List<Long> values = strongRepunitsBelow(limit);
        long sum = 0;
        for (long v : values) {
            sum += v;
        }
        return String.valueOf(sum);
    }

    public static void main(String[] args) {
        System.out.println(solve());
    }
}