Problem 352: Blood Tests
View on Project EulerProject Euler Problem 352 Solution
EulerSolve provides an optimized solution for Project Euler Problem 352, Blood Tests, with C++, Python, Java, and a step-by-step mathematical explanation.
Problem Summary Each person is infected independently with probability \(p\). A pooled blood test on any subset is negative if and only if every person in that subset is healthy. For a fixed group size \(s\), let \(T(s,p)\) be the minimum possible expected number of tests needed to determine all infection statuses. Problem 352 asks for $$\sum_{i=1}^{50} T\left(10000,\frac{i}{100}\right).$$ A brute-force search over all adaptive testing trees is infeasible. The repository solution succeeds because the optimal strategy can be expressed with two one-dimensional dynamic programming states whose transitions depend only on group size. Mathematical Approach Step 1: Two states and the basic probabilities Write \(q=1-p\). Then \(q^n\) is the probability that all \(n\) people are healthy, and \(1-q^n\) is the probability that at least one person in a group of size \(n\) is infected. Define \(A_n\) as the optimal expected number of tests for \(n\) people with no prior information, and \(B_n\) as the optimal expected number of tests for \(n\) people under the condition that at least one of them is infected. Because the people are exchangeable, only the size \(k\) of the next tested subgroup matters. The exact identities of the chosen people do not matter. The base values are forced: $$A_0=B_0=0,\qquad A_1=1,\qquad B_1=0.$$ \(A_1=1\) because one unconstrained person must be tested once....
Detailed mathematical approach
Problem Summary
A flock of \(s\) sheep must be screened for a virus that every animal carries independently with probability \(p\). A test on a pooled sample of any set of animals is negative exactly when every animal in it is virus-free, and every sheep must end up with a verdict. One restriction shapes the problem: whenever a mixed sample is tested, all the animals contributing to it must be fully screened before any other animal is examined. Then \(T(s,p)\) is the average number of tests of the cheapest strategy. For \(25\) sheep at \(p=0.02\) the statement computes \(7.40198008\) tests for five groups of five against only \(4.155452\) for the optimal strategy, and records \(T(25,0.10)=12.702124\). We must find the sum of \(T(10000,p)\) over \(p=0.01,0.02,\dots,0.50\), to six decimal places. [1]
The whole solution in one sentence: the restriction forces every strategy to be a nest of blocks, so the cheapest cost depends only on a block's size and on whether we already know that it contains an infected animal, and those two quantities satisfy a pair of recurrences that a table fills in \(O(s^2)\) steps per probability.
Mathematical Approach
1. The warm-up scheme
Write \(q=1-p\) for the probability that one animal is virus-free. Since the animals are independent, a pool of \(k\) of them is negative with probability \(q^k\). In the statement's flock \(q=0.98\), so a group of five is negative with probability \(0.98^5=0.9039207968\); otherwise five individual tests follow, and the group costs
$$1+5\,(1-0.98^5)=1.480396016$$
tests on average, so five groups cost \(7.40198008\), a saving of \(70.39\%\) over \(25\) separate tests. This two-stage plan is Dorfman's procedure of 1943. [2] The statement hints at three improvements to combine: pool all \(25\) samples first, since \(0.98^{25}=0.6034647297\ldots\) finishes \(60.35\%\) of flocks with one test; leave the last animal of a positive group untested when the others are clear; and choose the group sizes freely.
2. What the restriction forbids
A pool need only hold animals whose status is still open: a known virus-free animal leaves the outcome unchanged, a known infected one makes it certainly positive. While a tested pool still holds unresolved animals, every further test must stay inside it; a negative pool is settled at once, and attention moves outside it. So the work always happens inside one block of \(n\) interchangeable animals that has to be finished first, and a test keeps that block in one of two states: a negative sub-pool hands the block's guarantee on to the rest of it, while a positive sub-pool satisfies that guarantee itself and leaves the rest with its original independent distribution. About a block we therefore know either nothing, or one fact, that at least one of its animals is infected.
3. Two sequences
Let \(A_n\) be the cheapest average number of tests that finishes a block of \(n\) animals about which nothing is known, and \(B_n\) the same for a block known to contain at least one infected animal. The flock is a block of the first kind, so \(T(s,p)=A_s\). The base cases are forced: \(A_0=0\); \(A_1=1\), since a lone animal with no information must be tested; and \(B_1=0\), since a lone animal known to be infected needs no test — the statement's remark about the fifth animal of a positive group. The code also stores \(B_0=0\), never read, since conditioning an empty block on an infection means nothing.
4. The recurrence without prior information
In a block of \(n\) animals about which nothing is known, let the first pool hold \(k\le n\) of them. With probability \(q^k\) it is negative, those \(k\) are virus-free, and the block shrinks to the \(n-k\) others with no information. With probability \(1-q^k\) it is positive; the restriction forces us to finish those \(k\) first, knowing only that one of them is infected, which costs \(B_k\). The outcome concerns only the pool, so the animals outside stay unconditioned. By the law of total expectation [5] the cost is \(1+(1-q^k)B_k+A_{n-k}\), where \(A_{n-k}\) has weight \(1\) because it is paid in both branches. Hence
$$A_n=\min_{1\le k\le n}\bigl(1+(1-q^k)B_k+A_{n-k}\bigr).$$
The case \(k=n\) is the choice "pool the whole block". Naming the cost of finishing a block of \(k\) animals in one piece,
$$\begin{aligned}C_k&=1+(1-q^k)B_k,\\ A_n&=\min_{1\le k\le n}\bigl(C_k+A_{n-k}\bigr),\end{aligned}$$
makes \(C_1=1\) plain individual testing, and shows what \(A_n\) is: the cheapest total over all ways of cutting \(n\) animals into blocks, with cost \(C_k\) for a block of size \(k\).
5. The recurrence with one infected animal guaranteed
Now the block is known to contain an infected animal. Pooling all \(n\) would be positive for certain and waste a test, so the pool holds \(k\lt n\) animals. By the definition of conditional probability [4] it is negative with probability
$$\alpha_k=\frac{q^k\,(1-q^{n-k})}{1-q^n},$$
because a negative pool together with the known fact forces an infected animal among the other \(n-k\); a positive pool already implies one, so its conditional probability is \(1-\alpha_k=(1-q^k)/(1-q^n)\). In the negative branch the tested animals are clear and the remaining \(n-k\) inherit the guarantee, costing \(B_{n-k}\); in the positive branch the pool is finished first, costing \(B_k\), and the other \(n-k\) are unconditioned, costing \(A_{n-k}\). Therefore
$$\begin{aligned}B_n=\min_{1\le k\lt n}\Bigl(&1+\alpha_k B_{n-k}\\&+(1-\alpha_k)\bigl(B_k+A_{n-k}\bigr)\Bigr).\end{aligned}$$
Each expression is a lower bound for every legal strategy whose first pool has that size, and a minimising pool followed by optimal continuations attains it, so induction on \(n\) proves that \(A_n\) and \(B_n\) are the true optima. Both recurrences use indices below \(n\), except that \(A_n\) uses \(B_n\), so the table is filled for \(n=2,\dots,s\) with \(B_n\) before \(A_n\): bottom-up dynamic programming, each subproblem solved once. [6] These are the recurrences of Sobel and Groll's 1959 procedure \(R_3\), the variant that forbids recombining sets, with \(H_3=A\), \(G_3=B\). [3]
6. Two and three sheep by hand
For two animals known to contain an infected one, the only choice is to test one of them. It is clear with probability \(q(1-q)/(1-q^2)=q/(1+q)\), and then its partner is infected and needs no test; otherwise the tested animal is infected and the partner is still open, costing \(A_1=1\). Hence
$$\begin{aligned}B_2&=1+\frac1{1+q},\\ A_2&=\min\bigl(3-q-q^2,\,2\bigr),\end{aligned}$$
the first entry of the minimum being \(C_2\) and the second two individual tests. At \(p=0.1\): \(B_2=29/19=1.526315789\ldots\) and \(A_2=1.29\) exactly.
For three animals with the guarantee, testing one first gives \(\alpha_1=q(1+q)/(1+q+q^2)\) and the cost \(1+\alpha_1B_2+(1-\alpha_1)A_2\), while testing two first gives \(1+(3+2q)/(1+q+q^2)\); at \(q=0.9\) these are \(661/271\) and \(751/271\), so \(B_3=661/271=2.439114391\ldots\), and \(A_3\) then compares \(C_1+A_2=2.29\), \(C_2+A_1=2.29\) and \(C_3\), where the whole-block option wins:
$$A_3=1+0.271\cdot\tfrac{661}{271}=1.661.$$
7. Five and twenty-five sheep at \(p=0.02\)
Now \(q=0.98\). For five animals the table says: pool all five, finishing the \(90.39\%\) of flocks whose test is negative. Otherwise we are in the guaranteed state with \(n=5\), whose best first pool holds two animals, \(\alpha_2=0.5878400457\ldots\):
$$B_5=1+\alpha_2B_3+(1-\alpha_2)\bigl(B_2+A_3\bigr).$$
With \(B_3=2.353557339\ldots\), \(B_2=1.505050505\ldots\) and \(A_3=1.138408\) this is \(B_5=3.473042990\ldots\), so
$$\begin{aligned}A_5&=1+0.0960792032\cdot B_5\\&=1.333687203\ldots,\end{aligned}$$
against \(1.480396016\) for "pool the five, then test all five". For \(25\) animals the table again pools the whole flock first; the best second pool holds \(8\) of the \(25\), and \(B_{25}=7.957557479\ldots\) gives
$$\begin{aligned}A_{25}&=1+(1-0.98^{25})B_{25}\\&=4.155452205\ldots,\end{aligned}$$
which rounds to the checkpoint \(4.155452\) and saves \(83.38\%\) of \(25\) separate tests.
8. Blocks, and the cost per animal
Since \(A_n\) is the cheapest sum of block costs, the ratio \(C_k/k\) is the number of tests per animal that a block of size \(k\) charges, and \(A_n\ge n\cdot\min_k(C_k/k)\). At \(p=0.10\) the cheapest ratio belongs to \(k=8\), where \(C_8=4.05204279\), and the second checkpoint falls out as three blocks, \(25=8+8+9\):
$$2\,C_8+C_9=12.702124091$$
with \(C_9=4.598038511\), while \(8+8+8+1\) would cost \(13.15612837\), because a leftover single animal pays a whole test: the best ratio alone does not decide the answer, the best composition does. As \(p\) falls the ideal block grows: for \(s=10000\) its size is \(2\) at \(p=0.30\), \(8\) at \(p=0.10\) and \(89\) at \(p=0.01\), where \(T(10000,0.01)\approx964.186678\), about \(0.0964\) tests per animal.
9. Where pooling stops paying
Pooling a pair beats two individual tests exactly when \(C_2\lt2\), that is \(3-q-q^2\lt2\), or \(q^2+q\gt1\). The positive root of \(q^2+q=1\) is \(1/\varphi=\varphi-1\) for the golden ratio \(\varphi=(1+\sqrt5)/2\), and the identity \(1=1/\varphi+1/\varphi^2\) turns the condition into [7]
$$\begin{aligned}p&\lt\varphi^{-2}=\frac{3-\sqrt5}{2}\\&=0.3819660112501\ldots\end{aligned}$$
Ungar proved in 1960 that this is the exact cutoff of the whole problem, unrestricted procedures included: at or above it individual testing is optimal — at the cutoff \(C_2=2\), so pooling a pair only ties — and below it pooling wins for every flock of at least two animals, while no general optimal procedure below the cutoff is known. [2] That is why this problem imposes its restriction. Since \(A_n\) is a sum of block costs, \(A_s=s\) holds exactly when \(C_k\ge k\) for every \(k\le s\), and the table confirms \(A_{10000}=10000\) for every \(p\ge0.39\): twelve of the fifty terms are plain individual testing. Just below the cutoff pooling pairs still pays, and \(p=0.38\) gives \(5000\,C_2=9978\) exactly.
How the Code Works
The C++ function T(const int s, const long double p) [9] is Sections 3 to 5 line by line. It returns \(0\) for \(s\le0\) and \(1\) for \(s=1\), builds qpow with qpow[i]\({}=q^i\) by repeated multiplication, and fills A, B of length \(s+1\) from A[1]\({}=1\), B[1]\({}=0\). For each \(n\) it computes pe\({}=1-q^n\) once, scans \(k=1,\dots,n-1\) with pa\({}=\alpha_k\), pb\({}=1-\alpha_k\) and keeps the smallest conditional cost in bestB, whose sentinel 1e100L is always replaced since \(n\ge2\); then it sets bestA to the \(k=n\) term \(1+(1-q^n)\)B[n] of Section 4 and compares it with the split terms \(1+\)A[n-k]\({}+(1-q^k)\)B[k]. solve adds the fifty values T(10000, i/100), each computed independently.
Only the C++ validates itself: run_checkpoints compares T(25,0.02) and T(25,0.10) with \(4.155452\) and \(12.702124\) through approx_equal_6dp, which demands a difference below \(5\cdot10^{-7}\); main takes only --skip-checkpoints, returns the error code \(2\) on a failed checkpoint, and otherwise prints the sum with setprecision(6). The Java port repeats the recurrence with double[] arrays but has no checkpoint; if collecting its results throws, it prints a stack trace and then formats the subtotal reached so far, which is not a finished computation. The Python file is no second implementation: it compiles the C++ at -O2 into a cached binary when that is missing or stale, runs it, and reads the last answer: line, else the last = line, else the last non-empty line — adding --skip-checkpoints whenever that string occurs in the C++ source, so the checkpoints are skipped on this path.
The two real implementations differ in their arithmetic type. The format of long double is left to the implementation [8]; on the arm64 Mac used here its significand has Java's \(53\) bits, on many x86-64 compilers \(64\). Equal precision is not identical rounding: a C++ compiler may fuse a multiplication and an addition into one rounded operation, and the C++ and Java values of \(T(25,0.02)\) differ in the sixteenth significant digit — compiling the C++ without that fusion reproduces the Java value exactly. In qpow the powers reach the subnormal range, where repeated multiplication may stall at a tiny positive value instead of reaching zero: at \(p=0.10\) qpow[10000] ends at about \(2.5\cdot10^{-323}\), at \(p=0.50\) at zero, both far below what six decimals see. The denominator stays safe, since \(p\ge0.01\) and \(n\ge2\) give pe\({}\ge1-0.99^2=0.0199\); only a \(p\) with \(1-p\) rounding to \(1\) would divide by zero.
Complexity and Verification
For one probability the two scans examine
$$\sum_{n=2}^{s}2\,(n-1)=s\,(s-1)=99\,990\,000$$
candidate pool sizes at \(s=10000\), so the time is \(\Theta(s^2)\) and the memory \(\Theta(s)\): three arrays of \(s+1\) numbers, about \(240\) kilobytes at eight bytes each. The fifty probabilities need \(4\,999\,500\,000\) candidates in all; measured here one takes about \(60\) milliseconds compiled with -O2, so the fifty are a few seconds of processor work, and Java is comparable.
The checks behind this article are these.
- Both checkpoints come out, \(4.155452205\ldots\) and \(12.702124091\), and the C++ stops with the error code \(2\) if either is wrong. C++ and Java print the same six decimals; the Python wrapper reproduces them by running the C++ build, so it is not independent evidence.
- Every nested strategy for a block of at most five animals was enumerated in exact rational arithmetic at four probabilities, and its minimum equals the recurrence in all cases: \(A_5=416777251/312500000\) at \(p=0.02\), \(A_5=5\) at \(p=0.4\). A second computation over arbitrary subsets of up to nine animals, choosing freely which animals to pool, returns the same values: only a block's size matters, as Section 2 claims.
- A separate program drove the pool sizes the table selects on random flocks and counted the tests performed: three million flocks of \(25\) animals averaged \(4.155907\) at \(p=0.02\) and \(12.700457\) at \(p=0.10\), within two standard errors of the table at each of three seeds, one standard error of the mean being \(0.0024\) and \(0.0030\). A strategy attaining the recurrence therefore exists.
- Against exact rational arithmetic the double table is off by at most \(7.2\cdot10^{-15}\) at \(s=30\), and against \(40\)-digit arithmetic by \(4.3\cdot10^{-12}\) at \(s=1200\) and \(2.2\cdot10^{-11}\) at \(s=2400\). The error grows with \(s\), so these checks support the implementation at the sizes tested; certifying the sixth decimal would need a bound at \(s=10000\) and over all fifty terms.
- The block form of Section 4, evaluated separately for \(n\le600\) at four probabilities, agrees with the published order of the additions to within \(1.5\cdot10^{-14}\), and \(A_{10000}=10000\) holds exactly at the twelve probabilities above Ungar's cutoff, as it must.
Footnotes and References
The references below supply the background facts used above. The block decomposition, the two recurrences, the worked examples and the operation counts are derived explicitly in this article.
- Project Euler 352 — Blood Tests. The official statement puts the five-groups-of-five scheme at 7.40198008 tests, requires every animal in a mixed sample to be fully screened before others are examined, gives T(25, 0.02) = 4.155452 and T(25, 0.10) = 12.702124, and asks for the sum of T(10000, p) over the fifty probabilities.
- Wikipedia — Group testing. The article credits Robert Dorfman with introducing group testing in 1943 to screen army recruits, grouping the men and testing the members of positive groups individually. It also gives Peter Ungar's cutoff of 1960, p_u = (3 − √5)/2 ≈ 0.38, above which individual testing minimises the expected number of tests, and states that below it a general optimal procedure is still unknown.
- M. Sobel and P. A. Groll, Group testing to eliminate efficiently all defectives in a binomial sample, Bell System Technical Journal 38 (1959), pages 1179–1252, in the Internet Archive. Each unit is an independent observation from a binomial population with a known probability of being good. Appendix B defines procedure R3, in which two sets once formed cannot be recombined, so sets are only broken into smaller ones, "yielding a nested set of partitions"; its equations (141) and (142) are the recurrences used here, H3(n) for a set with no known defective and G3(m) for one known to contain a defective, with H3(0) = G3(1) = 0.
- Wikipedia — Conditional probability. The article defines P(A | B) = P(A ∩ B) / P(B) for P(B) greater than zero and calls A and B independent when the probability of their intersection is the product of their probabilities.
- Wikipedia — Law of total expectation. For a partition into events Aᵢ the article gives E(X) = Σᵢ E(X | Aᵢ) P(Aᵢ), the form Sections 4 and 5 apply to the two branches of a pooled test.
- Wikipedia — Dynamic programming. The article explains optimal substructure, where a solution is built from optimal solutions of subproblems, and overlapping subproblems, which dynamic programming solves once each, bottom-up in tabular form.
- Wikipedia — Golden ratio. The article gives φ = (1 + √5)/2, states that φ² = φ + 1 and 1/φ = φ − 1 = 0.618033…, and records the identity 1 = 1/φ + 1/φ², which Section 9 uses to identify the cutoff with φ⁻².
- cppreference — Fundamental types. The page calls long double an extended-precision type that need not map to an IEEE-754 format, and notes that many x86 and x86-64 implementations use the 80-bit x87 format, one notable implementation the binary64 format of double, and some ARM64 implementations binary128.
- C++ — Euler352.cpp. The linked immutable C++ revision contains
T,solve,approx_equal_6dp,run_checkpointsandmain, and compares T(25, 0.02) and T(25, 0.10) with the two checkpoints. It is the implementation record for this article.
Problem 352 source code
C++
#include <cmath>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <vector>
namespace {
long double T(const int s, const long double p) {
if (s <= 0) {
return 0.0L;
}
if (s == 1) {
return 1.0L;
}
const long double q = 1.0L - p;
std::vector<long double> qpow(static_cast<std::size_t>(s + 1), 1.0L);
for (int i = 1; i <= s; ++i) {
qpow[static_cast<std::size_t>(i)] = qpow[static_cast<std::size_t>(i - 1)] * q;
}
std::vector<long double> A(static_cast<std::size_t>(s + 1), 0.0L);
std::vector<long double> B(static_cast<std::size_t>(s + 1), 0.0L);
A[0] = 0.0L;
B[0] = 0.0L;
A[1] = 1.0L;
B[1] = 0.0L; // known infected for size 1 => no test needed
for (int n = 2; n <= s; ++n) {
const long double pe = 1.0L - qpow[static_cast<std::size_t>(n)]; // P(at least one infected in n)
// State B(n): at least one infected is guaranteed.
long double bestB = 1e100L;
for (int k = 1; k < n; ++k) {
const long double pa = qpow[static_cast<std::size_t>(k)] *
(1.0L - qpow[static_cast<std::size_t>(n - k)]) / pe;
const long double pb = 1.0L - pa;
const long double cost = 1.0L +
pa * B[static_cast<std::size_t>(n - k)] +
pb * (B[static_cast<std::size_t>(k)] + A[static_cast<std::size_t>(n - k)]);
if (cost < bestB) {
bestB = cost;
}
}
B[static_cast<std::size_t>(n)] = bestB;
// State A(n): no prior condition.
long double bestA = 1.0L + (1.0L - qpow[static_cast<std::size_t>(n)]) * B[static_cast<std::size_t>(n)];
for (int k = 1; k < n; ++k) {
const long double cost = 1.0L +
A[static_cast<std::size_t>(n - k)] +
(1.0L - qpow[static_cast<std::size_t>(k)]) * B[static_cast<std::size_t>(k)];
if (cost < bestA) {
bestA = cost;
}
}
A[static_cast<std::size_t>(n)] = bestA;
}
return A[static_cast<std::size_t>(s)];
}
long double solve() {
struct WorkerCtx {
int start_i;
int step_i;
long double partial_sum;
};
auto worker_main = [](void* ptr) -> void* {
auto* ctx = static_cast<WorkerCtx*>(ptr);
long double local = 0.0L;
for (int i = ctx->start_i; i <= 50; i += ctx->step_i) {
const long double p = static_cast<long double>(i) / 100.0L;
local += T(10000, p);
}
ctx->partial_sum = local;
return nullptr;
};
long cpu_count = sysconf(_SC_NPROCESSORS_ONLN);
unsigned thread_count = (cpu_count > 0) ? static_cast<unsigned>(cpu_count) : 1U;
if (thread_count > 8U) {
thread_count = 8U;
}
if (thread_count > 50U) {
thread_count = 50U;
}
if (thread_count <= 1U) {
long double total = 0.0L;
for (int i = 1; i <= 50; ++i) {
const long double p = static_cast<long double>(i) / 100.0L;
total += T(10000, p);
}
return total;
}
std::vector<pthread_t> threads(thread_count);
std::vector<WorkerCtx> ctx(thread_count);
unsigned created = 0U;
bool failed = false;
for (unsigned t = 0; t < thread_count; ++t) {
ctx[t] = WorkerCtx{static_cast<int>(t) + 1, static_cast<int>(thread_count), 0.0L};
if (pthread_create(&threads[t], nullptr, worker_main, &ctx[t]) != 0) {
failed = true;
break;
}
++created;
}
for (unsigned t = 0; t < created; ++t) {
pthread_join(threads[t], nullptr);
}
if (failed) {
long double total = 0.0L;
for (int i = 1; i <= 50; ++i) {
const long double p = static_cast<long double>(i) / 100.0L;
total += T(10000, p);
}
return total;
}
long double total = 0.0L;
for (const WorkerCtx& w : ctx) {
total += w.partial_sum;
}
return total;
}
bool approx_equal_6dp(const long double x, const long double expected) {
return std::fabsl(x - expected) < 0.0000005L;
}
bool run_checkpoints() {
if (!approx_equal_6dp(T(25, 0.02L), 4.155452L)) {
std::cerr << "Checkpoint failed: T(25,0.02)\n";
return false;
}
if (!approx_equal_6dp(T(25, 0.10L), 12.702124L)) {
std::cerr << "Checkpoint failed: T(25,0.10)\n";
return false;
}
return true;
}
} // namespace
int main(int argc, char** argv) {
bool skip_checkpoints = false;
for (int i = 1; i < argc; ++i) {
const std::string arg(argv[i]);
if (arg == "--skip-checkpoints") {
skip_checkpoints = true;
} else {
std::cerr << "Unknown argument: " << arg << '\n';
return 1;
}
}
if (!skip_checkpoints && !run_checkpoints()) {
return 2;
}
const long double answer = solve();
std::cout << std::fixed << std::setprecision(6) << answer << '\n';
return 0;
}
Python
from __future__ import annotations
import re
import shutil
import subprocess
from pathlib import Path
ANSWER_RE = re.compile(r"answer\s*:\s*(.+)$", re.IGNORECASE)
EQUAL_RE = re.compile(r"=\s*(.+)$")
def parse_output(stdout: str) -> str:
lines = [line.strip() for line in stdout.splitlines() if line.strip()]
if not lines:
return ""
answers = []
equals = []
for line in lines:
m1 = ANSWER_RE.search(line)
if m1:
answers.append(m1.group(1).strip())
m2 = EQUAL_RE.search(line)
if m2:
equals.append(m2.group(1).strip())
if answers:
return answers[-1]
if equals:
return equals[-1]
return lines[-1]
def should_skip_cpp_checkpoints(src: Path) -> bool:
try:
text = src.read_text(encoding="utf-8", errors="ignore")
except OSError:
return False
return "--skip-checkpoints" in text
def run_cpp(binary: Path, src: Path, root: Path) -> str:
cmd = [str(binary)]
if should_skip_cpp_checkpoints(src):
cmd.append("--skip-checkpoints")
try:
return subprocess.check_output(cmd, text=True, cwd=root)
except subprocess.CalledProcessError:
return subprocess.check_output(cmd, text=True, cwd=src.parent)
def solve() -> str:
problem_id = __file__.split("Euler")[-1].split(".")[0]
root = Path(__file__).resolve().parent.parent
src = root / "solutionsCpp" / f"Euler{problem_id}.cpp"
binary = root / "solutionsCpp" / f".euler{problem_id}_py_bridge"
if not binary.exists() or src.stat().st_mtime > binary.stat().st_mtime:
compiler = shutil.which("clang++") or shutil.which("g++")
if not compiler:
raise RuntimeError("No C++ compiler found (clang++/g++).")
subprocess.check_call([compiler, "-std=c++17", "-O2", str(src), "-o", str(binary)])
output = run_cpp(binary=binary, src=src, root=root)
parsed = parse_output(output)
if not parsed:
raise RuntimeError(f"Euler{problem_id} bridge produced empty output.")
return parsed
if __name__ == "__main__":
print(solve())
Java
import java.util.*;
import java.util.concurrent.*;
public class Euler352 {
static double T(int s, double p) {
if (s <= 0)
return 0.0;
if (s == 1)
return 1.0;
double q = 1.0 - p;
double[] qpow = new double[s + 1];
qpow[0] = 1.0;
for (int i = 1; i <= s; ++i) {
qpow[i] = qpow[i - 1] * q;
}
double[] A = new double[s + 1];
double[] B = new double[s + 1];
A[0] = 0.0;
B[0] = 0.0;
A[1] = 1.0;
B[1] = 0.0;
for (int n = 2; n <= s; ++n) {
double pe = 1.0 - qpow[n];
double bestB = 1e100;
for (int k = 1; k < n; ++k) {
double pa = qpow[k] * (1.0 - qpow[n - k]) / pe;
double pb = 1.0 - pa;
double cost = 1.0 + pa * B[n - k] + pb * (B[k] + A[n - k]);
if (cost < bestB)
bestB = cost;
}
B[n] = bestB;
double bestA = 1.0 + (1.0 - qpow[n]) * B[n];
for (int k = 1; k < n; ++k) {
double cost = 1.0 + A[n - k] + (1.0 - qpow[k]) * B[k];
if (cost < bestA)
bestA = cost;
}
A[n] = bestA;
}
return A[s];
}
public static String solve() {
int threads = Runtime.getRuntime().availableProcessors();
if (threads == 0)
threads = 1;
ExecutorService executor = Executors.newFixedThreadPool(threads);
List<Future<Double>> futures = new ArrayList<>();
for (int i = 1; i <= 50; i++) {
final int p_idx = i;
futures.add(executor.submit(() -> {
double p = p_idx / 100.0;
return T(10000, p);
}));
}
double total = 0.0;
try {
for (Future<Double> f : futures) {
total += f.get();
}
} catch (Exception e) {
e.printStackTrace();
}
executor.shutdown();
return String.format(Locale.US, "%.6f", total);
}
public static void main(String[] args) {
System.out.println(solve());
}
}