From 5bd9869a2589b7c95253660c19bd89cc45502b8f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Jul 2026 06:58:45 +0000 Subject: [PATCH 1/3] Refactor acl_string: convert namespace class to proper String class Replace the static-method-only `class string` (which was just a namespace) with a `String` class that wraps a sequence and exposes suffix_array, lcp_array, and z_algorithm as instance methods. Internal helper _sa_is is now a module-level private function. Added __len__, __getitem__, and __repr__ for basic sequence protocol support. --- acl_string.py | 238 ++++++++++++++++++++------------------- tests/test_acl_string.py | 45 +++++--- 2 files changed, 152 insertions(+), 131 deletions(-) diff --git a/acl_string.py b/acl_string.py index 6110343..8fe8617 100644 --- a/acl_string.py +++ b/acl_string.py @@ -1,122 +1,121 @@ -class string: - def sa_is(s, upper): - n = len(s) - if n == 0: - return [] - if n == 1: - return [0] - if n == 2: - if s[0] < s[1]: - return [0, 1] - else: - return [1, 0] - sa = [0] * n - ls = [0] * n - for i in range(n - 2, -1, -1): - ls[i] = ls[i + 1] if (s[i] == s[i + 1]) else (s[i] < s[i + 1]) - sum_l = [0] * (upper + 1) - sum_s = [0] * (upper + 1) - for i in range(n): - if not (ls[i]): - sum_s[s[i]] += 1 - else: - sum_l[s[i] + 1] += 1 - for i in range(upper + 1): - sum_s[i] += sum_l[i] - if i < upper: - sum_l[i + 1] += sum_s[i] +def _sa_is(s, upper): + n = len(s) + if n == 0: + return [] + if n == 1: + return [0] + if n == 2: + if s[0] < s[1]: + return [0, 1] + else: + return [1, 0] + sa = [0] * n + ls = [0] * n + for i in range(n - 2, -1, -1): + ls[i] = ls[i + 1] if (s[i] == s[i + 1]) else (s[i] < s[i + 1]) + sum_l = [0] * (upper + 1) + sum_s = [0] * (upper + 1) + for i in range(n): + if not (ls[i]): + sum_s[s[i]] += 1 + else: + sum_l[s[i] + 1] += 1 + for i in range(upper + 1): + sum_s[i] += sum_l[i] + if i < upper: + sum_l[i + 1] += sum_s[i] - def induce(lms): - for i in range(n): - sa[i] = -1 - buf = sum_s[:] - for d in lms: - if d == n: - continue - sa[buf[s[d]]] = d - buf[s[d]] += 1 - buf = sum_l[:] - sa[buf[s[n - 1]]] = n - 1 - buf[s[n - 1]] += 1 - for i in range(n): - v = sa[i] - if v >= 1 and not (ls[v - 1]): - sa[buf[s[v - 1]]] = v - 1 - buf[s[v - 1]] += 1 - buf = sum_l[:] - for i in range(n - 1, -1, -1): - v = sa[i] - if v >= 1 and ls[v - 1]: - buf[s[v - 1] + 1] -= 1 - sa[buf[s[v - 1] + 1]] = v - 1 + def induce(lms): + for i in range(n): + sa[i] = -1 + buf = sum_s[:] + for d in lms: + if d == n: + continue + sa[buf[s[d]]] = d + buf[s[d]] += 1 + buf = sum_l[:] + sa[buf[s[n - 1]]] = n - 1 + buf[s[n - 1]] += 1 + for i in range(n): + v = sa[i] + if v >= 1 and not (ls[v - 1]): + sa[buf[s[v - 1]]] = v - 1 + buf[s[v - 1]] += 1 + buf = sum_l[:] + for i in range(n - 1, -1, -1): + v = sa[i] + if v >= 1 and ls[v - 1]: + buf[s[v - 1] + 1] -= 1 + sa[buf[s[v - 1] + 1]] = v - 1 - lms_map = [-1] * (n + 1) - m = 0 - for i in range(1, n): - if not (ls[i - 1]) and ls[i]: - lms_map[i] = m - m += 1 - lms = [] - for i in range(1, n): - if not (ls[i - 1]) and ls[i]: - lms.append(i) - induce(lms) - if m: - sorted_lms = [] - for v in sa: - if lms_map[v] != -1: - sorted_lms.append(v) - rec_s = [0] * m - rec_upper = 0 - rec_s[lms_map[sorted_lms[0]]] = 0 - for i in range(1, m): - l = sorted_lms[i - 1] - r = sorted_lms[i] - end_l = lms[lms_map[l] + 1] if (lms_map[l] + 1 < m) else n - end_r = lms[lms_map[r] + 1] if (lms_map[r] + 1 < m) else n - same = True - if end_l - l != end_r - r: + lms_map = [-1] * (n + 1) + m = 0 + for i in range(1, n): + if not (ls[i - 1]) and ls[i]: + lms_map[i] = m + m += 1 + lms = [] + for i in range(1, n): + if not (ls[i - 1]) and ls[i]: + lms.append(i) + induce(lms) + if m: + sorted_lms = [] + for v in sa: + if lms_map[v] != -1: + sorted_lms.append(v) + rec_s = [0] * m + rec_upper = 0 + rec_s[lms_map[sorted_lms[0]]] = 0 + for i in range(1, m): + l = sorted_lms[i - 1] + r = sorted_lms[i] + end_l = lms[lms_map[l] + 1] if (lms_map[l] + 1 < m) else n + end_r = lms[lms_map[r] + 1] if (lms_map[r] + 1 < m) else n + same = True + if end_l - l != end_r - r: + same = False + else: + while l < end_l: + if s[l] != s[r]: + break + l += 1 + r += 1 + if (l == n) or (s[l] != s[r]): same = False - else: - while l < end_l: - if s[l] != s[r]: - break - l += 1 - r += 1 - if (l == n) or (s[l] != s[r]): - same = False - if not (same): - rec_upper += 1 - rec_s[lms_map[sorted_lms[i]]] = rec_upper - rec_sa = string.sa_is(rec_s, rec_upper) - for i in range(m): - sorted_lms[i] = lms[rec_sa[i]] - induce(sorted_lms) - return sa + if not (same): + rec_upper += 1 + rec_s[lms_map[sorted_lms[i]]] = rec_upper + rec_sa = _sa_is(rec_s, rec_upper) + for i in range(m): + sorted_lms[i] = lms[rec_sa[i]] + induce(sorted_lms) + return sa + - def suffix_array_upper(s, upper): - assert 0 <= upper - for d in s: - assert 0 <= d and d <= upper - return string.sa_is(s, upper) +class String: + def __init__(self, s): + self._s = s - def suffix_array(s): + def suffix_array(self): + s = self._s n = len(s) - if type(s) == str: - s2 = [ord(i) for i in s] - return string.sa_is(s2, 255) - else: - idx = list(range(n)) - idx.sort(key=lambda x: s[x]) - s2 = [0] * n - now = 0 - for i in range(n): - if i and s[idx[i - 1]] != s[idx[i]]: - now += 1 - s2[idx[i]] = now - return string.sa_is(s2, now) + if isinstance(s, str): + return _sa_is([ord(c) for c in s], 255) + idx = sorted(range(n), key=lambda x: s[x]) + s2 = [0] * n + now = 0 + for i in range(n): + if i and s[idx[i - 1]] != s[idx[i]]: + now += 1 + s2[idx[i]] = now + return _sa_is(s2, now) - def lcp_array(s, sa): + def lcp_array(self, sa=None): + if sa is None: + sa = self.suffix_array() + s = self._s n = len(s) assert n >= 1 rnk = [0] * n @@ -137,7 +136,8 @@ def lcp_array(s, sa): lcp[rnk[i] - 1] = h return lcp - def z_algorithm(s): + def z_algorithm(self): + s = self._s n = len(s) if n == 0: return [] @@ -153,3 +153,15 @@ def z_algorithm(s): i += 1 z[0] = n return z + + def __len__(self): + return len(self._s) + + def __getitem__(self, idx): + return self._s[idx] + + def __str__(self): + return str(self._s) + + def __repr__(self): + return f"String({self._s!r})" diff --git a/tests/test_acl_string.py b/tests/test_acl_string.py index cf74311..d11c486 100644 --- a/tests/test_acl_string.py +++ b/tests/test_acl_string.py @@ -6,16 +6,15 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -import acl_string +from acl_string import String class TestACLString(unittest.TestCase): - """Test cases for acl_string module""" - - def practice2_i(self, S, answer): - sa = acl_string.string.suffix_array(S) - res = (len(S) * (len(S) + 1)) // 2 - for x in acl_string.string.lcp_array(S, sa): + def practice2_i(self, s, answer): + st = String(s) + sa = st.suffix_array() + res = (len(s) * (len(s) + 1)) // 2 + for x in st.lcp_array(sa): res -= x self.assertEqual(res, answer) @@ -26,19 +25,29 @@ def test_practice2_i(self): self.practice2_i("aaaaa", 5) def test_suffix_array(self): - """Test suffix array functionality""" - # TODO: Add test cases for suffix array - pass - - def test_z_algorithm(self): - """Test Z algorithm functionality""" - # TODO: Add test cases for Z algorithm - pass + sa = String("abab").suffix_array() + self.assertEqual(sa, [2, 0, 3, 1]) def test_lcp_array(self): - """Test LCP array functionality""" - # TODO: Add test cases for LCP array - pass + st = String("abab") + sa = st.suffix_array() + lcp = st.lcp_array(sa) + self.assertEqual(lcp, [2, 0, 1]) + + def test_lcp_array_auto_sa(self): + # lcp_array without explicit sa computes it internally + lcp = String("abab").lcp_array() + self.assertEqual(lcp, [2, 0, 1]) + + def test_z_algorithm(self): + self.assertEqual(String("aabxaa").z_algorithm(), [6, 1, 0, 0, 2, 1]) + self.assertEqual(String("abcabcabc").z_algorithm(), [9, 0, 0, 6, 0, 0, 3, 0, 0]) + + def test_len_and_getitem(self): + st = String("hello") + self.assertEqual(len(st), 5) + self.assertEqual(st[0], "h") + self.assertEqual(st[-1], "o") if __name__ == "__main__": From 79c515b5603aef839e9faebc39a07a99e4b0cb3b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Jul 2026 07:00:39 +0000 Subject: [PATCH 2/3] Add comprehensive tests for acl_string 31 test cases covering: - suffix_array: empty/single/two-char, all-same, known strings (abab, mississippi, abcbcba), integer list input, practice2-I problem - lcp_array: basic, auto-SA, all-same, all-distinct, single-char, mississippi boundary checks - z_algorithm: empty/single, all-same, known patterns, Z-property invariant, pattern search application - String protocol: __len__, __getitem__ (slice), __repr__, __str__, integer list getitem --- tests/test_acl_string.py | 181 +++++++++++++++++++++++++++++++++------ 1 file changed, 157 insertions(+), 24 deletions(-) diff --git a/tests/test_acl_string.py b/tests/test_acl_string.py index d11c486..471ea0e 100644 --- a/tests/test_acl_string.py +++ b/tests/test_acl_string.py @@ -9,45 +9,178 @@ from acl_string import String -class TestACLString(unittest.TestCase): - def practice2_i(self, s, answer): - st = String(s) - sa = st.suffix_array() - res = (len(s) * (len(s) + 1)) // 2 - for x in st.lcp_array(sa): - res -= x - self.assertEqual(res, answer) +class TestSuffixArray(unittest.TestCase): + def _check_sa(self, s, sa): + """suffix_array の正当性を検証: SA が辞書順ソートになっているか""" + n = len(s) + self.assertEqual(len(sa), n) + self.assertEqual(sorted(sa), list(range(n))) + suffixes = [s[i:] for i in sa] + for i in range(len(suffixes) - 1): + self.assertLessEqual(suffixes[i], suffixes[i + 1]) + + def test_empty(self): + self.assertEqual(String("").suffix_array(), []) + + def test_single_char(self): + self.assertEqual(String("a").suffix_array(), [0]) + + def test_two_chars_sorted(self): + self.assertEqual(String("ab").suffix_array(), [0, 1]) + + def test_two_chars_reversed(self): + self.assertEqual(String("ba").suffix_array(), [1, 0]) + + def test_all_same(self): + sa = String("aaaa").suffix_array() + self._check_sa("aaaa", sa) + + def test_abab(self): + self.assertEqual(String("abab").suffix_array(), [2, 0, 3, 1]) + + def test_known_mississippi(self): + s = "mississippi" + sa = String(s).suffix_array() + self._check_sa(s, sa) + + def test_known_abcbcba(self): + s = "abcbcba" + sa = String(s).suffix_array() + self._check_sa(s, sa) + + def test_integer_list(self): + s = [3, 1, 4, 1, 5, 9, 2, 6] + sa = String(s).suffix_array() + self._check_sa(s, sa) + + def test_integer_list_all_same(self): + s = [0, 0, 0, 0] + sa = String(s).suffix_array() + self._check_sa(s, sa) def test_practice2_i(self): - self.practice2_i("abcbcba", 21) - self.practice2_i("mississippi", 53) - self.practice2_i("ababacaca", 33) - self.practice2_i("aaaaa", 5) + """ACL practice2 問題 I: 異なる部分文字列の数""" + cases = [ + ("abcbcba", 21), + ("mississippi", 53), + ("ababacaca", 33), + ("aaaaa", 5), + ] + for s, expected in cases: + with self.subTest(s=s): + st = String(s) + sa = st.suffix_array() + res = (len(s) * (len(s) + 1)) // 2 + for x in st.lcp_array(sa): + res -= x + self.assertEqual(res, expected) - def test_suffix_array(self): - sa = String("abab").suffix_array() - self.assertEqual(sa, [2, 0, 3, 1]) - def test_lcp_array(self): +class TestLcpArray(unittest.TestCase): + def test_basic(self): st = String("abab") sa = st.suffix_array() + self.assertEqual(st.lcp_array(sa), [2, 0, 1]) + + def test_auto_sa(self): + # sa を省略すると内部で suffix_array() を計算する + self.assertEqual(String("abab").lcp_array(), [2, 0, 1]) + + def test_all_same(self): + st = String("aaaa") + sa = st.suffix_array() lcp = st.lcp_array(sa) - self.assertEqual(lcp, [2, 0, 1]) + # sa = [3,2,1,0] ("a","aa","aaa","aaaa") -> LCP は 1, 2, 3 + self.assertEqual(lcp, [1, 2, 3]) - def test_lcp_array_auto_sa(self): - # lcp_array without explicit sa computes it internally - lcp = String("abab").lcp_array() - self.assertEqual(lcp, [2, 0, 1]) + def test_all_distinct(self): + st = String("abcd") + sa = st.suffix_array() + lcp = st.lcp_array(sa) + self.assertEqual(lcp, [0, 0, 0]) + + def test_single_char(self): + st = String("a") + sa = st.suffix_array() + self.assertEqual(st.lcp_array(sa), []) - def test_z_algorithm(self): + def test_mississippi(self): + s = "mississippi" + st = String(s) + sa = st.suffix_array() + lcp = st.lcp_array(sa) + self.assertEqual(len(lcp), len(s) - 1) + # 各 LCP 値は非負かつ対応する suffix の長さを超えない + for i, v in enumerate(lcp): + self.assertGreaterEqual(v, 0) + self.assertLessEqual(v, min(len(s) - sa[i], len(s) - sa[i + 1])) + + +class TestZAlgorithm(unittest.TestCase): + def test_empty(self): + self.assertEqual(String("").z_algorithm(), []) + + def test_single_char(self): + self.assertEqual(String("a").z_algorithm(), [1]) + + def test_all_same(self): + self.assertEqual(String("aaaa").z_algorithm(), [4, 3, 2, 1]) + + def test_aabxaa(self): self.assertEqual(String("aabxaa").z_algorithm(), [6, 1, 0, 0, 2, 1]) + + def test_abcabcabc(self): self.assertEqual(String("abcabcabc").z_algorithm(), [9, 0, 0, 6, 0, 0, 3, 0, 0]) - def test_len_and_getitem(self): + def test_abacaba(self): + self.assertEqual(String("abacaba").z_algorithm(), [7, 0, 1, 0, 3, 0, 1]) + + def test_all_distinct(self): + z = String("abcde").z_algorithm() + self.assertEqual(z[0], 5) + for v in z[1:]: + self.assertEqual(v, 0) + + def test_z_property(self): + """z[i] は s[i:i+z[i]] == s[0:z[i]] を満たす""" + s = "aababcabc" + z = String(s).z_algorithm() + self.assertEqual(z[0], len(s)) + for i in range(1, len(s)): + self.assertEqual(s[: z[i]], s[i : i + z[i]]) + + def test_pattern_search(self): + """Z アルゴリズムによるパターン検索""" + text = "abcabcabc" + pat = "abc" + combined = pat + "$" + text + z = String(combined).z_algorithm() + plen = len(pat) + matches = [i - plen - 1 for i in range(plen + 1, len(combined)) if z[i] >= plen] + self.assertEqual(matches, [0, 3, 6]) + + +class TestStringProtocol(unittest.TestCase): + def test_len(self): + self.assertEqual(len(String("hello")), 5) + self.assertEqual(len(String("")), 0) + + def test_getitem(self): st = String("hello") - self.assertEqual(len(st), 5) self.assertEqual(st[0], "h") self.assertEqual(st[-1], "o") + self.assertEqual(st[1:3], "el") + + def test_repr(self): + self.assertIn("hello", repr(String("hello"))) + + def test_str(self): + self.assertIn("hello", str(String("hello"))) + + def test_integer_list_getitem(self): + st = String([1, 2, 3]) + self.assertEqual(st[0], 1) + self.assertEqual(st[1:], [2, 3]) if __name__ == "__main__": From 347ee429d3f5f50a4e34703c93102ac167159b26 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Jul 2026 07:05:44 +0000 Subject: [PATCH 3/3] Fix test_basic: update acl_string import check from 'string' to 'String' --- tests/test_basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_basic.py b/tests/test_basic.py index 158374a..75329bd 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -92,7 +92,7 @@ def test_string_import(self): import acl_string # Check for common string algorithms - self.assertTrue(hasattr(acl_string, "string")) + self.assertTrue(hasattr(acl_string, "String")) def test_two_sat_import(self): """Test 2-SAT module import"""