diff --git a/strings/Manacher.nim b/strings/Manacher.nim new file mode 100644 index 00000000..b7f0e650 --- /dev/null +++ b/strings/Manacher.nim @@ -0,0 +1,92 @@ +## Manacher's algorithm +## -------------------- +## +## Determine the longest palindrome in a string in linear time in its length +## with Manacher's algorithm. +## Inspired from: +## https://github.com/jilljenn/tryalgo/blob/master/tryalgo/manacher.py +## https://en.wikipedia.org/wiki/Longest_palindromic_substring +## +## :time complexity: O(n) where n is the string's length. + +import std/[strutils, sequtils, sets] +{.push raises: [].} + +runnableExamples: + let example1 = "cabbbab" + doAssert(manacherString(example1) == "abbba") + doAssert(manacherIndex(example1) == 1 .. 5) + doAssert(manacherLength(example1) == 5) + +func manacherIndex*(s: string): HSlice[int, int] {.raises: [ValueError].} = + ## Longest palindrome in a string by Manacher + ## :param s: string, lowercase ascii, no whitespace + ## :returns: indexes i,j such that s\[i:j\] is the longest palindrome in s + ## :time complexity: O(n) where n is the string's length. + ## All the indexes refer to an intermediate string t + ## of the form "^#a#b#a#a#$" for s="abaa" + if s.len == 0: + raise newException(ValueError, "Empty string") + let extraSymbols = toHashSet(['$', '^', '#']) + let letters = toHashSet(s.toLowerAscii) + assert disjoint(extraSymbols, letters) # Forbidden letters + if s == "": + return 0 .. 1 + let s = "^#" & join(s, "#") & "#$" + var + center = 1 + distance = 1 + p = repeat(0, len(s)) # Palindrome radii for each index in s + for index in 2 ..< len(s)-1: + # reflect index with respect to center + let mirror = 2 * center - index # = center - (index - center) + p[index] = max(0, min(distance - index, p[mirror])) + # grow palindrome centered in i + while s[index + 1 + p[index]] == s[index - 1 - p[index]]: + p[index] += 1 + # adjust center if necessary + if index + p[index] > distance: + center = index + distance = index + p[index] + # find the argmax index in p + var + j = maxIndex(p) + k = p[j] + return (j - k) div 2 ..< (j + k) div 2 # extract solution + +func manacherString*(s: string): string {.raises: [ValueError].} = + ## Returns the greatest palindromic substring in `s`. + return s[manacherIndex(s)] + +func manacherLength*(s: string): int {.raises: [ValueError].} = + ## Returns the length of the greatest palindromic substring in `s`. + let + res = manacherIndex(s) + (i, j) = (res.a, res.b) + return j - i + 1 + +when isMainModule: + import std/unittest + suite "Manacher's algorithm": + test "Simple palindrome": + check manacherIndex("abbbab") == 0 .. 4 + check manacherLength("abbbab") == 5 + check manacherString("abbbab") == "abbba" + + test "Single letter palindrome": + check manacherIndex("abcab") == 0 .. 0 + check manacherLength("abcab") == 1 + check manacherString("abcab") == "a" + + test "Palindrome is full string": + check manacherIndex("telet") == 0 .. 4 + check manacherLength("telet") == 5 + check manacherString("telet") == "telet" + + test "Empty string": + doAssertRaises(ValueError): + discard manacherIndex("") + doAssertRaises(ValueError): + discard manacherLength("") + doAssertRaises(ValueError): + discard manacherString("") diff --git a/strings/manacher.nim b/strings/manacher.nim new file mode 100644 index 00000000..1adad298 --- /dev/null +++ b/strings/manacher.nim @@ -0,0 +1,91 @@ +## Manacher's algorithm +## +## Determine the longest palindrome in a string in linear time in its length +## with Manacher's algorithm. +## +## Inspired from: +## https://github.com/jilljenn/tryalgo/blob/master/tryalgo/manacher.py +## https://en.wikipedia.org/wiki/Longest_palindromic_substring +## +import std/[strutils, sequtils, sets] +{.push raises: [].} + +runnableExamples: + let example1 = "cabbbab" + doAssert(manacherString(example1) == "abbba") + doAssert(manacherIndex(example1) == 1 .. 5) + doAssert(manacherLength(example1) == 5) + +func manacherIndex*(s: string): HSlice[int, int] {.raises: [ValueError].} = + ## Find the start and stop index for the longest palindrome in a string by Manacher + ## + ## :returns: indexes i and j such that s\[i:j\] is the longest palindrome in s + ## :param s: string, lowercase ascii, no whitespace + ## :time complexity: O(len(s)) + ## All the indexes refer to an intermediate string t + ## of the form "^#a#b#a#a#$" for s="abaa" + if s.len == 0: + raise newException(ValueError, "Empty string") + let extraSymbols = toHashSet(['$', '^', '#']) + let letters = toHashSet(s.toLowerAscii) + assert disjoint(extraSymbols, letters) # Forbidden letters + if s == "": + return 0 .. 1 + let s = "^#" & join(s, "#") & "#$" + var + center = 1 + distance = 1 + p = repeat(0, len(s)) # Palindrome radii for each index in s + for index in 2 ..< len(s)-1: + # reflect index with respect to center + let mirror = 2 * center - index # = center - (index - center) + p[index] = max(0, min(distance - index, p[mirror])) + # grow palindrome centered in i + while s[index + 1 + p[index]] == s[index - 1 - p[index]]: + p[index] += 1 + # adjust center if necessary + if index + p[index] > distance: + center = index + distance = index + p[index] + # find the argmax index in p + var + j = maxIndex(p) + k = p[j] + return (j - k) div 2 ..< (j + k) div 2 # extract solution + +func manacherString*(s: string): string {.raises: [ValueError].} = + ## Returns the longest palindrome + return s[manacherIndex(s)] + +func manacherLength*(s: string): int {.raises: [ValueError].} = + ## Returns the length of the longest palindrome + let + res = manacherIndex(s) + (i, j) = (res.a, res.b) + return j - i + 1 + +when isMainModule: + import std/unittest + suite "Manacher's algorithm": + test "Simple palindrome": + check manacherIndex("abbbab") == 0 .. 4 + check manacherLength("abbbab") == 5 + check manacherString("abbbab") == "abbba" + + test "Single letter palindrome": + check manacherIndex("abcab") == 0 .. 0 + check manacherLength("abcab") == 1 + check manacherString("abcab") == "a" + + test "Palindrome is full string": + check manacherIndex("telet") == 0 .. 4 + check manacherLength("telet") == 5 + check manacherString("telet") == "telet" + + test "Empty string": + doAssertRaises(ValueError): + discard manacherIndex("") + doAssertRaises(ValueError): + discard manacherLength("") + doAssertRaises(ValueError): + discard manacherString("")