Type a short RNA strand below (letters A, U, G, C). The demo runs Nussinov's algorithm: it builds a table where each cell holds the most base pairs achievable on one sub-stretch of the strand. Bigger stretches are built from smaller ones already solved â so the table fills from the diagonal outward, and the top-right corner is the answer.
<p class="hint">Type a short RNA strand (letters A, U, G, C). Nussinovâs algorithm fills a table: cell (i,j) holds the most base pairs achievable on the sub-stretch from position i to j. Press <b>Fold</b> and watch it fill from the diagonal outward â the top-right corner is the best fold.</p>
<div class="row">
<input id="seq" type="text" value="GGGAAAUCC" maxlength="14" spellcheck="false" />
<button id="fold" type="button">Fold</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div id="table" class="table"></div>
<div class="status" id="status">Press Fold to run Nussinovâs algorithm.</div>
<div id="pairs" class="pairs"></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 .7rem; line-height: 1.45; }
.row { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .7rem; }
input { font: 700 16px ui-monospace, monospace; letter-spacing: 2px; text-transform: uppercase;
padding: .4rem .6rem; border: 1px solid #1d3557; border-radius: 8px; width: 11rem; }
.table { display: inline-grid; gap: 2px; margin: .3rem 0; }
.c { width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;
font: 600 14px ui-monospace, monospace; border-radius: 6px; }
.hd { background: #1d3557; color: #fff; }
.dg { background: #eef2f6; color: #aab4bf; }
.cell { background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; }
.cell.fill { animation: pop .25s ease; background: #cfe3d6; border-color: #9ccfae; }
.cell.corner { background: #0a7d33; color: #fff; border-color: #0a7d33; }
@keyframes pop { from { transform: scale(.6); opacity: .2; } to { transform: scale(1); opacity: 1; } }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 0 .3rem; min-height: 1.4em; color: #0a7d33; }
.pairs { font: 600 15px ui-monospace, monospace; color: #1d3557; min-height: 1.4em; word-break: break-all; }
.btns { display: flex; gap: .5rem; }
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] ?? ''));
// Nussinov (1978): find the secondary structure of an RNA strand that maximizes
// the number of valid base pairs (A-U and G-C, here also G-U wobble off for clarity).
// DP table M[i][j] = max pairs on substring s[i..j]. Recurrence, for i<j:
// M[i][j] = max( M[i+1][j], // i unpaired
// max over k in (i,j] of: pair(i,k)+M[i+1][k-1]+M[k+1][j] )
// A min loop length keeps a base from pairing with an immediate neighbor.
const MINLOOP = 1;
const canPair = (a, b) => (a==='A'&&b==='U')||(a==='U'&&b==='A')||(a==='G'&&b==='C')||(a==='C'&&b==='G');
function nussinov(s){
const n = s.length;
const M = Array.from({length:n}, () => new Array(n).fill(0));
// fill by increasing substring length
for(let len=2; len<=n; len++){
for(let i=0; i+len-1<n; i++){
const j = i+len-1;
let best = M[i+1][j]; // i unpaired
for(let k=i+1; k<=j; k++){
if(k-i>MINLOOP && canPair(s[i],s[k])){
const inner = (i+1<=k-1) ? M[i+1][k-1] : 0;
const outer = (k+1<=j) ? M[k+1][j] : 0;
best = Math.max(best, 1 + inner + outer);
}
}
M[i][j] = best;
}
}
return M;
}
// Traceback to recover the actual list of pairs (i,j) of one optimal fold.
function traceback(s, M){
const pairs = [];
(function rec(i,j){
if(i>=j) return;
if(M[i][j]===M[i+1][j]){ rec(i+1,j); return; }
for(let k=i+1; k<=j; k++){
if(k-i>MINLOOP && canPair(s[i],s[k])){
const inner=(i+1<=k-1)?M[i+1][k-1]:0, outer=(k+1<=j)?M[k+1][j]:0;
if(M[i][j]===1+inner+outer){ pairs.push([i,k]); rec(i+1,k-1); rec(k+1,j); return; }
}
}
})(0, s.length-1);
return pairs;
}
let timer = null;
function clean(v){ return (v||'').toUpperCase().replace(/[^AUGC]/g,'').slice(0,14); }
function render(s, M, shown){
const n=s.length, t=document.getElementById('table');
t.style.gridTemplateColumns = `repeat(${n+1}, 34px)`;
t.innerHTML='';
t.appendChild(div('c hd',''));
for(let j=0;j<n;j++) t.appendChild(div('c hd', s[j]));
for(let i=0;i<n;i++){
t.appendChild(div('c hd', s[i]));
for(let j=0;j<n;j++){
if(j<i){ t.appendChild(div('c dg','')); continue; }
if(j===i){ t.appendChild(div('c dg','0')); continue; }
const isShown = shown.has(i+','+j);
const corner = (i===0 && j===n-1 && isShown);
const cls='c cell'+(isShown?' fill':'')+(corner?' corner':'');
t.appendChild(div(cls, isShown ? String(M[i][j]) : ''));
}
}
}
function div(cls,txt){ const d=document.createElement('div'); d.className=cls; d.textContent=txt; return d; }
function run(){
if(timer){ clearInterval(timer); timer=null; }
const s = clean(document.getElementById('seq').value);
document.getElementById('seq').value = s;
document.getElementById('pairs').textContent='';
if(s.length<2){ document.getElementById('status').textContent='Enter at least 2 letters.'; render(s,[],new Set()); return; }
const M = nussinov(s);
// animate the fill order: by increasing length
const order=[];
for(let len=2; len<=s.length; len++) for(let i=0;i+len-1<s.length;i++) order.push([i,i+len-1]);
const shown=new Set(); let idx=0;
render(s,M,shown);
document.getElementById('status').textContent='Filling the table, shorter stretches firstâŠ';
timer=setInterval(()=>{
if(idx>=order.length){
clearInterval(timer); timer=null;
const best=M[0][s.length-1];
document.getElementById('status').textContent=fmt('Best fold: %best% base pair%v%.', { best: best, v: best===1?'':'s' });
const ps=traceback(s,M).sort((a,b)=>a[0]-b[0]);
document.getElementById('pairs').textContent = s+'\n'+notation(s,ps);
console.log('seq',s,'maxPairs',best,'pairs',JSON.stringify(ps));
return;
}
const [i,j]=order[idx++]; shown.add(i+','+j); render(s,M,shown);
}, 90);
}
function notation(s,pairs){
const a=new Array(s.length).fill('.');
pairs.forEach(([i,j])=>{a[i]='(';a[j]=')';});
return a.join('');
}
document.getElementById('fold').addEventListener('click',run);
document.getElementById('reset').addEventListener('click',()=>{
if(timer){clearInterval(timer);timer=null;}
document.getElementById('seq').value='GGGAAAUCC';
document.getElementById('status').textContent='Press Fold to run Nussinovâs algorithm.';
document.getElementById('pairs').textContent='';
document.getElementById('table').innerHTML='';
});
run();
<p class="hint">Type a short RNA strand (letters A, U, G, C). Nussinovâs algorithm fills a table: cell (i,j) holds the most base pairs achievable on the sub-stretch from position i to j. Press <b>Fold</b> and watch it fill from the diagonal outward â the top-right corner is the best fold.</p>
<div class="row">
<input id="seq" type="text" value="GGGAAAUCC" maxlength="14" spellcheck="false" />
<button id="fold" type="button">Fold</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div id="table" class="table"></div>
<div class="status" id="status">Press Fold to run Nussinovâs algorithm.</div>
<div id="pairs" class="pairs"></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 .7rem; line-height: 1.45; }
.row { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .7rem; }
input { font: 700 16px ui-monospace, monospace; letter-spacing: 2px; text-transform: uppercase;
padding: .4rem .6rem; border: 1px solid #1d3557; border-radius: 8px; width: 11rem; }
.table { display: inline-grid; gap: 2px; margin: .3rem 0; }
.c { width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;
font: 600 14px ui-monospace, monospace; border-radius: 6px; }
.hd { background: #1d3557; color: #fff; }
.dg { background: #eef2f6; color: #aab4bf; }
.cell { background: #e8eef3; color: #1d3557; border: 1px solid #cdd9e3; }
.cell.fill { animation: pop .25s ease; background: #cfe3d6; border-color: #9ccfae; }
.cell.corner { background: #0a7d33; color: #fff; border-color: #0a7d33; }
@keyframes pop { from { transform: scale(.6); opacity: .2; } to { transform: scale(1); opacity: 1; } }
.status { font-size: 1rem; font-weight: 600; margin: .6rem 0 .3rem; min-height: 1.4em; color: #0a7d33; }
.pairs { font: 600 15px ui-monospace, monospace; color: #1d3557; min-height: 1.4em; word-break: break-all; }
.btns { display: flex; gap: .5rem; }
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] ?? ''));
// Nussinov (1978): find the secondary structure of an RNA strand that maximizes
// the number of valid base pairs (A-U and G-C, here also G-U wobble off for clarity).
// DP table M[i][j] = max pairs on substring s[i..j]. Recurrence, for i<j:
// M[i][j] = max( M[i+1][j], // i unpaired
// max over k in (i,j] of: pair(i,k)+M[i+1][k-1]+M[k+1][j] )
// A min loop length keeps a base from pairing with an immediate neighbor.
const MINLOOP = 1;
const canPair = (a, b) => (a==='A'&&b==='U')||(a==='U'&&b==='A')||(a==='G'&&b==='C')||(a==='C'&&b==='G');
function nussinov(s){
const n = s.length;
const M = Array.from({length:n}, () => new Array(n).fill(0));
// fill by increasing substring length
for(let len=2; len<=n; len++){
for(let i=0; i+len-1<n; i++){
const j = i+len-1;
let best = M[i+1][j]; // i unpaired
for(let k=i+1; k<=j; k++){
if(k-i>MINLOOP && canPair(s[i],s[k])){
const inner = (i+1<=k-1) ? M[i+1][k-1] : 0;
const outer = (k+1<=j) ? M[k+1][j] : 0;
best = Math.max(best, 1 + inner + outer);
}
}
M[i][j] = best;
}
}
return M;
}
// Traceback to recover the actual list of pairs (i,j) of one optimal fold.
function traceback(s, M){
const pairs = [];
(function rec(i,j){
if(i>=j) return;
if(M[i][j]===M[i+1][j]){ rec(i+1,j); return; }
for(let k=i+1; k<=j; k++){
if(k-i>MINLOOP && canPair(s[i],s[k])){
const inner=(i+1<=k-1)?M[i+1][k-1]:0, outer=(k+1<=j)?M[k+1][j]:0;
if(M[i][j]===1+inner+outer){ pairs.push([i,k]); rec(i+1,k-1); rec(k+1,j); return; }
}
}
})(0, s.length-1);
return pairs;
}
let timer = null;
function clean(v){ return (v||'').toUpperCase().replace(/[^AUGC]/g,'').slice(0,14); }
function render(s, M, shown){
const n=s.length, t=document.getElementById('table');
t.style.gridTemplateColumns = `repeat(${n+1}, 34px)`;
t.innerHTML='';
t.appendChild(div('c hd',''));
for(let j=0;j<n;j++) t.appendChild(div('c hd', s[j]));
for(let i=0;i<n;i++){
t.appendChild(div('c hd', s[i]));
for(let j=0;j<n;j++){
if(j<i){ t.appendChild(div('c dg','')); continue; }
if(j===i){ t.appendChild(div('c dg','0')); continue; }
const isShown = shown.has(i+','+j);
const corner = (i===0 && j===n-1 && isShown);
const cls='c cell'+(isShown?' fill':'')+(corner?' corner':'');
t.appendChild(div(cls, isShown ? String(M[i][j]) : ''));
}
}
}
function div(cls,txt){ const d=document.createElement('div'); d.className=cls; d.textContent=txt; return d; }
function run(){
if(timer){ clearInterval(timer); timer=null; }
const s = clean(document.getElementById('seq').value);
document.getElementById('seq').value = s;
document.getElementById('pairs').textContent='';
if(s.length<2){ document.getElementById('status').textContent='Enter at least 2 letters.'; render(s,[],new Set()); return; }
const M = nussinov(s);
// animate the fill order: by increasing length
const order=[];
for(let len=2; len<=s.length; len++) for(let i=0;i+len-1<s.length;i++) order.push([i,i+len-1]);
const shown=new Set(); let idx=0;
render(s,M,shown);
document.getElementById('status').textContent='Filling the table, shorter stretches firstâŠ';
timer=setInterval(()=>{
if(idx>=order.length){
clearInterval(timer); timer=null;
const best=M[0][s.length-1];
document.getElementById('status').textContent=fmt('Best fold: %best% base pair%v%.', { best: best, v: best===1?'':'s' });
const ps=traceback(s,M).sort((a,b)=>a[0]-b[0]);
document.getElementById('pairs').textContent = s+'\n'+notation(s,ps);
console.log('seq',s,'maxPairs',best,'pairs',JSON.stringify(ps));
return;
}
const [i,j]=order[idx++]; shown.add(i+','+j); render(s,M,shown);
}, 90);
}
function notation(s,pairs){
const a=new Array(s.length).fill('.');
pairs.forEach(([i,j])=>{a[i]='(';a[j]=')';});
return a.join('');
}
document.getElementById('fold').addEventListener('click',run);
document.getElementById('reset').addEventListener('click',()=>{
if(timer){clearInterval(timer);timer=null;}
document.getElementById('seq').value='GGGAAAUCC';
document.getElementById('status').textContent='Press Fold to run Nussinovâs algorithm.';
document.getElementById('pairs').textContent='';
document.getElementById('table').innerHTML='';
});
run();
Notice what the table is doing. To find the best fold of a stretch, it only ever combines answers it has already computed for shorter stretches. Nothing is recomputed and nothing is brute-forced. A problem with astronomically many possible folds collapses into a tidy grid of n Ă n cells â the signature move of dynamic programming.
Comments
Loading comments...