Type any text below. The demo runs Manacher's algorithm and highlights the longest palindromic substring in real time, showing the radius array and the current rightmost boundary.
<div class="hint">Type text below. Manacher's algorithm finds the <b>longest palindromic substring</b> in O(n) by reusing mirror symmetry.</div>
<div class="input-row">
<input id="txt" type="text" value="abacaba" maxlength="40" spellcheck="false" autocomplete="off" />
</div>
<div id="vis" class="vis"></div>
<div class="legend-row">
<span class="leg-item leg-center">current center</span>
<span class="leg-item leg-boundary">boundary</span>
<span class="leg-item leg-best">longest palindrome</span>
</div>
<div id="result" class="result"></div>
<div class="radius-label">Radius array <span class="radius-hint">(half-length of palindrome centered here)</span></div>
<div id="radii" class="radii"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 15px; }
.hint { font-size: .88rem; color: #444; margin-bottom: .6rem; line-height: 1.45; }
.input-row { margin-bottom: .7rem; }
#txt { font: 700 18px ui-monospace, monospace; padding: .4rem .7rem; border: 1.5px solid #c0c8d4;
border-radius: 8px; width: 100%; max-width: 420px; color: #1d3557; background: #f5f8fb; }
.vis { display: flex; flex-wrap: nowrap; gap: 3px; margin-bottom: .3rem; overflow-x: auto; padding-bottom: 2px; }
.ch { min-width: 30px; height: 36px; display: flex; align-items: center; justify-content: center;
font: 700 16px ui-monospace, monospace; border-radius: 6px; background: #e8eef3;
border: 1.5px solid #c9d4de; color: #1d3557; transition: background .15s; }
.ch.best { background: #d0f0e0; border-color: #5cb87e; color: #0a5e30; }
.ch.boundary { background: #fff3cd; border-color: #e8b628; color: #7a5a00; }
.ch.center { background: #dde8ff; border-color: #5d7de8; color: #1a2f8a; }
.ch.gap { color: #aaa; font-size: 11px; background: #f0f2f5; border-color: #dde0e5; min-width: 22px; }
.legend-row { display: flex; gap: .8rem; flex-wrap: wrap; margin: .4rem 0 .6rem; }
.leg-item { font-size: .78rem; padding: 2px 8px; border-radius: 4px; font-weight: 600; }
.leg-center { background: #dde8ff; color: #1a2f8a; border: 1px solid #5d7de8; }
.leg-boundary { background: #fff3cd; color: #7a5a00; border: 1px solid #e8b628; }
.leg-best { background: #d0f0e0; color: #0a5e30; border: 1px solid #5cb87e; }
.result { font-weight: 700; font-size: 1rem; color: #1d3557; min-height: 1.4em; margin-bottom: .5rem; }
.result span { color: #0a7d33; }
.radius-label { font-size: .82rem; font-weight: 600; color: #555; }
.radius-hint { font-weight: 400; color: #888; }
.radii { display: flex; flex-wrap: nowrap; gap: 3px; margin-top: .3rem; overflow-x: auto; }
.ri { min-width: 30px; height: 26px; display: flex; align-items: center; justify-content: center;
font: 600 13px ui-monospace, monospace; border-radius: 5px; background: #f0f2f5;
border: 1px solid #dde0e5; color: #555; }
.ri.gap { min-width: 22px; color: #ccc; background: #fafafa; border-color: #eee; }
.ri.best-r { background: #d0f0e0; border-color: #5cb87e; color: #0a5e30; font-weight: 700; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Manacher's algorithm — O(n) longest palindromic substring.
// We use the standard trick of inserting '#' separators so both odd and even
// palindromes are handled uniformly in the transformed string T.
// e.g. "abacaba" -> "#a#b#a#c#a#b#a#"
//
// p[i] = radius of palindrome centered at T[i] (not counting center itself).
// Answer: find max p[i], map back to original string indices.
function manacher(s) {
if (!s.length) return { p: [], bestStart: 0, bestLen: 0, T: '' };
// Build transformed string
const T = '#' + s.split('').join('#') + '#'; // length 2n+1
const n = T.length;
const p = new Array(n).fill(0);
let c = 0, r = 0; // center and right boundary of rightmost palindrome
let bestLen = 0, bestCenter = 0;
for (let i = 0; i < n; i++) {
const mirror = 2 * c - i;
if (i < r) {
p[i] = Math.min(r - i, p[mirror]);
}
// Attempt to expand around center i
let a = i - (p[i] + 1);
let b = i + (p[i] + 1);
while (a >= 0 && b < n && T[a] === T[b]) {
p[i]++;
a--;
b++;
}
// Update rightmost boundary
if (i + p[i] > r) {
c = i;
r = i + p[i];
}
if (p[i] > bestLen) {
bestLen = p[i];
bestCenter = i;
}
}
// Map back: center bestCenter in T, radius bestLen
// In T, T[i] maps to original index (i-1)/2 (only for odd i = real chars).
// The palindrome in T spans [bestCenter - bestLen, bestCenter + bestLen].
// In original string: start = (bestCenter - bestLen) / 2, length = bestLen.
const bestStart = (bestCenter - bestLen) / 2;
return { p, bestStart, bestLen, T };
}
function render() {
const raw = document.getElementById('txt').value;
const s = raw.replace(/[^a-zA-Z0-9]/g, '').toLowerCase() || '';
const { p, bestStart, bestLen, T } = manacher(s);
// Find max radius index for highlight
let maxR = 0, maxI = 0;
for (let i = 0; i < p.length; i++) if (p[i] > maxR) { maxR = p[i]; maxI = i; }
// Compute rightmost boundary in T-space at end of algorithm = center of max palindrome
// (recompute for display: boundary after all iterations)
let c = 0, r = 0;
for (let i = 0; i < T.length; i++) {
if (i + p[i] > r) { c = i; r = i + p[i]; }
}
const boundaryLeft = c - p[maxI]; // left edge of best palindrome in T
const boundaryRight = c + p[maxI]; // right edge
// Render character cells (T = transformed string)
const vis = document.getElementById('vis');
vis.innerHTML = '';
const radiiEl = document.getElementById('radii');
radiiEl.innerHTML = '';
for (let i = 0; i < T.length; i++) {
const isGap = (i % 2 === 0); // T[0], T[2], ... are '#'
const inBest = (i >= maxI - maxR && i <= maxI + maxR);
const isCenter = (i === maxI);
const isBoundary = !isCenter && (i === maxI - maxR || i === maxI + maxR);
const d = document.createElement('div');
let cls = 'ch' + (isGap ? ' gap' : '');
if (inBest && !isGap) cls += ' best';
if (isBoundary) cls += ' boundary';
if (isCenter) cls += ' center';
d.className = cls;
d.textContent = isGap ? '·' : T[i];
vis.appendChild(d);
const ri = document.createElement('div');
ri.className = 'ri' + (isGap ? ' gap' : '') + (i === maxI ? ' best-r' : '');
ri.textContent = isGap ? '' : p[i];
radiiEl.appendChild(ri);
}
const res = document.getElementById('result');
if (!s.length) {
res.innerHTML = 'Type some text to begin.';
return;
}
const palindrome = s.slice(bestStart, bestStart + bestLen);
res.innerHTML = bestLen <= 1
? 'No repeated palindrome found (every single character is trivially one).'
: fmt('Longest palindrome: <span>"%palindrome%"</span> (length %bestLen%, starting at index %bestStart%)', { palindrome: palindrome, bestLen: bestLen, bestStart: bestStart });
}
document.getElementById('txt').addEventListener('input', render);
render();
<div class="hint">Type text below. Manacher's algorithm finds the <b>longest palindromic substring</b> in O(n) by reusing mirror symmetry.</div>
<div class="input-row">
<input id="txt" type="text" value="abacaba" maxlength="40" spellcheck="false" autocomplete="off" />
</div>
<div id="vis" class="vis"></div>
<div class="legend-row">
<span class="leg-item leg-center">current center</span>
<span class="leg-item leg-boundary">boundary</span>
<span class="leg-item leg-best">longest palindrome</span>
</div>
<div id="result" class="result"></div>
<div class="radius-label">Radius array <span class="radius-hint">(half-length of palindrome centered here)</span></div>
<div id="radii" class="radii"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 15px; }
.hint { font-size: .88rem; color: #444; margin-bottom: .6rem; line-height: 1.45; }
.input-row { margin-bottom: .7rem; }
#txt { font: 700 18px ui-monospace, monospace; padding: .4rem .7rem; border: 1.5px solid #c0c8d4;
border-radius: 8px; width: 100%; max-width: 420px; color: #1d3557; background: #f5f8fb; }
.vis { display: flex; flex-wrap: nowrap; gap: 3px; margin-bottom: .3rem; overflow-x: auto; padding-bottom: 2px; }
.ch { min-width: 30px; height: 36px; display: flex; align-items: center; justify-content: center;
font: 700 16px ui-monospace, monospace; border-radius: 6px; background: #e8eef3;
border: 1.5px solid #c9d4de; color: #1d3557; transition: background .15s; }
.ch.best { background: #d0f0e0; border-color: #5cb87e; color: #0a5e30; }
.ch.boundary { background: #fff3cd; border-color: #e8b628; color: #7a5a00; }
.ch.center { background: #dde8ff; border-color: #5d7de8; color: #1a2f8a; }
.ch.gap { color: #aaa; font-size: 11px; background: #f0f2f5; border-color: #dde0e5; min-width: 22px; }
.legend-row { display: flex; gap: .8rem; flex-wrap: wrap; margin: .4rem 0 .6rem; }
.leg-item { font-size: .78rem; padding: 2px 8px; border-radius: 4px; font-weight: 600; }
.leg-center { background: #dde8ff; color: #1a2f8a; border: 1px solid #5d7de8; }
.leg-boundary { background: #fff3cd; color: #7a5a00; border: 1px solid #e8b628; }
.leg-best { background: #d0f0e0; color: #0a5e30; border: 1px solid #5cb87e; }
.result { font-weight: 700; font-size: 1rem; color: #1d3557; min-height: 1.4em; margin-bottom: .5rem; }
.result span { color: #0a7d33; }
.radius-label { font-size: .82rem; font-weight: 600; color: #555; }
.radius-hint { font-weight: 400; color: #888; }
.radii { display: flex; flex-wrap: nowrap; gap: 3px; margin-top: .3rem; overflow-x: auto; }
.ri { min-width: 30px; height: 26px; display: flex; align-items: center; justify-content: center;
font: 600 13px ui-monospace, monospace; border-radius: 5px; background: #f0f2f5;
border: 1px solid #dde0e5; color: #555; }
.ri.gap { min-width: 22px; color: #ccc; background: #fafafa; border-color: #eee; }
.ri.best-r { background: #d0f0e0; border-color: #5cb87e; color: #0a5e30; font-weight: 700; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Manacher's algorithm — O(n) longest palindromic substring.
// We use the standard trick of inserting '#' separators so both odd and even
// palindromes are handled uniformly in the transformed string T.
// e.g. "abacaba" -> "#a#b#a#c#a#b#a#"
//
// p[i] = radius of palindrome centered at T[i] (not counting center itself).
// Answer: find max p[i], map back to original string indices.
function manacher(s) {
if (!s.length) return { p: [], bestStart: 0, bestLen: 0, T: '' };
// Build transformed string
const T = '#' + s.split('').join('#') + '#'; // length 2n+1
const n = T.length;
const p = new Array(n).fill(0);
let c = 0, r = 0; // center and right boundary of rightmost palindrome
let bestLen = 0, bestCenter = 0;
for (let i = 0; i < n; i++) {
const mirror = 2 * c - i;
if (i < r) {
p[i] = Math.min(r - i, p[mirror]);
}
// Attempt to expand around center i
let a = i - (p[i] + 1);
let b = i + (p[i] + 1);
while (a >= 0 && b < n && T[a] === T[b]) {
p[i]++;
a--;
b++;
}
// Update rightmost boundary
if (i + p[i] > r) {
c = i;
r = i + p[i];
}
if (p[i] > bestLen) {
bestLen = p[i];
bestCenter = i;
}
}
// Map back: center bestCenter in T, radius bestLen
// In T, T[i] maps to original index (i-1)/2 (only for odd i = real chars).
// The palindrome in T spans [bestCenter - bestLen, bestCenter + bestLen].
// In original string: start = (bestCenter - bestLen) / 2, length = bestLen.
const bestStart = (bestCenter - bestLen) / 2;
return { p, bestStart, bestLen, T };
}
function render() {
const raw = document.getElementById('txt').value;
const s = raw.replace(/[^a-zA-Z0-9]/g, '').toLowerCase() || '';
const { p, bestStart, bestLen, T } = manacher(s);
// Find max radius index for highlight
let maxR = 0, maxI = 0;
for (let i = 0; i < p.length; i++) if (p[i] > maxR) { maxR = p[i]; maxI = i; }
// Compute rightmost boundary in T-space at end of algorithm = center of max palindrome
// (recompute for display: boundary after all iterations)
let c = 0, r = 0;
for (let i = 0; i < T.length; i++) {
if (i + p[i] > r) { c = i; r = i + p[i]; }
}
const boundaryLeft = c - p[maxI]; // left edge of best palindrome in T
const boundaryRight = c + p[maxI]; // right edge
// Render character cells (T = transformed string)
const vis = document.getElementById('vis');
vis.innerHTML = '';
const radiiEl = document.getElementById('radii');
radiiEl.innerHTML = '';
for (let i = 0; i < T.length; i++) {
const isGap = (i % 2 === 0); // T[0], T[2], ... are '#'
const inBest = (i >= maxI - maxR && i <= maxI + maxR);
const isCenter = (i === maxI);
const isBoundary = !isCenter && (i === maxI - maxR || i === maxI + maxR);
const d = document.createElement('div');
let cls = 'ch' + (isGap ? ' gap' : '');
if (inBest && !isGap) cls += ' best';
if (isBoundary) cls += ' boundary';
if (isCenter) cls += ' center';
d.className = cls;
d.textContent = isGap ? '·' : T[i];
vis.appendChild(d);
const ri = document.createElement('div');
ri.className = 'ri' + (isGap ? ' gap' : '') + (i === maxI ? ' best-r' : '');
ri.textContent = isGap ? '' : p[i];
radiiEl.appendChild(ri);
}
const res = document.getElementById('result');
if (!s.length) {
res.innerHTML = 'Type some text to begin.';
return;
}
const palindrome = s.slice(bestStart, bestStart + bestLen);
res.innerHTML = bestLen <= 1
? 'No repeated palindrome found (every single character is trivially one).'
: fmt('Longest palindrome: <span>"%palindrome%"</span> (length %bestLen%, starting at index %bestStart%)', { palindrome: palindrome, bestLen: bestLen, bestStart: bestStart });
}
document.getElementById('txt').addEventListener('input', render);
render();
Watch how the boundary (the right edge of the farthest-reaching palindrome found so far) advances. Each time a new center extends past it, the boundary jumps; all centers inside it can copy their mirror's radius directly — no expansion needed. That is the saving: most characters pay O(1), only boundary-pushers pay to expand.
Comments
Loading comments...