<p class="hint">This polynomial factors completely into four hidden roots over the finite field with 13 elements. Pick a shift t, click Split, and watch a random gcd peel off a real factor — most of the time.</p>
<div class="poly-row">
<span class="poly-label">f(x) =</span>
<span class="poly-box" id="fPoly"></span>
</div>
<div class="controls">
<label for="tSelect">Shift t for a(x) = x + t:</label>
<select id="tSelect"></select>
<button id="split" type="button">Split</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="status" id="status">Pick a value of t and click Split.</div>
<div class="factors" id="factors"></div>
<div class="log" id="log"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.poly-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .6rem; flex-wrap: wrap; }
.poly-label { font-weight: 700; color: #1d3557; }
.poly-box { font: 600 15px ui-monospace, monospace; background: #e8eef3; border: 1px solid #cdd9e3;
border-radius: 8px; padding: .35rem .6rem; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
.controls label { font-size: .9rem; font-weight: 600; color: #1d3557; }
select { font: 600 14px system-ui, sans-serif; padding: .4rem .5rem; border-radius: 8px; border: 1px solid #adb1b8; }
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; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info { color: #1d3557; }
.factors { display: flex; gap: .5rem; flex-wrap: wrap; margin: .4rem 0; }
.factor-chip { font: 600 14px ui-monospace, monospace; background: #fff3cd; border: 1px solid #e0b84a;
color: #6b4e00; border-radius: 8px; padding: .3rem .6rem; }
.factor-chip.root { background: #d7f5df; border-color: #7dcf95; color: #0a7d33; }
.log { font-size: .82rem; color: #555; margin-top: .5rem; line-height: 1.5; min-height: 1.2em; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Finite field setup: work modulo the prime P, with four hidden roots that f(x) is built from.
const P = 13;
const ROOTS = [2, 5, 8, 11]; // the four hidden roots (kept only to build f; the algorithm below never looks at this list directly)
// Polynomial arithmetic modulo P, represented as coefficient arrays (index = power of x).
function pmod(a, m) { return ((a % m) + m) % m; }
function trimPoly(a) { a = a.slice(); while (a.length > 1 && a[a.length - 1] === 0) a.pop(); return a; }
function polyMul(a, b) {
const r = new Array(a.length + b.length - 1).fill(0);
for (let i = 0; i < a.length; i++) { if (!a[i]) continue;
for (let j = 0; j < b.length; j++) r[i + j] = pmod(r[i + j] + a[i] * b[j], P); }
return trimPoly(r);
}
function egcdInt(a, b) { if (b === 0) return [a, 1, 0]; const [g, x1, y1] = egcdInt(b, a % b); return [g, y1, x1 - Math.floor(a / b) * y1]; }
function invMod(a) { const [g, x] = egcdInt(pmod(a, P), P); if (g !== 1) throw new Error('no inverse'); return pmod(x, P); }
function polyDivMod(a, b) {
a = trimPoly(a.slice()); b = trimPoly(b.slice()); let rem = a.slice();
const bd = b.length - 1, bli = invMod(b[bd]);
const q = new Array(Math.max(rem.length - bd, 1)).fill(0);
while (rem.length - 1 >= bd && !(rem.length === 1 && rem[0] === 0)) {
const dr = rem.length - 1; if (dr < bd) break;
const coef = pmod(rem[dr] * bli, P), sh = dr - bd;
q[sh] = pmod(q[sh] + coef, P);
for (let i = 0; i <= bd; i++) rem[i + sh] = pmod(rem[i + sh] - coef * b[i], P);
rem = trimPoly(rem);
if (rem.length === 1 && rem[0] === 0) break;
}
return [trimPoly(q), trimPoly(rem)];
}
function polyModF(a, f) { return polyDivMod(a, f)[1]; }
function polyGCD(a, b) {
a = trimPoly(a.slice()); b = trimPoly(b.slice());
while (!(b.length === 1 && b[0] === 0)) { const [, r] = polyDivMod(a, b); a = b; b = r; }
const lead = a[a.length - 1];
if (lead !== 0 && lead !== 1) { const iv = invMod(lead); a = a.map(c => pmod(c * iv, P)); }
return trimPoly(a);
}
// Modular exponentiation of a polynomial mod f via repeated squaring, O(log p) multiplications.
function polyPowMod(base, e, f) {
let result = [1], b = polyModF(base, f);
while (e > 0n) {
if (e & 1n) result = polyModF(polyMul(result, b), f);
b = polyModF(polyMul(b, b), f);
e >>= 1n;
}
return result;
}
function fromRoots(roots) { let f = [1]; for (const r of roots) f = polyMul(f, [pmod(-r, P), 1]); return f; }
function isOne(a) { a = trimPoly(a); return a.length === 1 && a[0] === 1; }
const F = fromRoots(ROOTS); // The target polynomial f(x), built once from the hidden roots above.
function polyToStr(a) {
a = trimPoly(a);
const deg = a.length - 1;
if (deg === 0) return String(a[0]);
const parts = [];
for (let i = deg; i >= 0; i--) {
const c = a[i];
if (c === 0) continue;
let term = i === 0 ? String(c) : (c === 1 ? '' : String(c)) + 'x' + (i > 1 ? '^' + i : '');
parts.push(term);
}
return parts.join(' + ');
}
let remainingFactors = [F]; // the pieces f(x) has been split into so far (starts as just [f]).
let tries = 0;
function renderFPoly() {
document.getElementById('fPoly').textContent = polyToStr(F) + ' (mod ' + P + ')';
}
function renderFactors() {
const box = document.getElementById('factors');
box.innerHTML = '';
remainingFactors.forEach(g => {
const chip = document.createElement('span');
const deg = trimPoly(g).length - 1;
chip.className = 'factor-chip' + (deg === 1 ? ' root' : '');
chip.textContent = polyToStr(g);
box.appendChild(chip);
});
}
function setStatus(text, cls) {
const s = document.getElementById('status');
s.textContent = text;
s.className = 'status ' + cls;
}
function log(text) {
document.getElementById('log').textContent = text;
}
function populateSelect() {
const sel = document.getElementById('tSelect');
sel.innerHTML = '';
for (let t = 0; t < P; t++) {
const opt = document.createElement('option');
opt.value = String(t);
opt.textContent = 't = ' + t;
sel.appendChild(opt);
}
}
function splitOnePiece(g) {
// Try to split one piece of the current factorization using the chosen shift t.
const deg = trimPoly(g).length - 1;
if (deg <= 1) return null; // a linear (degree <= 1) piece cannot be split further, so skip it.
const t = Number(document.getElementById('tSelect').value);
const a = [pmod(t, P), 1]; // the random-ish candidate polynomial a(x) = x + t chosen by the user.
const e = BigInt((P - 1) / 2);
const bigPow = polyPowMod(a, e, g);
const shifted = trimPoly(bigPow.map((c, i) => i === 0 ? pmod(c - 1, P) : c));
const h = polyGCD(shifted, g);
if (isOne(h) || trimPoly(h).length >= trimPoly(g).length) return null; // gcd was 1 (no overlap) or the whole piece (no separation) -- a trivial, useless split.
const [q] = polyDivMod(g, h);
return [h, q];
}
function split() {
tries++;
let splitAny = false;
const next = [];
for (const g of remainingFactors) {
const result = splitOnePiece(g);
if (result) { next.push(result[0], result[1]); splitAny = true; }
else next.push(g);
}
remainingFactors = next;
renderFactors();
const allLinear = remainingFactors.every(g => trimPoly(g).length - 1 <= 1);
if (allLinear) {
setStatus(i18nFmt('Fully factored into linear roots after %n% split(s).', { n: tries }), 'ok');
log(i18nFmt('Recovered roots (mod 13): %roots%', { roots: ROOTS.slice().sort((a, b) => a - b).join(', ') }));
} else if (splitAny) {
setStatus('A nontrivial factor appeared — click Split again to keep dividing.', 'ok');
log(i18nFmt('gcd(a(x)^((p-1)/2) - 1, piece) with t = %t% produced a genuine factor.', { t: document.getElementById('tSelect').value }));
} else {
setStatus('That shift gave a trivial split (gcd = 1 or the whole piece). Try a different t.', 'bad');
log(i18nFmt('gcd(a(x)^((p-1)/2) - 1, piece) with t = %t% was trivial for every remaining piece — no progress this round.', { t: document.getElementById('tSelect').value }));
}
}
function reset() {
remainingFactors = [F];
tries = 0;
renderFactors();
setStatus('Pick a value of t and click Split.', 'info');
log('');
}
populateSelect();
renderFPoly();
renderFactors();
document.getElementById('split').addEventListener('click', split);
document.getElementById('reset').addEventListener('click', reset);
<p class="hint">This polynomial factors completely into four hidden roots over the finite field with 13 elements. Pick a shift t, click Split, and watch a random gcd peel off a real factor — most of the time.</p>
<div class="poly-row">
<span class="poly-label">f(x) =</span>
<span class="poly-box" id="fPoly"></span>
</div>
<div class="controls">
<label for="tSelect">Shift t for a(x) = x + t:</label>
<select id="tSelect"></select>
<button id="split" type="button">Split</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="status" id="status">Pick a value of t and click Split.</div>
<div class="factors" id="factors"></div>
<div class="log" id="log"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.poly-row { display: flex; align-items: center; gap: .5rem; margin-bottom: .6rem; flex-wrap: wrap; }
.poly-label { font-weight: 700; color: #1d3557; }
.poly-box { font: 600 15px ui-monospace, monospace; background: #e8eef3; border: 1px solid #cdd9e3;
border-radius: 8px; padding: .35rem .6rem; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
.controls label { font-size: .9rem; font-weight: 600; color: #1d3557; }
select { font: 600 14px system-ui, sans-serif; padding: .4rem .5rem; border-radius: 8px; border: 1px solid #adb1b8; }
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; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info { color: #1d3557; }
.factors { display: flex; gap: .5rem; flex-wrap: wrap; margin: .4rem 0; }
.factor-chip { font: 600 14px ui-monospace, monospace; background: #fff3cd; border: 1px solid #e0b84a;
color: #6b4e00; border-radius: 8px; padding: .3rem .6rem; }
.factor-chip.root { background: #d7f5df; border-color: #7dcf95; color: #0a7d33; }
.log { font-size: .82rem; color: #555; margin-top: .5rem; line-height: 1.5; min-height: 1.2em; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Finite field setup: work modulo the prime P, with four hidden roots that f(x) is built from.
const P = 13;
const ROOTS = [2, 5, 8, 11]; // the four hidden roots (kept only to build f; the algorithm below never looks at this list directly)
// Polynomial arithmetic modulo P, represented as coefficient arrays (index = power of x).
function pmod(a, m) { return ((a % m) + m) % m; }
function trimPoly(a) { a = a.slice(); while (a.length > 1 && a[a.length - 1] === 0) a.pop(); return a; }
function polyMul(a, b) {
const r = new Array(a.length + b.length - 1).fill(0);
for (let i = 0; i < a.length; i++) { if (!a[i]) continue;
for (let j = 0; j < b.length; j++) r[i + j] = pmod(r[i + j] + a[i] * b[j], P); }
return trimPoly(r);
}
function egcdInt(a, b) { if (b === 0) return [a, 1, 0]; const [g, x1, y1] = egcdInt(b, a % b); return [g, y1, x1 - Math.floor(a / b) * y1]; }
function invMod(a) { const [g, x] = egcdInt(pmod(a, P), P); if (g !== 1) throw new Error('no inverse'); return pmod(x, P); }
function polyDivMod(a, b) {
a = trimPoly(a.slice()); b = trimPoly(b.slice()); let rem = a.slice();
const bd = b.length - 1, bli = invMod(b[bd]);
const q = new Array(Math.max(rem.length - bd, 1)).fill(0);
while (rem.length - 1 >= bd && !(rem.length === 1 && rem[0] === 0)) {
const dr = rem.length - 1; if (dr < bd) break;
const coef = pmod(rem[dr] * bli, P), sh = dr - bd;
q[sh] = pmod(q[sh] + coef, P);
for (let i = 0; i <= bd; i++) rem[i + sh] = pmod(rem[i + sh] - coef * b[i], P);
rem = trimPoly(rem);
if (rem.length === 1 && rem[0] === 0) break;
}
return [trimPoly(q), trimPoly(rem)];
}
function polyModF(a, f) { return polyDivMod(a, f)[1]; }
function polyGCD(a, b) {
a = trimPoly(a.slice()); b = trimPoly(b.slice());
while (!(b.length === 1 && b[0] === 0)) { const [, r] = polyDivMod(a, b); a = b; b = r; }
const lead = a[a.length - 1];
if (lead !== 0 && lead !== 1) { const iv = invMod(lead); a = a.map(c => pmod(c * iv, P)); }
return trimPoly(a);
}
// Modular exponentiation of a polynomial mod f via repeated squaring, O(log p) multiplications.
function polyPowMod(base, e, f) {
let result = [1], b = polyModF(base, f);
while (e > 0n) {
if (e & 1n) result = polyModF(polyMul(result, b), f);
b = polyModF(polyMul(b, b), f);
e >>= 1n;
}
return result;
}
function fromRoots(roots) { let f = [1]; for (const r of roots) f = polyMul(f, [pmod(-r, P), 1]); return f; }
function isOne(a) { a = trimPoly(a); return a.length === 1 && a[0] === 1; }
const F = fromRoots(ROOTS); // The target polynomial f(x), built once from the hidden roots above.
function polyToStr(a) {
a = trimPoly(a);
const deg = a.length - 1;
if (deg === 0) return String(a[0]);
const parts = [];
for (let i = deg; i >= 0; i--) {
const c = a[i];
if (c === 0) continue;
let term = i === 0 ? String(c) : (c === 1 ? '' : String(c)) + 'x' + (i > 1 ? '^' + i : '');
parts.push(term);
}
return parts.join(' + ');
}
let remainingFactors = [F]; // the pieces f(x) has been split into so far (starts as just [f]).
let tries = 0;
function renderFPoly() {
document.getElementById('fPoly').textContent = polyToStr(F) + ' (mod ' + P + ')';
}
function renderFactors() {
const box = document.getElementById('factors');
box.innerHTML = '';
remainingFactors.forEach(g => {
const chip = document.createElement('span');
const deg = trimPoly(g).length - 1;
chip.className = 'factor-chip' + (deg === 1 ? ' root' : '');
chip.textContent = polyToStr(g);
box.appendChild(chip);
});
}
function setStatus(text, cls) {
const s = document.getElementById('status');
s.textContent = text;
s.className = 'status ' + cls;
}
function log(text) {
document.getElementById('log').textContent = text;
}
function populateSelect() {
const sel = document.getElementById('tSelect');
sel.innerHTML = '';
for (let t = 0; t < P; t++) {
const opt = document.createElement('option');
opt.value = String(t);
opt.textContent = 't = ' + t;
sel.appendChild(opt);
}
}
function splitOnePiece(g) {
// Try to split one piece of the current factorization using the chosen shift t.
const deg = trimPoly(g).length - 1;
if (deg <= 1) return null; // a linear (degree <= 1) piece cannot be split further, so skip it.
const t = Number(document.getElementById('tSelect').value);
const a = [pmod(t, P), 1]; // the random-ish candidate polynomial a(x) = x + t chosen by the user.
const e = BigInt((P - 1) / 2);
const bigPow = polyPowMod(a, e, g);
const shifted = trimPoly(bigPow.map((c, i) => i === 0 ? pmod(c - 1, P) : c));
const h = polyGCD(shifted, g);
if (isOne(h) || trimPoly(h).length >= trimPoly(g).length) return null; // gcd was 1 (no overlap) or the whole piece (no separation) -- a trivial, useless split.
const [q] = polyDivMod(g, h);
return [h, q];
}
function split() {
tries++;
let splitAny = false;
const next = [];
for (const g of remainingFactors) {
const result = splitOnePiece(g);
if (result) { next.push(result[0], result[1]); splitAny = true; }
else next.push(g);
}
remainingFactors = next;
renderFactors();
const allLinear = remainingFactors.every(g => trimPoly(g).length - 1 <= 1);
if (allLinear) {
setStatus(i18nFmt('Fully factored into linear roots after %n% split(s).', { n: tries }), 'ok');
log(i18nFmt('Recovered roots (mod 13): %roots%', { roots: ROOTS.slice().sort((a, b) => a - b).join(', ') }));
} else if (splitAny) {
setStatus('A nontrivial factor appeared — click Split again to keep dividing.', 'ok');
log(i18nFmt('gcd(a(x)^((p-1)/2) - 1, piece) with t = %t% produced a genuine factor.', { t: document.getElementById('tSelect').value }));
} else {
setStatus('That shift gave a trivial split (gcd = 1 or the whole piece). Try a different t.', 'bad');
log(i18nFmt('gcd(a(x)^((p-1)/2) - 1, piece) with t = %t% was trivial for every remaining piece — no progress this round.', { t: document.getElementById('tSelect').value }));
}
}
function reset() {
remainingFactors = [F];
tries = 0;
renderFactors();
setStatus('Pick a value of t and click Split.', 'info');
log('');
}
populateSelect();
renderFPoly();
renderFactors();
document.getElementById('split').addEventListener('click', split);
document.getElementById('reset').addEventListener('click', reset);
Comments
Loading comments...