【问题标题】:Find all pairs of strings in two lists that contain no common characters查找两个列表中不包含公共字符的所有字符串对
【发布时间】:2023-03-22 03:49:01
【问题描述】:

我有两个字符串列表,并希望找到它们之间所有不包含公共字符的字符串对。例如

list1 = ['abc', 'cde']
list2 = ['aij', 'xyz', 'abc']

desired output = [('abc', 'xyz'), ('cde', 'aij'), ('cde', 'xyz')]

我需要它尽可能高效,因为我正在处理包含数百万个字符串的列表。 目前,我的代码遵循以下一般模式:

output = []

for str1 in list1:    
    for str2 in list2:
        if len(set(str1) & set(str2)) == 0: 
             output.append((str1, str2))

这是 O(n^2) 并且需要很多小时才能运行,有人对如何加快速度有一些建议吗?也许有一种方法可以利用每个正在排序的字符串中的字符?

非常感谢您!

【问题讨论】:

  • 你需要每一对还是 99% 就足够了?
  • 你能提供一些关于字符串本身的信息吗?它们都是'abc'(3个字母)格式吗?它们是否可以有类似的字母,例如“aab”,或者它们总是不同的?列表也可以有类似的字符串吗? (例如 'xyz' 2 次等)
  • 您可以使用慢速解决方案通过对每个列表的 1/1000 进行采样,运行慢速算法,然后乘以 100 万来估算对数。可能有太多对了。
  • 99% 确实足够了,关于字符串的更多信息,它们按字母顺序排序,1 到 26 个字母之间的任意位置,字母表中的每个字母只能出现一次。字符串在两个列表中也是唯一的。
  • 伟大的建议大卫,不幸的是我怀疑我会找到任何解决方案,因此抽样方法可能会错过百万分之一的解决方案

标签: python string algorithm comparison


【解决方案1】:

这是另一种策略,专注于将集合操作降低为位旋转和组合表示同一组字母的单词:

import collections
import string


def build_index(words):
    index = collections.defaultdict(list)
    for word in words:
        chi = sum(1 << string.ascii_lowercase.index(letter) for letter in set(word))
        index[chi].append(word)
    return index


def disjoint_pairs(words1, words2):
    index1 = build_index(words1)
    index2 = build_index(words2)
    for chi1, words1 in index1.items():
        for chi2, words2 in index2.items():
            if chi1 & chi2:
                continue
            for word1 in words1:
                for word2 in words2:
                    yield word1, word2


print(list(disjoint_pairs(["abc", "cde"], ["aij", "xyz", "abc"])))

【讨论】:

  • 难以置信!这个解决方案只用了 9.5 分钟就完成了我的列表(分别包含大约 200 万个字符串)!非常感谢您的帮助:D 为高效的哈希检查建立索引的好主意
【解决方案2】:

试试这个并告诉我是否有任何改进:

import itertools

[i for i in itertools.product(list1, list2) if len(i[0]+i[1])==len(set(i[0]+i[1]))]

输出:

[('abc', 'xyz'), ('cde', 'aij'), ('cde', 'xyz')]

【讨论】:

  • 您仍在循环数万亿对,不知道为什么这会明显更好。
【解决方案3】:

分析这个算法的运行时间很棘手,但这是我首先尝试的。这个想法是,给定一个字母,我们可以将问题分成三个子问题:(没有字母的词,没有字母的词),(没有字母的词,有字母的词),(有字母的词,没有字母的词)信)。下面的代码选择这个字母(“枢轴”)来最大化消除的对数。在基本情况下,不能消除任何对,我们只是输出所有对。

Python 3,针对运行时的可读性进行了优化。

import collections


def frequencies(words):
    return collections.Counter(letter for word in words for letter in set(word))


def partition(pivot, words):
    return (
        [word for word in words if pivot not in word],
        [word for word in words if pivot in word],
    )


def disjoint_pairs(words1, words2):
    freq1 = frequencies(words1)
    freq2 = frequencies(words2)
    pivots = set(freq1.keys()) & set(freq2.keys())
    if pivots:
        pivot = max(pivots, key=lambda letter: freq1[letter] * freq2[letter])
        no1, yes1 = partition(pivot, words1)
        no2, yes2 = partition(pivot, words2)
        yield from disjoint_pairs(no1, no2)
        yield from disjoint_pairs(no1, yes2)
        yield from disjoint_pairs(yes1, no2)
    else:
        for word1 in words1:
            for word2 in words2:
                yield (word1, word2)


print(list(disjoint_pairs(["abc", "cde"], ["aij", "xyz", "abc"])))

【讨论】:

  • 我有同样的想法,但未能实现,我会试一试。感谢您的宝贵时间!
  • 从我的尝试中收到类似的结果,看来(可能)该算法中的开销实际上导致运行时间比蛮力方法慢约 100 倍(通过非常粗略的近似,即正负10 级)
  • 我想知道是否有办法让这种方法与other 一样高效。它是如此优雅。 (并不是说另一个不是。)
  • @גלעדברקן 对具有相同字母集的单词进行优化分组。我认为要缩小其余的差距,我们可能会切换到静态枢轴订单,以节省重新分区的成本。然后本质上我们可以构建一个 crit-bit 树,然后使用与此类似的算法遍历它。
【解决方案4】:

您可以将递归与生成器一起使用:

from functools import reduce
list1 = ['abc', 'cde']
list2 = ['aij', 'xyz', 'abc']
def pairs(d, c = []):
   if not d and not reduce(lambda x, y:set(x)&set(y), c):
      yield tuple(c)
   elif d:
      yield from [i for k in d[0] for i in pairs(d[1:], c+[k])]

print(list(pairs([list1, list2])))

输出:

[('abc', 'xyz'), ('cde', 'aij'), ('cde', 'xyz')]

此答案使用functools.reduce 来处理输入列表数量大于两个的情况。这样,潜在子列表中所有元素的集合交集就可以更容易地计算出来。

【讨论】:

    【解决方案5】:

    我在考虑如何利用字符串是有序的这一事实,并想出了以下粗略的想法:

    第 1 步:对第二个列表进行排序,仅针对字符串的第一个字符:

    from itertools import product
    
    list1 = ['abc', 'cde']
    list2 = ['aij', 'xyz', 'abc']
    list2 = sorted(list2, key=(lambda s: s[0]))
    

    第 2 步:为字母表中的每个字符c 查找list2 中第一个元素s 的索引,其中第一个字符s[0] 大于c。 (这里我假设字符串中的所有元素实际上都是字母表中的字符并且都是小写的!)

    alphabet = 'abcdefghijklmnopqrstuvwxyz'
    bounds = {}
    for c in alphabet:
        bounds[c] = len(list2)
        for i, s in enumerate(list2):
            if c < s[0]:
                bounds[c] = i
                break
    

    第 3 步:通过这些准备,可以稍微优化两个列表的迭代。对于来自list1 的元素str1,您很可能不必遍历list2 中的所有元素并进行检查:在某一点上,您知道list2 中的其余字符串与str1.

    output = []
    for str1 in list1:
        last_char = str1[-1]
        for str2 in list2[:bounds[last_char]]:
            if len(set(str1) & set(str2)) == 0:
                output.append((str1, str2))
        output += [*product([str1], list2[bounds[last_char]:])]
    

    注意事项:我不知道这是否真的有帮助!您必须对list2 进行排序,并且仍然必须收集某些组合(这部分:output += [*product([str1], list2[bounds[last_char]:])]),而我没有'不知道要多少钱。

    PS:这里的代码只是为了说明这个想法(我希望它不包含任何错误——这里已经很晚了),实际的实现看起来会有所不同。

    【讨论】:

    • 不用担心,在这个问题上排序基本上是无成本的
    猜你喜欢
    • 2016-04-20
    • 1970-01-01
    • 1970-01-01
    • 2013-09-13
    • 1970-01-01
    • 2011-10-01
    相关资源
    最近更新 更多