<!-- Erdős–Straus explorer: solve 4/n = 1/x + 1/y + 1/z, chart how many solutions each n has, and compare with the greedy algorithm. -->
<p class="hint">The conjecture says that for every n ≥ 2 the fraction 4/n is a sum of three unit fractions 1/x + 1/y + 1/z. Type any n: the page tells you whether a <b>modular identity</b> solves it instantly or whether it lands in one of the six <b>hard classes mod 840</b> where only search works, then finds and checks the triples.</p>
<div class="row">
<label for="n">n =</label>
<input id="n" type="number" min="2" max="10000000" value="1009" aria-label="Whole number n between 2 and 10 million">
<button id="find" type="button">Find solutions</button>
</div>
<div class="btns">
<button id="rand" type="button" class="ghost">Random prime</button>
<button id="hard" type="button" class="ghost">Hard class: 1009</button>
<button id="easy" type="button" class="ghost">Easy: 1003</button>
</div>
<div class="out" id="out" aria-live="polite"></div>
<h4>How many solutions does each n have?</h4>
<button id="chartbtn" type="button">Count solutions for n ≤ 300</button>
<canvas id="cv" width="600" height="240" aria-label="Bar chart of the number of solutions for each n from 2 to 300"></canvas>
<p class="legend">Dark bars: primes, the only cases that really matter. Light bars: composites, which inherit extra solutions from their factors. Vertical axis is logarithmic. Tap a bar to load that n.</p>
<div class="out" id="chartout" aria-live="polite"></div>
<h4>For comparison, the greedy Egyptian-fraction algorithm</h4>
<div class="row">
<input id="frac" type="text" value="4/17" spellcheck="false" autocomplete="off" aria-label="A fraction p/q such as 4/17">
<button id="greedy" type="button">Expand greedily</button>
</div>
<div class="out" id="gout" aria-live="polite"></div>
/* Light theme first, dark theme in the media query at the end. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; background: #fff; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
h4 { margin: 1rem 0 .4rem; font-size: .95rem; color: #1d3557; }
.row { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; margin-bottom: .5rem; font-size: .9rem; font-weight: 600; }
input { font: 15px ui-monospace, monospace; padding: .4rem .5rem; border: 1px solid #b8c3cf; border-radius: 8px; background: #fff; color: #222; width: 9.5rem; max-width: 100%; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .5; cursor: default; }
canvas { width: 100%; max-width: 600px; aspect-ratio: 600 / 240; display: block; margin-top: .5rem; border-radius: 10px; border: 1px solid #dde3ea; cursor: pointer; }
.legend { font-size: .78rem; color: #555; margin: .35rem 0 .5rem; line-height: 1.4; }
.out { font-size: .88rem; line-height: 1.45; }
.line { margin: .15rem 0; overflow-wrap: anywhere; }
.line.lbl { font-weight: 700; margin-top: .5rem; color: #1d3557; }
.line.mono { font-family: ui-monospace, monospace; font-size: .83rem; background: #f1f4f7; padding: .2rem .4rem; border-radius: 6px; }
.line.ok { color: #0a7d33; font-weight: 600; }
.line.bad { color: #c92f3c; font-weight: 600; }
.line.warn { color: #8a5a00; font-weight: 600; }
.sols { max-height: 180px; overflow: auto; }
@media (prefers-color-scheme: dark) {
body { color: #e3e8ee; background: #0f1720; }
.hint, .legend { color: #b4c0cc; }
h4 { color: #9fc4ff; }
input { background: #16202b; color: #e3e8ee; border-color: #3a4a5c; }
canvas { border-color: #2c3b4b; }
button { background: #7fb2ff; border-color: #7fb2ff; color: #0f1720; }
button.ghost { background: transparent; color: #7fb2ff; }
.line.lbl { color: #9fc4ff; }
.line.mono { background: #1b2733; }
.line.ok { color: #5fd08a; }
.line.bad { color: #ff7b86; }
.line.warn { color: #f0c060; }
}
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const gcd = (a, b) => { while (b) { const t = a % b; a = b; b = t; } return a; };
const bgcd = (a, b) => { while (b) { const t = a % b; a = b; b = t; } return a; };
// Trial-division factorisation; fine for numbers up to ten million.
function factor(m, f) {
for (let p = 2; p * p <= m; p += (p === 2 ? 1 : 2)) {
while (m % p === 0) { f.set(p, (f.get(p) || 0) + 1); m /= p; }
}
if (m > 1) f.set(m, (f.get(m) || 0) + 1);
return f;
}
const isPrime = (m) => { if (m < 2) return false; const f = factor(m, new Map()); return f.size === 1 && f.get(m) === 1; };
// For a fixed x, the rest a/b = 4/n − 1/x splits as 1/y + 1/z exactly when (ay − b)(az − b) = b², so we walk the divisors d ≤ b of b².
function pairsFor(n, x, limit, out) {
let a = 4 * x - n, b = n * x;
const g = gcd(a, b); a /= g; b /= g;
const f = new Map(); factor(n, f); factor(x, f);
const pe = [];
for (const p of f.keys()) { let e = 0, t = b; while (t % p === 0) { t /= p; e++; } if (e) pe.push([p, 2 * e]); }
let divs = [1];
for (const [p, e] of pe) {
const nd = [];
for (const d of divs) { let v = d; for (let i = 0; i <= e; i++) { if (v > b) break; nd.push(v); v *= p; } }
divs = nd;
}
divs.sort((u, v) => u - v);
const B = BigInt(b), A = BigInt(a);
for (const d of divs) {
if (d > b) break;
if ((d + b) % a !== 0) continue;
const y = (d + b) / a;
if (y < x) continue;
out.push([BigInt(x), BigInt(y), (B * B / BigInt(d) + B) / A]);
if (out.length >= limit) return;
}
}
// The smallest denominator x satisfies n/4 < x ≤ 3n/4; try each x in that window.
function solutions(n, limit) {
const out = [];
const lo = Math.floor(n / 4) + 1, hi = Math.floor(3 * n / 4);
let tried = 0;
for (let x = lo; x <= hi && out.length < limit; x++) { tried++; pairsFor(n, x, limit, out); }
return { out, tried, lo: lo - 1, hi };
}
const checks = (n, s) => { const N = BigInt(n), [x, y, z] = s; return 4n * x * y * z === N * (y * z + x * z + x * y); };
const OPEN = [1, 121, 169, 289, 361, 529];
// Modular identities: when n falls in one of these residue classes, a formula hands us a solution with no search at all.
function identity(n) {
const B = BigInt(n);
if (n % 2 === 0) return { key: 'mod2', sol: [B / 2n, B, B] };
if (n % 4 === 3) { const a = (B + 1n) / 4n; return { key: 'mod4', sol: [a, 2n * a * B, 2n * a * B] }; }
if (n % 3 === 2) { const b = (B + 1n) / 3n; return { key: 'mod3', sol: [b, B, b * B] }; }
if (n % 8 === 5) { const c = (B + 3n) / 8n; return { key: 'mod8', sol: [2n * c, c * B, 2n * c * B] }; }
if (n % 5 === 2 || n % 5 === 3) return { key: 'mod5', r: n % 5 };
if (n % 7 === 3 || n % 7 === 5 || n % 7 === 6) return { key: 'mod7', r: n % 7 };
if (n % 3 === 0) { const m = B / 3n; return { key: 'mod3z', sol: [m, 4n * m, 12n * m] }; }
if (n % 5 === 0) { const m = B / 5n; return { key: 'mod5z', sol: [2n * m, 4n * m, 20n * m] }; }
if (n % 7 === 0) { const m = B / 7n; return { key: 'mod7z', sol: [2n * m, 15n * m, 210n * m] }; }
for (const p of factor(n, new Map()).keys()) {
if (p === n) continue;
const s = identity(p);
if (s.key !== 'open') { const K = BigInt(n / p); return { key: 'scale', p: p, k: n / p, sol: s.sol ? s.sol.map((v) => v * K) : null }; }
}
return { key: 'open', r: n % 840 };
}
const CLS = {
mod2: "n is even, so 4/n = 1/(n/2) + 1/n + 1/n.", mod4: "n ≡ 3 (mod 4): with a = (n + 1)/4, 4/n = 1/a + 1/(2an) + 1/(2an).", mod3: "n ≡ 2 (mod 3): with b = (n + 1)/3, 4/n = 1/b + 1/n + 1/(bn).", mod8: "n ≡ 5 (mod 8): with c = (n + 3)/8, 4/n = 1/(2c) + 1/(cn) + 1/(2cn).",
mod5: "n ≡ %r% (mod 5): one of Mordell’s polynomial identities covers this class; the search below produces the triple.", mod7: "n ≡ %r% (mod 7): one of Mordell’s polynomial identities covers this class; the search below produces the triple.", mod3z: "n is a multiple of 3: scale 4/3 = 1/1 + 1/4 + 1/12 by n/3.", mod5z: "n is a multiple of 5: scale 4/5 = 1/2 + 1/4 + 1/20 by n/5.",
mod7z: "n is a multiple of 7: scale 4/7 = 1/2 + 1/15 + 1/210 by n/7.", scale: "n = %p% × %k%: solve 4/%p% with an identity and multiply every denominator by %k%.", open: "n ≡ %r% (mod 840): one of the six hard classes (1, 121, 169, 289, 361, 529). No known identity covers it — only a search finds the triple."
};
const tri = (s) => '1/' + s[0] + ' + 1/' + s[1] + ' + 1/' + s[2];
// Interface: classify n, show the identity (if any), then search and list the triples.
const $ = (id) => document.getElementById(id);
function addLine(box, cls, text) { const d = document.createElement('div'); d.className = 'line ' + cls; d.textContent = text; box.appendChild(d); return d; }
function run(n) {
const out = $('out'); out.innerHTML = '';
if (!Number.isInteger(n) || n < 2 || n > 10000000) { addLine(out, 'bad', "Enter a whole number from 2 to 10,000,000."); return; }
$('n').value = n;
const id = identity(n);
addLine(out, 'lbl', i18nFmt("n = %n% (%p%), n mod 840 = %r%", { n: n, r: n % 840, p: isPrime(n) ? "prime" : "composite" }));
addLine(out, id.key === 'open' ? 'warn' : 'ok', i18nFmt(CLS[id.key], { r: id.r, p: id.p, k: id.k }));
if (id.sol) {
addLine(out, 'mono', '4/' + n + ' = ' + tri(id.sol) + (checks(n, id.sol) ? ' ✓' : ' ✗'));
}
const t0 = performance.now();
const big = n > 5000;
const res = solutions(n, big ? 8 : 100000);
const ms = Math.max(0, performance.now() - t0).toFixed(1);
if (!res.out.length) { addLine(out, 'bad', "No solution found. That would be a counterexample to the conjecture — it has never happened."); return; }
const first = solutions(n, 1);
addLine(out, 'lbl', i18nFmt("Direct search: the first triple appeared after trying %t% value(s) of x in the window (%lo%, %hi%].", { t: first.tried, lo: first.lo, hi: first.hi }));
addLine(out, 'mono', '4/' + n + ' = ' + tri(first.out[0]) + (checks(n, first.out[0]) ? ' ✓' : ' ✗'));
addLine(out, 'lbl', i18nFmt(big ? "n is large, so the list stops after the first %c% solutions (%ms% ms):" : "All %c% solutions with x ≤ y ≤ z (%ms% ms):", { c: res.out.length, ms: ms }));
const box = document.createElement('div'); box.className = 'sols'; out.appendChild(box);
for (const s of res.out.slice(0, 200)) addLine(box, 'mono', '(' + s.join(', ') + ')');
}
$('find').addEventListener('click', () => run(Number($('n').value)));
$('n').addEventListener('keydown', (e) => { if (e.key === 'Enter') run(Number($('n').value)); });
$('hard').addEventListener('click', () => run(1009));
$('easy').addEventListener('click', () => run(1003));
$('rand').addEventListener('click', () => {
// Half the time pick a random prime from one of the six hard classes mod 840, otherwise any random prime.
const want = Math.random() < 0.5;
for (let k = 0; k < 5000; k++) {
const p = 2 + Math.floor(Math.random() * 9999990);
if (!isPrime(p)) continue;
if (want && !OPEN.includes(p % 840)) continue;
run(p); return;
}
run(1009);
});
// Solution counts f(n) for n ≤ 300, computed in small chunks so the page stays responsive; log-scale bars.
const counts = [];
function dark() { return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; }
function drawChart(sel) {
const cv = $('cv'), ctx = cv.getContext('2d'), W = cv.width, H = cv.height, D = dark();
ctx.fillStyle = D ? '#16202b' : '#f7f9fb'; ctx.fillRect(0, 0, W, H);
const L = 34, Bm = 22, T = 10, R = 6;
const maxC = Math.max(2, ...counts.filter(Boolean));
const ly = (c) => H - Bm - (Math.log10(c) / Math.log10(maxC)) * (H - Bm - T);
ctx.strokeStyle = D ? '#2c3b4b' : '#dde3ea'; ctx.fillStyle = D ? '#b4c0cc' : '#556'; ctx.font = '11px system-ui, sans-serif'; ctx.lineWidth = 1;
for (let c = 1; c <= maxC; c *= 10) { const y = ly(c); ctx.beginPath(); ctx.moveTo(L, y); ctx.lineTo(W - R, y); ctx.stroke(); ctx.fillText(String(c), 4, y + 4); }
for (let n = 50; n <= 300; n += 50) ctx.fillText(String(n), L + (n - 2) / 299 * (W - L - R) - 8, H - 6);
const bw = (W - L - R) / 299;
for (let n = 2; n <= 300; n++) {
const c = counts[n]; if (!c) continue;
const pr = isPrime(n);
ctx.fillStyle = n === sel ? (D ? '#f0c060' : '#d08700') : pr ? (D ? '#7fb2ff' : '#1d3557') : (D ? '#3a5068' : '#b9c7d8');
const x = L + (n - 2) * bw, y = Math.min(ly(Math.max(c, 1)), H - Bm - 2);
ctx.fillRect(x, y, Math.max(1, bw - 0.4), H - Bm - y);
}
}
$('chartbtn').addEventListener('click', () => {
const btn = $('chartbtn'), co = $('chartout'); btn.disabled = true;
let n = 2;
function step() {
const until = Math.min(300, n + 12);
for (; n <= until; n++) counts[n] = solutions(n, 100000).out.length;
co.textContent = i18nFmt("Counting… n = %n% of 300", { n: n - 1 });
drawChart(0);
if (n <= 300) { setTimeout(step, 0); return; }
let bp = 0, bm = 2;
for (let k = 2; k <= 300; k++) { if (isPrime(k) && (!bp || counts[k] < counts[bp])) bp = k; if (counts[k] > counts[bm]) bm = k; }
co.textContent = i18nFmt("Done. Fewest solutions among primes: f(%p%) = %c%. Most solutions overall: f(%m%) = %mc%.", { p: bp, c: counts[bp], m: bm, mc: counts[bm] });
btn.disabled = false;
}
step();
});
$('cv').addEventListener('click', (e) => {
if (counts.length < 300) return;
const r = $('cv').getBoundingClientRect(), W = $('cv').width;
const px = (e.clientX - r.left) * W / r.width;
const n = Math.floor(2 + (px - 34) / ((W - 40) / 299));
if (n >= 2 && n <= 300) { drawChart(n); run(n); }
});
// Greedy (Fibonacci–Sylvester): always subtract the largest unit fraction that fits.
function greedy(p, q) {
let P = BigInt(p), Q = BigInt(q); const terms = [];
while (P > 0n && terms.length < 10) {
const d = (Q + P - 1n) / P; terms.push(d);
P = P * d - Q; Q = Q * d;
if (P) { const g = bgcd(P, Q); P /= g; Q /= g; }
}
return { terms, done: P === 0n };
}
const short = (b) => { const s = b.toString(); return s.length <= 24 ? s : s.slice(0, 10) + '…' + i18nFmt("(%d% digits)", { d: s.length }); };
$('greedy').addEventListener('click', () => {
const g = $('gout'); g.innerHTML = '';
const m = /^\s*(\d+)\s*\/\s*(\d+)\s*$/.exec($('frac').value);
const p = m ? Number(m[1]) : 0, q = m ? Number(m[2]) : 0;
if (!m || p < 1 || p >= q || q > 1000000000) { addLine(g, 'bad', "Type a fraction like 4/17 with 0 < p < q ≤ 1,000,000,000."); return; }
const r = greedy(p, q);
addLine(g, 'mono', p + '/' + q + ' = ' + r.terms.map((d) => '1/' + short(d)).join(' + ') + (r.done ? '' : ' + …'));
addLine(g, r.done ? 'ok' : 'warn', i18nFmt(r.done ? "The greedy algorithm needed %k% term(s)." : "The greedy algorithm is still going after %k% terms and the denominators are exploding.", { k: r.terms.length }));
const k = gcd(p, q);
if (p / k === 4 && q / k <= 10000000) {
const s = solutions(q / k, 1).out[0];
if (s) addLine(g, 'lbl', i18nFmt("Yet a three-term expansion exists: 4/%q% = %t%.", { q: q / k, t: tri(s) }));
}
});
run(1009);
<!-- Erdős–Straus explorer: solve 4/n = 1/x + 1/y + 1/z, chart how many solutions each n has, and compare with the greedy algorithm. -->
<p class="hint">The conjecture says that for every n ≥ 2 the fraction 4/n is a sum of three unit fractions 1/x + 1/y + 1/z. Type any n: the page tells you whether a <b>modular identity</b> solves it instantly or whether it lands in one of the six <b>hard classes mod 840</b> where only search works, then finds and checks the triples.</p>
<div class="row">
<label for="n">n =</label>
<input id="n" type="number" min="2" max="10000000" value="1009" aria-label="Whole number n between 2 and 10 million">
<button id="find" type="button">Find solutions</button>
</div>
<div class="btns">
<button id="rand" type="button" class="ghost">Random prime</button>
<button id="hard" type="button" class="ghost">Hard class: 1009</button>
<button id="easy" type="button" class="ghost">Easy: 1003</button>
</div>
<div class="out" id="out" aria-live="polite"></div>
<h4>How many solutions does each n have?</h4>
<button id="chartbtn" type="button">Count solutions for n ≤ 300</button>
<canvas id="cv" width="600" height="240" aria-label="Bar chart of the number of solutions for each n from 2 to 300"></canvas>
<p class="legend">Dark bars: primes, the only cases that really matter. Light bars: composites, which inherit extra solutions from their factors. Vertical axis is logarithmic. Tap a bar to load that n.</p>
<div class="out" id="chartout" aria-live="polite"></div>
<h4>For comparison, the greedy Egyptian-fraction algorithm</h4>
<div class="row">
<input id="frac" type="text" value="4/17" spellcheck="false" autocomplete="off" aria-label="A fraction p/q such as 4/17">
<button id="greedy" type="button">Expand greedily</button>
</div>
<div class="out" id="gout" aria-live="polite"></div>
/* Light theme first, dark theme in the media query at the end. */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; background: #fff; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
h4 { margin: 1rem 0 .4rem; font-size: .95rem; color: #1d3557; }
.row { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; margin-bottom: .5rem; font-size: .9rem; font-weight: 600; }
input { font: 15px ui-monospace, monospace; padding: .4rem .5rem; border: 1px solid #b8c3cf; border-radius: 8px; background: #fff; color: #222; width: 9.5rem; max-width: 100%; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .5; cursor: default; }
canvas { width: 100%; max-width: 600px; aspect-ratio: 600 / 240; display: block; margin-top: .5rem; border-radius: 10px; border: 1px solid #dde3ea; cursor: pointer; }
.legend { font-size: .78rem; color: #555; margin: .35rem 0 .5rem; line-height: 1.4; }
.out { font-size: .88rem; line-height: 1.45; }
.line { margin: .15rem 0; overflow-wrap: anywhere; }
.line.lbl { font-weight: 700; margin-top: .5rem; color: #1d3557; }
.line.mono { font-family: ui-monospace, monospace; font-size: .83rem; background: #f1f4f7; padding: .2rem .4rem; border-radius: 6px; }
.line.ok { color: #0a7d33; font-weight: 600; }
.line.bad { color: #c92f3c; font-weight: 600; }
.line.warn { color: #8a5a00; font-weight: 600; }
.sols { max-height: 180px; overflow: auto; }
@media (prefers-color-scheme: dark) {
body { color: #e3e8ee; background: #0f1720; }
.hint, .legend { color: #b4c0cc; }
h4 { color: #9fc4ff; }
input { background: #16202b; color: #e3e8ee; border-color: #3a4a5c; }
canvas { border-color: #2c3b4b; }
button { background: #7fb2ff; border-color: #7fb2ff; color: #0f1720; }
button.ghost { background: transparent; color: #7fb2ff; }
.line.lbl { color: #9fc4ff; }
.line.mono { background: #1b2733; }
.line.ok { color: #5fd08a; }
.line.bad { color: #ff7b86; }
.line.warn { color: #f0c060; }
}
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const gcd = (a, b) => { while (b) { const t = a % b; a = b; b = t; } return a; };
const bgcd = (a, b) => { while (b) { const t = a % b; a = b; b = t; } return a; };
// Trial-division factorisation; fine for numbers up to ten million.
function factor(m, f) {
for (let p = 2; p * p <= m; p += (p === 2 ? 1 : 2)) {
while (m % p === 0) { f.set(p, (f.get(p) || 0) + 1); m /= p; }
}
if (m > 1) f.set(m, (f.get(m) || 0) + 1);
return f;
}
const isPrime = (m) => { if (m < 2) return false; const f = factor(m, new Map()); return f.size === 1 && f.get(m) === 1; };
// For a fixed x, the rest a/b = 4/n − 1/x splits as 1/y + 1/z exactly when (ay − b)(az − b) = b², so we walk the divisors d ≤ b of b².
function pairsFor(n, x, limit, out) {
let a = 4 * x - n, b = n * x;
const g = gcd(a, b); a /= g; b /= g;
const f = new Map(); factor(n, f); factor(x, f);
const pe = [];
for (const p of f.keys()) { let e = 0, t = b; while (t % p === 0) { t /= p; e++; } if (e) pe.push([p, 2 * e]); }
let divs = [1];
for (const [p, e] of pe) {
const nd = [];
for (const d of divs) { let v = d; for (let i = 0; i <= e; i++) { if (v > b) break; nd.push(v); v *= p; } }
divs = nd;
}
divs.sort((u, v) => u - v);
const B = BigInt(b), A = BigInt(a);
for (const d of divs) {
if (d > b) break;
if ((d + b) % a !== 0) continue;
const y = (d + b) / a;
if (y < x) continue;
out.push([BigInt(x), BigInt(y), (B * B / BigInt(d) + B) / A]);
if (out.length >= limit) return;
}
}
// The smallest denominator x satisfies n/4 < x ≤ 3n/4; try each x in that window.
function solutions(n, limit) {
const out = [];
const lo = Math.floor(n / 4) + 1, hi = Math.floor(3 * n / 4);
let tried = 0;
for (let x = lo; x <= hi && out.length < limit; x++) { tried++; pairsFor(n, x, limit, out); }
return { out, tried, lo: lo - 1, hi };
}
const checks = (n, s) => { const N = BigInt(n), [x, y, z] = s; return 4n * x * y * z === N * (y * z + x * z + x * y); };
const OPEN = [1, 121, 169, 289, 361, 529];
// Modular identities: when n falls in one of these residue classes, a formula hands us a solution with no search at all.
function identity(n) {
const B = BigInt(n);
if (n % 2 === 0) return { key: 'mod2', sol: [B / 2n, B, B] };
if (n % 4 === 3) { const a = (B + 1n) / 4n; return { key: 'mod4', sol: [a, 2n * a * B, 2n * a * B] }; }
if (n % 3 === 2) { const b = (B + 1n) / 3n; return { key: 'mod3', sol: [b, B, b * B] }; }
if (n % 8 === 5) { const c = (B + 3n) / 8n; return { key: 'mod8', sol: [2n * c, c * B, 2n * c * B] }; }
if (n % 5 === 2 || n % 5 === 3) return { key: 'mod5', r: n % 5 };
if (n % 7 === 3 || n % 7 === 5 || n % 7 === 6) return { key: 'mod7', r: n % 7 };
if (n % 3 === 0) { const m = B / 3n; return { key: 'mod3z', sol: [m, 4n * m, 12n * m] }; }
if (n % 5 === 0) { const m = B / 5n; return { key: 'mod5z', sol: [2n * m, 4n * m, 20n * m] }; }
if (n % 7 === 0) { const m = B / 7n; return { key: 'mod7z', sol: [2n * m, 15n * m, 210n * m] }; }
for (const p of factor(n, new Map()).keys()) {
if (p === n) continue;
const s = identity(p);
if (s.key !== 'open') { const K = BigInt(n / p); return { key: 'scale', p: p, k: n / p, sol: s.sol ? s.sol.map((v) => v * K) : null }; }
}
return { key: 'open', r: n % 840 };
}
const CLS = {
mod2: "n is even, so 4/n = 1/(n/2) + 1/n + 1/n.", mod4: "n ≡ 3 (mod 4): with a = (n + 1)/4, 4/n = 1/a + 1/(2an) + 1/(2an).", mod3: "n ≡ 2 (mod 3): with b = (n + 1)/3, 4/n = 1/b + 1/n + 1/(bn).", mod8: "n ≡ 5 (mod 8): with c = (n + 3)/8, 4/n = 1/(2c) + 1/(cn) + 1/(2cn).",
mod5: "n ≡ %r% (mod 5): one of Mordell’s polynomial identities covers this class; the search below produces the triple.", mod7: "n ≡ %r% (mod 7): one of Mordell’s polynomial identities covers this class; the search below produces the triple.", mod3z: "n is a multiple of 3: scale 4/3 = 1/1 + 1/4 + 1/12 by n/3.", mod5z: "n is a multiple of 5: scale 4/5 = 1/2 + 1/4 + 1/20 by n/5.",
mod7z: "n is a multiple of 7: scale 4/7 = 1/2 + 1/15 + 1/210 by n/7.", scale: "n = %p% × %k%: solve 4/%p% with an identity and multiply every denominator by %k%.", open: "n ≡ %r% (mod 840): one of the six hard classes (1, 121, 169, 289, 361, 529). No known identity covers it — only a search finds the triple."
};
const tri = (s) => '1/' + s[0] + ' + 1/' + s[1] + ' + 1/' + s[2];
// Interface: classify n, show the identity (if any), then search and list the triples.
const $ = (id) => document.getElementById(id);
function addLine(box, cls, text) { const d = document.createElement('div'); d.className = 'line ' + cls; d.textContent = text; box.appendChild(d); return d; }
function run(n) {
const out = $('out'); out.innerHTML = '';
if (!Number.isInteger(n) || n < 2 || n > 10000000) { addLine(out, 'bad', "Enter a whole number from 2 to 10,000,000."); return; }
$('n').value = n;
const id = identity(n);
addLine(out, 'lbl', i18nFmt("n = %n% (%p%), n mod 840 = %r%", { n: n, r: n % 840, p: isPrime(n) ? "prime" : "composite" }));
addLine(out, id.key === 'open' ? 'warn' : 'ok', i18nFmt(CLS[id.key], { r: id.r, p: id.p, k: id.k }));
if (id.sol) {
addLine(out, 'mono', '4/' + n + ' = ' + tri(id.sol) + (checks(n, id.sol) ? ' ✓' : ' ✗'));
}
const t0 = performance.now();
const big = n > 5000;
const res = solutions(n, big ? 8 : 100000);
const ms = Math.max(0, performance.now() - t0).toFixed(1);
if (!res.out.length) { addLine(out, 'bad', "No solution found. That would be a counterexample to the conjecture — it has never happened."); return; }
const first = solutions(n, 1);
addLine(out, 'lbl', i18nFmt("Direct search: the first triple appeared after trying %t% value(s) of x in the window (%lo%, %hi%].", { t: first.tried, lo: first.lo, hi: first.hi }));
addLine(out, 'mono', '4/' + n + ' = ' + tri(first.out[0]) + (checks(n, first.out[0]) ? ' ✓' : ' ✗'));
addLine(out, 'lbl', i18nFmt(big ? "n is large, so the list stops after the first %c% solutions (%ms% ms):" : "All %c% solutions with x ≤ y ≤ z (%ms% ms):", { c: res.out.length, ms: ms }));
const box = document.createElement('div'); box.className = 'sols'; out.appendChild(box);
for (const s of res.out.slice(0, 200)) addLine(box, 'mono', '(' + s.join(', ') + ')');
}
$('find').addEventListener('click', () => run(Number($('n').value)));
$('n').addEventListener('keydown', (e) => { if (e.key === 'Enter') run(Number($('n').value)); });
$('hard').addEventListener('click', () => run(1009));
$('easy').addEventListener('click', () => run(1003));
$('rand').addEventListener('click', () => {
// Half the time pick a random prime from one of the six hard classes mod 840, otherwise any random prime.
const want = Math.random() < 0.5;
for (let k = 0; k < 5000; k++) {
const p = 2 + Math.floor(Math.random() * 9999990);
if (!isPrime(p)) continue;
if (want && !OPEN.includes(p % 840)) continue;
run(p); return;
}
run(1009);
});
// Solution counts f(n) for n ≤ 300, computed in small chunks so the page stays responsive; log-scale bars.
const counts = [];
function dark() { return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; }
function drawChart(sel) {
const cv = $('cv'), ctx = cv.getContext('2d'), W = cv.width, H = cv.height, D = dark();
ctx.fillStyle = D ? '#16202b' : '#f7f9fb'; ctx.fillRect(0, 0, W, H);
const L = 34, Bm = 22, T = 10, R = 6;
const maxC = Math.max(2, ...counts.filter(Boolean));
const ly = (c) => H - Bm - (Math.log10(c) / Math.log10(maxC)) * (H - Bm - T);
ctx.strokeStyle = D ? '#2c3b4b' : '#dde3ea'; ctx.fillStyle = D ? '#b4c0cc' : '#556'; ctx.font = '11px system-ui, sans-serif'; ctx.lineWidth = 1;
for (let c = 1; c <= maxC; c *= 10) { const y = ly(c); ctx.beginPath(); ctx.moveTo(L, y); ctx.lineTo(W - R, y); ctx.stroke(); ctx.fillText(String(c), 4, y + 4); }
for (let n = 50; n <= 300; n += 50) ctx.fillText(String(n), L + (n - 2) / 299 * (W - L - R) - 8, H - 6);
const bw = (W - L - R) / 299;
for (let n = 2; n <= 300; n++) {
const c = counts[n]; if (!c) continue;
const pr = isPrime(n);
ctx.fillStyle = n === sel ? (D ? '#f0c060' : '#d08700') : pr ? (D ? '#7fb2ff' : '#1d3557') : (D ? '#3a5068' : '#b9c7d8');
const x = L + (n - 2) * bw, y = Math.min(ly(Math.max(c, 1)), H - Bm - 2);
ctx.fillRect(x, y, Math.max(1, bw - 0.4), H - Bm - y);
}
}
$('chartbtn').addEventListener('click', () => {
const btn = $('chartbtn'), co = $('chartout'); btn.disabled = true;
let n = 2;
function step() {
const until = Math.min(300, n + 12);
for (; n <= until; n++) counts[n] = solutions(n, 100000).out.length;
co.textContent = i18nFmt("Counting… n = %n% of 300", { n: n - 1 });
drawChart(0);
if (n <= 300) { setTimeout(step, 0); return; }
let bp = 0, bm = 2;
for (let k = 2; k <= 300; k++) { if (isPrime(k) && (!bp || counts[k] < counts[bp])) bp = k; if (counts[k] > counts[bm]) bm = k; }
co.textContent = i18nFmt("Done. Fewest solutions among primes: f(%p%) = %c%. Most solutions overall: f(%m%) = %mc%.", { p: bp, c: counts[bp], m: bm, mc: counts[bm] });
btn.disabled = false;
}
step();
});
$('cv').addEventListener('click', (e) => {
if (counts.length < 300) return;
const r = $('cv').getBoundingClientRect(), W = $('cv').width;
const px = (e.clientX - r.left) * W / r.width;
const n = Math.floor(2 + (px - 34) / ((W - 40) / 299));
if (n >= 2 && n <= 300) { drawChart(n); run(n); }
});
// Greedy (Fibonacci–Sylvester): always subtract the largest unit fraction that fits.
function greedy(p, q) {
let P = BigInt(p), Q = BigInt(q); const terms = [];
while (P > 0n && terms.length < 10) {
const d = (Q + P - 1n) / P; terms.push(d);
P = P * d - Q; Q = Q * d;
if (P) { const g = bgcd(P, Q); P /= g; Q /= g; }
}
return { terms, done: P === 0n };
}
const short = (b) => { const s = b.toString(); return s.length <= 24 ? s : s.slice(0, 10) + '…' + i18nFmt("(%d% digits)", { d: s.length }); };
$('greedy').addEventListener('click', () => {
const g = $('gout'); g.innerHTML = '';
const m = /^\s*(\d+)\s*\/\s*(\d+)\s*$/.exec($('frac').value);
const p = m ? Number(m[1]) : 0, q = m ? Number(m[2]) : 0;
if (!m || p < 1 || p >= q || q > 1000000000) { addLine(g, 'bad', "Type a fraction like 4/17 with 0 < p < q ≤ 1,000,000,000."); return; }
const r = greedy(p, q);
addLine(g, 'mono', p + '/' + q + ' = ' + r.terms.map((d) => '1/' + short(d)).join(' + ') + (r.done ? '' : ' + …'));
addLine(g, r.done ? 'ok' : 'warn', i18nFmt(r.done ? "The greedy algorithm needed %k% term(s)." : "The greedy algorithm is still going after %k% terms and the denominators are exploding.", { k: r.terms.length }));
const k = gcd(p, q);
if (p / k === 4 && q / k <= 10000000) {
const s = solutions(q / k, 1).out[0];
if (s) addLine(g, 'lbl', i18nFmt("Yet a three-term expansion exists: 4/%q% = %t%.", { q: q / k, t: tri(s) }));
}
});
run(1009);
Comments
Loading comments...