这是一种动态编程的方法——想法是将字符串表示为一个矩阵来比较每个字符对。例如,“你好黄色”可以翻译成下面的矩阵。请注意,我们所追求的序列现在可以识别为1s 的最长对角序列(当然不包括对角线),这给了我们“hello”。
# h e l l o y e l l o w
h [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
e [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]
l [0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0]
l [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0]
o [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0]
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
y [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
e [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
l [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
l [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
o [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
w [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
事实上,我们甚至不需要完整的网格。将具有相同字符的坐标收集到一个集合中,然后得到最长的子序列就足够了,这或多或少是直截了当的。
def get_pairs(s):
"""
Input: a string or list of characters
Output: a set {(x, y), (x2, y2), ...} where s[x] == s[y]
"""
n = len(s)
pairs = set()
# This iterates through the upper half of the imaginary matrix
for y in range(n):
for x in range(y+1, n):
if s[x] == s[y]:
pairs.add((x,y)) # collect matching pairs
return pairs
# There's some potential for optimization here, but you get the idea.
def longest_subsequence(pairs):
"""
Input: A sequence of coordinates [(x, y), (x2, y2), ...]
Output: The longest subsequence where [(x, y), (x+1, y+1), (x+2, y+2)] applies
"""
longest = []
for p in pairs:
seq = [p]
x, y = p
# keep collecting items on the diagonal
while True:
x, y = x+1, y+1
if (x,y) in pairs:
seq.append((x,y))
else:
break
if len(seq) > len(longest):
longest = seq
return longest
运行一些测试,这似乎按预期工作,但有一个问题:如果字符串仅包含一个字符,则结果似乎违反直觉。一般来说,重叠匹配似乎是个问题,但你对边界情况没有多说。
test = ['hello yellow',
'hello hello world',
'aaaaabaaaaa',
'aaaaaaaaaa' # this is problematic
]
for s in test:
pairs = get_pairs(s)
result = longest_subsequence(pairs)
print(f'{s=}')
print(f'{result=}')
print(repr(''.join(s[i] for i,_ in result)))
print()
结果:
s='hello yellow'
result=[(7, 1), (8, 2), (9, 3), (10, 4)]
'ello'
s='hello hello world'
result=[(6, 0), (7, 1), (8, 2), (9, 3), (10, 4), (11, 5)]
'hello '
s='aaaaabaaaaa'
result=[(6, 0), (7, 1), (8, 2), (9, 3), (10, 4)]
'aaaaa'
s='aaaaaaaaaa'
result=[(1, 0), (2, 1), (3, 2), (4, 3), (5, 4), (6, 5), (7, 6), (8, 7), (9, 8)]
'aaaaaaaaa'
编辑:忘了概括。该矩阵还包含足够的信息来收集多个匹配项。为此,我们可以将第二个函数替换为以下收集所有子序列的函数。
from collections import defaultdict
def find_all_subsequences(pairs):
"""
Input: A sequence of coordinates [(x, y), (x2, y2), ...]
Output: A dictionary mapping substrings to sets of subsequences {str: {((x, y),), ...}}
"""
def to_string(result):
return ''.join(s[i] for i,_ in result)
output = defaultdict(set)
seen = set() # to prevent double matches
for p in pairs:
if p in seen:
continue
seq = [p]
x, y = p
# collect diagonal sequences
while True:
x, y = x+1, y+1
if (x,y) in pairs:
seq.append((x,y))
# keep track of visited elements
seen.add((x, y))
# add to the output
output[to_string(seq)].add(tuple(seq))
else:
output[to_string(seq)].add(tuple(seq))
break
return output
这将类似于上面的函数,但获取所有组合,例如,
s = "hello yellow marshmellow, don't yell"
pairs = get_pairs(s)
result = find_all_subsequences(pairs)
for k in sorted(result, key=len, reverse=True):
v = result[k]
print(f"Sequence: {k!r}, length: {len(k)}, occurrences: {len(v)+1}")
#print(v) # uncomment to see the raw data
返回所有子序列,如下所示。您可以根据需要对其进行过滤。
Sequence: ' yell', length: 5, occurrences: 2
Sequence: 'ellow', length: 5, occurrences: 2
Sequence: ' yel', length: 4, occurrences: 2
Sequence: 'llow', length: 4, occurrences: 2
Sequence: 'ello', length: 4, occurrences: 4
Sequence: ' ye', length: 3, occurrences: 2
Sequence: 'llo', length: 3, occurrences: 2
Sequence: 'ell', length: 3, occurrences: 6
Sequence: ' y', length: 2, occurrences: 2
Sequence: 'll', length: 2, occurrences: 2
Sequence: 'el', length: 2, occurrences: 6
Sequence: 'lo', length: 2, occurrences: 2
Sequence: 'h', length: 1, occurrences: 2
Sequence: 'o', length: 1, occurrences: 4
Sequence: 'l', length: 1, occurrences: 17
Sequence: 'm', length: 1, occurrences: 2
Sequence: ' ', length: 1, occurrences: 6