<p class="hint">Each row is a <b>clause</b> — its literals are joined by OR, so the
clause is green when <b>at least one</b> literal is true. You can't satisfy every
clause here, so satisfy <b>as many as you can</b>. Toggle the variables and watch
the score climb.</p>
<div id="vars" class="vars"></div>
<div id="clauses" class="clauses"></div>
<div class="score" id="score">Satisfied: 0 / 0</div>
<div class="btns">
<button id="find" type="button">Find optimum</button>
<button id="random" type="button" class="ghost">Random coin flip</button>
<button id="reset" type="button" class="ghost">All false</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; }
.vars { display: flex; gap: .5rem; flex-wrap: wrap; margin: 0 0 .9rem; }
.var { font: 700 15px ui-monospace, monospace; padding: .4rem .8rem; border-radius: 999px;
border: 1px solid #1d3557; cursor: pointer; user-select: none; transition: all .12s; }
.var.t { background: #1d3557; color: #fff; }
.var.f { background: #fff; color: #1d3557; }
.clauses { display: flex; flex-direction: column; gap: 5px; margin: .3rem 0 .8rem; }
.clause { display: flex; align-items: center; gap: .5rem; font: 600 15px ui-monospace, monospace;
padding: .4rem .7rem; border-radius: 8px; border: 1px solid #cdd9e3; background: #f3f6f9; }
.clause.sat { background: #e3f6e9; border-color: #9ed8b4; }
.clause.unsat { background: #fdeaec; border-color: #f0aeb5; }
.lit { padding: .1rem .35rem; border-radius: 5px; }
.lit.on { background: #0a7d33; color: #fff; }
.lit.off { background: #e7e9ec; color: #777; }
.mark { margin-left: auto; font-weight: 800; }
.clause.sat .mark { color: #0a7d33; }
.clause.unsat .mark { color: #c92f3c; }
.score { font-size: 1.1rem; font-weight: 800; margin: .4rem 0 .8rem; }
.score b { color: #1d3557; }
.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] ?? ''));
// MAX-SAT: given a CNF formula, find the assignment that satisfies the MOST
// clauses. Scoring an assignment is a quick scan; finding the maximum in general
// is NP-hard. This formula is deliberately UNSATISFIABLE, so the best you can do
// is fewer than all clauses — that is the whole point of "MAX".
//
// Literals: positive n = "var n is true", negative -n = "var n is false".
const VARS = ['x1','x2','x3'];
const CLAUSES = [
[ 1, 2, 3], // x1 OR x2 OR x3
[-1, -2, -3], // NOT x1 OR NOT x2 OR NOT x3
[ 1, -2, 3], // x1 OR NOT x2 OR x3
[-1, 2, -3], // NOT x1 OR x2 OR NOT x3
[ 1, 2, -3], // x1 OR x2 OR NOT x3
[-1, -2, 3], // NOT x1 OR NOT x2 OR x3
[ 1, -2, -3], // x1 OR NOT x2 OR NOT x3
[-1, 2, 3] // NOT x1 OR x2 OR x3
];
// assign[i] = boolean value of variable i (0-indexed: x1=index0)
let assign = VARS.map(() => false);
const litTrue = (lit, a) => lit > 0 ? a[lit - 1] : !a[-lit - 1];
const clauseSat = (cl, a) => cl.some(l => litTrue(l, a));
const score = a => CLAUSES.filter(cl => clauseSat(cl, a)).length;
const litText = l => (l < 0 ? '¬' : '') + VARS[Math.abs(l) - 1];
function renderVars() {
const v = document.getElementById('vars'); v.innerHTML = '';
VARS.forEach((name, i) => {
const d = document.createElement('div');
d.className = 'var ' + (assign[i] ? 't' : 'f');
d.textContent = name + ' = ' + (assign[i] ? 'true' : 'false');
d.addEventListener('click', () => { assign[i] = !assign[i]; render(); });
v.appendChild(d);
});
}
function renderClauses() {
const wrap = document.getElementById('clauses'); wrap.innerHTML = '';
CLAUSES.forEach(cl => {
const sat = clauseSat(cl, assign);
const row = document.createElement('div');
row.className = 'clause ' + (sat ? 'sat' : 'unsat');
const inner = document.createElement('div');
inner.style.display = 'flex'; inner.style.gap = '.3rem'; inner.style.alignItems = 'center';
cl.forEach((l, idx) => {
const s = document.createElement('span');
s.className = 'lit ' + (litTrue(l, assign) ? 'on' : 'off');
s.textContent = litText(l);
inner.appendChild(s);
if (idx < cl.length - 1) { const o = document.createElement('span'); o.textContent = '∨'; o.style.color = '#999'; inner.appendChild(o); }
});
row.appendChild(inner);
const m = document.createElement('span');
m.className = 'mark'; m.textContent = sat ? '✓' : '✗';
row.appendChild(m);
wrap.appendChild(row);
});
}
function renderScore() {
const s = score(assign);
document.getElementById('score').innerHTML =
fmt('Satisfied: <b>%s% / %length%</b>', { s: s, length: CLAUSES.length });
}
function render() { renderVars(); renderClauses(); renderScore(); }
function findOptimum() {
let best = null, bestScore = -1, tried = 0;
for (let mask = 0; mask < (1 << VARS.length); mask++) {
tried++;
const a = VARS.map((_, i) => !!(mask & (1 << i)));
const sc = score(a);
if (sc > bestScore) { bestScore = sc; best = a; }
}
assign = best; render();
console.log(fmt('Best satisfies %bestScore%/%length% clauses, found after trying all %tried% assignments. (This formula is unsatisfiable, so the max is %bestScore%, not %length%.)', { bestScore: bestScore, length: CLAUSES.length, tried: tried }));
}
function randomFlip() {
assign = VARS.map(() => Math.random() < 0.5);
render();
}
document.getElementById('find').addEventListener('click', findOptimum);
document.getElementById('random').addEventListener('click', randomFlip);
document.getElementById('reset').addEventListener('click', () => { assign = VARS.map(() => false); render(); });
render();
<p class="hint">Each row is a <b>clause</b> — its literals are joined by OR, so the
clause is green when <b>at least one</b> literal is true. You can't satisfy every
clause here, so satisfy <b>as many as you can</b>. Toggle the variables and watch
the score climb.</p>
<div id="vars" class="vars"></div>
<div id="clauses" class="clauses"></div>
<div class="score" id="score">Satisfied: 0 / 0</div>
<div class="btns">
<button id="find" type="button">Find optimum</button>
<button id="random" type="button" class="ghost">Random coin flip</button>
<button id="reset" type="button" class="ghost">All false</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; }
.vars { display: flex; gap: .5rem; flex-wrap: wrap; margin: 0 0 .9rem; }
.var { font: 700 15px ui-monospace, monospace; padding: .4rem .8rem; border-radius: 999px;
border: 1px solid #1d3557; cursor: pointer; user-select: none; transition: all .12s; }
.var.t { background: #1d3557; color: #fff; }
.var.f { background: #fff; color: #1d3557; }
.clauses { display: flex; flex-direction: column; gap: 5px; margin: .3rem 0 .8rem; }
.clause { display: flex; align-items: center; gap: .5rem; font: 600 15px ui-monospace, monospace;
padding: .4rem .7rem; border-radius: 8px; border: 1px solid #cdd9e3; background: #f3f6f9; }
.clause.sat { background: #e3f6e9; border-color: #9ed8b4; }
.clause.unsat { background: #fdeaec; border-color: #f0aeb5; }
.lit { padding: .1rem .35rem; border-radius: 5px; }
.lit.on { background: #0a7d33; color: #fff; }
.lit.off { background: #e7e9ec; color: #777; }
.mark { margin-left: auto; font-weight: 800; }
.clause.sat .mark { color: #0a7d33; }
.clause.unsat .mark { color: #c92f3c; }
.score { font-size: 1.1rem; font-weight: 800; margin: .4rem 0 .8rem; }
.score b { color: #1d3557; }
.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] ?? ''));
// MAX-SAT: given a CNF formula, find the assignment that satisfies the MOST
// clauses. Scoring an assignment is a quick scan; finding the maximum in general
// is NP-hard. This formula is deliberately UNSATISFIABLE, so the best you can do
// is fewer than all clauses — that is the whole point of "MAX".
//
// Literals: positive n = "var n is true", negative -n = "var n is false".
const VARS = ['x1','x2','x3'];
const CLAUSES = [
[ 1, 2, 3], // x1 OR x2 OR x3
[-1, -2, -3], // NOT x1 OR NOT x2 OR NOT x3
[ 1, -2, 3], // x1 OR NOT x2 OR x3
[-1, 2, -3], // NOT x1 OR x2 OR NOT x3
[ 1, 2, -3], // x1 OR x2 OR NOT x3
[-1, -2, 3], // NOT x1 OR NOT x2 OR x3
[ 1, -2, -3], // x1 OR NOT x2 OR NOT x3
[-1, 2, 3] // NOT x1 OR x2 OR x3
];
// assign[i] = boolean value of variable i (0-indexed: x1=index0)
let assign = VARS.map(() => false);
const litTrue = (lit, a) => lit > 0 ? a[lit - 1] : !a[-lit - 1];
const clauseSat = (cl, a) => cl.some(l => litTrue(l, a));
const score = a => CLAUSES.filter(cl => clauseSat(cl, a)).length;
const litText = l => (l < 0 ? '¬' : '') + VARS[Math.abs(l) - 1];
function renderVars() {
const v = document.getElementById('vars'); v.innerHTML = '';
VARS.forEach((name, i) => {
const d = document.createElement('div');
d.className = 'var ' + (assign[i] ? 't' : 'f');
d.textContent = name + ' = ' + (assign[i] ? 'true' : 'false');
d.addEventListener('click', () => { assign[i] = !assign[i]; render(); });
v.appendChild(d);
});
}
function renderClauses() {
const wrap = document.getElementById('clauses'); wrap.innerHTML = '';
CLAUSES.forEach(cl => {
const sat = clauseSat(cl, assign);
const row = document.createElement('div');
row.className = 'clause ' + (sat ? 'sat' : 'unsat');
const inner = document.createElement('div');
inner.style.display = 'flex'; inner.style.gap = '.3rem'; inner.style.alignItems = 'center';
cl.forEach((l, idx) => {
const s = document.createElement('span');
s.className = 'lit ' + (litTrue(l, assign) ? 'on' : 'off');
s.textContent = litText(l);
inner.appendChild(s);
if (idx < cl.length - 1) { const o = document.createElement('span'); o.textContent = '∨'; o.style.color = '#999'; inner.appendChild(o); }
});
row.appendChild(inner);
const m = document.createElement('span');
m.className = 'mark'; m.textContent = sat ? '✓' : '✗';
row.appendChild(m);
wrap.appendChild(row);
});
}
function renderScore() {
const s = score(assign);
document.getElementById('score').innerHTML =
fmt('Satisfied: <b>%s% / %length%</b>', { s: s, length: CLAUSES.length });
}
function render() { renderVars(); renderClauses(); renderScore(); }
function findOptimum() {
let best = null, bestScore = -1, tried = 0;
for (let mask = 0; mask < (1 << VARS.length); mask++) {
tried++;
const a = VARS.map((_, i) => !!(mask & (1 << i)));
const sc = score(a);
if (sc > bestScore) { bestScore = sc; best = a; }
}
assign = best; render();
console.log(fmt('Best satisfies %bestScore%/%length% clauses, found after trying all %tried% assignments. (This formula is unsatisfiable, so the max is %bestScore%, not %length%.)', { bestScore: bestScore, length: CLAUSES.length, tried: tried }));
}
function randomFlip() {
assign = VARS.map(() => Math.random() < 0.5);
render();
}
document.getElementById('find').addEventListener('click', findOptimum);
document.getElementById('random').addEventListener('click', randomFlip);
document.getElementById('reset').addEventListener('click', () => { assign = VARS.map(() => false); render(); });
render();
Toggle each variable between true and false and watch the satisfied-clause counter move. Notice the asymmetry: scoring any assignment is instant — you just walk the clauses and count. Finding the assignment with the highest score is the hard part. Press Find optimum to let the computer try all 2n assignments by brute force; with a handful of variables that's fine, but the count doubles with every variable you add.
Comments
Loading comments...