<p class="hint">Give every vertex a different number from 0 to n−1. Each edge shows the difference of its two ends. The labelling is <b>graceful</b> when the edge differences are exactly 1, 2, …, n−1 — each once. Tap a vertex, then a number.</p>
<div class="row presets">
<button type="button" class="ghost" data-preset="path">Path</button>
<button type="button" class="ghost" data-preset="star">Star</button>
<button type="button" class="ghost" data-preset="cat">Caterpillar</button>
<button type="button" class="ghost" data-preset="random">Random tree</button>
</div>
<svg id="tree" viewBox="0 0 340 240" role="img" aria-label="The tree being labelled; tap a vertex to select it."></svg>
<div class="cap">Edge differences needed (green = used once, red = repeated)</div>
<div class="diffs" id="diffs"></div>
<div class="cap">Labels (tap to assign to the selected vertex)</div>
<div class="chips" id="chips"></div>
<div class="status" id="status" aria-live="polite"></div>
<div class="row">
<button type="button" id="leaf" class="ghost">Add leaves: off</button>
<button type="button" id="solve">Solve</button>
<button type="button" id="clear" class="ghost">Clear labels</button>
<button type="button" id="stress">Stress test 300 trees</button>
</div>
/* Colours as variables so the dark theme only has to swap values. */
:root { --fg: #1f2933; --muted: #52606d; --line: #9aa8b6; --panel: #eef2f6; --accent: #1d3557;
--node: #ffffff; --ok: #0a7d33; --okbg: #d8f3e2; --bad: #c92f3c; --badbg: #fde2e4; --sel: #f4a261; }
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: var(--fg); margin: 0; background: #ffffff; }
.hint { font-size: .88rem; color: var(--muted); margin: 0 0 .6rem; line-height: 1.45; }
.row { display: flex; gap: .4rem; flex-wrap: wrap; margin: .35rem 0; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .75rem; border: 1px solid var(--accent);
background: var(--accent); color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: transparent; color: var(--accent); }
button.on { background: var(--sel); border-color: var(--sel); color: #1f2933; }
button:disabled { opacity: .45; cursor: default; }
svg#tree { width: 100%; max-width: 460px; height: auto; display: block; background: var(--panel);
border-radius: 10px; margin: .3rem 0; touch-action: manipulation; }
.edge { stroke: var(--line); stroke-width: 2.2; }
.edge.dup { stroke: var(--bad); }
.vtx circle { fill: var(--node); stroke: var(--accent); stroke-width: 2; cursor: pointer; }
.vtx.lab circle { fill: var(--okbg); }
.vtx.sel circle { stroke: var(--sel); stroke-width: 3.5; }
.vtx text { font: 700 12px ui-monospace, monospace; fill: var(--fg); text-anchor: middle; dominant-baseline: central; pointer-events: none; }
.elab rect { fill: var(--panel); }
.elab text { font: 700 10px ui-monospace, monospace; fill: var(--ok); text-anchor: middle; dominant-baseline: central; }
.elab.dup text { fill: var(--bad); }
.cap { font-size: .75rem; color: var(--muted); margin-top: .35rem; text-transform: uppercase; letter-spacing: .04em; }
.diffs, .chips { display: flex; flex-wrap: wrap; gap: 3px; margin: .2rem 0; }
.d { min-width: 24px; height: 22px; padding: 0 3px; display: flex; align-items: center; justify-content: center;
font: 700 11px ui-monospace, monospace; border-radius: 5px; border: 1px dashed var(--line); color: var(--muted); }
.d.hit { background: var(--okbg); border: 1px solid var(--ok); color: var(--ok); }
.d.dup { background: var(--badbg); border: 1px solid var(--bad); color: var(--bad); }
.chip { min-width: 30px; padding: .3rem .2rem; font: 700 13px ui-monospace, monospace; }
.chip.used { opacity: .4; }
.chip.mine { opacity: 1; background: var(--sel); border-color: var(--sel); color: #1f2933; }
.status { font-size: .92rem; font-weight: 600; margin: .45rem 0; min-height: 2.6em; line-height: 1.35; }
.status.ok { color: var(--ok); }
.status.bad { color: var(--bad); }
@media (prefers-color-scheme: dark) {
:root { --fg: #e4e9ef; --muted: #a3b1bf; --line: #5b6878; --panel: #1b222b; --accent: #9fb2c8;
--node: #12171d; --ok: #5fd08a; --okbg: #17382a; --bad: #ff7b86; --badbg: #3d1d22; --sel: #f4a261; }
body { background: #12171d; }
button { color: #12171d; }
button.ghost { background: transparent; color: var(--accent); }
}
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// State: the tree (n vertices, edge list), the labels (-1 = empty) and the selected vertex.
const MAXN = 16, W = 340, H = 240, NS = 'http://www.w3.org/2000/svg';
let n = 0, edges = [], adj = [], lab = [], sel = -1, addMode = false, busy = false, msg = null;
const $ = (id) => document.getElementById(id);
function buildAdj() {
adj = Array.from({ length: n }, () => []);
for (const [a, b] of edges) { adj[a].push(b); adj[b].push(a); }
}
function setTree(N, E, L) {
n = N; edges = E.map((e) => e.slice()); buildAdj();
lab = L ? L.slice() : new Array(n).fill(-1); sel = -1;
}
// A uniformly random labelled tree, decoded from a random Prüfer sequence.
function randomTree(N) {
if (N < 2) return [];
const P = []; for (let i = 0; i < N - 2; i++) P.push(Math.floor(Math.random() * N));
const deg = new Array(N).fill(1); for (const x of P) deg[x]++;
const E = [];
for (const x of P) { for (let j = 0; j < N; j++) { if (deg[j] === 1) { E.push([j, x]); deg[j]--; deg[x]--; break; } } }
const r = []; for (let j = 0; j < N; j++) if (deg[j] === 1) r.push(j);
E.push([r[0], r[1]]);
return E;
}
// The preset trees. Each entry returns [number of vertices, edge list].
const PRESETS = {
path: () => [7, [[0,1],[1,2],[2,3],[3,4],[4,5],[5,6]]],
star: () => [7, [[0,1],[0,2],[0,3],[0,4],[0,5],[0,6]]],
cat: () => [10, [[0,1],[1,2],[2,3],[0,4],[1,5],[1,6],[2,7],[3,8],[3,9]]],
random: () => { const N = 6 + Math.floor(Math.random() * 5); return [N, randomTree(N)]; }
};
// Backtracking by differences: the largest difference m must join labels 0 and m,
// then m−1 joins 0 and m−1 or 1 and m, and so on downwards. Each step places the
// next missing difference on some edge; nodes counts every placement we try.
function solve(N, E, budget) {
const m = E.length;
const A = Array.from({ length: N }, () => []); for (const [a, b] of E) { A[a].push(b); A[b].push(a); }
const L = new Array(N).fill(-1), owner = new Array(m + 1).fill(-1), usedD = new Array(m + 1).fill(false);
let nodes = 0, aborted = false;
function put(v, l) {
const ds = [];
for (const w of A[v]) { if (L[w] < 0) continue; const d = Math.abs(l - L[w]);
if (d === 0 || usedD[d] || ds.includes(d)) return null; ds.push(d); }
L[v] = l; owner[l] = v; for (const d of ds) usedD[d] = true; return ds;
}
function undo(v, ds) { owner[L[v]] = -1; L[v] = -1; for (const d of ds) usedD[d] = false; }
function rec(d) {
while (d > 0 && usedD[d]) d--;
if (d === 0) return true;
// Case 1: the difference grows out of a vertex that already has a label.
for (let x = 0; x + d <= m; x++) {
const y = x + d;
for (const [have, want] of [[x, y], [y, x]]) {
if (owner[have] < 0 || owner[want] >= 0) continue;
for (const v of A[owner[have]]) { if (L[v] >= 0) continue;
if (nodes >= budget) { aborted = true; return false; } nodes++;
const ds = put(v, want); if (!ds) continue;
if (rec(d - 1)) return true; undo(v, ds); if (aborted) return false; }
}
}
// Case 2: the difference sits on an edge whose two ends are both still empty.
for (let x = 0; x + d <= m; x++) {
const y = x + d; if (owner[x] >= 0 || owner[y] >= 0) continue;
for (const [a, b] of E) { if (L[a] >= 0 || L[b] >= 0) continue;
for (const [p, q] of [[a, b], [b, a]]) {
if (nodes >= budget) { aborted = true; return false; } nodes++;
const d1 = put(p, x); if (!d1) continue;
const d2 = put(q, y); if (!d2) { undo(p, d1); continue; }
if (rec(d - 1)) return true; undo(q, d2); undo(p, d1); if (aborted) return false; }
}
}
return false;
}
if (N === 1) return { labels: [0], nodes: 1, aborted: false };
const found = rec(m);
return { labels: found ? L.slice() : null, nodes, aborted };
}
// Independent check: distinct labels and n−1 distinct non-zero edge differences.
function isGraceful(N, E, L) {
if (!L || new Set(L).size !== N || L.some((x) => x < 0 || x > E.length)) return false;
const D = new Set(E.map(([a, b]) => Math.abs(L[a] - L[b])));
return D.size === E.length && !D.has(0);
}
// Layout: root the tree at a centre vertex, then place leaves left to right by depth.
function bfs(src) {
const d = new Array(n).fill(-1), par = new Array(n).fill(-1); d[src] = 0; const q = [src];
while (q.length) { const v = q.shift(); for (const w of adj[v]) if (d[w] < 0) { d[w] = d[v] + 1; par[w] = v; q.push(w); } }
return { d, par };
}
function layout() {
let root = 0, best = Infinity;
for (let v = 0; v < n; v++) { const e = Math.max(...bfs(v).d); if (e < best) { best = e; root = v; } }
const { d, par } = bfs(root);
const kids = Array.from({ length: n }, () => []);
for (let v = 0; v < n; v++) if (par[v] >= 0) kids[par[v]].push(v);
const xs = new Array(n); let leaf = 0;
(function place(v) {
if (!kids[v].length) { xs[v] = leaf++; return; }
kids[v].forEach(place); xs[v] = (xs[kids[v][0]] + xs[kids[v][kids[v].length - 1]]) / 2;
})(root);
const maxD = Math.max(...d);
return xs.map((x, v) => ({ x: leaf > 1 ? 22 + x * (W - 44) / (leaf - 1) : W / 2,
y: maxD ? 24 + d[v] * (H - 48) / maxD : H / 2 }));
}
function el(tag, attrs, parent) {
const e = document.createElementNS(NS, tag);
for (const k in attrs) e.setAttribute(k, attrs[k]);
parent.appendChild(e); return e;
}
function diffCounts() {
const c = new Array(Math.max(n, 1)).fill(0);
for (const [a, b] of edges) if (lab[a] >= 0 && lab[b] >= 0) c[Math.abs(lab[a] - lab[b])]++;
return c;
}
function draw() {
const svg = $('tree'); svg.innerHTML = '';
const P = layout(), R = n > 11 ? 10 : 13, cnt = diffCounts();
for (const [a, b] of edges) {
const dd = (lab[a] >= 0 && lab[b] >= 0) ? Math.abs(lab[a] - lab[b]) : 0;
el('line', { x1: P[a].x, y1: P[a].y, x2: P[b].x, y2: P[b].y, class: 'edge' + (dd && cnt[dd] > 1 ? ' dup' : '') }, svg);
}
for (const [a, b] of edges) {
if (lab[a] < 0 || lab[b] < 0) continue;
const dd = Math.abs(lab[a] - lab[b]);
const g = el('g', { class: 'elab' + (cnt[dd] > 1 ? ' dup' : '') }, svg);
const mx = (P[a].x + P[b].x) / 2, my = (P[a].y + P[b].y) / 2;
el('rect', { x: mx - 9, y: my - 7, width: 18, height: 14, rx: 4 }, g);
el('text', { x: mx, y: my }, g).textContent = dd;
}
for (let v = 0; v < n; v++) {
const g = el('g', { class: 'vtx' + (lab[v] >= 0 ? ' lab' : '') + (v === sel ? ' sel' : '') }, svg);
el('circle', { cx: P[v].x, cy: P[v].y, r: R }, g);
el('text', { x: P[v].x, y: P[v].y }, g).textContent = lab[v] >= 0 ? lab[v] : '';
g.addEventListener('click', () => tapVertex(v));
}
const db = $('diffs'); db.innerHTML = '';
for (let d = 1; d < n; d++) {
const s = document.createElement('div');
s.className = 'd' + (cnt[d] === 1 ? ' hit' : cnt[d] > 1 ? ' dup' : '');
s.textContent = d; db.appendChild(s);
}
const cb = $('chips'); cb.innerHTML = '';
for (let l = 0; l < n; l++) {
const b = document.createElement('button'); b.type = 'button';
b.className = 'chip' + (lab.includes(l) ? ' used' : '') + (sel >= 0 && lab[sel] === l ? ' mine' : '');
b.textContent = l; b.disabled = busy;
b.addEventListener('click', () => pick(l)); cb.appendChild(b);
}
status(cnt);
}
function setStatus(t, cls) { const s = $('status'); s.textContent = t; s.className = 'status ' + cls; }
function status(cnt) {
if (msg) { setStatus(msg.t, msg.cls); return; }
const m = n - 1, fixed = edges.filter(([a, b]) => lab[a] >= 0 && lab[b] >= 0).length;
const dup = cnt.findIndex((x, i) => i > 0 && x > 1);
if (dup > 0) setStatus(i18nFmt("Difference %d% appears on more than one edge (red). Every difference may appear only once.", { d: dup }), 'bad');
else if (fixed === m && lab.every((x) => x >= 0)) setStatus(i18nFmt("✓ Graceful! The differences are exactly 1 to %m%, each used once.", { m }), 'ok');
else if (fixed === 0) setStatus("Tap a vertex, then tap a number to label it.", '');
else setStatus(i18nFmt("%k% of %m% edge differences fixed — no repeats so far.", { k: fixed, m }), '');
}
function tapVertex(v) {
if (busy) return;
if (addMode) {
if (n >= MAXN) { msg = { t: i18nFmt("This demo stops at %max% vertices — the search stays instant up to there.", { max: MAXN }), cls: 'bad' }; draw(); return; }
edges.push([v, n]); lab.push(-1); n++; buildAdj(); msg = { t: "Tap any vertex to hang a new leaf on it. Turn the mode off to label.", cls: '' }; draw(); return;
}
sel = v; msg = null; draw();
}
function pick(l) {
if (busy) return;
if (sel < 0) { msg = { t: "Select a vertex first, then tap a number.", cls: '' }; draw(); return; }
msg = null; const o = lab.indexOf(l);
if (o === sel) { lab[sel] = -1; draw(); return; }
if (o >= 0) lab[o] = lab[sel];
lab[sel] = l;
// Jump to the next vertex that still has no label.
sel = lab.indexOf(-1);
draw();
}
function setBusy(b) {
busy = b; document.querySelectorAll('button').forEach((x) => { x.disabled = b; });
}
function runSolve() {
if (busy) return;
const t0 = performance.now(), r = solve(n, edges, 3000000), ms = Math.max(1, Math.round(performance.now() - t0));
if (r.labels && isGraceful(n, edges, r.labels)) { lab = r.labels; sel = -1; msg = { t: i18nFmt("✓ Backtracking found a graceful labelling after exploring %nodes% search nodes (%ms% ms).", { nodes: r.nodes, ms }), cls: 'ok' }; }
else msg = { t: i18nFmt("Gave up after %nodes% search nodes. This is a budget limit, not a counterexample: every tree this small is known to be graceful.", { nodes: r.nodes }), cls: 'bad' };
draw();
}
// Stress test: hundreds of random trees, each solved and checked independently.
function stress() {
if (busy) return;
setBusy(true); addMode = false; leafBtn();
const TOTAL = 300; let done = 0, ok = 0, tot = 0, worst = 0;
function chunk() {
let last = null;
for (let i = 0; i < 12 && done < TOTAL; i++) {
const N = 5 + Math.floor(Math.random() * 10), E = randomTree(N), r = solve(N, E, 200000);
done++; tot += r.nodes; worst = Math.max(worst, r.nodes);
if (isGraceful(N, E, r.labels)) ok++;
last = [N, E, r.labels];
}
setTree(last[0], last[1], last[2] || null);
if (done < TOTAL) {
msg = { t: i18nFmt("Testing random trees… %done% / %total% done, %ok% labelled gracefully.", { done, total: TOTAL, ok }), cls: '' }; draw();
setTimeout(chunk, 40);
} else {
setBusy(false);
msg = ok === TOTAL
? { t: i18nFmt("✓ All %total% random trees (5–14 vertices) received a graceful labelling. Average %avg% search nodes, worst %worst%. No counterexample — as always.", { total: TOTAL, avg: Math.round(tot / TOTAL), worst }), cls: 'ok' }
: { t: i18nFmt("%ok% of %total% trees labelled within the budget. The rest just ran out of search time — they are not counterexamples.", { ok, total: TOTAL }), cls: 'bad' };
draw();
}
}
chunk();
}
function leafBtn() {
const b = $('leaf'); b.textContent = addMode ? "Add leaves: on" : "Add leaves: off";
b.classList.toggle('on', addMode);
}
document.querySelectorAll('[data-preset]').forEach((b) => b.addEventListener('click', () => {
if (busy) return; const [N, E] = PRESETS[b.dataset.preset](); setTree(N, E); msg = null; draw();
}));
$('leaf').addEventListener('click', () => {
if (busy) return; addMode = !addMode; leafBtn(); sel = -1;
msg = addMode ? { t: "Tap any vertex to hang a new leaf on it. Turn the mode off to label.", cls: '' } : null; draw();
});
$('solve').addEventListener('click', runSolve);
$('clear').addEventListener('click', () => { if (busy) return; lab = new Array(n).fill(-1); sel = -1; msg = null; draw(); });
$('stress').addEventListener('click', stress);
const [N0, E0] = PRESETS.cat(); setTree(N0, E0); draw();
<p class="hint">Give every vertex a different number from 0 to n−1. Each edge shows the difference of its two ends. The labelling is <b>graceful</b> when the edge differences are exactly 1, 2, …, n−1 — each once. Tap a vertex, then a number.</p>
<div class="row presets">
<button type="button" class="ghost" data-preset="path">Path</button>
<button type="button" class="ghost" data-preset="star">Star</button>
<button type="button" class="ghost" data-preset="cat">Caterpillar</button>
<button type="button" class="ghost" data-preset="random">Random tree</button>
</div>
<svg id="tree" viewBox="0 0 340 240" role="img" aria-label="The tree being labelled; tap a vertex to select it."></svg>
<div class="cap">Edge differences needed (green = used once, red = repeated)</div>
<div class="diffs" id="diffs"></div>
<div class="cap">Labels (tap to assign to the selected vertex)</div>
<div class="chips" id="chips"></div>
<div class="status" id="status" aria-live="polite"></div>
<div class="row">
<button type="button" id="leaf" class="ghost">Add leaves: off</button>
<button type="button" id="solve">Solve</button>
<button type="button" id="clear" class="ghost">Clear labels</button>
<button type="button" id="stress">Stress test 300 trees</button>
</div>
/* Colours as variables so the dark theme only has to swap values. */
:root { --fg: #1f2933; --muted: #52606d; --line: #9aa8b6; --panel: #eef2f6; --accent: #1d3557;
--node: #ffffff; --ok: #0a7d33; --okbg: #d8f3e2; --bad: #c92f3c; --badbg: #fde2e4; --sel: #f4a261; }
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: var(--fg); margin: 0; background: #ffffff; }
.hint { font-size: .88rem; color: var(--muted); margin: 0 0 .6rem; line-height: 1.45; }
.row { display: flex; gap: .4rem; flex-wrap: wrap; margin: .35rem 0; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .75rem; border: 1px solid var(--accent);
background: var(--accent); color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: transparent; color: var(--accent); }
button.on { background: var(--sel); border-color: var(--sel); color: #1f2933; }
button:disabled { opacity: .45; cursor: default; }
svg#tree { width: 100%; max-width: 460px; height: auto; display: block; background: var(--panel);
border-radius: 10px; margin: .3rem 0; touch-action: manipulation; }
.edge { stroke: var(--line); stroke-width: 2.2; }
.edge.dup { stroke: var(--bad); }
.vtx circle { fill: var(--node); stroke: var(--accent); stroke-width: 2; cursor: pointer; }
.vtx.lab circle { fill: var(--okbg); }
.vtx.sel circle { stroke: var(--sel); stroke-width: 3.5; }
.vtx text { font: 700 12px ui-monospace, monospace; fill: var(--fg); text-anchor: middle; dominant-baseline: central; pointer-events: none; }
.elab rect { fill: var(--panel); }
.elab text { font: 700 10px ui-monospace, monospace; fill: var(--ok); text-anchor: middle; dominant-baseline: central; }
.elab.dup text { fill: var(--bad); }
.cap { font-size: .75rem; color: var(--muted); margin-top: .35rem; text-transform: uppercase; letter-spacing: .04em; }
.diffs, .chips { display: flex; flex-wrap: wrap; gap: 3px; margin: .2rem 0; }
.d { min-width: 24px; height: 22px; padding: 0 3px; display: flex; align-items: center; justify-content: center;
font: 700 11px ui-monospace, monospace; border-radius: 5px; border: 1px dashed var(--line); color: var(--muted); }
.d.hit { background: var(--okbg); border: 1px solid var(--ok); color: var(--ok); }
.d.dup { background: var(--badbg); border: 1px solid var(--bad); color: var(--bad); }
.chip { min-width: 30px; padding: .3rem .2rem; font: 700 13px ui-monospace, monospace; }
.chip.used { opacity: .4; }
.chip.mine { opacity: 1; background: var(--sel); border-color: var(--sel); color: #1f2933; }
.status { font-size: .92rem; font-weight: 600; margin: .45rem 0; min-height: 2.6em; line-height: 1.35; }
.status.ok { color: var(--ok); }
.status.bad { color: var(--bad); }
@media (prefers-color-scheme: dark) {
:root { --fg: #e4e9ef; --muted: #a3b1bf; --line: #5b6878; --panel: #1b222b; --accent: #9fb2c8;
--node: #12171d; --ok: #5fd08a; --okbg: #17382a; --bad: #ff7b86; --badbg: #3d1d22; --sel: #f4a261; }
body { background: #12171d; }
button { color: #12171d; }
button.ghost { background: transparent; color: var(--accent); }
}
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// State: the tree (n vertices, edge list), the labels (-1 = empty) and the selected vertex.
const MAXN = 16, W = 340, H = 240, NS = 'http://www.w3.org/2000/svg';
let n = 0, edges = [], adj = [], lab = [], sel = -1, addMode = false, busy = false, msg = null;
const $ = (id) => document.getElementById(id);
function buildAdj() {
adj = Array.from({ length: n }, () => []);
for (const [a, b] of edges) { adj[a].push(b); adj[b].push(a); }
}
function setTree(N, E, L) {
n = N; edges = E.map((e) => e.slice()); buildAdj();
lab = L ? L.slice() : new Array(n).fill(-1); sel = -1;
}
// A uniformly random labelled tree, decoded from a random Prüfer sequence.
function randomTree(N) {
if (N < 2) return [];
const P = []; for (let i = 0; i < N - 2; i++) P.push(Math.floor(Math.random() * N));
const deg = new Array(N).fill(1); for (const x of P) deg[x]++;
const E = [];
for (const x of P) { for (let j = 0; j < N; j++) { if (deg[j] === 1) { E.push([j, x]); deg[j]--; deg[x]--; break; } } }
const r = []; for (let j = 0; j < N; j++) if (deg[j] === 1) r.push(j);
E.push([r[0], r[1]]);
return E;
}
// The preset trees. Each entry returns [number of vertices, edge list].
const PRESETS = {
path: () => [7, [[0,1],[1,2],[2,3],[3,4],[4,5],[5,6]]],
star: () => [7, [[0,1],[0,2],[0,3],[0,4],[0,5],[0,6]]],
cat: () => [10, [[0,1],[1,2],[2,3],[0,4],[1,5],[1,6],[2,7],[3,8],[3,9]]],
random: () => { const N = 6 + Math.floor(Math.random() * 5); return [N, randomTree(N)]; }
};
// Backtracking by differences: the largest difference m must join labels 0 and m,
// then m−1 joins 0 and m−1 or 1 and m, and so on downwards. Each step places the
// next missing difference on some edge; nodes counts every placement we try.
function solve(N, E, budget) {
const m = E.length;
const A = Array.from({ length: N }, () => []); for (const [a, b] of E) { A[a].push(b); A[b].push(a); }
const L = new Array(N).fill(-1), owner = new Array(m + 1).fill(-1), usedD = new Array(m + 1).fill(false);
let nodes = 0, aborted = false;
function put(v, l) {
const ds = [];
for (const w of A[v]) { if (L[w] < 0) continue; const d = Math.abs(l - L[w]);
if (d === 0 || usedD[d] || ds.includes(d)) return null; ds.push(d); }
L[v] = l; owner[l] = v; for (const d of ds) usedD[d] = true; return ds;
}
function undo(v, ds) { owner[L[v]] = -1; L[v] = -1; for (const d of ds) usedD[d] = false; }
function rec(d) {
while (d > 0 && usedD[d]) d--;
if (d === 0) return true;
// Case 1: the difference grows out of a vertex that already has a label.
for (let x = 0; x + d <= m; x++) {
const y = x + d;
for (const [have, want] of [[x, y], [y, x]]) {
if (owner[have] < 0 || owner[want] >= 0) continue;
for (const v of A[owner[have]]) { if (L[v] >= 0) continue;
if (nodes >= budget) { aborted = true; return false; } nodes++;
const ds = put(v, want); if (!ds) continue;
if (rec(d - 1)) return true; undo(v, ds); if (aborted) return false; }
}
}
// Case 2: the difference sits on an edge whose two ends are both still empty.
for (let x = 0; x + d <= m; x++) {
const y = x + d; if (owner[x] >= 0 || owner[y] >= 0) continue;
for (const [a, b] of E) { if (L[a] >= 0 || L[b] >= 0) continue;
for (const [p, q] of [[a, b], [b, a]]) {
if (nodes >= budget) { aborted = true; return false; } nodes++;
const d1 = put(p, x); if (!d1) continue;
const d2 = put(q, y); if (!d2) { undo(p, d1); continue; }
if (rec(d - 1)) return true; undo(q, d2); undo(p, d1); if (aborted) return false; }
}
}
return false;
}
if (N === 1) return { labels: [0], nodes: 1, aborted: false };
const found = rec(m);
return { labels: found ? L.slice() : null, nodes, aborted };
}
// Independent check: distinct labels and n−1 distinct non-zero edge differences.
function isGraceful(N, E, L) {
if (!L || new Set(L).size !== N || L.some((x) => x < 0 || x > E.length)) return false;
const D = new Set(E.map(([a, b]) => Math.abs(L[a] - L[b])));
return D.size === E.length && !D.has(0);
}
// Layout: root the tree at a centre vertex, then place leaves left to right by depth.
function bfs(src) {
const d = new Array(n).fill(-1), par = new Array(n).fill(-1); d[src] = 0; const q = [src];
while (q.length) { const v = q.shift(); for (const w of adj[v]) if (d[w] < 0) { d[w] = d[v] + 1; par[w] = v; q.push(w); } }
return { d, par };
}
function layout() {
let root = 0, best = Infinity;
for (let v = 0; v < n; v++) { const e = Math.max(...bfs(v).d); if (e < best) { best = e; root = v; } }
const { d, par } = bfs(root);
const kids = Array.from({ length: n }, () => []);
for (let v = 0; v < n; v++) if (par[v] >= 0) kids[par[v]].push(v);
const xs = new Array(n); let leaf = 0;
(function place(v) {
if (!kids[v].length) { xs[v] = leaf++; return; }
kids[v].forEach(place); xs[v] = (xs[kids[v][0]] + xs[kids[v][kids[v].length - 1]]) / 2;
})(root);
const maxD = Math.max(...d);
return xs.map((x, v) => ({ x: leaf > 1 ? 22 + x * (W - 44) / (leaf - 1) : W / 2,
y: maxD ? 24 + d[v] * (H - 48) / maxD : H / 2 }));
}
function el(tag, attrs, parent) {
const e = document.createElementNS(NS, tag);
for (const k in attrs) e.setAttribute(k, attrs[k]);
parent.appendChild(e); return e;
}
function diffCounts() {
const c = new Array(Math.max(n, 1)).fill(0);
for (const [a, b] of edges) if (lab[a] >= 0 && lab[b] >= 0) c[Math.abs(lab[a] - lab[b])]++;
return c;
}
function draw() {
const svg = $('tree'); svg.innerHTML = '';
const P = layout(), R = n > 11 ? 10 : 13, cnt = diffCounts();
for (const [a, b] of edges) {
const dd = (lab[a] >= 0 && lab[b] >= 0) ? Math.abs(lab[a] - lab[b]) : 0;
el('line', { x1: P[a].x, y1: P[a].y, x2: P[b].x, y2: P[b].y, class: 'edge' + (dd && cnt[dd] > 1 ? ' dup' : '') }, svg);
}
for (const [a, b] of edges) {
if (lab[a] < 0 || lab[b] < 0) continue;
const dd = Math.abs(lab[a] - lab[b]);
const g = el('g', { class: 'elab' + (cnt[dd] > 1 ? ' dup' : '') }, svg);
const mx = (P[a].x + P[b].x) / 2, my = (P[a].y + P[b].y) / 2;
el('rect', { x: mx - 9, y: my - 7, width: 18, height: 14, rx: 4 }, g);
el('text', { x: mx, y: my }, g).textContent = dd;
}
for (let v = 0; v < n; v++) {
const g = el('g', { class: 'vtx' + (lab[v] >= 0 ? ' lab' : '') + (v === sel ? ' sel' : '') }, svg);
el('circle', { cx: P[v].x, cy: P[v].y, r: R }, g);
el('text', { x: P[v].x, y: P[v].y }, g).textContent = lab[v] >= 0 ? lab[v] : '';
g.addEventListener('click', () => tapVertex(v));
}
const db = $('diffs'); db.innerHTML = '';
for (let d = 1; d < n; d++) {
const s = document.createElement('div');
s.className = 'd' + (cnt[d] === 1 ? ' hit' : cnt[d] > 1 ? ' dup' : '');
s.textContent = d; db.appendChild(s);
}
const cb = $('chips'); cb.innerHTML = '';
for (let l = 0; l < n; l++) {
const b = document.createElement('button'); b.type = 'button';
b.className = 'chip' + (lab.includes(l) ? ' used' : '') + (sel >= 0 && lab[sel] === l ? ' mine' : '');
b.textContent = l; b.disabled = busy;
b.addEventListener('click', () => pick(l)); cb.appendChild(b);
}
status(cnt);
}
function setStatus(t, cls) { const s = $('status'); s.textContent = t; s.className = 'status ' + cls; }
function status(cnt) {
if (msg) { setStatus(msg.t, msg.cls); return; }
const m = n - 1, fixed = edges.filter(([a, b]) => lab[a] >= 0 && lab[b] >= 0).length;
const dup = cnt.findIndex((x, i) => i > 0 && x > 1);
if (dup > 0) setStatus(i18nFmt("Difference %d% appears on more than one edge (red). Every difference may appear only once.", { d: dup }), 'bad');
else if (fixed === m && lab.every((x) => x >= 0)) setStatus(i18nFmt("✓ Graceful! The differences are exactly 1 to %m%, each used once.", { m }), 'ok');
else if (fixed === 0) setStatus("Tap a vertex, then tap a number to label it.", '');
else setStatus(i18nFmt("%k% of %m% edge differences fixed — no repeats so far.", { k: fixed, m }), '');
}
function tapVertex(v) {
if (busy) return;
if (addMode) {
if (n >= MAXN) { msg = { t: i18nFmt("This demo stops at %max% vertices — the search stays instant up to there.", { max: MAXN }), cls: 'bad' }; draw(); return; }
edges.push([v, n]); lab.push(-1); n++; buildAdj(); msg = { t: "Tap any vertex to hang a new leaf on it. Turn the mode off to label.", cls: '' }; draw(); return;
}
sel = v; msg = null; draw();
}
function pick(l) {
if (busy) return;
if (sel < 0) { msg = { t: "Select a vertex first, then tap a number.", cls: '' }; draw(); return; }
msg = null; const o = lab.indexOf(l);
if (o === sel) { lab[sel] = -1; draw(); return; }
if (o >= 0) lab[o] = lab[sel];
lab[sel] = l;
// Jump to the next vertex that still has no label.
sel = lab.indexOf(-1);
draw();
}
function setBusy(b) {
busy = b; document.querySelectorAll('button').forEach((x) => { x.disabled = b; });
}
function runSolve() {
if (busy) return;
const t0 = performance.now(), r = solve(n, edges, 3000000), ms = Math.max(1, Math.round(performance.now() - t0));
if (r.labels && isGraceful(n, edges, r.labels)) { lab = r.labels; sel = -1; msg = { t: i18nFmt("✓ Backtracking found a graceful labelling after exploring %nodes% search nodes (%ms% ms).", { nodes: r.nodes, ms }), cls: 'ok' }; }
else msg = { t: i18nFmt("Gave up after %nodes% search nodes. This is a budget limit, not a counterexample: every tree this small is known to be graceful.", { nodes: r.nodes }), cls: 'bad' };
draw();
}
// Stress test: hundreds of random trees, each solved and checked independently.
function stress() {
if (busy) return;
setBusy(true); addMode = false; leafBtn();
const TOTAL = 300; let done = 0, ok = 0, tot = 0, worst = 0;
function chunk() {
let last = null;
for (let i = 0; i < 12 && done < TOTAL; i++) {
const N = 5 + Math.floor(Math.random() * 10), E = randomTree(N), r = solve(N, E, 200000);
done++; tot += r.nodes; worst = Math.max(worst, r.nodes);
if (isGraceful(N, E, r.labels)) ok++;
last = [N, E, r.labels];
}
setTree(last[0], last[1], last[2] || null);
if (done < TOTAL) {
msg = { t: i18nFmt("Testing random trees… %done% / %total% done, %ok% labelled gracefully.", { done, total: TOTAL, ok }), cls: '' }; draw();
setTimeout(chunk, 40);
} else {
setBusy(false);
msg = ok === TOTAL
? { t: i18nFmt("✓ All %total% random trees (5–14 vertices) received a graceful labelling. Average %avg% search nodes, worst %worst%. No counterexample — as always.", { total: TOTAL, avg: Math.round(tot / TOTAL), worst }), cls: 'ok' }
: { t: i18nFmt("%ok% of %total% trees labelled within the budget. The rest just ran out of search time — they are not counterexamples.", { ok, total: TOTAL }), cls: 'bad' };
draw();
}
}
chunk();
}
function leafBtn() {
const b = $('leaf'); b.textContent = addMode ? "Add leaves: on" : "Add leaves: off";
b.classList.toggle('on', addMode);
}
document.querySelectorAll('[data-preset]').forEach((b) => b.addEventListener('click', () => {
if (busy) return; const [N, E] = PRESETS[b.dataset.preset](); setTree(N, E); msg = null; draw();
}));
$('leaf').addEventListener('click', () => {
if (busy) return; addMode = !addMode; leafBtn(); sel = -1;
msg = addMode ? { t: "Tap any vertex to hang a new leaf on it. Turn the mode off to label.", cls: '' } : null; draw();
});
$('solve').addEventListener('click', runSolve);
$('clear').addEventListener('click', () => { if (busy) return; lab = new Array(n).fill(-1); sel = -1; msg = null; draw(); });
$('stress').addEventListener('click', stress);
const [N0, E0] = PRESETS.cat(); setTree(N0, E0); draw();
By hand, even ten vertices can be surprisingly fiddly: fixing one red edge tends to break another. Press Solve and a backtracking search places the largest differences first and reports how many search nodes it explored — from a handful to a few thousand, in a few milliseconds. Then press Stress test: 300 uniformly random trees with 5 to 14 vertices, each solved and independently checked. Every one comes out graceful. That is exactly the situation mathematicians are in — overwhelming evidence, no proof.
Comments
Loading comments...