-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Expand file tree
/
Copy pathManachersAlgorithm.cs
More file actions
393 lines (360 loc) · 16.3 KB
/
ManachersAlgorithm.cs
File metadata and controls
393 lines (360 loc) · 16.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
namespace Algorithms.Strings;
/// <summary>
/// Manacher's Algorithm is used to find the longest palindromic substring in linear time O(n).
/// This algorithm is significantly more efficient than the naive O(n²) approach.
///
/// KEY CONCEPTS:
/// 1. String Transformation: Inserts special characters to handle even/odd palindromes uniformly.
/// 2. Palindrome Radius: For each position, stores how far the palindrome extends.
/// 3. Center Expansion: Expands around each potential center to find palindromes.
/// 4. Symmetry Property: Uses previously computed palindrome information to skip redundant checks.
/// 5. Right Boundary Tracking: Maintains the rightmost boundary of any palindrome found.
///
/// WHY IT'S FAST:
/// The algorithm achieves O(n) time by ensuring each character is examined at most twice:
/// - Once when it's inside a known palindrome (using mirror property).
/// - Once when expanding beyond the known boundary.
///
/// Reference: "A New Linear-Time On-Line Algorithm for Finding the Smallest Initial Palindrome of a String"
/// by Glenn Manacher (1975), Journal of the ACM.
/// </summary>
public static class ManachersAlgorithm
{
/// <summary>
/// Finds the longest palindromic substring using Manacher's Algorithm.
///
/// ALGORITHM STEPS:
/// 1. PREPROCESSING: Transform "abc" to "^#a#b#c#$" to handle even/odd palindromes uniformly.
/// - ^ and $ are sentinels that prevent boundary checks.
/// - # characters create uniform spacing.
///
/// 2. INITIALIZATION: Set up tracking variables.
/// - palindromeRadii[i]: How far palindrome extends from position i.
/// - center: Center of rightmost palindrome found.
/// - rightBoundary: Right edge of rightmost palindrome.
///
/// 3. MAIN LOOP: For each position i in transformed string.
/// a) If i is within rightBoundary, use mirror property:
/// - mirror = 2 * center - i (symmetric position).
/// - Start with min(palindromeRadii[mirror], rightBoundary - i).
/// b) Expand palindrome by comparing characters on both sides.
/// c) Update center and rightBoundary if palindrome extends further right.
/// d) Track the longest palindrome found.
///
/// 4. EXTRACTION: Convert transformed coordinates back to original string.
///
/// Time Complexity: O(n) - Each character examined at most twice.
/// Space Complexity: O(n) - For transformed string and radius array.
/// </summary>
/// <param name="input">The input string to search for palindromes.</param>
/// <returns>The longest palindromic substring found in the input.</returns>
/// <exception cref="ArgumentException">Thrown when the input string is null.</exception>
/// <example>
/// Input: "babad".
/// Output: "bab" or "aba" (both are valid longest palindromes with length 3).
///
/// Detailed Example:
/// Input: "abaxyz".
/// Transformed: "^#a#b#a#x#y#z#$".
/// Process finds "aba" at indices 1-3 with radius 3 in transformed string.
/// Maps back to indices 0-2 in original string.
/// Output: "aba".
/// </example>
public static string FindLongestPalindrome(string input)
{
// Validate input
if (input == null)
{
throw new ArgumentException("Input string cannot be null.", nameof(input));
}
// Handle edge cases
if (input.Length == 0)
{
return string.Empty;
}
if (input.Length == 1)
{
return input;
}
// STEP 1: Transform the string to handle even-length palindromes uniformly
// Example: "abc" becomes "^#a#b#c#$"
//
// WHY THIS WORKS:
// - Original "aba" (odd): Center is 'b' at index 1.
// - Original "abba" (even): No single center character.
// - Transformed "#a#b#b#a#": Both have clear centers (the middle #).
//
// SENTINELS (^ and $):
// - Prevent index out of bounds during expansion.
// - Never match any character, so expansion stops naturally.
string transformed = PreprocessString(input);
int n = transformed.Length;
// STEP 2: Initialize data structures
// palindromeRadii[i] = radius of palindrome centered at position i.
// Example: If transformed[i] is center of "#a#b#a#", radius = 3.
// (3 characters on each side match).
int[] palindromeRadii = new int[n];
// Track the rightmost palindrome boundary for optimization.
// center: Position of the palindrome that extends furthest right.
// rightBoundary: The rightmost index covered by that palindrome.
// These help us use symmetry to avoid redundant comparisons.
int center = 0;
int rightBoundary = 0;
// Track the longest palindrome found during the scan.
// maxLength: Radius of the longest palindrome.
// maxCenter: Position where the longest palindrome is centered.
int maxLength = 0;
int maxCenter = 0;
// STEP 3: Process each position in the transformed string.
// Skip first and last positions (sentinels ^ and $).
for (int i = 1; i < n - 1; i++)
{
// OPTIMIZATION: Use mirror property for positions within known palindrome
//
// If we have a palindrome centered at 'center' extending to 'rightBoundary':
// center - radius ... center ... center + radius
// mirror i
//
// The mirror position reflects i across the center:
// mirror = center - (i - center) = 2 * center - i.
int mirror = 2 * center - i;
// KEY INSIGHT: If i is within rightBoundary, we can use symmetry.
// The palindrome at position i might be similar to the one at mirror position.
if (i < rightBoundary)
{
// We can safely copy the radius from the mirror position, BUT:
// 1. palindromeRadii[mirror]: What we know from the mirror side.
// 2. rightBoundary - i: We can't assume anything beyond rightBoundary.
//
// Take the minimum because:
// - If mirror's palindrome fits within bounds, we can use it.
// - If it extends beyond, we only know up to rightBoundary.
palindromeRadii[i] = Math.Min(rightBoundary - i, palindromeRadii[mirror]);
}
// EXPANSION PHASE: Try to extend the palindrome further.
// We start from palindromeRadii[i] (not 0) to avoid redundant checks.
// This is why the algorithm is O(n) - we never re-check characters.
//
// Example: If palindromeRadii[i] = 2, we already know:
// transformed[i-2] == transformed[i+2] and
// transformed[i-1] == transformed[i+1].
// So we start checking at distance 3.
//
// The sentinels (^ and $) guarantee we never go out of bounds.
// Expansion stops naturally when characters don't match.
while (transformed[i + palindromeRadii[i] + 1] == transformed[i - palindromeRadii[i] - 1])
{
palindromeRadii[i]++;
}
// UPDATE TRACKING: If this palindrome extends further right than any before.
//
// WHY THIS MATTERS:
// By tracking the rightmost boundary, we can use the mirror property
// for future positions, avoiding redundant character comparisons.
//
// Example: If we find a large palindrome early, all positions within it
// can benefit from the symmetry property.
if (i + palindromeRadii[i] > rightBoundary)
{
center = i; // This position is now our reference center.
rightBoundary = i + palindromeRadii[i]; // Update the rightmost boundary.
}
// TRACK MAXIMUM: Remember the longest palindrome found so far.
// We need both the length and position to extract it later.
if (palindromeRadii[i] > maxLength)
{
maxLength = palindromeRadii[i]; // Radius of longest palindrome.
maxCenter = i; // Where it's centered in transformed string.
}
}
// STEP 4: Extract the longest palindrome from the original string.
//
// COORDINATE MAPPING:
// Transformed string has format: ^#a#b#c#$.
// Position in transformed -> Position in original: (pos - 1) / 2.
//
// Example: "aba" -> "^#a#b#a#$".
// - maxCenter = 4 (the middle 'b' in transformed).
// - maxLength = 3 (radius).
// - Original center = (4 - 1) / 2 = 1 (index of 'b' in "aba").
// - Start = (maxCenter - maxLength) / 2 = (4 - 3) / 2 = 0.
// - Length = maxLength = 3.
// - Result: input.Substring(0, 3) = "aba".
int start = (maxCenter - maxLength) / 2;
return input.Substring(start, maxLength);
}
/// <summary>
/// Finds the longest palindromic substring and returns detailed information.
/// This method provides more detailed information than FindLongestPalindrome,
/// including the exact starting position and length in the original string.
///
/// USE CASE:
/// When you need to know WHERE the palindrome is located, not just what it is.
/// Useful for highlighting, replacing, or further processing the palindrome.
/// </summary>
/// <param name="input">The input string to search for palindromes.</param>
/// <returns>
/// A tuple containing:
/// - The longest palindromic substring
/// - The starting index of the longest palindrome in the original string
/// - The length of the longest palindrome.
/// </returns>
/// <exception cref="ArgumentException">Thrown when the input string is null.</exception>
/// <example>
/// Input: "babad".
/// Output: (Palindrome: "bab", StartIndex: 0, Length: 3).
/// </example>
public static (string Palindrome, int StartIndex, int Length) FindLongestPalindromeWithDetails(string input)
{
// Validate input
if (input == null)
{
throw new ArgumentException("Input string cannot be null.", nameof(input));
}
// Handle edge cases
if (input.Length == 0)
{
return (string.Empty, 0, 0);
}
if (input.Length == 1)
{
return (input, 0, 1);
}
// Apply the same algorithm as FindLongestPalindrome.
// (See detailed comments in that method for step-by-step explanation).
string transformed = PreprocessString(input);
int n = transformed.Length;
int[] palindromeRadii = new int[n];
int center = 0;
int rightBoundary = 0;
int maxLength = 0;
int maxCenter = 0;
// Main algorithm loop
for (int i = 1; i < n - 1; i++)
{
// Use mirror property if within known palindrome.
int mirror = 2 * center - i;
if (i < rightBoundary)
{
palindromeRadii[i] = Math.Min(rightBoundary - i, palindromeRadii[mirror]);
}
// Expand palindrome.
// Sentinels guarantee no out-of-bounds access.
while (transformed[i + palindromeRadii[i] + 1] == transformed[i - palindromeRadii[i] - 1])
{
palindromeRadii[i]++;
}
// Update rightmost boundary.
if (i + palindromeRadii[i] > rightBoundary)
{
center = i;
rightBoundary = i + palindromeRadii[i];
}
// Track maximum.
if (palindromeRadii[i] > maxLength)
{
maxLength = palindromeRadii[i];
maxCenter = i;
}
}
// Calculate the start index and extract the palindrome.
// Map from transformed coordinates to original string coordinates.
int startIndex = (maxCenter - maxLength) / 2;
string palindrome = input.Substring(startIndex, maxLength);
// Return all three pieces of information.
return (palindrome, startIndex, maxLength);
}
/// <summary>
/// Checks if the entire string is a palindrome using Manacher's Algorithm.
///
/// EFFICIENCY:
/// - This approach: O(n) time using Manacher's algorithm.
/// - Naive approach: O(n) time for reversing + O(n) for comparison.
/// - Both are O(n), but this avoids creating a reversed copy.
///
/// LOGIC:
/// If the longest palindrome in the string equals the string length,
/// then the entire string must be a palindrome.
/// </summary>
/// <param name="input">The string to check.</param>
/// <returns>True if the entire string is a palindrome, false otherwise.</returns>
/// <exception cref="ArgumentException">Thrown when the input string is null.</exception>
/// <example>
/// Input: "racecar".
/// Output: true.
/// </example>
public static bool IsPalindrome(string input)
{
if (input == null)
{
throw new ArgumentException("Input string cannot be null.", nameof(input));
}
// Strings of length 0 or 1 are always palindromes.
if (input.Length <= 1)
{
return true;
}
// Find the longest palindrome in the string.
// If it spans the entire string, then the string is a palindrome.
var (_, _, length) = FindLongestPalindromeWithDetails(input);
return length == input.Length;
}
/// <summary>
/// Preprocesses the input string by inserting special characters.
/// This transformation is the KEY to making Manacher's algorithm work efficiently.
///
/// PROBLEM IT SOLVES:
/// - Odd-length palindromes ("aba") have a center character.
/// - Even-length palindromes ("abba") have a center between characters.
/// - Without transformation, we'd need separate logic for each case.
///
/// SOLUTION:
/// Insert '#' between every character, making all palindromes odd-length.
///
/// EXAMPLES:
/// - "aba" (odd) -> "^#a#b#a#$" (center is 'b').
/// - "abba" (even) -> "^#a#b#b#a#$" (center is '#' between the two 'b's).
///
/// SENTINELS (^ and $):
/// - Placed at start and end.
/// - Never match any character (including each other).
/// - Automatically stop expansion without explicit boundary checks.
/// - Prevent IndexOutOfBoundsException.
///
/// COORDINATE MAPPING:
/// - Original index i maps to transformed index (2*i + 2).
/// - Transformed index j maps to original index (j - 1) / 2.
/// </summary>
/// <param name="input">The original input string.</param>
/// <returns>The transformed string with inserted special characters.</returns>
private static string PreprocessString(string input)
{
// Calculate the size of transformed string.
// Original: n characters.
// Transformed: ^ + # + (n chars with # between each) + # + $.
// Total: 1 + 1 + n + (n-1) + 1 + 1 = 2n + 3.
int n = input.Length;
char[] transformed = new char[n * 2 + 3];
// Place sentinels at boundaries.
transformed[0] = '^'; // Start sentinel (never matches anything).
transformed[n * 2 + 2] = '$'; // End sentinel (never matches anything).
// Build the transformed string: #a#b#c#.
// For input "abc":
// Position 0: ^ (sentinel).
// Position 1: # (separator).
// Position 2: a (input[0]).
// Position 3: # (separator).
// Position 4: b (input[1]).
// Position 5: # (separator).
// Position 6: c (input[2]).
// Position 7: # (separator).
// Position 8: $ (sentinel).
for (int i = 0; i < n; i++)
{
transformed[2 * i + 1] = '#'; // Separator before character.
transformed[2 * i + 2] = input[i]; // Original character.
}
transformed[n * 2 + 1] = '#'; // Final separator.
return new string(transformed);
}
}