The demo below simulates solving a graph 3-colorability problem on n vertices. The brute-force approach tries all 3n colorings (equivalent to 2n after pruning symmetry, shown as the red bar). The optimized algorithm uses branch-and-reduce: it picks an uncolored vertex with the highest degree and tries its legal color options, pruning branches early โ reducing the effective base to roughly 1.33n.
Drag the slider to change n and watch the step counts diverge.
<p class="hint">Drag the slider to change <em>n</em> (problem size). Both algorithms find an exact 3-coloring of a random graph on <em>n</em> vertices. Watch the <b>step counts</b> diverge โ same answer, very different work.</p>
<div class="controls">
<label>n = <span id="nval">14</span> vertices
<input type="range" id="slider" min="5" max="30" value="14">
</label>
<button id="run" type="button">Run comparison</button>
</div>
<div class="bars" id="bars">
<div class="bar-row">
<span class="label">Brute force (โ 2โฟ)</span>
<div class="track"><div class="fill red" id="bar-brute"></div></div>
<span class="count" id="cnt-brute">โ</span>
</div>
<div class="bar-row">
<span class="label">Optimized (โ 1.33โฟ)</span>
<div class="track"><div class="fill blue" id="bar-opt"></div></div>
<span class="count" id="cnt-opt">โ</span>
</div>
</div>
<div class="result" id="result"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.controls { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem; }
label { font-size: .95rem; display: flex; align-items: center; gap: .5rem; }
input[type=range] { width: 160px; cursor: pointer; }
button { font: 600 14px system-ui; padding: .45rem 1rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button:hover { background: #16294a; }
.bars { display: flex; flex-direction: column; gap: .7rem; margin-bottom: .9rem; }
.bar-row { display: grid; grid-template-columns: 140px 1fr 90px; align-items: center; gap: .5rem; }
.label { font-size: .82rem; color: #333; white-space: nowrap; }
.track { height: 22px; background: #e8eef3; border-radius: 6px; overflow: hidden; position: relative; }
.fill { height: 100%; border-radius: 6px; width: 0; transition: width .5s ease; }
.fill.red { background: #e63946; }
.fill.blue { background: #1d7ebc; }
.count { font: 700 .8rem ui-monospace, monospace; color: #333; text-align: right; white-space: nowrap; }
.result { font-size: .9rem; min-height: 1.4em; color: #0a7d33; font-weight: 600; }
.result.bad { color: #c92f3c; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// โโ Graph 3-colorability: exact comparison demo โโโโโโโโโโโโโโโโโโโโโโโโโโ
// We build a random Erdos-Renyi graph G(n, p=0.45) then run two algorithms:
// Brute force: enumerate all 3^n assignments, check each in O(n+m).
// Optimized: branch-and-reduce โ pick the highest-degree uncolored vertex,
// try its legal colors, propagate forced assignments.
// Both return the step count (not real wall time) to keep the demo responsive.
// We cap brute force at 2e7 steps to avoid freezing the tab.
const BRUTE_CAP = 2e7;
let rng = 42;
function rand() { rng = (rng * 1664525 + 1013904223) >>> 0; return rng / 0xFFFFFFFF; }
function seedRng(s) { rng = s >>> 0; }
function buildGraph(n, seed) {
seedRng(seed);
const adj = Array.from({length: n}, () => []);
for (let u = 0; u < n; u++)
for (let v = u + 1; v < n; v++)
if (rand() < 0.45) { adj[u].push(v); adj[v].push(u); }
return adj;
}
// โโ Brute force: try all 3^n colorings โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function bruteForce(adj) {
const n = adj.length;
const col = new Int8Array(n);
let steps = 0;
function check() {
for (let u = 0; u < n; u++)
for (const v of adj[u]) if (col[u] === col[v]) return false;
return true;
}
function rec(i) {
if (steps >= BRUTE_CAP) return null;
if (i === n) { steps++; return check() ? col.slice() : null; }
for (let c = 0; c < 3; c++) {
col[i] = c; steps++;
const r = rec(i + 1);
if (r) return r;
}
return null;
}
const sol = rec(0);
return { steps, sol, capped: steps >= BRUTE_CAP };
}
// โโ Optimized: branch-and-reduce โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Pick the highest-degree uncolored vertex; try each legal color;
// immediately propagate: if a neighbor has all-but-one color forbidden, force it.
function optimized(adj) {
const n = adj.length;
const col = new Int8Array(n).fill(-1); // -1 = uncolored
let steps = 0;
// available[u] = bitmask of colors still legal for u (bits 0,1,2)
const avail = new Int32Array(n).fill(0b111);
function assign(u, c) {
// returns false if contradiction
col[u] = c;
avail[u] = 1 << c;
for (const v of adj[u]) {
if (col[v] === c) return false; // conflict
avail[v] &= ~(1 << c);
if (avail[v] === 0) return false; // no legal color left
}
return true;
}
function unassign(u, savedAvail) {
col[u] = -1;
for (let v = 0; v < n; v++) avail[v] = savedAvail[v];
}
function rec() {
steps++;
// pick first uncolored vertex with highest degree
let best = -1, bestDeg = -1;
for (let u = 0; u < n; u++) {
if (col[u] === -1 && adj[u].length > bestDeg) { best = u; bestDeg = adj[u].length; }
}
if (best === -1) return true; // all colored
const savedAvail = avail.slice();
const legalColors = [];
for (let c = 0; c < 3; c++) if (avail[best] & (1 << c)) legalColors.push(c);
for (const c of legalColors) {
const ok = assign(best, c);
steps++;
if (ok) {
if (rec()) return true;
}
unassign(best, savedAvail);
}
return false;
}
const found = rec();
return { steps, sol: found ? col.slice() : null };
}
// โโ UI โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const slider = document.getElementById('slider');
const nval = document.getElementById('nval');
const barBrute = document.getElementById('bar-brute');
const barOpt = document.getElementById('bar-opt');
const cntBrute = document.getElementById('cnt-brute');
const cntOpt = document.getElementById('cnt-opt');
const result = document.getElementById('result');
slider.addEventListener('input', () => { nval.textContent = slider.value; });
function fmt(n) {
if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'k';
return String(n);
}
document.getElementById('run').addEventListener('click', () => {
const n = parseInt(slider.value, 10);
const adj = buildGraph(n, n * 7 + 13);
const b = bruteForce(adj);
const o = optimized(adj);
const maxSteps = Math.max(b.steps, o.steps, 1);
barBrute.style.width = Math.min(100, (b.steps / maxSteps) * 100) + '%';
barOpt.style.width = Math.min(100, (o.steps / maxSteps) * 100) + '%';
cntBrute.textContent = fmt(b.steps) + (b.capped ? '+ (capped)' : ' steps');
cntOpt.textContent = i18nFmt('%fmt% steps', { fmt: fmt(o.steps) });
const ratio = b.capped ? '>10ร' : (b.steps / Math.max(o.steps, 1)).toFixed(1) + 'ร';
const colorable = o.sol ? 'Graph IS 3-colorable' : 'Graph is NOT 3-colorable';
result.textContent = i18nFmt('%colorable% โ optimized used %ratio% fewer steps.', { colorable: colorable, ratio: ratio });
result.className = 'result';
console.log('n=' + n, '| | brute steps:', b.steps, '| | optimized steps:', o.steps,
'| | ratio:', (b.steps / Math.max(o.steps, 1)).toFixed(2),
'| | 3-colorable:', !!o.sol);
});
// Run once on load
document.getElementById('run').click();
<p class="hint">Drag the slider to change <em>n</em> (problem size). Both algorithms find an exact 3-coloring of a random graph on <em>n</em> vertices. Watch the <b>step counts</b> diverge โ same answer, very different work.</p>
<div class="controls">
<label>n = <span id="nval">14</span> vertices
<input type="range" id="slider" min="5" max="30" value="14">
</label>
<button id="run" type="button">Run comparison</button>
</div>
<div class="bars" id="bars">
<div class="bar-row">
<span class="label">Brute force (โ 2โฟ)</span>
<div class="track"><div class="fill red" id="bar-brute"></div></div>
<span class="count" id="cnt-brute">โ</span>
</div>
<div class="bar-row">
<span class="label">Optimized (โ 1.33โฟ)</span>
<div class="track"><div class="fill blue" id="bar-opt"></div></div>
<span class="count" id="cnt-opt">โ</span>
</div>
</div>
<div class="result" id="result"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.controls { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem; }
label { font-size: .95rem; display: flex; align-items: center; gap: .5rem; }
input[type=range] { width: 160px; cursor: pointer; }
button { font: 600 14px system-ui; padding: .45rem 1rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button:hover { background: #16294a; }
.bars { display: flex; flex-direction: column; gap: .7rem; margin-bottom: .9rem; }
.bar-row { display: grid; grid-template-columns: 140px 1fr 90px; align-items: center; gap: .5rem; }
.label { font-size: .82rem; color: #333; white-space: nowrap; }
.track { height: 22px; background: #e8eef3; border-radius: 6px; overflow: hidden; position: relative; }
.fill { height: 100%; border-radius: 6px; width: 0; transition: width .5s ease; }
.fill.red { background: #e63946; }
.fill.blue { background: #1d7ebc; }
.count { font: 700 .8rem ui-monospace, monospace; color: #333; text-align: right; white-space: nowrap; }
.result { font-size: .9rem; min-height: 1.4em; color: #0a7d33; font-weight: 600; }
.result.bad { color: #c92f3c; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// โโ Graph 3-colorability: exact comparison demo โโโโโโโโโโโโโโโโโโโโโโโโโโ
// We build a random Erdos-Renyi graph G(n, p=0.45) then run two algorithms:
// Brute force: enumerate all 3^n assignments, check each in O(n+m).
// Optimized: branch-and-reduce โ pick the highest-degree uncolored vertex,
// try its legal colors, propagate forced assignments.
// Both return the step count (not real wall time) to keep the demo responsive.
// We cap brute force at 2e7 steps to avoid freezing the tab.
const BRUTE_CAP = 2e7;
let rng = 42;
function rand() { rng = (rng * 1664525 + 1013904223) >>> 0; return rng / 0xFFFFFFFF; }
function seedRng(s) { rng = s >>> 0; }
function buildGraph(n, seed) {
seedRng(seed);
const adj = Array.from({length: n}, () => []);
for (let u = 0; u < n; u++)
for (let v = u + 1; v < n; v++)
if (rand() < 0.45) { adj[u].push(v); adj[v].push(u); }
return adj;
}
// โโ Brute force: try all 3^n colorings โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function bruteForce(adj) {
const n = adj.length;
const col = new Int8Array(n);
let steps = 0;
function check() {
for (let u = 0; u < n; u++)
for (const v of adj[u]) if (col[u] === col[v]) return false;
return true;
}
function rec(i) {
if (steps >= BRUTE_CAP) return null;
if (i === n) { steps++; return check() ? col.slice() : null; }
for (let c = 0; c < 3; c++) {
col[i] = c; steps++;
const r = rec(i + 1);
if (r) return r;
}
return null;
}
const sol = rec(0);
return { steps, sol, capped: steps >= BRUTE_CAP };
}
// โโ Optimized: branch-and-reduce โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Pick the highest-degree uncolored vertex; try each legal color;
// immediately propagate: if a neighbor has all-but-one color forbidden, force it.
function optimized(adj) {
const n = adj.length;
const col = new Int8Array(n).fill(-1); // -1 = uncolored
let steps = 0;
// available[u] = bitmask of colors still legal for u (bits 0,1,2)
const avail = new Int32Array(n).fill(0b111);
function assign(u, c) {
// returns false if contradiction
col[u] = c;
avail[u] = 1 << c;
for (const v of adj[u]) {
if (col[v] === c) return false; // conflict
avail[v] &= ~(1 << c);
if (avail[v] === 0) return false; // no legal color left
}
return true;
}
function unassign(u, savedAvail) {
col[u] = -1;
for (let v = 0; v < n; v++) avail[v] = savedAvail[v];
}
function rec() {
steps++;
// pick first uncolored vertex with highest degree
let best = -1, bestDeg = -1;
for (let u = 0; u < n; u++) {
if (col[u] === -1 && adj[u].length > bestDeg) { best = u; bestDeg = adj[u].length; }
}
if (best === -1) return true; // all colored
const savedAvail = avail.slice();
const legalColors = [];
for (let c = 0; c < 3; c++) if (avail[best] & (1 << c)) legalColors.push(c);
for (const c of legalColors) {
const ok = assign(best, c);
steps++;
if (ok) {
if (rec()) return true;
}
unassign(best, savedAvail);
}
return false;
}
const found = rec();
return { steps, sol: found ? col.slice() : null };
}
// โโ UI โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const slider = document.getElementById('slider');
const nval = document.getElementById('nval');
const barBrute = document.getElementById('bar-brute');
const barOpt = document.getElementById('bar-opt');
const cntBrute = document.getElementById('cnt-brute');
const cntOpt = document.getElementById('cnt-opt');
const result = document.getElementById('result');
slider.addEventListener('input', () => { nval.textContent = slider.value; });
function fmt(n) {
if (n >= 1e9) return (n / 1e9).toFixed(1) + 'B';
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'k';
return String(n);
}
document.getElementById('run').addEventListener('click', () => {
const n = parseInt(slider.value, 10);
const adj = buildGraph(n, n * 7 + 13);
const b = bruteForce(adj);
const o = optimized(adj);
const maxSteps = Math.max(b.steps, o.steps, 1);
barBrute.style.width = Math.min(100, (b.steps / maxSteps) * 100) + '%';
barOpt.style.width = Math.min(100, (o.steps / maxSteps) * 100) + '%';
cntBrute.textContent = fmt(b.steps) + (b.capped ? '+ (capped)' : ' steps');
cntOpt.textContent = i18nFmt('%fmt% steps', { fmt: fmt(o.steps) });
const ratio = b.capped ? '>10ร' : (b.steps / Math.max(o.steps, 1)).toFixed(1) + 'ร';
const colorable = o.sol ? 'Graph IS 3-colorable' : 'Graph is NOT 3-colorable';
result.textContent = i18nFmt('%colorable% โ optimized used %ratio% fewer steps.', { colorable: colorable, ratio: ratio });
result.className = 'result';
console.log('n=' + n, '| | brute steps:', b.steps, '| | optimized steps:', o.steps,
'| | ratio:', (b.steps / Math.max(o.steps, 1)).toFixed(2),
'| | 3-colorable:', !!o.sol);
});
// Run once on load
document.getElementById('run').click();
At small n the difference is modest. By n = 30 the brute-force count is in the billions while the optimized count stays in the thousands. This is the entire point of the field: the answer is the same (exact), but the work is orders of magnitude less.
Comments
Loading comments...