Problem 238: Infinite String Tour
View on Project EulerProject Euler Problem 238 Solution
EulerSolve provides an optimized solution for Project Euler Problem 238, Infinite String Tour, with C++, Python, Java, and a step-by-step mathematical explanation.
Problem Summary Start with \(s_0=14025256\) and iterate $$s_{n+1}=s_n^2 \pmod{20300713}.$$ Write the decimal expansions of \(s_0,s_1,s_2,\dots\) one after another to obtain an infinite digit string \(w=w_1w_2w_3\cdots\). For each positive integer \(k\), define \(p(k)\) to be the smallest starting position \(z\ge 1\) for which there exists an ending position \(y\ge z\) such that the digit sum of the block \(w_z w_{z+1}\dots w_y\) is exactly \(k\). The problem asks for $$\sum_{k=1}^{2\cdot 10^{15}} p(k).$$ A direct attack is impossible: the outer summation is enormous, and the string itself is infinite. The solution used by the implementations turns the problem into finite data extracted from one period of the quadratic generator. Mathematical Approach The key objects are the state cycle of the modular recurrence, the resulting periodic digit block, and the prefix sums of that block taken modulo one full-period digit sum. The modular squaring sequence is purely periodic The recurrence evolves in a finite state space, so some state must eventually repeat. Once a state repeats, the future trajectory repeats as well. In this problem the repetition is especially clean: the first repeated state is the initial seed itself, so there is no transient prefix before the cycle starts. Therefore the sequence of generated integers is a pure cycle from \(s_0\) onward....
Detailed mathematical approach
Problem Summary
Start with \(s_0=14025256\) and iterate
$$s_{n+1}=s_n^2 \pmod{20300713}.$$
Write the decimal expansions of \(s_0,s_1,s_2,\dots\) one after another to obtain an infinite digit string \(w=w_1w_2w_3\cdots\). For each positive integer \(k\), define \(p(k)\) to be the smallest starting position \(z\ge 1\) for which there exists an ending position \(y\ge z\) such that the digit sum of the block \(w_z w_{z+1}\dots w_y\) is exactly \(k\). The problem asks for
$$\sum_{k=1}^{2\cdot 10^{15}} p(k).$$
A direct attack is impossible: the outer summation is enormous, and the string itself is infinite. The solution used by the implementations turns the problem into finite data extracted from one period of the quadratic generator.
Mathematical Approach
The key objects are the state cycle of the modular recurrence, the resulting periodic digit block, and the prefix sums of that block taken modulo one full-period digit sum.
The modular squaring sequence is purely periodic
The recurrence evolves in a finite state space, so some state must eventually repeat. Once a state repeats, the future trajectory repeats as well. In this problem the repetition is especially clean: the first repeated state is the initial seed itself, so there is no transient prefix before the cycle starts.
Therefore the sequence of generated integers is a pure cycle from \(s_0\) onward. Concatenating their decimal representations gives a digit block
$$d_1,d_2,\dots,d_L$$
that repeats forever. The implementations discover this period directly and obtain
$$L=18{,}886{,}117,\qquad S=\sum_{i=1}^{L} d_i = 80{,}846{,}691.$$
Here \(L\) is the number of digits in one full period, and \(S\) is the sum of the digits in that period.
Prefix sums linearize substring sums
Define the one-period prefix sums by
$$P_0=0,\qquad P_i=\sum_{t=1}^{i} d_t\quad (1\le i\le L).$$
Then \(P_L=S\). Because the digit block repeats, every prefix sum of the infinite string has the form
$$T_{mL+i}=mS+P_i,\qquad m\ge 0,\ 0\le i\le L,$$
where \(T_n=w_1+\cdots+w_n\) is the sum of the first \(n\) digits of the infinite string.
A block beginning at position \(z\) and ending at position \(y\) has digit sum \(k\) exactly when
$$T_y-T_{z-1}=k.$$
So \(p(k)\) is the smallest \(z\) for which this equation has at least one solution \(y\ge z\).
Residues modulo \(S\) are the right state space
Write \(z-1=mL+b\) with \(0\le b<L\). Then
$$T_{z-1}=mS+P_b.$$
If we want a block of digit sum \(k\) starting at \(z\), we need
$$T_y=mS+P_b+k.$$
But every \(T_y\) is some \(qS+P_i\), so the existence of such a \(y\) is equivalent to the congruence
$$P_i\equiv P_b+k \pmod S$$
for at least one \(i\in\{0,1,\dots,L\}\). This leads to the residue set
$$R=\{P_i\bmod S:0\le i\le L\}.$$
Now the entire question “does position \(z\) work for target sum \(k\)?” becomes the finite test
$$\big(P_b+k\big)\bmod S\in R.$$
That is exactly the condition checked in the implementations. The infinite search over end positions is reduced to a residue lookup.
Periodicity of \(p(k)\)
The validity test depends on \(k\) only through \(k\bmod S\). Therefore the set of starting positions that work for \(k\) is the same as the set of starting positions that work for \(k+S\), so
$$p(k+S)=p(k).$$
There is a second periodicity as well: the starting residue repeats every \(L\) digits, so whether position \(z\) works depends only on \((z-1)\bmod L\). Hence, whenever a target sum is attainable, its earliest valid start lies within the first period.
As a result, it is enough to compute \(p(1),p(2),\dots,p(S)\) once. If \(N=2\cdot10^{15}\), then
$$\sum_{k=1}^{N} p(k)=\left\lfloor\frac{N}{S}\right\rfloor\sum_{k=1}^{S}p(k)+\sum_{k=1}^{N\bmod S}p(k).$$
This is the central reduction in the problem: a huge outer range collapses to one finite period in \(k\).
Worked Example from the Beginning of the String
The infinite string begins with the decimal expansions of \(14025256,7410149,5847003,\dots\), so its first digits are
$$1,4,0,2,5,2,5,6,7,4,1,0,1,4,9,\dots$$
The corresponding prefix sums begin
$$0,1,5,5,7,12,14,19,25,32,36,37,37,38,42,51,\dots$$
Take \(k=6\). Starting at \(z=1\) would require a later prefix sum equal to \(6\), but the running totals jump from \(5\) to \(7\), so no substring beginning at the first digit has digit sum \(6\).
Starting at \(z=2\), however, the starting prefix sum is \(1\), and \(1+6=7\) does appear later as a prefix sum. Therefore the block \(4,0,2\) has digit sum \(6\), so
$$p(6)=2.$$
This is a miniature version of the full method: subtract the prefix sum at the starting position, look for the shifted target among the visited prefix residues, and take the earliest start that succeeds.
How the Code Works
Building one full digit period
The C++, Python, and Java implementations iterate the quadratic recurrence from the given seed, remember the first occurrence of each generated state, and stop when a state repeats. Because the repeated state is the seed, the collected decimal digits already form one complete period of the infinite string. The implementations then build the one-period prefix sums and the total digit sum \(S\).
Preparing the residue tables
Next, the implementation marks every residue \(P_i\bmod S\) in a dense membership table and also stores the repeating sequence of starting residues \(P_0,P_1,\dots,P_{L-1}\bmod S\). These two arrays encode the mathematical criterion above: for a candidate start position and a target \(k\), one modular addition and one table lookup decide whether that start position works.
Computing \(p(k)\) for one full \(k\)-period
For each \(k=1,2,\dots,S\), the implementation scans starting positions \(z=1,2,3,\dots\) until the residue condition succeeds, and records that first successful position as \(p(k)\). The C++ and Java implementations split the independent \(k\)-ranges across multiple threads; the Python implementation performs the same search serially. Once the table \(p(1),\dots,p(S)\) is available, the final answer is obtained from the block decomposition formula.
Complexity Analysis
Let \(L\) be the number of digits in one period and \(S\) the digit sum of one period. Constructing the state cycle, the digit block, and the prefix sums costs \(O(L)\) time. Marking the residue set also costs \(O(L)\) time and uses \(O(S)\) memory for the membership table.
The dominant cost in these implementations is the direct search for each \(p(k)\). In the worst case, one value of \(k\) can scan up to one whole digit period before the first success, so the overall worst-case running time is \(O(SL)\). The memory usage is \(O(S+L)\). Parallel execution in the compiled implementations reduces wall-clock time, but the main mathematical gain comes from periodicity: instead of handling \(2\cdot10^{15}\) targets separately, the algorithm computes one finite period and reuses it.
Footnotes and References
- Problem page: https://projecteuler.net/problem=238
- Prefix sum: Wikipedia - Prefix sum
- Modular arithmetic: Wikipedia - Modular arithmetic
- Pigeonhole principle: Wikipedia - Pigeonhole principle
- Periodic sequence: Wikipedia - Periodic sequence
Problem 238 source code
C++
#include <atomic>
#include <iostream>
#include <string>
#include <thread>
#include <unordered_map>
#include <vector>
// Type aliases for easier reading
using u64 = unsigned long long;
// BBS Parameters
const u64 MODULUS = 20300713;
const u64 SEED = 14025256;
const u64 TARGET_K_SMALL = 1000;
const u64 EXPECTED_SMALL_SUM = 4742;
// Function to generate the sequence of digits and find the period
struct BBSSequence {
std::string w_str;
std::vector<int> w_digits;
std::vector<u64> prefix_sum;
u64 total_sum_per_period;
void generate() {
u64 s = SEED;
std::unordered_map<u64, int> seen;
int index = 0;
// We know it will cycle eventually. Since modulus is ~2e7, cycle is shorter
// than that. Actually, we need to generate until we hit a repeated state
// s_n. The problem statement says s_0 = 14025256, s_{n+1} = s_n^2 mod
// 20300713. Digits are concatenated.
while (seen.find(s) == seen.end()) {
seen[s] = index;
// Append digits of s to w
std::string s_str = std::to_string(s);
w_str += s_str;
for (char c : s_str) {
w_digits.push_back(c - '0');
}
s = (s * s) % MODULUS;
index++;
}
// Note: The problem implies w starts from s0.
// "Concatenate these numbers s0s1s2... to create a string w"
// Cycle detected at state 's'.
// However, for the purpose of the sum of digits, we treat w as the sequence
// of digits. The "infinite length" string is formed by repeating the cycle.
// Wait, does the string w start repeating exactly?
// s0 -> s1 -> ... -> sk -> ... -> sk+L -> ... where sk = sk+L
// The digits from s0 to sk-1 are the pre-period.
// But usually BBS cycles are pure if s0 is a quadratic residue?
// Or if we enter a cycle, the digits corresponding to the cycle repeat.
// Let's verify if s0 is part of the cycle or pre-period.
std::cout << "Cycle detected at index " << index << " (value: " << s << ")"
<< std::endl;
std::cout << "First seen at index " << seen[s] << std::endl;
// For this specific problem, let's assume valid range logic later.
// Just construct the full digit sequence for one period if it's pure,
// or enough to cover the non-periodic part + one period.
// Prefix sums
prefix_sum.push_back(0);
u64 current_sum = 0;
for (int d : w_digits) {
current_sum += d;
prefix_sum.push_back(current_sum);
}
total_sum_per_period =
current_sum; // This might be slightly wrong if pre-period exists.
// If pre-period exists, the "infinite string" isn't just repeating
// 'total_sum_per_period' from the start. But let's check if pre-period is
// empty (0).
if (seen[s] != 0) {
std::cerr
<< "Warning: Pre-period detected. Simplification assumption failed."
<< std::endl;
}
}
};
// Full solver
void solve_full() {
BBSSequence bbs;
std::cout << "Generating sequence..." << std::endl;
bbs.generate();
u64 S = bbs.total_sum_per_period;
size_t L = bbs.w_digits.size();
std::cout << "S (Sum): " << S << std::endl;
std::cout << "L (Length): " << L << std::endl;
// Build set R of residues present in prefix sums
// std::vector<bool> is space efficient (1 bit per bool)
std::cout << "Building residue table..." << std::endl;
std::vector<bool> R(S + 1, false);
std::vector<u64> P_mod_S(L); // Cache P[i] % S for fast access
// P[0] = 0
R[0] = true;
// Fill R and P_mod_S
// prefix_sum has L+1 entries: 0, w1, w1+w2...
// prefix_sum[i] corresponds to sum of first i digits.
// P_mod_S[i] should store prefix_sum[i] % S.
// NOTE: In the search logic, we use P[z-1].
// z=1 -> P[0]. z=2 -> P[1].
// So we need P_mod_S to store the sequence of prefix sums modulo S.
// Reconstruct just the mods to save memory if needed (though bbs has them)
// We already have bbs.prefix_sum.
for (size_t i = 0; i < L; ++i) {
u64 val = bbs.prefix_sum[i] % S;
P_mod_S[i] = val;
// The residue is present.
// Wait, R represents the set of prefix sums that exist.
// We iterate through period.
// prefix_sum[0]...prefix_sum[L-1].
// prefix_sum[L] = S == 0 mod S.
// So we just mark all prefix_sum[i] % S.
}
for (size_t i = 0; i <= L; ++i) { // Include L to ensure 0/S is marked
R[bbs.prefix_sum[i] % S] = true;
}
std::cout << "Computing p(k) for all k..." << std::endl;
// We need to sum p(k) for k = 1 to S.
// Store them to handle the full range multiplication.
std::vector<u64> p_vals(S + 1);
std::atomic<u64> processed_count(0);
unsigned int num_threads = std::thread::hardware_concurrency();
std::vector<std::thread> threads;
u64 chunk_size = S / num_threads;
auto worker = [&](u64 start_k, u64 end_k) {
for (u64 k = start_k; k <= end_k; ++k) {
// Find minimal z such that (P[z-1] + k) % S is in R.
u64 z = 1;
while (true) {
// accessing P_mod_S[(z-1) % L]
// and comparing with k
u64 start_residue = P_mod_S[(z - 1) % L];
u64 target = (start_residue + k);
if (target >= S)
target -= S; // manually mod S
if (R[target]) {
p_vals[k] = z;
break;
}
z++;
}
}
processed_count += (end_k - start_k + 1);
};
for (unsigned int i = 0; i < num_threads; ++i) {
u64 start = 1 + i * chunk_size;
u64 end = (i == num_threads - 1) ? S : (start + chunk_size - 1);
if (start <= end) {
threads.emplace_back(worker, start, end);
}
}
// Monitor progress
// Move to join
for (auto &t : threads) {
if (t.joinable())
t.join();
}
std::cout << "All p(k) computed." << std::endl;
// Calculate total answer
// Limit: 2 * 10^15
u64 LIMIT = 2000000000000000ULL;
u64 num_periods = LIMIT / S;
u64 remainder = LIMIT % S;
u64 sum_p_k_one_period = 0;
u64 sum_p_k_remainder = 0;
// Parallel reduction for sum? Or just linear is fast enough (10^8 adds ~
// 0.1s)
for (u64 k = 1; k <= S; ++k) {
sum_p_k_one_period += p_vals[k];
}
for (u64 k = 1; k <= remainder; ++k) {
sum_p_k_remainder += p_vals[k];
}
u64 total_ans = num_periods * sum_p_k_one_period + sum_p_k_remainder;
std::cout << "Sum for one period: " << sum_p_k_one_period << std::endl;
std::cout << "Remainder sum (" << remainder << "): " << sum_p_k_remainder
<< std::endl;
std::cout << "Total Answer: " << total_ans << std::endl;
}
int main() {
// Run validation if argument provided or just default
// For now, let's run full solver which includes generation
// Maybe verify small case again inside logic?
// The previous main verified small case.
// Let's keep a quick check.
// Re-instantiate for small check? No, waste of time.
// Just run the fast solver.
solve_full();
return 0;
}
Python
def solve():
MODULUS = 20300713
SEED = 14025256
s = SEED
seen = {}
index = 0
w_digits = []
while s not in seen:
seen[s] = index
s_str = str(s)
for c in s_str:
w_digits.append(int(c))
s = (s * s) % MODULUS
index += 1
L = len(w_digits)
prefix_sum = [0] * (L + 1)
for i in range(L):
prefix_sum[i + 1] = prefix_sum[i] + w_digits[i]
S = prefix_sum[L]
R = bytearray(S + 1)
for i in range(L + 1):
R[prefix_sum[i] % S] = 1
P_mod_S = [prefix_sum[i] % S for i in range(L)]
p_vals = [0] * (S + 1)
for k in range(1, S + 1):
z = 1
while True:
start_residue = P_mod_S[(z - 1) % L]
target = start_residue + k
if target >= S:
target -= S
if R[target]:
p_vals[k] = z
break
z += 1
LIMIT = 2_000_000_000_000_000
num_periods = LIMIT // S
remainder = LIMIT % S
sum_one = sum(p_vals[1:S+1])
sum_rem = sum(p_vals[1:remainder+1])
answer = num_periods * sum_one + sum_rem
return str(answer)
if __name__ == '__main__':
print(solve())
Java
import java.util.*;
import java.util.concurrent.*;
public class Euler238 {
static final long MODULUS = 20300713;
static final long SEED = 14025256;
static final long LIMIT = 2000000000000000L;
public static String solve() {
List<Integer> wDigits = new ArrayList<>();
List<Long> prefixSum = new ArrayList<>();
Map<Long, Integer> seen = new HashMap<>();
long s = SEED;
int index = 0;
while (!seen.containsKey(s)) {
seen.put(s, index);
String sStr = String.valueOf(s);
for (int i = 0; i < sStr.length(); i++) {
wDigits.add(sStr.charAt(i) - '0');
}
s = (s * s) % MODULUS;
index++;
}
prefixSum.add(0L);
long currentSum = 0;
for (int d : wDigits) {
currentSum += d;
prefixSum.add(currentSum);
}
long S = currentSum;
int L = wDigits.size();
byte[] R = new byte[(int) S + 1];
long[] PModS = new long[L];
for (int i = 0; i < L; ++i) {
PModS[i] = prefixSum.get(i) % S;
}
for (int i = 0; i <= L; ++i) {
R[(int) (prefixSum.get(i) % S)] = 1;
}
long[] pVals = new long[(int) S + 1];
int numThreads = Math.max(1, Runtime.getRuntime().availableProcessors());
long chunkSize = S / numThreads;
ExecutorService executor = Executors.newFixedThreadPool(numThreads);
List<Future<?>> futures = new ArrayList<>();
for (int i = 0; i < numThreads; ++i) {
final long startK = 1 + i * chunkSize;
final long endK = (i == numThreads - 1) ? S : (startK + chunkSize - 1);
if (startK <= endK) {
futures.add(executor.submit(() -> {
for (long k = startK; k <= endK; ++k) {
long z = 1;
while (true) {
long startResidue = PModS[(int) ((z - 1) % L)];
long target = startResidue + k;
if (target >= S) {
target -= S;
}
if (R[(int) target] == 1) {
pVals[(int) k] = z;
break;
}
z++;
}
}
}));
}
}
for (Future<?> f : futures) {
try {
f.get();
} catch (Exception e) {
}
}
executor.shutdown();
long numPeriods = LIMIT / S;
long remainder = LIMIT % S;
long sumPKOnePeriod = 0;
long sumPKRemainder = 0;
for (int k = 1; k <= S; ++k) {
sumPKOnePeriod += pVals[k];
}
for (int k = 1; k <= remainder; ++k) {
sumPKRemainder += pVals[k];
}
long totalAns = numPeriods * sumPKOnePeriod + sumPKRemainder;
return String.valueOf(totalAns);
}
public static void main(String[] args) {
System.out.println(solve());
}
}