<p class="hint">Color the four regions so that <b>no two touching regions share a color</b>.
Variables = regions, domain = {Red, Green, Blue}, constraints = the shared borders.
Run <b>Propagate</b> (arc consistency) to prune impossible colors, then <b>Solve</b>
(backtracking) to finish — and see how many search steps propagation saved.</p>
<div class="map" id="map"></div>
<div class="domains" id="domains"></div>
<div class="status" id="status">Pick a region color, or just press Propagate / Solve.</div>
<div class="btns">
<button id="propagate" type="button">Propagate (AC-3)</button>
<button id="solve" type="button">Solve (backtracking)</button>
<button id="reset" type="button" class="ghost">Reset</button>
</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 .8rem; line-height: 1.45; }
.map { display: grid; grid-template-columns: repeat(2, 120px); grid-template-rows: repeat(2, 70px);
gap: 6px; margin: .4rem 0 .8rem; }
.region { display: flex; flex-direction: column; align-items: center; justify-content: center;
border: 2px solid #1d3557; border-radius: 10px; font-weight: 700; cursor: pointer;
background: #eef2f6; color: #1d3557; user-select: none; transition: background .15s; }
.region small { font-weight: 600; font-size: .72rem; opacity: .8; }
.region.R { background: #e63946; color: #fff; border-color: #c92f3c; }
.region.G { background: #2a9d4a; color: #fff; border-color: #218038; }
.region.B { background: #3a6ea5; color: #fff; border-color: #2c5580; }
.domains { display: flex; flex-wrap: wrap; gap: .5rem; margin: 0 0 .6rem; font-size: .85rem; }
.domains .dom { background: #f1f4f7; border: 1px solid #d3dce4; border-radius: 6px; padding: .2rem .5rem; }
.domains .dom b { color: #1d3557; }
.swatch { display: inline-block; width: .7em; height: .7em; border-radius: 2px; margin: 0 1px -1px; }
.sw-R { background: #e63946; } .sw-G { background: #2a9d4a; } .sw-B { background: #3a6ea5; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
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; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A constraint satisfaction problem (CSP): variables = 4 map regions, domain =
// {R,G,B}, constraints = "adjacent regions must differ". Deciding whether a CSP
// has any solution is NP-complete (via SAT; Cook-Levin 1971). Here we (1) run
// arc consistency / AC-3 (Mackworth 1977) to prune values, then (2) backtracking
// search to find a full assignment, counting steps so propagation's effect shows.
const COLORS = ['R', 'G', 'B'];
const NAMES = { R: 'Red', G: 'Green', B: 'Blue' };
const VARS = ['A', 'B', 'C', 'D'];
// Adjacency: A-B, A-C, B-C, B-D, C-D (A and D do NOT touch).
const EDGES = [['A','B'],['A','C'],['B','C'],['B','D'],['C','D']];
const neighbors = v => EDGES.filter(e => e.includes(v)).map(e => e[0] === v ? e[1] : e[0]);
let domains, assigned;
function init() {
domains = {}; assigned = {};
VARS.forEach(v => { domains[v] = COLORS.slice(); assigned[v] = null; });
}
init();
// AC-3: for each arc (X,Y), drop any value of X that no value of Y can satisfy.
// Constraint here is "different color", so X-value x is unsupported only when Y's
// domain is exactly {x}. Returns false if some domain becomes empty (no solution).
function arcConsistency() {
const queue = [];
EDGES.forEach(([x, y]) => { queue.push([x, y]); queue.push([y, x]); });
let removed = 0;
while (queue.length) {
const [x, y] = queue.shift();
let changed = false;
domains[x] = domains[x].filter(vx => {
const supported = domains[y].some(vy => vy !== vx); // exists a different color
if (!supported) { changed = true; removed++; }
return supported;
});
if (domains[x].length === 0) return { ok: false, removed };
if (changed) neighbors(x).forEach(n => { if (n !== y) queue.push([n, x]); });
}
return { ok: true, removed };
}
// Backtracking search over the (possibly pruned) domains. Counts assignments tried.
function backtrack() {
let steps = 0;
const result = {};
function rec(i) {
if (i === VARS.length) return true;
const v = VARS[i];
for (const c of domains[v]) {
steps++;
const clash = neighbors(v).some(n => result[n] === c);
if (clash) continue;
result[v] = c;
if (rec(i + 1)) return true;
delete result[v];
}
return false;
}
const ok = rec(0);
return { ok, steps, result };
}
function setStatus(t, cls) { const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + (cls || ''); }
function render() {
const m = document.getElementById('map'); m.innerHTML = '';
VARS.forEach(v => {
const d = document.createElement('div');
d.className = 'region' + (assigned[v] ? ' ' + assigned[v] : '');
d.innerHTML = '<span>' + v + '</span><small>' + (assigned[v] ? NAMES[assigned[v]] : 'click to set') + '</small>';
d.addEventListener('click', () => {
const cur = assigned[v];
const next = cur === null ? 'R' : cur === 'R' ? 'G' : cur === 'G' ? 'B' : null;
assigned[v] = next;
domains[v] = next ? [next] : COLORS.slice();
render();
setStatus(fmt('Region %v%%v2%. Press Propagate or Solve.', { v: v, v2: next ? ' set to ' + NAMES[next] : ' cleared' }), '');
});
m.appendChild(d);
});
const dd = document.getElementById('domains'); dd.innerHTML = '';
VARS.forEach(v => {
const sw = domains[v].map(c => '<span class="swatch sw-' + c + '"></span>').join('');
const el = document.createElement('div'); el.className = 'dom';
el.innerHTML = '<b>' + v + '</b>: ' + sw + ' ' + domains[v].length;
dd.appendChild(el);
});
}
document.getElementById('propagate').addEventListener('click', () => {
const r = arcConsistency();
render();
if (!r.ok) setStatus('A domain went empty — these choices have no solution.', 'bad');
else if (r.removed === 0) setStatus('Arc consistency removed nothing more; search still needed.', '');
else setStatus(fmt('Arc consistency pruned %removed% value(s). Domains are smaller — now Solve.', { removed: r.removed }), 'ok');
});
document.getElementById('solve').addEventListener('click', () => {
const r = backtrack();
if (!r.ok) { setStatus('No coloring satisfies the constraints.', 'bad'); return; }
VARS.forEach(v => { assigned[v] = r.result[v]; });
render();
setStatus(fmt('Solved in %steps% search step(s): %map%.', { steps: r.steps, map: VARS.map(v => v + '=' + r.result[v]).join(', ') }), 'ok');
console.log('Solution', r.result, '| backtracking steps', r.steps);
});
document.getElementById('reset').addEventListener('click', () => { init(); render(); setStatus('Reset. Pick a region color, or press Propagate / Solve.', ''); });
render();
<p class="hint">Color the four regions so that <b>no two touching regions share a color</b>.
Variables = regions, domain = {Red, Green, Blue}, constraints = the shared borders.
Run <b>Propagate</b> (arc consistency) to prune impossible colors, then <b>Solve</b>
(backtracking) to finish — and see how many search steps propagation saved.</p>
<div class="map" id="map"></div>
<div class="domains" id="domains"></div>
<div class="status" id="status">Pick a region color, or just press Propagate / Solve.</div>
<div class="btns">
<button id="propagate" type="button">Propagate (AC-3)</button>
<button id="solve" type="button">Solve (backtracking)</button>
<button id="reset" type="button" class="ghost">Reset</button>
</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 .8rem; line-height: 1.45; }
.map { display: grid; grid-template-columns: repeat(2, 120px); grid-template-rows: repeat(2, 70px);
gap: 6px; margin: .4rem 0 .8rem; }
.region { display: flex; flex-direction: column; align-items: center; justify-content: center;
border: 2px solid #1d3557; border-radius: 10px; font-weight: 700; cursor: pointer;
background: #eef2f6; color: #1d3557; user-select: none; transition: background .15s; }
.region small { font-weight: 600; font-size: .72rem; opacity: .8; }
.region.R { background: #e63946; color: #fff; border-color: #c92f3c; }
.region.G { background: #2a9d4a; color: #fff; border-color: #218038; }
.region.B { background: #3a6ea5; color: #fff; border-color: #2c5580; }
.domains { display: flex; flex-wrap: wrap; gap: .5rem; margin: 0 0 .6rem; font-size: .85rem; }
.domains .dom { background: #f1f4f7; border: 1px solid #d3dce4; border-radius: 6px; padding: .2rem .5rem; }
.domains .dom b { color: #1d3557; }
.swatch { display: inline-block; width: .7em; height: .7em; border-radius: 2px; margin: 0 1px -1px; }
.sw-R { background: #e63946; } .sw-G { background: #2a9d4a; } .sw-B { background: #3a6ea5; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
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; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A constraint satisfaction problem (CSP): variables = 4 map regions, domain =
// {R,G,B}, constraints = "adjacent regions must differ". Deciding whether a CSP
// has any solution is NP-complete (via SAT; Cook-Levin 1971). Here we (1) run
// arc consistency / AC-3 (Mackworth 1977) to prune values, then (2) backtracking
// search to find a full assignment, counting steps so propagation's effect shows.
const COLORS = ['R', 'G', 'B'];
const NAMES = { R: 'Red', G: 'Green', B: 'Blue' };
const VARS = ['A', 'B', 'C', 'D'];
// Adjacency: A-B, A-C, B-C, B-D, C-D (A and D do NOT touch).
const EDGES = [['A','B'],['A','C'],['B','C'],['B','D'],['C','D']];
const neighbors = v => EDGES.filter(e => e.includes(v)).map(e => e[0] === v ? e[1] : e[0]);
let domains, assigned;
function init() {
domains = {}; assigned = {};
VARS.forEach(v => { domains[v] = COLORS.slice(); assigned[v] = null; });
}
init();
// AC-3: for each arc (X,Y), drop any value of X that no value of Y can satisfy.
// Constraint here is "different color", so X-value x is unsupported only when Y's
// domain is exactly {x}. Returns false if some domain becomes empty (no solution).
function arcConsistency() {
const queue = [];
EDGES.forEach(([x, y]) => { queue.push([x, y]); queue.push([y, x]); });
let removed = 0;
while (queue.length) {
const [x, y] = queue.shift();
let changed = false;
domains[x] = domains[x].filter(vx => {
const supported = domains[y].some(vy => vy !== vx); // exists a different color
if (!supported) { changed = true; removed++; }
return supported;
});
if (domains[x].length === 0) return { ok: false, removed };
if (changed) neighbors(x).forEach(n => { if (n !== y) queue.push([n, x]); });
}
return { ok: true, removed };
}
// Backtracking search over the (possibly pruned) domains. Counts assignments tried.
function backtrack() {
let steps = 0;
const result = {};
function rec(i) {
if (i === VARS.length) return true;
const v = VARS[i];
for (const c of domains[v]) {
steps++;
const clash = neighbors(v).some(n => result[n] === c);
if (clash) continue;
result[v] = c;
if (rec(i + 1)) return true;
delete result[v];
}
return false;
}
const ok = rec(0);
return { ok, steps, result };
}
function setStatus(t, cls) { const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + (cls || ''); }
function render() {
const m = document.getElementById('map'); m.innerHTML = '';
VARS.forEach(v => {
const d = document.createElement('div');
d.className = 'region' + (assigned[v] ? ' ' + assigned[v] : '');
d.innerHTML = '<span>' + v + '</span><small>' + (assigned[v] ? NAMES[assigned[v]] : 'click to set') + '</small>';
d.addEventListener('click', () => {
const cur = assigned[v];
const next = cur === null ? 'R' : cur === 'R' ? 'G' : cur === 'G' ? 'B' : null;
assigned[v] = next;
domains[v] = next ? [next] : COLORS.slice();
render();
setStatus(fmt('Region %v%%v2%. Press Propagate or Solve.', { v: v, v2: next ? ' set to ' + NAMES[next] : ' cleared' }), '');
});
m.appendChild(d);
});
const dd = document.getElementById('domains'); dd.innerHTML = '';
VARS.forEach(v => {
const sw = domains[v].map(c => '<span class="swatch sw-' + c + '"></span>').join('');
const el = document.createElement('div'); el.className = 'dom';
el.innerHTML = '<b>' + v + '</b>: ' + sw + ' ' + domains[v].length;
dd.appendChild(el);
});
}
document.getElementById('propagate').addEventListener('click', () => {
const r = arcConsistency();
render();
if (!r.ok) setStatus('A domain went empty — these choices have no solution.', 'bad');
else if (r.removed === 0) setStatus('Arc consistency removed nothing more; search still needed.', '');
else setStatus(fmt('Arc consistency pruned %removed% value(s). Domains are smaller — now Solve.', { removed: r.removed }), 'ok');
});
document.getElementById('solve').addEventListener('click', () => {
const r = backtrack();
if (!r.ok) { setStatus('No coloring satisfies the constraints.', 'bad'); return; }
VARS.forEach(v => { assigned[v] = r.result[v]; });
render();
setStatus(fmt('Solved in %steps% search step(s): %map%.', { steps: r.steps, map: VARS.map(v => v + '=' + r.result[v]).join(', ') }), 'ok');
console.log('Solution', r.result, '| backtracking steps', r.steps);
});
document.getElementById('reset').addEventListener('click', () => { init(); render(); setStatus('Reset. Pick a region color, or press Propagate / Solve.', ''); });
render();
Comments
Loading comments...