Problem 563: Robot Welders

View on Project Euler

Project Euler Problem 563 Solution

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

Problem Summary For each integer \(k\), let \(M(k)\) be the smallest rectangle area that has exactly \(k\) manufacturable variants. A variant is an admissible factor pair \((x,y)\) with \(x\le y\), both sides manufacturable, and $$10y\le 11x,$$ so the rectangle is within \(10\%\) of a square. The required answer is $$\sum_{k=2}^{100} M(k).$$ The implementations encode the manufacturing rule through admissible side lengths: a positive integer side is manufacturable exactly when all of its prime factors belong to \(\{2,3,5,7,11,13,17,19,23\}\). Mathematical Approach Let $$P=\{2,3,5,7,11,13,17,19,23\}$$ and define the manufacturable side set $$\mathcal{S}=\left\{n\in \mathbb{Z}_{>0}: \text{every prime divisor of } n \text{ lies in } P\right\}.$$ For any area \(A\), define its admissible multiplicity by $$r(A)=\#\left\{(x,y)\in \mathcal{S}^2 : x\le y,\ xy=A,\ 10y\le 11x\right\}.$$ Then $$M(k)=\min\{A:r(A)=k\}.$$ Step 1: Characterize the manufacturable side lengths The allowed rescalings only introduce prime factors at most \(23\). Conversely, every prime in \(P\) is itself an allowed multiplier, so repeated valid rescalings can build any positive integer whose prime divisors all lie in \(P\). Therefore the manufacturable side lengths are exactly the \(23\)-smooth positive integers....

Detailed mathematical approach

Problem Summary

For each integer \(k\), let \(M(k)\) be the smallest rectangle area that has exactly \(k\) manufacturable variants. A variant is an admissible factor pair \((x,y)\) with \(x\le y\), both sides manufacturable, and

$$10y\le 11x,$$

so the rectangle is within \(10\%\) of a square. The required answer is

$$\sum_{k=2}^{100} M(k).$$

The implementations encode the manufacturing rule through admissible side lengths: a positive integer side is manufacturable exactly when all of its prime factors belong to \(\{2,3,5,7,11,13,17,19,23\}\).

Mathematical Approach

Let

$$P=\{2,3,5,7,11,13,17,19,23\}$$

and define the manufacturable side set

$$\mathcal{S}=\left\{n\in \mathbb{Z}_{>0}: \text{every prime divisor of } n \text{ lies in } P\right\}.$$

For any area \(A\), define its admissible multiplicity by

$$r(A)=\#\left\{(x,y)\in \mathcal{S}^2 : x\le y,\ xy=A,\ 10y\le 11x\right\}.$$

Then

$$M(k)=\min\{A:r(A)=k\}.$$

Step 1: Characterize the manufacturable side lengths

The allowed rescalings only introduce prime factors at most \(23\). Conversely, every prime in \(P\) is itself an allowed multiplier, so repeated valid rescalings can build any positive integer whose prime divisors all lie in \(P\).

Therefore the manufacturable side lengths are exactly the \(23\)-smooth positive integers. This converts the original manufacturing constraint into a clean number-theoretic set \(\mathcal{S}\).

Step 2: Turn rectangle variants into near-square factor pairs

After ordering the sides so that \(x\le y\), every rectangle variant corresponds to one factorization \(A=xy\) with

$$x\le y,\qquad 10y\le 11x.$$

The inequality forces the two factors to lie close to \(\sqrt{A}\), and the condition \(x\le y\) removes the reflected duplicate \((y,x)\).

Because products of \(23\)-smooth numbers are still \(23\)-smooth, every admissible area is itself \(23\)-smooth. Also, every divisor of a \(23\)-smooth number is again \(23\)-smooth, so once an area \(A\) is fixed, counting manufacturable variants is the same as counting admissible divisor pairs of \(A\) near its square root.

Step 3: Define the multiplicity function and the search target

The function \(r(A)\) records how many admissible near-square factorizations produce the same area. Different areas can have the same multiplicity, and \(M(k)\) selects the smallest area with multiplicity exactly \(k\).

So the whole problem becomes

$$\sum_{k=2}^{100} M(k).$$

This viewpoint separates the task into two layers: enumerate all relevant side lengths, then aggregate areas by the number of admissible factor pairs that generate them.

Step 4: Why the expanding side cap is correct

Suppose we only enumerate side lengths up to some cap \(L\). Let \(r_L(A)\) be the same count as \(r(A)\), but restricted to pairs with \(x,y\le L\). As \(L\) grows, the set of visible pairs only increases, so \(r_L(A)\) is monotone.

Now consider any admissible pair with area \(A\). Since \(x\le y\) and \(xy=A\), we have \(x\le \sqrt{A}\le y\). Combining this with \(10y\le 11x\) gives

$$y\le \frac{11}{10}x\le \frac{11}{10}\sqrt{A}.$$

Hence every admissible pair for area \(A\) is guaranteed to be visible once

$$L\ge \left\lceil \frac{11}{10}\left\lceil \sqrt{A}\right\rceil \right\rceil.$$

If all needed values \(M(2),\dots,M(100)\) have already been found and this inequality also holds for

$$A=\max_{2\le k\le 100} M(k),$$

then no unseen pair can alter any relevant minimum, so the search is finished.

Step 5: Worked Example Using the Checkpoint \(M(3)=889200\)

The implementations verify the checkpoint

$$M(3)=889200.$$

For this area, the admissible factor pairs are

$$ (900,988),\qquad (912,975),\qquad (936,950). $$

Each pair satisfies \(x\le y\), each product is \(889200\), and each ratio \(y/x\) is below \(1.1\). Every side is also \(23\)-smooth:

$$\begin{aligned} 900&=2^2\cdot 3^2\cdot 5^2, & 988&=2^2\cdot 13\cdot 19,\\ 912&=2^4\cdot 3\cdot 19, & 975&=3\cdot 5^2\cdot 13,\\ 936&=2^3\cdot 3^2\cdot 13, & 950&=2\cdot 5^2\cdot 19. \end{aligned}$$

Therefore \(r(889200)=3\), and this area is the first one with multiplicity \(3\).

How the Code Works

The C++, Python, and Java implementations start from a moderate side cap and recursively generate every \(23\)-smooth side length up to that cap. The list is then sorted and deduplicated so each manufacturable side is processed once.

Next, the implementation scans the sorted side list with \(x\le y\). For each shorter side \(x\), it only considers longer sides up to \(\left\lfloor \frac{11x}{10}\right\rfloor\), because larger values would violate the near-square rule. Each admissible pair contributes one count to its area \(A=xy\). After all pairs are processed, the smallest area for each multiplicity \(k\in[2,100]\) is extracted from the area-count table.

If some \(M(k)\) is still missing, the side cap is multiplied by \(10\) and the process repeats. Once every \(M(k)\) from \(2\) through \(100\) has appeared, the implementation checks the coverage bound

$$L\ge \left\lceil \frac{11}{10}\left\lceil \sqrt{\max_k M(k)}\right\rceil \right\rceil$$

to prove that no admissible pair for any relevant area lies beyond the current cap. At that point the final sum is returned.

Complexity Analysis

Let \(s(L)\) be the number of \(23\)-smooth integers up to the final cap \(L\). Generating the smooth numbers is output-sensitive; after sorting and deduplication it costs \(O(s(L)\log s(L))\) time and \(O(s(L))\) memory.

The pair scan is worst-case \(O(s(L)^2)\), although the constraint \(10y\le 11x\) keeps only a narrow band near the diagonal and makes the practical workload much smaller. The area-count table uses linear memory in the number of distinct admissible areas encountered. Because the cap grows geometrically, the last successful round dominates the total runtime.

Footnotes and References

  1. Problem page: https://projecteuler.net/problem=563
  2. Smooth numbers: Wikipedia — Smooth number
  3. Divisor function and factor pairs: Wikipedia — Divisor function
  4. Prime factorization: Wikipedia — Integer factorization
  5. Geometric mean and the square-root bound: Wikipedia — Geometric mean

Problem 563 source code

C++

#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdint>
#include <iostream>
#include <limits>
#include <string>
#include <unordered_map>
#include <vector>

namespace {

using u64 = std::uint64_t;
using u128 = unsigned __int128;

static std::string to_string_u128(u128 value) {
    if (value == 0) return "0";
    std::string s;
    while (value > 0) {
        const int digit = static_cast<int>(value % 10);
        s.push_back(static_cast<char>('0' + digit));
        value /= 10;
    }
    std::reverse(s.begin(), s.end());
    return s;
}

static u64 isqrt_u64(u64 x) {
    u64 r = static_cast<u64>(std::sqrt(static_cast<long double>(x)));
    while (r * r > x) --r;
    while ((r + 1) > r && (r + 1) * (r + 1) <= x) ++r;
    return r;
}

static void gen_smooth_rec(const std::vector<u64>& primes, std::size_t idx, u64 cur, u64 limit, std::vector<u64>& out) {
    if (idx == primes.size()) {
        out.push_back(cur);
        return;
    }
    const u64 p = primes[idx];
    u64 v = cur;
    while (true) {
        gen_smooth_rec(primes, idx + 1, v, limit, out);
        if (v > limit / p) break;
        v *= p;
    }
}

static std::vector<u64> gen_smooth(u64 limit) {
    // Prime factors allowed by multiplying dimensions by any k<=25: primes <= 23.
    const std::vector<u64> primes = {2, 3, 5, 7, 11, 13, 17, 19, 23};
    std::vector<u64> nums;
    nums.reserve(200000);
    gen_smooth_rec(primes, 0, 1, limit, nums);
    std::sort(nums.begin(), nums.end());
    nums.erase(std::unique(nums.begin(), nums.end()), nums.end());
    return nums;
}

struct MResult {
    std::vector<u64> M;  // M[k] = minimal area with exactly k variants (k up to max_k), or INF if missing.
    u64 max_filled = 0;
};

static MResult compute_M(u64 side_limit, int max_k) {
    const auto smooth = gen_smooth(side_limit);

    // Count, for each area A, how many factor pairs (x<=y) satisfy y*10 <= 11*x and x*y=A.
    // For 23-smooth A, every divisor is 23-smooth, so "manufacturable" matches "has such factor pair".
    std::unordered_map<u64, std::uint16_t> cnt;
    cnt.reserve(smooth.size() * 4);

    for (std::size_t i = 0; i < smooth.size(); ++i) {
        const u64 x = smooth[i];
        const u64 ymax = (11 * x) / 10;  // floor(1.1*x)
        auto it_end = std::upper_bound(smooth.begin() + static_cast<std::ptrdiff_t>(i), smooth.end(), ymax);
        const std::size_t j_end = static_cast<std::size_t>(it_end - smooth.begin());
        for (std::size_t j = i; j < j_end; ++j) {
            const u64 y = smooth[j];
            const u128 area128 = static_cast<u128>(x) * static_cast<u128>(y);
            if (area128 > std::numeric_limits<u64>::max()) continue;
            const u64 area = static_cast<u64>(area128);
            auto& v = cnt[area];
            if (v <= static_cast<std::uint16_t>(max_k)) ++v;  // clamp above max_k+1
        }
    }

    const u64 INF = std::numeric_limits<u64>::max();
    std::vector<u64> M(static_cast<std::size_t>(max_k + 1), INF);
    for (const auto& [area, k] : cnt) {
        if (k < 2 || k > max_k) continue;
        if (area < M[k]) M[k] = area;
    }

    u64 max_filled = 0;
    for (int k = 2; k <= max_k; ++k) {
        if (M[k] != INF && M[k] > max_filled) max_filled = M[k];
    }
    return {std::move(M), max_filled};
}

static u128 solve_sum_M(int max_k) {
    u64 side_limit = 1'000'000;  // start moderately; we will raise this if needed.

    while (true) {
        const auto res = compute_M(side_limit, max_k);
        const u64 INF = std::numeric_limits<u64>::max();

        bool all_found = true;
        for (int k = 2; k <= max_k; ++k) {
            if (res.M[k] == INF) {
                all_found = false;
                break;
            }
        }
        if (!all_found) {
            side_limit *= 10;
            continue;
        }

        // If all M(k) are found, ensure the side limit is large enough that every area <= maxM
        // has all its near-square factor pairs represented.
        const u64 maxM = res.max_filled;
        u64 s = isqrt_u64(maxM);
        if (s * s < maxM) ++s;  // ceil(sqrt(maxM))
        const u64 needed = (11 * s + 9) / 10;  // ceil(1.1*s)
        if (side_limit < needed) {
            side_limit = needed;
            continue;
        }

        // Statement check: M(3)=889200.
        assert(res.M[3] == 889200ULL);

        u128 sum = 0;
        for (int k = 2; k <= max_k; ++k) sum += static_cast<u128>(res.M[k]);
        return sum;
    }
}

}  // namespace

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    std::cout << to_string_u128(solve_sum_M(100)) << '\n';
    return 0;
}

Python

def solve():
    max_k = 100
    primes = [2,3,5,7,11,13,17,19,23]

    def gen_smooth(limit):
        result = []
        def rec(idx, cur):
            if idx == len(primes):
                result.append(cur); return
            p = primes[idx]; v = cur
            while v <= limit:
                rec(idx+1, v)
                if v > limit // p: break
                v *= p
        rec(0, 1)
        return sorted(set(result))

    side_limit = 1000000
    while True:
        smooth = gen_smooth(side_limit)
        cnt = {}
        for i, x in enumerate(smooth):
            ymax = 11 * x // 10
            for j in range(i, len(smooth)):
                y = smooth[j]
                if y > ymax: break
                area = x * y
                cnt[area] = cnt.get(area, 0) + 1

        INF = float('inf')
        M = [INF] * (max_k + 1)
        for area, k in cnt.items():
            if 2 <= k <= max_k and area < M[k]: M[k] = area

        if all(M[k] < INF for k in range(2, max_k+1)):
            maxM = max(M[k] for k in range(2, max_k+1))
            import math
            s = math.isqrt(maxM)
            if s*s < maxM: s += 1
            needed = (11*s + 9) // 10
            if side_limit >= needed:
                return str(sum(M[k] for k in range(2, max_k+1)))
            side_limit = needed
        else:
            side_limit *= 10

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

Java

import java.util.*;

public class Euler563 {
    static void genSmoothRec(long[] primes, int idx, long cur, long limit, List<Long> out) {
        if (idx == primes.length) {
            out.add(cur);
            return;
        }
        long p = primes[idx];
        long v = cur;
        while (true) {
            genSmoothRec(primes, idx + 1, v, limit, out);
            if (v > limit / p)
                break;
            v *= p;
        }
    }

    static List<Long> genSmooth(long limit) {
        long[] primes = { 2, 3, 5, 7, 11, 13, 17, 19, 23 };
        List<Long> nums = new ArrayList<>(200000);
        genSmoothRec(primes, 0, 1, limit, nums);
        Set<Long> set = new HashSet<>(nums);
        List<Long> uniqueNums = new ArrayList<>(set);
        Collections.sort(uniqueNums);
        return uniqueNums;
    }

    static class MResult {
        long[] M;
        long maxFilled;
    }

    static int upperBound(List<Long> list, int from, long val) {
        int left = from;
        int right = list.size();
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (list.get(mid) > val) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }

    static MResult computeM(long sideLimit, int maxK) {
        List<Long> smooth = genSmooth(sideLimit);
        Map<Long, Integer> cnt = new HashMap<>(smooth.size() * 4);

        for (int i = 0; i < smooth.size(); i++) {
            long x = smooth.get(i);
            long ymax = (11 * x) / 10;
            int jEnd = upperBound(smooth, i, ymax);
            for (int j = i; j < jEnd; j++) {
                long y = smooth.get(j);
                long area = x * y;
                Integer currentCount = cnt.get(area);
                if (currentCount == null) {
                    cnt.put(area, 1);
                } else if (currentCount <= maxK) {
                    cnt.put(area, currentCount + 1);
                }
            }
        }

        long INF = Long.MAX_VALUE;
        long[] M = new long[maxK + 1];
        Arrays.fill(M, INF);

        for (Map.Entry<Long, Integer> entry : cnt.entrySet()) {
            long area = entry.getKey();
            int k = entry.getValue();
            if (k >= 2 && k <= maxK) {
                if (area < M[k]) {
                    M[k] = area;
                }
            }
        }

        long maxFilled = 0;
        for (int k = 2; k <= maxK; k++) {
            if (M[k] != INF && M[k] > maxFilled) {
                maxFilled = M[k];
            }
        }

        MResult res = new MResult();
        res.M = M;
        res.maxFilled = maxFilled;
        return res;
    }

    static long isqrt(long x) {
        if (x < 0)
            return 0;
        long r = (long) Math.sqrt(x);
        while (r * r > x)
            r--;
        while ((r + 1) * (r + 1) <= x && r + 1 > 0)
            r++;
        return r;
    }

    static long solveSumM(int maxK) {
        long sideLimit = 1000000;

        while (true) {
            MResult res = computeM(sideLimit, maxK);
            long INF = Long.MAX_VALUE;
            boolean allFound = true;

            for (int k = 2; k <= maxK; k++) {
                if (res.M[k] == INF) {
                    allFound = false;
                    break;
                }
            }

            if (!allFound) {
                sideLimit *= 10;
                continue;
            }

            long maxM = res.maxFilled;
            long s = isqrt(maxM);
            if (s * s < maxM)
                s++;

            long needed = (11 * s + 9) / 10;
            if (sideLimit < needed) {
                sideLimit = needed;
                continue;
            }

            long sum = 0;
            for (int k = 2; k <= maxK; k++) {
                sum += res.M[k];
            }
            return sum;
        }
    }

    public static String solve() {
        return Long.toString(solveSumM(100));
    }

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