Below are two satisfiability puzzles. Each clause is a box that must end up true, and a clause is true when at least one of its literals is true (a bar over a name means "not invited"). Toggle the variables and watch the clauses light up.
<p class="hint">Two satisfiability puzzles. Each clause must be <b>true</b>; a clause is true when at least one literal is true. A bar like <span class="bar">x</span> means "NOT x". Click the variable chips to flip true/false, or let each solver run.</p>
<div class="panel">
<h4>2-SAT · 2 literals per clause · <span class="tag p">in P (linear)</span></h4>
<div id="vars2" class="vars"></div>
<div id="cl2" class="clauses"></div>
<div class="btns">
<button id="solve2" type="button">Solve 2-SAT</button>
<button id="reset2" type="button" class="ghost">Reset</button>
</div>
<div id="st2" class="status"></div>
</div>
<div class="panel">
<h4>3-SAT · 3 literals per clause · <span class="tag np">NP-complete</span></h4>
<div id="vars3" class="vars"></div>
<div id="cl3" class="clauses"></div>
<div class="btns">
<button id="solve3" type="button">Solve 3-SAT</button>
<button id="reset3" type="button" class="ghost">Reset</button>
</div>
<div id="st3" class="status"></div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #1c2530; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.bar { border-top: 2px solid currentColor; }
.panel { border: 1px solid #d4dde6; border-radius: 12px; padding: .8rem .9rem; margin-bottom: 1rem; background: #f7fafc; }
.panel h4 { margin: 0 0 .6rem; font-size: .98rem; }
.tag { font-size: .72rem; padding: .12rem .5rem; border-radius: 999px; vertical-align: middle; }
.tag.p { background: #d6f0dc; color: #0a7d33; }
.tag.np { background: #fadbdf; color: #c92f3c; }
.vars { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: .7rem; }
.chip { font: 700 14px ui-monospace, monospace; padding: .35rem .7rem; border-radius: 8px;
border: 1px solid #adb8c2; background: #fff; cursor: pointer; user-select: none; min-width: 64px; text-align: center; }
.chip.t { background: #1d3557; color: #fff; border-color: #1d3557; }
.chip.f { background: #fff; color: #1d3557; }
.clauses { display: flex; flex-wrap: wrap; gap: .4rem; }
.clause { font: 600 13px ui-monospace, monospace; padding: .3rem .55rem; border-radius: 8px;
border: 1px solid #cdd9e3; background: #fff; }
.clause.sat { background: #e7f6ec; border-color: #9bd6ad; color: #0a6b2c; }
.clause.unsat { background: #fdeef0; border-color: #f0aeb5; color: #b3303c; }
.lit { display: inline-block; }
.neg { text-decoration: overline; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .7rem; }
button { font: 600 13px system-ui, sans-serif; padding: .42rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.status { font-size: .9rem; font-weight: 600; margin-top: .5rem; min-height: 1.3em; line-height: 1.4; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// ---- Two SAT instances over variables encoded as integers 0..n-1. ----
// A literal is {v: index, neg: bool}. A clause is an array of literals.
// Clause is satisfied if ANY literal evaluates true under assignment a[].
// 2-SAT instance (satisfiable). Vars: a=0, b=1, c=2.
const V2 = ['a','b','c'];
const C2 = [
[{v:0,neg:false},{v:1,neg:false}], // a OR b
[{v:1,neg:true }, {v:2,neg:false}], // ¬b OR c
[{v:0,neg:true }, {v:2,neg:false}], // ¬a OR c
[{v:2,neg:false},{v:0,neg:false}], // c OR a
];
// 3-SAT instance (satisfiable). Vars: x=0, y=1, z=2, w=3.
const V3 = ['x','y','z','w'];
const C3 = [
[{v:0,neg:false},{v:1,neg:false},{v:2,neg:false}], // x OR y OR z
[{v:0,neg:true }, {v:1,neg:true }, {v:3,neg:false}],// ¬x OR ¬y OR w
[{v:1,neg:false},{v:2,neg:true }, {v:3,neg:true }], // y OR ¬z OR ¬w
[{v:0,neg:true }, {v:2,neg:false},{v:3,neg:false}], // ¬x OR z OR w
[{v:0,neg:false},{v:1,neg:true }, {v:3,neg:true }], // x OR ¬y OR ¬w
];
const a2 = new Array(V2.length).fill(false);
const a3 = new Array(V3.length).fill(false);
function litTrue(l, a){ return a[l.v] !== l.neg; }
function clauseSat(cl, a){ return cl.some(l => litTrue(l, a)); }
function allSat(C, a){ return C.every(cl => clauseSat(cl, a)); }
function litLabel(names, l){
return '<span class="lit'+(l.neg?' neg':'')+'">'+names[l.v]+'</span>';
}
function render(names, C, a, varsEl, clEl){
const vw = document.getElementById(varsEl); vw.innerHTML='';
names.forEach((nm,i)=>{
const c=document.createElement('div');
c.className='chip '+(a[i]?'t':'f');
c.textContent=nm+'='+(a[i]?'T':'F');
c.addEventListener('click',()=>{ a[i]=!a[i]; render(names,C,a,varsEl,clEl); });
vw.appendChild(c);
});
const cw = document.getElementById(clEl); cw.innerHTML='';
C.forEach(cl=>{
const d=document.createElement('div');
const ok=clauseSat(cl,a);
d.className='clause '+(ok?'sat':'unsat');
d.innerHTML='('+cl.map(l=>litLabel(names,l)).join(' ∨ ')+')';
cw.appendChild(d);
});
}
function setStatus(id,t,cls){ const s=document.getElementById(id); s.innerHTML=t; s.className='status '+cls; }
// ---- 2-SAT solver: implication graph + SCC (Tarjan), linear time. ----
// Node 2*v = literal "v true", node 2*v+1 = literal "v false".
function node(v,val){ return val ? 2*v : 2*v+1; } // val=true -> v is true
function solve2sat(C, n){
const N=2*n, adj=Array.from({length:N},()=>[]);
// clause (p OR q): ¬p -> q and ¬q -> p
for(const cl of C){
const [p,q]=cl;
// p means literal: var p.v is (true if !p.neg). "p false" node implies "q true".
const pTrue=node(p.v,!p.neg), pFalse=node(p.v,p.neg);
const qTrue=node(q.v,!q.neg), qFalse=node(q.v,q.neg);
adj[pFalse].push(qTrue);
adj[qFalse].push(pTrue);
}
// Tarjan SCC
let idx=0; const low=new Array(N).fill(-1), num=new Array(N).fill(-1);
const onst=new Array(N).fill(false), stk=[], comp=new Array(N).fill(-1); let cc=0;
function dfs(u){
num[u]=low[u]=idx++; stk.push(u); onst[u]=true;
for(const w of adj[u]){
if(num[w]<0){ dfs(w); low[u]=Math.min(low[u],low[w]); }
else if(onst[w]){ low[u]=Math.min(low[u],num[w]); }
}
if(low[u]===num[u]){
while(true){ const x=stk.pop(); onst[x]=false; comp[x]=cc; if(x===u)break; }
cc++;
}
}
for(let u=0;u<N;u++) if(num[u]<0) dfs(u);
// unsat if v-true and v-false share a component
const a=new Array(n).fill(false);
for(let v=0;v<n;v++){
if(comp[2*v]===comp[2*v+1]) return null;
// Tarjan numbers components in reverse topological order:
// assign the literal whose component index is SMALLER (later in topo) = true.
a[v]= comp[2*v] < comp[2*v+1];
}
return a;
}
// ---- 3-SAT solver: brute force over 2^n assignments (the hard part). ----
function solve3sat(C, n){
let steps=0;
for(let mask=0; mask<(1<<n); mask++){
steps++;
const a=new Array(n);
for(let i=0;i<n;i++) a[i]=!!(mask&(1<<i));
if(allSat(C,a)) return {a, steps, total:(1<<n)};
}
return {a:null, steps, total:(1<<n)};
}
document.getElementById('solve2').addEventListener('click',()=>{
const sol=solve2sat(C2,V2.length);
if(sol){ for(let i=0;i<sol.length;i++) a2[i]=sol[i]; render(V2,C2,a2,'vars2','cl2');
setStatus('st2','✓ Solved in one linear pass (implication graph + SCC). No search needed.','ok'); }
else setStatus('st2','✗ Unsatisfiable (a variable and its negation share a component).','bad');
});
document.getElementById('reset2').addEventListener('click',()=>{ a2.fill(false); render(V2,C2,a2,'vars2','cl2'); setStatus('st2','','');});
document.getElementById('solve3').addEventListener('click',()=>{
const r=solve3sat(C3,V3.length);
if(r.a){ for(let i=0;i<r.a.length;i++) a3[i]=r.a[i]; render(V3,C3,a3,'vars3','cl3');
setStatus('st3',fmt('✓ Found after trying %steps% of %total% assignments — brute-force search, 2ⁿ in the worst case.', { steps: r.steps, total: r.total }),'ok');
console.log('3-SAT solution:', r.a, '|', 'tried', r.steps, 'of', r.total); }
else setStatus('st3',fmt('✗ No assignment satisfies all clauses (checked all %total%).', { total: r.total }),'bad');
});
document.getElementById('reset3').addEventListener('click',()=>{ a3.fill(false); render(V3,C3,a3,'vars3','cl3'); setStatus('st3','','');});
render(V2,C2,a2,'vars2','cl2');
render(V3,C3,a3,'vars3','cl3');
<p class="hint">Two satisfiability puzzles. Each clause must be <b>true</b>; a clause is true when at least one literal is true. A bar like <span class="bar">x</span> means "NOT x". Click the variable chips to flip true/false, or let each solver run.</p>
<div class="panel">
<h4>2-SAT · 2 literals per clause · <span class="tag p">in P (linear)</span></h4>
<div id="vars2" class="vars"></div>
<div id="cl2" class="clauses"></div>
<div class="btns">
<button id="solve2" type="button">Solve 2-SAT</button>
<button id="reset2" type="button" class="ghost">Reset</button>
</div>
<div id="st2" class="status"></div>
</div>
<div class="panel">
<h4>3-SAT · 3 literals per clause · <span class="tag np">NP-complete</span></h4>
<div id="vars3" class="vars"></div>
<div id="cl3" class="clauses"></div>
<div class="btns">
<button id="solve3" type="button">Solve 3-SAT</button>
<button id="reset3" type="button" class="ghost">Reset</button>
</div>
<div id="st3" class="status"></div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #1c2530; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.bar { border-top: 2px solid currentColor; }
.panel { border: 1px solid #d4dde6; border-radius: 12px; padding: .8rem .9rem; margin-bottom: 1rem; background: #f7fafc; }
.panel h4 { margin: 0 0 .6rem; font-size: .98rem; }
.tag { font-size: .72rem; padding: .12rem .5rem; border-radius: 999px; vertical-align: middle; }
.tag.p { background: #d6f0dc; color: #0a7d33; }
.tag.np { background: #fadbdf; color: #c92f3c; }
.vars { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: .7rem; }
.chip { font: 700 14px ui-monospace, monospace; padding: .35rem .7rem; border-radius: 8px;
border: 1px solid #adb8c2; background: #fff; cursor: pointer; user-select: none; min-width: 64px; text-align: center; }
.chip.t { background: #1d3557; color: #fff; border-color: #1d3557; }
.chip.f { background: #fff; color: #1d3557; }
.clauses { display: flex; flex-wrap: wrap; gap: .4rem; }
.clause { font: 600 13px ui-monospace, monospace; padding: .3rem .55rem; border-radius: 8px;
border: 1px solid #cdd9e3; background: #fff; }
.clause.sat { background: #e7f6ec; border-color: #9bd6ad; color: #0a6b2c; }
.clause.unsat { background: #fdeef0; border-color: #f0aeb5; color: #b3303c; }
.lit { display: inline-block; }
.neg { text-decoration: overline; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .7rem; }
button { font: 600 13px system-ui, sans-serif; padding: .42rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.status { font-size: .9rem; font-weight: 600; margin-top: .5rem; min-height: 1.3em; line-height: 1.4; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// ---- Two SAT instances over variables encoded as integers 0..n-1. ----
// A literal is {v: index, neg: bool}. A clause is an array of literals.
// Clause is satisfied if ANY literal evaluates true under assignment a[].
// 2-SAT instance (satisfiable). Vars: a=0, b=1, c=2.
const V2 = ['a','b','c'];
const C2 = [
[{v:0,neg:false},{v:1,neg:false}], // a OR b
[{v:1,neg:true }, {v:2,neg:false}], // ¬b OR c
[{v:0,neg:true }, {v:2,neg:false}], // ¬a OR c
[{v:2,neg:false},{v:0,neg:false}], // c OR a
];
// 3-SAT instance (satisfiable). Vars: x=0, y=1, z=2, w=3.
const V3 = ['x','y','z','w'];
const C3 = [
[{v:0,neg:false},{v:1,neg:false},{v:2,neg:false}], // x OR y OR z
[{v:0,neg:true }, {v:1,neg:true }, {v:3,neg:false}],// ¬x OR ¬y OR w
[{v:1,neg:false},{v:2,neg:true }, {v:3,neg:true }], // y OR ¬z OR ¬w
[{v:0,neg:true }, {v:2,neg:false},{v:3,neg:false}], // ¬x OR z OR w
[{v:0,neg:false},{v:1,neg:true }, {v:3,neg:true }], // x OR ¬y OR ¬w
];
const a2 = new Array(V2.length).fill(false);
const a3 = new Array(V3.length).fill(false);
function litTrue(l, a){ return a[l.v] !== l.neg; }
function clauseSat(cl, a){ return cl.some(l => litTrue(l, a)); }
function allSat(C, a){ return C.every(cl => clauseSat(cl, a)); }
function litLabel(names, l){
return '<span class="lit'+(l.neg?' neg':'')+'">'+names[l.v]+'</span>';
}
function render(names, C, a, varsEl, clEl){
const vw = document.getElementById(varsEl); vw.innerHTML='';
names.forEach((nm,i)=>{
const c=document.createElement('div');
c.className='chip '+(a[i]?'t':'f');
c.textContent=nm+'='+(a[i]?'T':'F');
c.addEventListener('click',()=>{ a[i]=!a[i]; render(names,C,a,varsEl,clEl); });
vw.appendChild(c);
});
const cw = document.getElementById(clEl); cw.innerHTML='';
C.forEach(cl=>{
const d=document.createElement('div');
const ok=clauseSat(cl,a);
d.className='clause '+(ok?'sat':'unsat');
d.innerHTML='('+cl.map(l=>litLabel(names,l)).join(' ∨ ')+')';
cw.appendChild(d);
});
}
function setStatus(id,t,cls){ const s=document.getElementById(id); s.innerHTML=t; s.className='status '+cls; }
// ---- 2-SAT solver: implication graph + SCC (Tarjan), linear time. ----
// Node 2*v = literal "v true", node 2*v+1 = literal "v false".
function node(v,val){ return val ? 2*v : 2*v+1; } // val=true -> v is true
function solve2sat(C, n){
const N=2*n, adj=Array.from({length:N},()=>[]);
// clause (p OR q): ¬p -> q and ¬q -> p
for(const cl of C){
const [p,q]=cl;
// p means literal: var p.v is (true if !p.neg). "p false" node implies "q true".
const pTrue=node(p.v,!p.neg), pFalse=node(p.v,p.neg);
const qTrue=node(q.v,!q.neg), qFalse=node(q.v,q.neg);
adj[pFalse].push(qTrue);
adj[qFalse].push(pTrue);
}
// Tarjan SCC
let idx=0; const low=new Array(N).fill(-1), num=new Array(N).fill(-1);
const onst=new Array(N).fill(false), stk=[], comp=new Array(N).fill(-1); let cc=0;
function dfs(u){
num[u]=low[u]=idx++; stk.push(u); onst[u]=true;
for(const w of adj[u]){
if(num[w]<0){ dfs(w); low[u]=Math.min(low[u],low[w]); }
else if(onst[w]){ low[u]=Math.min(low[u],num[w]); }
}
if(low[u]===num[u]){
while(true){ const x=stk.pop(); onst[x]=false; comp[x]=cc; if(x===u)break; }
cc++;
}
}
for(let u=0;u<N;u++) if(num[u]<0) dfs(u);
// unsat if v-true and v-false share a component
const a=new Array(n).fill(false);
for(let v=0;v<n;v++){
if(comp[2*v]===comp[2*v+1]) return null;
// Tarjan numbers components in reverse topological order:
// assign the literal whose component index is SMALLER (later in topo) = true.
a[v]= comp[2*v] < comp[2*v+1];
}
return a;
}
// ---- 3-SAT solver: brute force over 2^n assignments (the hard part). ----
function solve3sat(C, n){
let steps=0;
for(let mask=0; mask<(1<<n); mask++){
steps++;
const a=new Array(n);
for(let i=0;i<n;i++) a[i]=!!(mask&(1<<i));
if(allSat(C,a)) return {a, steps, total:(1<<n)};
}
return {a:null, steps, total:(1<<n)};
}
document.getElementById('solve2').addEventListener('click',()=>{
const sol=solve2sat(C2,V2.length);
if(sol){ for(let i=0;i<sol.length;i++) a2[i]=sol[i]; render(V2,C2,a2,'vars2','cl2');
setStatus('st2','✓ Solved in one linear pass (implication graph + SCC). No search needed.','ok'); }
else setStatus('st2','✗ Unsatisfiable (a variable and its negation share a component).','bad');
});
document.getElementById('reset2').addEventListener('click',()=>{ a2.fill(false); render(V2,C2,a2,'vars2','cl2'); setStatus('st2','','');});
document.getElementById('solve3').addEventListener('click',()=>{
const r=solve3sat(C3,V3.length);
if(r.a){ for(let i=0;i<r.a.length;i++) a3[i]=r.a[i]; render(V3,C3,a3,'vars3','cl3');
setStatus('st3',fmt('✓ Found after trying %steps% of %total% assignments — brute-force search, 2ⁿ in the worst case.', { steps: r.steps, total: r.total }),'ok');
console.log('3-SAT solution:', r.a, '|', 'tried', r.steps, 'of', r.total); }
else setStatus('st3',fmt('✗ No assignment satisfies all clauses (checked all %total%).', { total: r.total }),'bad');
});
document.getElementById('reset3').addEventListener('click',()=>{ a3.fill(false); render(V3,C3,a3,'vars3','cl3'); setStatus('st3','','');});
render(V2,C2,a2,'vars2','cl2');
render(V3,C3,a3,'vars3','cl3');
Comments
Loading comments...