Type any string into the box below. The demo builds the eertree character by character and lists every distinct palindromic substring it finds — including the trivial single characters.
<p class="hint">Type a string below. The eertree is built character by character — each step can add <b>at most one</b> new distinct palindromic substring. Watch the count and the list update in real time.</p>
<div class="input-row">
<input id="txt" type="text" maxlength="40" placeholder="e.g. abaab" autocomplete="off" spellcheck="false" />
<button id="clear" type="button" class="ghost">Clear</button>
</div>
<div class="status-row">
<span class="label">Distinct palindromic substrings:</span>
<span id="count" class="count">0</span>
</div>
<div id="pal-list" class="pal-list"></div>
<div class="brute-row">
<button id="brute" type="button">Verify with brute force</button>
<span id="brute-msg" class="brute-msg"></span>
</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; }
.input-row { display: flex; gap: .5rem; margin-bottom: .7rem; }
#txt { flex: 1; font: 600 16px ui-monospace, monospace; padding: .45rem .7rem;
border: 1px solid #adb1b8; border-radius: 8px; outline: none; }
#txt:focus { border-color: #1d3557; box-shadow: 0 0 0 2px #1d355722; }
.status-row { display: flex; align-items: baseline; gap: .5rem; margin-bottom: .5rem; }
.label { font-size: .9rem; color: #555; }
.count { font: 700 1.6rem ui-monospace, monospace; color: #1d3557; min-width: 2.5rem; }
.pal-list { display: flex; flex-wrap: wrap; gap: .35rem; min-height: 2.2rem; margin-bottom: .9rem; }
.pal-chip { background: #e8eef3; border: 1px solid #cdd9e3; color: #1d3557;
font: 600 13px ui-monospace, monospace; padding: .2rem .55rem; border-radius: 20px; }
.pal-chip.new { background: #d0f0e0; border-color: #4caf88; color: #0a5c34; animation: pop .25s ease; }
@keyframes pop { 0%{transform:scale(.7);opacity:0} 100%{transform:scale(1);opacity:1} }
.brute-row { display: flex; align-items: center; gap: .7rem; 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; }
.brute-msg { font-size: .9rem; color: #444; }
.brute-msg.ok { color: #0a7d33; font-weight: 600; }
.brute-msg.bad { color: #c92f3c; font-weight: 600; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Eertree (Palindromic Tree) implementation.
// Two imaginary roots: root_odd (length -1) and root_even (length 0).
// Each node stores: len, suffLink, children (map char->node).
// Core invariant: after processing s[0..i], `last` points to the node
// Adding s[i]: walk up via suffLink until s[i - node.len - 1] == s[i],
function buildEertree(s) {
const nodes = [];
function newNode(len, suffLink) {
nodes.push({ len, suffLink, children: {}, label: null });
return nodes.length - 1;
}
const ROOT_ODD = newNode(-1, 0); // imaginary root with length -1
const ROOT_EVEN = newNode( 0, ROOT_ODD); // imaginary root with length 0
let last = ROOT_EVEN;
const newPalindromes = []; // list of {step, label} for newly added nodes
for (let i = 0; i < s.length; i++) {
const c = s[i];
// Find the longest palindromic suffix of s[0..i-1] such that
// s[i - len - 1] == s[i] (so we can extend it on both sides).
let cur = last;
while (true) {
const j = i - nodes[cur].len - 1;
if (j >= 0 && s[j] === c) break;
cur = nodes[cur].suffLink;
}
if (nodes[cur].children[c] !== undefined) {
last = nodes[cur].children[c];
} else {
// Create a new node: its length is nodes[cur].len + 2
const newLen = nodes[cur].len + 2;
const newIdx = newNode(newLen, -1); // suffLink to be determined
nodes[cur].children[c] = newIdx;
// Determine suffix link for the new node
if (newLen === 1) {
nodes[newIdx].suffLink = ROOT_EVEN;
} else {
let tmp = nodes[cur].suffLink;
while (true) {
const j = i - nodes[tmp].len - 1;
if (j >= 0 && s[j] === c) break;
tmp = nodes[tmp].suffLink;
}
nodes[newIdx].suffLink = nodes[tmp].children[c] !== undefined
? nodes[tmp].children[c]
: ROOT_EVEN;
}
// Recover the actual palindrome label
const halfLen = Math.floor(newLen / 2);
const startIdx = i - newLen + 1;
nodes[newIdx].label = s.slice(startIdx, startIdx + newLen);
newPalindromes.push({ step: i, label: nodes[newIdx].label });
last = newIdx;
}
}
// Collect all distinct palindromes (real nodes only, excluding the two imaginary roots)
const all = nodes.slice(2).filter(n => n.label !== null).map(n => n.label);
return { all, newPalindromes };
}
// Brute-force: collect all distinct palindromic substrings by checking every substring.
function bruteForcePalindromes(s) {
const set = new Set();
for (let i = 0; i < s.length; i++) {
for (let j = i + 1; j <= s.length; j++) {
const sub = s.slice(i, j);
const rev = sub.split('').reverse().join('');
if (sub === rev) set.add(sub);
}
}
return [...set].sort((a, b) => a.length - b.length || a.localeCompare(b));
}
let prevLabels = new Set();
function update() {
const s = document.getElementById('txt').value;
const list = document.getElementById('pal-list');
const countEl = document.getElementById('count');
const bruteMsg = document.getElementById('brute-msg');
bruteMsg.textContent = '';
bruteMsg.className = 'brute-msg';
if (!s) {
list.innerHTML = '';
countEl.textContent = '0';
prevLabels = new Set();
return;
}
const { all } = buildEertree(s);
const sorted = [...all].sort((a, b) => a.length - b.length || a.localeCompare(b));
const newSet = new Set(all);
list.innerHTML = '';
sorted.forEach(p => {
const chip = document.createElement('span');
chip.className = 'pal-chip' + (!prevLabels.has(p) ? ' new' : '');
chip.textContent = p;
list.appendChild(chip);
});
countEl.textContent = sorted.length;
prevLabels = newSet;
}
document.getElementById('txt').addEventListener('input', update);
document.getElementById('clear').addEventListener('click', () => {
document.getElementById('txt').value = '';
update();
});
document.getElementById('brute').addEventListener('click', () => {
const s = document.getElementById('txt').value;
const msg = document.getElementById('brute-msg');
if (!s) { msg.textContent = 'Enter a string first.'; msg.className = 'brute-msg'; return; }
const { all: eertreeAll } = buildEertree(s);
const brute = bruteForcePalindromes(s);
const eertreeSet = new Set(eertreeAll);
const bruteSet = new Set(brute);
const match = eertreeAll.length === brute.length &&
brute.every(p => eertreeSet.has(p));
if (match) {
msg.textContent = fmt('✓ Eertree and brute force agree: %length% distinct palindrome(s).', { length: brute.length });
msg.className = 'brute-msg ok';
} else {
msg.textContent = fmt('✗ Mismatch! Eertree: %length%, brute force: %length2%', { length: eertreeAll.length, length2: brute.length });
msg.className = 'brute-msg bad';
}
});
// Pre-load with a nice example
document.getElementById('txt').value = 'abaab';
update();
<p class="hint">Type a string below. The eertree is built character by character — each step can add <b>at most one</b> new distinct palindromic substring. Watch the count and the list update in real time.</p>
<div class="input-row">
<input id="txt" type="text" maxlength="40" placeholder="e.g. abaab" autocomplete="off" spellcheck="false" />
<button id="clear" type="button" class="ghost">Clear</button>
</div>
<div class="status-row">
<span class="label">Distinct palindromic substrings:</span>
<span id="count" class="count">0</span>
</div>
<div id="pal-list" class="pal-list"></div>
<div class="brute-row">
<button id="brute" type="button">Verify with brute force</button>
<span id="brute-msg" class="brute-msg"></span>
</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; }
.input-row { display: flex; gap: .5rem; margin-bottom: .7rem; }
#txt { flex: 1; font: 600 16px ui-monospace, monospace; padding: .45rem .7rem;
border: 1px solid #adb1b8; border-radius: 8px; outline: none; }
#txt:focus { border-color: #1d3557; box-shadow: 0 0 0 2px #1d355722; }
.status-row { display: flex; align-items: baseline; gap: .5rem; margin-bottom: .5rem; }
.label { font-size: .9rem; color: #555; }
.count { font: 700 1.6rem ui-monospace, monospace; color: #1d3557; min-width: 2.5rem; }
.pal-list { display: flex; flex-wrap: wrap; gap: .35rem; min-height: 2.2rem; margin-bottom: .9rem; }
.pal-chip { background: #e8eef3; border: 1px solid #cdd9e3; color: #1d3557;
font: 600 13px ui-monospace, monospace; padding: .2rem .55rem; border-radius: 20px; }
.pal-chip.new { background: #d0f0e0; border-color: #4caf88; color: #0a5c34; animation: pop .25s ease; }
@keyframes pop { 0%{transform:scale(.7);opacity:0} 100%{transform:scale(1);opacity:1} }
.brute-row { display: flex; align-items: center; gap: .7rem; 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; }
.brute-msg { font-size: .9rem; color: #444; }
.brute-msg.ok { color: #0a7d33; font-weight: 600; }
.brute-msg.bad { color: #c92f3c; font-weight: 600; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Eertree (Palindromic Tree) implementation.
// Two imaginary roots: root_odd (length -1) and root_even (length 0).
// Each node stores: len, suffLink, children (map char->node).
// Core invariant: after processing s[0..i], `last` points to the node
// Adding s[i]: walk up via suffLink until s[i - node.len - 1] == s[i],
function buildEertree(s) {
const nodes = [];
function newNode(len, suffLink) {
nodes.push({ len, suffLink, children: {}, label: null });
return nodes.length - 1;
}
const ROOT_ODD = newNode(-1, 0); // imaginary root with length -1
const ROOT_EVEN = newNode( 0, ROOT_ODD); // imaginary root with length 0
let last = ROOT_EVEN;
const newPalindromes = []; // list of {step, label} for newly added nodes
for (let i = 0; i < s.length; i++) {
const c = s[i];
// Find the longest palindromic suffix of s[0..i-1] such that
// s[i - len - 1] == s[i] (so we can extend it on both sides).
let cur = last;
while (true) {
const j = i - nodes[cur].len - 1;
if (j >= 0 && s[j] === c) break;
cur = nodes[cur].suffLink;
}
if (nodes[cur].children[c] !== undefined) {
last = nodes[cur].children[c];
} else {
// Create a new node: its length is nodes[cur].len + 2
const newLen = nodes[cur].len + 2;
const newIdx = newNode(newLen, -1); // suffLink to be determined
nodes[cur].children[c] = newIdx;
// Determine suffix link for the new node
if (newLen === 1) {
nodes[newIdx].suffLink = ROOT_EVEN;
} else {
let tmp = nodes[cur].suffLink;
while (true) {
const j = i - nodes[tmp].len - 1;
if (j >= 0 && s[j] === c) break;
tmp = nodes[tmp].suffLink;
}
nodes[newIdx].suffLink = nodes[tmp].children[c] !== undefined
? nodes[tmp].children[c]
: ROOT_EVEN;
}
// Recover the actual palindrome label
const halfLen = Math.floor(newLen / 2);
const startIdx = i - newLen + 1;
nodes[newIdx].label = s.slice(startIdx, startIdx + newLen);
newPalindromes.push({ step: i, label: nodes[newIdx].label });
last = newIdx;
}
}
// Collect all distinct palindromes (real nodes only, excluding the two imaginary roots)
const all = nodes.slice(2).filter(n => n.label !== null).map(n => n.label);
return { all, newPalindromes };
}
// Brute-force: collect all distinct palindromic substrings by checking every substring.
function bruteForcePalindromes(s) {
const set = new Set();
for (let i = 0; i < s.length; i++) {
for (let j = i + 1; j <= s.length; j++) {
const sub = s.slice(i, j);
const rev = sub.split('').reverse().join('');
if (sub === rev) set.add(sub);
}
}
return [...set].sort((a, b) => a.length - b.length || a.localeCompare(b));
}
let prevLabels = new Set();
function update() {
const s = document.getElementById('txt').value;
const list = document.getElementById('pal-list');
const countEl = document.getElementById('count');
const bruteMsg = document.getElementById('brute-msg');
bruteMsg.textContent = '';
bruteMsg.className = 'brute-msg';
if (!s) {
list.innerHTML = '';
countEl.textContent = '0';
prevLabels = new Set();
return;
}
const { all } = buildEertree(s);
const sorted = [...all].sort((a, b) => a.length - b.length || a.localeCompare(b));
const newSet = new Set(all);
list.innerHTML = '';
sorted.forEach(p => {
const chip = document.createElement('span');
chip.className = 'pal-chip' + (!prevLabels.has(p) ? ' new' : '');
chip.textContent = p;
list.appendChild(chip);
});
countEl.textContent = sorted.length;
prevLabels = newSet;
}
document.getElementById('txt').addEventListener('input', update);
document.getElementById('clear').addEventListener('click', () => {
document.getElementById('txt').value = '';
update();
});
document.getElementById('brute').addEventListener('click', () => {
const s = document.getElementById('txt').value;
const msg = document.getElementById('brute-msg');
if (!s) { msg.textContent = 'Enter a string first.'; msg.className = 'brute-msg'; return; }
const { all: eertreeAll } = buildEertree(s);
const brute = bruteForcePalindromes(s);
const eertreeSet = new Set(eertreeAll);
const bruteSet = new Set(brute);
const match = eertreeAll.length === brute.length &&
brute.every(p => eertreeSet.has(p));
if (match) {
msg.textContent = fmt('✓ Eertree and brute force agree: %length% distinct palindrome(s).', { length: brute.length });
msg.className = 'brute-msg ok';
} else {
msg.textContent = fmt('✗ Mismatch! Eertree: %length%, brute force: %length2%', { length: eertreeAll.length, length2: brute.length });
msg.className = 'brute-msg bad';
}
});
// Pre-load with a nice example
document.getElementById('txt').value = 'abaab';
update();
Notice how the count of distinct palindromes grows at most by one per character added. That is not a coincidence — it is the core theorem that makes the eertree work. Each new character can introduce at most one new palindromic suffix not seen before, so the tree stays linear in size no matter how long the text grows.
Compare this with the brute-force approach: for a string of length n there are O(n2) substrings to check, and checking each one for palindromicity takes O(n) time — a total of O(n3) without clever tricks. The eertree does the whole job in O(n).
Comments
Loading comments...