diff --git a/DIRECTORY.md b/DIRECTORY.md index 52b15110f236..5dbce818c8a9 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -1615,6 +1615,7 @@ * [Levenshtein Distance](strings/levenshtein_distance.py) * [Longest Word In Sentence](strings/longest_word_in_sentence.py) * [Lower](strings/lower.py) + * [Lyndon Factorization](strings/lyndon_factorization.py) * [Manacher](strings/manacher.py) * [Min Cost String Conversion](strings/min_cost_string_conversion.py) * [Min Window Substring](strings/min_window_substring.py) diff --git a/strings/lyndon_factorization.py b/strings/lyndon_factorization.py new file mode 100644 index 000000000000..e6647b28d469 --- /dev/null +++ b/strings/lyndon_factorization.py @@ -0,0 +1,66 @@ +""" +https://en.wikipedia.org/wiki/Lyndon_word +https://cp-algorithms.com/string/lyndon_factorization.html + +Lyndon factorization (Duval's algorithm) + +A Lyndon word is a non-empty string that is strictly smaller than all of its +proper rotations. Every string can be written uniquely as a concatenation of +Lyndon words w1 >= w2 >= ... >= wk. Duval's algorithm finds this factorization. + +Time Complexity: O(n) - where n is the length of the string +Space Complexity: O(1) extra, apart from the output +""" + + +def lyndon_factorization(input_str: str) -> list[str]: + """ + Return the Lyndon factorization of the given string. + + >>> lyndon_factorization("abab") + ['ab', 'ab'] + >>> lyndon_factorization("banana") + ['b', 'an', 'an', 'a'] + >>> lyndon_factorization("aaa") + ['a', 'a', 'a'] + >>> lyndon_factorization("abc") + ['abc'] + >>> lyndon_factorization("cba") + ['c', 'b', 'a'] + >>> lyndon_factorization("a") + ['a'] + >>> lyndon_factorization("") + [] + >>> lyndon_factorization(123) + Traceback (most recent call last): + ... + TypeError: object of type 'int' has no len() + """ + length = len(input_str) + factors: list[str] = [] + start = 0 + + while start < length: + # `compare` scans ahead while the text keeps repeating the current word + compare = start + 1 + candidate = start + while compare < length and input_str[candidate] <= input_str[compare]: + if input_str[candidate] < input_str[compare]: + candidate = start + else: + candidate += 1 + compare += 1 + + # The repeated Lyndon word has length (compare - candidate) + word_length = compare - candidate + while start <= candidate: + factors.append(input_str[start : start + word_length]) + start += word_length + + return factors + + +if __name__ == "__main__": + import doctest + + doctest.testmod()