Problem 267: Billionaire

View on Project Euler

Project Euler Problem 267 Solution

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

Problem Summary A gambler starts with capital \(1\). In each of \(n\) fair tosses, the gambler bets the same fixed fraction \(f\) of the current capital. A win triples the stake portion, so total capital is multiplied by \((1+2f)\); a loss destroys the stake portion, so total capital is multiplied by \((1-f)\). For the original Project Euler problem, \(n=1000\) and the target is \(T=10^9\). The C++ solution keeps both quantities as parameters, so the same derivation works for any \(n\ge 1\) and \(T>1\). Mathematical Approach 1. Wealth After Exactly \(k\) Wins If exactly \(k\) of the \(n\) tosses are wins, then the order of wins and losses does not matter, because multiplication is commutative. The final wealth is therefore $$W_k(f)=(1+2f)^k(1-f)^{n-k}.$$ So for each possible win count \(k\), the problem is a one-variable optimization over the admissible range \(0\le f<1\). 2. Why the Code Optimizes the Logarithm Directly maximizing \(W_k(f)\) is equivalent to maximizing its logarithm, because \(\ln(x)\) is strictly increasing on \(x>0\). The code therefore works with $$g_k(f)=\ln W_k(f)=k\ln(1+2f)+(n-k)\ln(1-f).$$ This removes huge intermediate numbers and turns the product into a sum. It also makes the derivative easy to analyze. 3....

Detailed mathematical approach

Problem Summary

A gambler starts with capital \(1\). In each of \(n\) fair tosses, the gambler bets the same fixed fraction \(f\) of the current capital. A win triples the stake portion, so total capital is multiplied by \((1+2f)\); a loss destroys the stake portion, so total capital is multiplied by \((1-f)\).

For the original Project Euler problem, \(n=1000\) and the target is \(T=10^9\). The C++ solution keeps both quantities as parameters, so the same derivation works for any \(n\ge 1\) and \(T>1\).

Mathematical Approach

1. Wealth After Exactly \(k\) Wins

If exactly \(k\) of the \(n\) tosses are wins, then the order of wins and losses does not matter, because multiplication is commutative. The final wealth is therefore

$$W_k(f)=(1+2f)^k(1-f)^{n-k}.$$

So for each possible win count \(k\), the problem is a one-variable optimization over the admissible range \(0\le f<1\).

2. Why the Code Optimizes the Logarithm

Directly maximizing \(W_k(f)\) is equivalent to maximizing its logarithm, because \(\ln(x)\) is strictly increasing on \(x>0\). The code therefore works with

$$g_k(f)=\ln W_k(f)=k\ln(1+2f)+(n-k)\ln(1-f).$$

This removes huge intermediate numbers and turns the product into a sum. It also makes the derivative easy to analyze.

3. Best Fixed Fraction for a Given \(k\)

Differentiating gives

$$g_k'(f)=\frac{2k}{1+2f}-\frac{n-k}{1-f}.$$

Setting the derivative to zero yields the critical point

$$f_*=\frac{3k-n}{2n}.$$

The second derivative is

$$g_k''(f)=-\frac{4k}{(1+2f)^2}-\frac{n-k}{(1-f)^2}<0,$$

so \(g_k\) is concave and any interior critical point is automatically the global maximum.

This gives exactly the three branches used in minimum_wins_needed:

1. If \(k=n\), then every toss is a win, so \(W_n(f)=(1+2f)^n\) is increasing and the optimum is the boundary limit \(f\to1^{-}\), giving \(\max g_n = n\ln 3\).

2. If \(f_*\le 0\), then the concave maximum lies to the left of the admissible interval, so the best admissible point is \(f=0\), with \(W_k(0)=1\) and therefore \(g_k(0)=0\).

3. If \(0<f_*<1\), then the optimum is achieved at that interior point and the code evaluates \(g_k(f_*)\) directly.

4. Turning Optimization into a Threshold in \(k\)

For a fixed admissible \(f\), replacing one loss by one win multiplies wealth by

$$\frac{1+2f}{1-f}>1 \qquad (0<f<1).$$

So for every fixed \(f\), the quantity \(W_k(f)\) is nondecreasing in \(k\), and strictly increasing when \(f>0\). Taking the maximum over all admissible \(f\) preserves this monotonicity, so \(\max_f g_k(f)\) also increases with \(k\).

That means there is a clean threshold \(k_{\min}\): the smallest number of wins for which the target becomes reachable, namely

$$k_{\min}=\min\{k:\max_f g_k(f)\ge \ln T\}.$$

Once this threshold is found, every outcome with at least \(k_{\min}\) wins is successful, and every outcome below it is impossible to rescue with any fixed fraction.

5. The Probability Is a Binomial Tail

Because the tosses are fair, the number of wins \(K\) follows

$$K\sim\mathrm{Binomial}(n,1/2).$$

Therefore the answer is

$$P_{\mathrm{succ}}=\Pr[K\ge k_{\min}]=\sum_{k=k_{\min}}^{n}\binom{n}{k}2^{-n}.$$

The program does not compute factorials. Instead it starts with

$$P_0=2^{-n}$$

and uses the recurrence

$$P_{k+1}=P_k\frac{n-k}{k+1}.$$

This is the standard ratio between consecutive binomial masses and is much more stable numerically.

6. Worked Checkpoint: \(n=2,\;T=2\)

The code contains this test and expects the answer \(1/4\). Let us derive it exactly.

If \(k=1\), then

$$f_*=\frac{3\cdot1-2}{2\cdot2}=\frac14.$$

At this optimum,

$$W_1\!\left(\frac14\right)=\left(1+\frac12\right)\left(1-\frac14\right)=\frac32\cdot\frac34=\frac98<2.$$

So one win is not enough.

If \(k=2\), then every toss is a win and the optimum is the boundary \(f\to1^{-}\), where

$$W_2(f)\to 3^2=9>2.$$

Hence \(k_{\min}=2\). The success probability is just the probability of two wins in two fair tosses:

$$P_{\mathrm{succ}}=\binom22 2^{-2}=\frac14.$$

7. Worked Checkpoint: \(n=4,\;T=4\)

The second internal test expects \(5/16\). Again we can derive it by hand.

If \(k=2\), then

$$f_*=\frac{3\cdot2-4}{2\cdot4}=\frac14,$$

so

$$W_2\!\left(\frac14\right)=\left(\frac32\right)^2\left(\frac34\right)^2=\frac{81}{64}<4.$$

Thus two wins are still insufficient.

If \(k=3\), then

$$f_*=\frac{3\cdot3-4}{2\cdot4}=\frac58,$$

and

$$W_3\!\left(\frac58\right)=\left(1+\frac{10}{8}\right)^3\left(1-\frac58\right)=\left(\frac94\right)^3\frac38=\frac{2187}{512}>4.$$

Therefore \(k_{\min}=3\). The success probability is the binomial tail

$$\Pr[K\ge3]=\binom43 2^{-4}+\binom44 2^{-4}=\frac{4+1}{16}=\frac{5}{16}.$$

How the Code Works

parse_arguments reads three command-line controls: --tosses=<n>, --target=<T>, and --skip-checkpoints.

minimum_wins_needed scans \(k=0,1,\dots,n\). For each \(k\), it computes the best possible log-wealth according to the three-case rule above. The first \(k\) whose best log-wealth reaches \(\ln T\) is returned.

binomial_tail_probability builds the probabilities \(P_k=\Pr[K=k]\) from \(P_0=2^{-n}\) using the binomial ratio recurrence, then sums from \(k_{\min}\) to \(n\).

solve_probability is just the composition of those two steps. The two checkpoints in run_checkpoints verify that both the threshold search and the tail summation are working correctly before the main Euler instance is evaluated.

Complexity Analysis

The threshold search over \(k\) is \(O(n)\). Building and summing the binomial probabilities is also \(O(n)\). The implementation stores a probability vector of length \(n+1\), so the memory usage is \(O(n)\).

A streaming implementation could reduce the memory to \(O(1)\), because only the current probability is needed, but the vector version is simpler to read and perfectly adequate for \(n=1000\).

Footnotes and References

  1. Problem page: https://projecteuler.net/problem=267
  2. Kelly criterion intuition: Wikipedia - Kelly criterion
  3. Binomial distribution: Wikipedia - Binomial distribution

Problem 267 source code

C++

#include <cmath>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>

namespace {

struct Options {
    int tosses = 1000;
    long double target = 1.0e9L;
    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_ld_after_prefix(const std::string& arg, const std::string& prefix, long double& value) {
    if (arg.rfind(prefix, 0U) != 0U) {
        return false;
    }
    const std::string tail = arg.substr(prefix.size());
    if (tail.empty()) {
        return false;
    }
    char* end_ptr = nullptr;
    value = std::strtold(tail.c_str(), &end_ptr);
    return end_ptr != nullptr && *end_ptr == '\0';
}

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, "--tosses=", options.tosses) ||
            parse_ld_after_prefix(arg, "--target=", options.target)) {
            continue;
        }
        std::cerr << "Unknown argument: " << arg << '\n';
        return false;
    }
    return options.tosses >= 1 && options.target > 1.0L;
}

long double binomial_tail_probability(const int n, const int k_min) {
    if (k_min <= 0) {
        return 1.0L;
    }
    if (k_min > n) {
        return 0.0L;
    }

    std::vector<long double> prob(static_cast<std::size_t>(n + 1), 0.0L);
    prob[0] = std::ldexp(1.0L, -n);  // 2^-n
    for (int k = 0; k < n; ++k) {
        prob[static_cast<std::size_t>(k + 1)] =
            prob[static_cast<std::size_t>(k)] * static_cast<long double>(n - k) /
            static_cast<long double>(k + 1);
    }

    long double sum = 0.0L;
    for (int k = k_min; k <= n; ++k) {
        sum += prob[static_cast<std::size_t>(k)];
    }
    return sum;
}

int minimum_wins_needed(const int n, const long double target) {
    const long double ln_target = std::log(target);
    for (int k = 0; k <= n; ++k) {
        long double best_gain = -1.0e300L;
        if (k == n) {
            best_gain = static_cast<long double>(n) * std::log(3.0L);
        } else {
            const long double f_star =
                (3.0L * static_cast<long double>(k) - static_cast<long double>(n)) /
                (2.0L * static_cast<long double>(n));
            if (f_star > 0.0L && f_star < 1.0L) {
                best_gain =
                    static_cast<long double>(k) * std::log(1.0L + 2.0L * f_star) +
                    static_cast<long double>(n - k) * std::log(1.0L - f_star);
            } else if (f_star <= 0.0L) {
                best_gain = 0.0L;
            }
        }
        if (best_gain >= ln_target) {
            return k;
        }
    }
    return n + 1;
}

long double solve_probability(const int tosses, const long double target) {
    const int k_min = minimum_wins_needed(tosses, target);
    return binomial_tail_probability(tosses, k_min);
}

bool run_checkpoints() {
    // For two tosses and target 2.0, requiring exactly two wins gives probability 1/4.
    {
        const long double p = solve_probability(2, 2.0L);
        if (std::fabsl(p - 0.25L) > 1e-15L) {
            std::cerr << "Checkpoint failed for tosses=2 target=2.0" << '\n';
            return false;
        }
    }
    // For four tosses and target 4.0, at least three wins are required at optimum.
    {
        const long double p = solve_probability(4, 4.0L);
        const long double expected = 5.0L / 16.0L;
        if (std::fabsl(p - expected) > 1e-15L) {
            std::cerr << "Checkpoint failed for tosses=4 target=4.0" << '\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;
    }
    const long double ans = solve_probability(options.tosses, options.target);
    std::cout << std::fixed << std::setprecision(12) << static_cast<double>(ans) << '\n';
    return 0;
}

Python

import math

def binomial_tail_probability(n, k_min):
    if k_min <= 0:
        return 1.0
    if k_min > n:
        return 0.0
        
    prob = [0.0] * (n + 1)
    # math.ldexp is available in math package but pow(2.0, -n) is also fine
    prob[0] = math.ldexp(1.0, -n)
    
    for k in range(n):
        prob[k + 1] = prob[k] * (n - k) / (k + 1)
        
    return sum(prob[k_min : n + 1])

def minimum_wins_needed(n, target):
    ln_target = math.log(target)
    for k in range(n + 1):
        best_gain = -1.0e300
        if k == n:
            best_gain = n * math.log(3.0)
        else:
            f_star = (3.0 * k - n) / (2.0 * n)
            if 0.0 < f_star < 1.0:
                best_gain = k * math.log(1.0 + 2.0 * f_star) + (n - k) * math.log(1.0 - f_star)
            elif f_star <= 0.0:
                best_gain = 0.0
                
        if best_gain >= ln_target:
            return k
            
    return n + 1

def solve_probability(tosses, target):
    k_min = minimum_wins_needed(tosses, target)
    return binomial_tail_probability(tosses, k_min)

def solve():
    ans = solve_probability(1000, 1.0e9)
    return f"{ans:.12f}"

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

Java

import java.util.Locale;

public class Euler267 {
    static double binomialTailProbability(int n, int kMin) {
        if (kMin <= 0)
            return 1.0;
        if (kMin > n)
            return 0.0;

        double[] prob = new double[n + 1];
        prob[0] = Math.pow(2.0, -n);

        for (int k = 0; k < n; k++) {
            prob[k + 1] = prob[k] * (n - k) / (k + 1);
        }

        double sum = 0.0;
        for (int k = kMin; k <= n; k++) {
            sum += prob[k];
        }
        return sum;
    }

    static int minimumWinsNeeded(int n, double target) {
        double lnTarget = Math.log(target);
        for (int k = 0; k <= n; k++) {
            double bestGain = -1.0e300;
            if (k == n) {
                bestGain = n * Math.log(3.0);
            } else {
                double fStar = (3.0 * k - n) / (2.0 * n);
                if (fStar > 0.0 && fStar < 1.0) {
                    bestGain = k * Math.log(1.0 + 2.0 * fStar) + (n - k) * Math.log(1.0 - fStar);
                } else if (fStar <= 0.0) {
                    bestGain = 0.0;
                }
            }
            if (bestGain >= lnTarget) {
                return k;
            }
        }
        return n + 1;
    }

    static double solveProbability(int tosses, double target) {
        int kMin = minimumWinsNeeded(tosses, target);
        return binomialTailProbability(tosses, kMin);
    }

    public static String solve() {
        double ans = solveProbability(1000, 1.0e9);
        return String.format(Locale.US, "%.12f", ans);
    }

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