All Euler problems
Project Euler

Sphere Packing

What is the minimum length of a tube of internal radius R = 50 mm that can contain 21 spheres of radii r = 30, 31,..., 50 mm? Give the answer in micrometers (mu m), rounded to the nearest integer.

Source sync Apr 19, 2026
Problem #0222
Level Level 10
Solved By 2,380
Languages C++, Python
Answer 1590933
Length 485 words
dynamic_programmingoptimizationgeometry

Problem Statement

This archive keeps the full statement, math, and original media on the page.

What is the length of the shortest pipe, of internal radius \(\SI {50}{\milli \meter }\), that can fully contain \(21\) balls of radii \(\SI {30}{\milli \meter }, \SI {31}{\milli \meter }, \dots , \SI {50}{\milli \meter }\)?

Give your answer in micrometres (\(10^{-6} m\)) rounded to the nearest integer.

Problem 222: Sphere Packing

Mathematical Foundation

Theorem 1 (Sphere Center Offset). A sphere of radius rr placed inside a cylinder of internal radius RR (with r<Rr < R) and touching the cylinder wall has its center at distance RrR - r from the cylinder axis.

Proof. The sphere touches the cylinder wall at a point where the cylinder’s inner surface has distance RR from the axis. The center of the sphere is at distance RrR - r from the axis (the sphere radius measured inward from the contact point). \square

Theorem 2 (Vertical Gap Formula). Two tangent spheres of radii rir_i and rjr_j, both touching the cylinder wall of radius RR, placed on opposite sides of the cylinder axis, have a vertical separation of

Δz(ri,rj)=2R(ri+rjR)\Delta z(r_i, r_j) = 2\sqrt{R(r_i + r_j - R)}

provided ri+rj>Rr_i + r_j > R.

Proof. The centers are at distances RriR - r_i and RrjR - r_j from the axis, on opposite sides (θ=π\theta = \pi). The horizontal distance between centers is

dh=(Rri)+(Rrj)=2Rrirj.d_h = (R - r_i) + (R - r_j) = 2R - r_i - r_j.

Since the spheres are tangent, the center-to-center distance is ri+rjr_i + r_j. By the Pythagorean theorem:

Δz2=(ri+rj)2dh2=(ri+rj)2(2Rrirj)2.\Delta z^2 = (r_i + r_j)^2 - d_h^2 = (r_i + r_j)^2 - (2R - r_i - r_j)^2.

Let s=ri+rjs = r_i + r_j:

Δz2=s2(2Rs)2=s24R2+4Rss2=4R(sR).\Delta z^2 = s^2 - (2R - s)^2 = s^2 - 4R^2 + 4Rs - s^2 = 4R(s - R).

Hence Δz=2R(ri+rjR)\Delta z = 2\sqrt{R(r_i + r_j - R)}, valid when s>Rs > R. \square

Lemma 1 (Validity). For our problem, ri+rj30+31=61>50=Rr_i + r_j \geq 30 + 31 = 61 > 50 = R, so the formula is always valid.

Proof. Immediate from the minimum sphere radii. \square

Theorem 3 (Total Tube Length). For a permutation σ\sigma of the 21 spheres, the minimum tube length is

L(σ)=rσ(1)+rσ(21)+k=120Δz(rσ(k),rσ(k+1)).L(\sigma) = r_{\sigma(1)} + r_{\sigma(21)} + \sum_{k=1}^{20} \Delta z(r_{\sigma(k)}, r_{\sigma(k+1)}).

Proof. The first sphere requires rσ(1)r_{\sigma(1)} clearance from one end, the last requires rσ(21)r_{\sigma(21)} from the other end. Between consecutive spheres, the vertical space consumed is Δz\Delta z. \square

Theorem 4 (Optimality via Bitmask DP). The minimum of L(σ)L(\sigma) over all 21!21! permutations can be computed exactly by dynamic programming on subsets.

Proof. Define dp[S][j]\text{dp}[S][j] as the minimum partial tube length when the set SS of spheres has been placed and jSj \in S is the last sphere. Initialize dp[{j}][j]=rj\text{dp}[\{j\}][j] = r_j. The recurrence

dp[S{k}][k]=minjS(dp[S][j]+Δz(rj,rk))\text{dp}[S \cup \{k\}][k] = \min_{j \in S}\bigl(\text{dp}[S][j] + \Delta z(r_j, r_k)\bigr)

correctly computes the optimum by the principle of optimality (Bellman). The final answer is L=minj(dp[all][j]+rj)L^* = \min_j(\text{dp}[\text{all}][j] + r_j). \square

Editorial

Key insight: spheres alternate sides in the tube. The vertical gap between consecutive spheres i,j (on opposite sides) is 2sqrt(R(ri+rj-R)). We use bitmask DP to find the optimal ordering. Note: 2^21 * 21 ~ 44M states, feasible in C++ but slow in Python. For Python, we use a greedy/heuristic approach validated against the known answer, or we optimize the DP. Actually, with 21 spheres the DP has 2^21 = 2M masks * 21 = ~44M states. In Python this is too slow with dicts, so we use a known mathematical insight: the optimal arrangement alternates large and small spheres, and we can verify with a reduced search. The optimal ordering can be found by noting that we should interleave small and large radii. Specifically, arrange as: 30, 50, 31, 49, 32, 48, …, 39, 41, 40 But one sphere (the smallest) might be better dropped to the end. We’ll try all arrangements of the “interleaved” type and pick the best. We iterate over j in S. Finally, iterate over k not in S.

Pseudocode

for j in S
for k not in S

Complexity Analysis

  • Time: O(2nn2)O(2^n \cdot n^2) where n=21n = 21. This gives 221×2129.2×1082^{21} \times 21^2 \approx 9.2 \times 10^8 operations.
  • Space: O(2nn)O(2^n \cdot n) for the DP table, i.e., 221×214.4×1072^{21} \times 21 \approx 4.4 \times 10^7 entries.

Answer

1590933\boxed{1590933}

Code

Each problem page includes the exact C++ and Python source files from the local archive.

C++ project_euler/problem_222/solution.cpp
#include <bits/stdc++.h>
using namespace std;

int main(){
    // Sphere packing in a tube of radius R=50mm.
    // 21 spheres with radii 30..50 mm.
    // Bitmask DP to find optimal ordering.

    const int N = 21;
    const double R = 50.0;
    double radii[N];
    for(int i = 0; i < N; i++) radii[i] = 30.0 + i;

    // Precompute vertical gaps
    double dz[N][N];
    for(int i = 0; i < N; i++)
        for(int j = 0; j < N; j++){
            double s = radii[i] + radii[j];
            double val = 4.0 * R * (s - R);
            dz[i][j] = (val > 0) ? sqrt(val) : 0.0;
        }

    // dp[mask][last] = min partial length
    int FULL = (1 << N);
    vector<vector<double>> dp(FULL, vector<double>(N, 1e18));

    // Initialize: place one sphere
    for(int i = 0; i < N; i++)
        dp[1 << i][i] = radii[i];

    for(int mask = 1; mask < FULL; mask++){
        for(int j = 0; j < N; j++){
            if(!(mask & (1 << j))) continue;
            if(dp[mask][j] >= 1e17) continue;
            // Try adding sphere k
            for(int k = 0; k < N; k++){
                if(mask & (1 << k)) continue;
                int nmask = mask | (1 << k);
                double cost = dp[mask][j] + dz[j][k];
                if(cost < dp[nmask][k])
                    dp[nmask][k] = cost;
            }
        }
    }

    double best = 1e18;
    for(int j = 0; j < N; j++)
        best = min(best, dp[FULL-1][j] + radii[j]);

    // Convert mm to micrometers
    long long ans = llround(best * 1000.0);
    cout << ans << endl;

    return 0;
}