From 916c27b738d90db4049fb969f508bcc2a802beed Mon Sep 17 00:00:00 2001 From: Dimitri LESNOFF Date: Sun, 26 Mar 2023 15:12:47 +0200 Subject: [PATCH 01/10] Manacher algorithm --- strings/manacher.nim | 88 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 strings/manacher.nim diff --git a/strings/manacher.nim b/strings/manacher.nim new file mode 100644 index 00000000..f9ffe129 --- /dev/null +++ b/strings/manacher.nim @@ -0,0 +1,88 @@ +# 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].} = + #[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 + :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].} = + return s[manacher_index(s)] + +func manacherLength*(s: string): int {.raises: [ValueError].} = + let + res = manacher_index(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("") From 8cf7475f097698e83ae9f7e672520cb1b52fb427 Mon Sep 17 00:00:00 2001 From: dlesnoff <54949944+dlesnoff@users.noreply.github.com> Date: Tue, 20 Jun 2023 10:29:03 +0200 Subject: [PATCH 02/10] Fix title Co-authored-by: Satin Wuker <74630829+SatinWuker@users.noreply.github.com> --- strings/manacher.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/strings/manacher.nim b/strings/manacher.nim index f9ffe129..87bcde2f 100644 --- a/strings/manacher.nim +++ b/strings/manacher.nim @@ -1,4 +1,4 @@ -# Manacher's algorithm +## Manacher's algorithm #[ Determine the longest palindrome in a string in linear time in its length with Manacher's algorithm From db0e10be79041113e31101e2dc79c8e388545b65 Mon Sep 17 00:00:00 2001 From: Dimitri LESNOFF Date: Thu, 6 Jul 2023 12:26:49 +0200 Subject: [PATCH 03/10] Doc: fix docstring and title header --- strings/manacher.nim | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/strings/manacher.nim b/strings/manacher.nim index 87bcde2f..d3c6dfa2 100644 --- a/strings/manacher.nim +++ b/strings/manacher.nim @@ -1,11 +1,12 @@ ## 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 -]# +## -------------------- +## +## 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: [].} @@ -78,7 +79,7 @@ when isMainModule: check manacherIndex("telet") == 0 .. 4 check manacherLength("telet") == 5 check manacherString("telet") == "telet" - + test "Empty string": doAssertRaises(ValueError): discard manacherIndex("") From 8e4d2d035107600fd4b43e521b172b5cfcfe9f94 Mon Sep 17 00:00:00 2001 From: Dimitri LESNOFF Date: Fri, 7 Jul 2023 10:15:09 +0200 Subject: [PATCH 04/10] Fix time complexity and variable's name --- strings/manacher.nim | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/strings/manacher.nim b/strings/manacher.nim index d3c6dfa2..c3f90c4a 100644 --- a/strings/manacher.nim +++ b/strings/manacher.nim @@ -7,6 +7,8 @@ ## 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: [].} @@ -20,7 +22,7 @@ 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 - :complexity: O(len(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" ]# @@ -54,11 +56,11 @@ func manacherIndex*(s: string): HSlice[int, int] {.raises: [ValueError].} = return (j - k) div 2 ..< (j + k) div 2 # extract solution func manacherString*(s: string): string {.raises: [ValueError].} = - return s[manacher_index(s)] + return s[manacherIndex(s)] func manacherLength*(s: string): int {.raises: [ValueError].} = let - res = manacher_index(s) + res = manacherIndex(s) (i, j) = (res.a, res.b) return j - i + 1 From 5bbbd432a74dcc04ca7207ccec5a43f3ab7bbbb4 Mon Sep 17 00:00:00 2001 From: Dimitri LESNOFF Date: Fri, 7 Jul 2023 10:16:28 +0200 Subject: [PATCH 05/10] Fix indentiation with nimpretty --- strings/manacher.nim | 70 ++++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/strings/manacher.nim b/strings/manacher.nim index c3f90c4a..0ffe4ef9 100644 --- a/strings/manacher.nim +++ b/strings/manacher.nim @@ -19,41 +19,41 @@ runnableExamples: 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 + #[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].} = return s[manacherIndex(s)] From 67a419ccadce27141df81dda89390de1147f23d3 Mon Sep 17 00:00:00 2001 From: Dimitri LESNOFF Date: Fri, 7 Jul 2023 10:20:09 +0200 Subject: [PATCH 06/10] Fix docstrings of functions --- strings/manacher.nim | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/strings/manacher.nim b/strings/manacher.nim index 0ffe4ef9..b7f0e650 100644 --- a/strings/manacher.nim +++ b/strings/manacher.nim @@ -19,13 +19,12 @@ runnableExamples: 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" - ]# + ## 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(['$', '^', '#']) @@ -56,9 +55,11 @@ func manacherIndex*(s: string): HSlice[int, int] {.raises: [ValueError].} = 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) From 27d6764f20d043952f34c51ba7b78e8db8c0a044 Mon Sep 17 00:00:00 2001 From: Dimitri LESNOFF Date: Fri, 7 Jul 2023 10:21:11 +0200 Subject: [PATCH 07/10] Capitalize Manacher --- strings/{manacher.nim => Manacher.nim} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename strings/{manacher.nim => Manacher.nim} (100%) diff --git a/strings/manacher.nim b/strings/Manacher.nim similarity index 100% rename from strings/manacher.nim rename to strings/Manacher.nim From 32f6841fa98790413eb2773203a8f278cf8e59c9 Mon Sep 17 00:00:00 2001 From: Dimitri Lesnoff Date: Tue, 27 Jun 2023 23:37:13 +0200 Subject: [PATCH 08/10] docs: docstring refactoring --- strings/manacher.nim | 91 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 strings/manacher.nim diff --git a/strings/manacher.nim b/strings/manacher.nim new file mode 100644 index 00000000..22ded38b --- /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[manacher_index(s)] + +func manacherLength*(s: string): int {.raises: [ValueError].} = + ## Returns the length of the longest palindrome + let + res = manacher_index(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("") From 6d8e9b546b2d72ce3cf9f95d0a3019f86afe04ce Mon Sep 17 00:00:00 2001 From: Dimitri LESNOFF Date: Mon, 23 Sep 2024 09:45:38 +0200 Subject: [PATCH 09/10] Prettify the code --- strings/manacher.nim | 72 ++++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/strings/manacher.nim b/strings/manacher.nim index 22ded38b..e30ee106 100644 --- a/strings/manacher.nim +++ b/strings/manacher.nim @@ -17,41 +17,41 @@ runnableExamples: 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 + ## 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 @@ -81,7 +81,7 @@ when isMainModule: check manacherIndex("telet") == 0 .. 4 check manacherLength("telet") == 5 check manacherString("telet") == "telet" - + test "Empty string": doAssertRaises(ValueError): discard manacherIndex("") From 066764bbb54fbfa05b776c4cde6735b482c6f90b Mon Sep 17 00:00:00 2001 From: Dimitri LESNOFF Date: Mon, 23 Sep 2024 09:50:59 +0200 Subject: [PATCH 10/10] Fix variable names --- strings/manacher.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/strings/manacher.nim b/strings/manacher.nim index e30ee106..1adad298 100644 --- a/strings/manacher.nim +++ b/strings/manacher.nim @@ -55,12 +55,12 @@ func manacherIndex*(s: string): HSlice[int, int] {.raises: [ValueError].} = func manacherString*(s: string): string {.raises: [ValueError].} = ## Returns the longest palindrome - return s[manacher_index(s)] + return s[manacherIndex(s)] func manacherLength*(s: string): int {.raises: [ValueError].} = ## Returns the length of the longest palindrome let - res = manacher_index(s) + res = manacherIndex(s) (i, j) = (res.a, res.b) return j - i + 1