1

让我坦率地说,我编码是为了好玩,这是我过去几天在业余时间一直在努力的一个编码挑战。挑战在于我得到一堆由空格(文档)分隔的单词,然后是列表中的几个搜索词。我必须在文档中找到最接近这些 searchTerms 的位置。基本上,找到包含所有 searchTerms 的文档的最小子集并输出该子集。到目前为止,我的功能似乎在我的系统上运行。但是,当我上传时,我被告知我的算法执行时间太长。我的想法是在文档中找到 searchTerm 的每个实例,然后针对它运行 itertools.product()。然后我测试每一个,根据索引值确定哪一个是最短的。这是我到目前为止所拥有的:

def answer(document, searchTerms):
    from itertools import product

    #build a list of the input document
    document = document.split()

    index = []
    #find all indexes for the searchTerms and build a list of lists 
    for w in searchTerms:
        index.append([i for i,x in enumerate(document) if x == w])

    #build iterator of all possible combinations of indexes for each of the searchTerms
    combinations = product(*index)

    #recover memory
    del index

    #build tuple of minimum distance between all search terms
    shortest  = min(((max(x) - min(x),x) for x in combinations),key=lambda x: x[0])

    return (' '.join(document[min(shortest[1]):max(shortest[1])+1]))

我尝试使用多处理来加速我的代码部分,但还没有完全掌握语法。例如:

from multiprocessing import Pool
p = Pool(processes=2)
shortest = p.map(min_max,combinations)

def min_max(combinations):
    return min(((max(x) - min(x),x) for x in combinations))

结果是:

Traceback (most recent call last):
File "./searchTerms2.py", line 65, in <module>
print (answer(document,searchTerms))
File "./searchTerms2.py", line 45, in answer
shortest = p.map(min_max,combinations)
File "/usr/lib/python2.7/multiprocessing/pool.py", line 251, in map
return self.map_async(func, iterable, chunksize).get()
File "/usr/lib/python2.7/multiprocessing/pool.py", line 567, in get
raise self._value
TypeError: 'int' object is not iterable

任何指针将不胜感激。有没有更好的方法来解决这个问题?有没有可以提高效率的地方?

--EDIT-- 问题的进一步解释:

document = 'this is a song that never ends it goes on and on my friend some people started singing it not knowing what it was and they will continue singing it forever just because this is the song'

searchTerms = ['this', 'goes','on']

应该导致:

'this is a song that never ends it goes on'

这适用于我当前的算法,但如果给定更大的文档和 searchTerms,则速度不够快。我希望这更清楚...

我一直在计时我的代码,看起来我最大的性能打击来自:

shortest  = min(((max(x) - min(x),x) for x in combinations),key=lambda x: x[0])

当我增加“文档”中的单词数量并在“搜索词”中添加额外的搜索词时,我看到该行的性能受到很大影响。其他一切都与我能说的相差无几。

4

2 回答 2

1

我已经考虑这个问题一天了,我觉得它很有趣。将“文档”视为一条线,将每个“单词”视为该线上的一个点会有所帮助。然后,任何解决方案都是用左侧(开始)和右侧(结束)覆盖这条线的一部分的窗口/范围。

尝试的解决方案: Mad Physicist 的解决方案不起作用的原因是它从这条线上的一个点开始,并将该点与每个其他点之间的距离视为正交,而实际上它们包含大量重叠。它只选择每个匹配搜索词的最近点,这限制了搜索的解决方案空间,因此错过了一些解决方案。不难找到一个例子,比如:

document = 'a x x d x x x a j x x'
searchTerms = 'a d j'.split()

它从最接近的 开始,d然后选择最接近的a,当进一步a将产生更短的整体解决方案时。

蛮力解决方案: 您在问题中的解决方案product用于生成可能的解决方案并检查每个解决方案。对于像您还发布的示例这样的小问题来说,这很好并且实际上非常快,但是随着文档长度的增长,尤其是搜索词的数量,组合的数量product会迅速增长。

新解决方案: 我们可以做的第一件事是意识到任何不包括其最小和最大索引之间的所有点的组合都是无效的。这消除了很多组合,因此您实际上只选择(开始结束)点的组合,而不管搜索词的数量。

虽然可能有一些花哨的数学公式来生成这些组合,但我采取了不同的方法......

如果您将每个搜索词的索引单独视为从最低索引到最高索引的迷你窗口,则很明显,解决方案的结束索引的下限是所有这些范围的最大起始索引。这是因为结束索引较低的任何窗口都不会包含此搜索词。起始索引的下限就是最低匹配索引。

这(开始结束)必须是一个解决方案,因此我们将其用作初始猜测,然后遵循以下过程:

它有助于创建所有匹配索引的平面列表并在此列表上工作,因为所有其他索引都不相关。在这种情况下start = 0.

开始索引提升到下一个匹配索引(start++在平面列表上)。这会将最左边的匹配项弹出窗口。取不小于start的匹配的下一个索引。如果这个索引已经在范围内,那么我们减少了冗余匹配并有另一个解决方案。如果此索引超出右侧范围,则移动end以扩大范围以再次包含此匹配项。如果没有更多可用的匹配项,那么我们已经用完了解决方案。

重复这个过程,直到没有更多的解决方案,跟踪哪个解决方案产生最短的. 这是最终的解决方案。 range = end - start

测试:

为了在测试中获得更多变化并验证我的解决方案产生的解决方案与原始解决方案相同,我随机k从以下内容中获取搜索词document

import random
terms = random.sample(document.split(), random.randint(3,5))
print(terms)
s1 = answer_product(document, terms)
s2 = answer_window(document, terms)

assert s1 == s2

然后尝试做一个我使用的简单基准测试:

import timeit
N = 1000
for j in xrange(2,8):
    terms = random.sample(document.split(), j)
    print(N,j)
    print('window: %s s'%timeit.timeit(lambda: answer_window(document*4, terms), number=N))
    print('product: %s s'%timeit.timeit(lambda: answer_product(document*4, terms), number=N))

在我的电脑上,对于 . 的小机箱N=1000,k=2,它们都非常快t~=0.03s。但是,随着k增长到k=7answer_product增长到t>20swhile所需的时间answer_window仍然是t~=0.03s。请注意,由于我没有用于测试的实际“文档”,因此我只是将示例乘以 4 以增加搜索量。

╔═════╦═══╦═══════════════════╦════════════════════╦═══════╗ ║ N ║ k ║ answer_window (s) ║ answer_product (s) ║ p/w ║ ╠═════╬═══╬═══════════════════╬════════════════════╬═══════╣ ║ 1e3 ║ 2 ║ 0.0231 ║ 0.0347 ║ 1.5 ║ ║ 1e3 ║ 3 ║ 0.0227 ║ 0.058 ║ 2.55 ║ ║ 1e3 ║ 4 ║ 0.025 ║ 0.242 ║ 9.68 ║ ║ 1e3 ║ 5 ║ 0.0326 ║ 3.044 ║ 93.4 ║ ║ 1e3 ║ 6 ║ 0.035 ║ 11.55 ║ 330 ║ ║ 1e3 ║ 7 ║ 0.0299 ║ 23.82 ║ 797 ║ ║ 1e5 ║ 2 ║ 2.2 ║ 2.524 ║ 1.15 ║ ║ 1e5 ║ 3 ║ 2.195 ║ 2.743 ║ 1.25 ║ ║ 1e5 ║ 4 ║ 3.272 ║ 46.51 ║ 14.2 ║ ║ 1e5 ║ 5 ║ 3.74 ║ 67.71 ║ 18.1 ║ ║ 1e5 ║ 6 ║ 3.52 ║ 1137 ║ 323 ║ ║ 1e5 ║ 7 ║ 3.98 ║ 4519 ║ 1135 ║ ╚═════╩═══╩═══════════════════╩════════════════════╩═══════╝

代码:

def answer_window(doc, terms):
    doc = doc.split()
    # create a grouping of indices by match and a flat array of all match
    # indices
    index = {w:[] for w in terms}
    indices = []
    j = 0
    for (i, w) in enumerate(doc):
        if w in index:
            # save real doc indices in flat array and use indices into that
            # array to simplify stepping.  both are automatically ordered
            indices.append(i)
            index[w].append(j)
            j += 1
    # find the maximum leftmost match index.  this is the lower bound on the
    # right side of the solution window
    highest_min = max(v[0] for v in index.values())

    # start with lowest minimum index (first) and highest minimum index (which
    # is the lower bound on the right side). this must be a solution.
    # then look for a shorter one by stepping the left side, replacing lost
    # matches from the right (expanding when necessary) until the left cannot
    # be advanced anymore.  this will cover all possible solution windows and the
    # one with the shortest length is saved
    start, end = 0, highest_min
    sol = start, end
    dsol = indices[sol[1]]-indices[sol[0]]
    while True:
        # pop leftmost match
        pop = doc[indices[start]]
        start += 1
        # need to make sure we still have the match we popped in the range
        for j in index[pop]:
            if j >= start:
                # another copy to the right!
                if j > end:
                    # must expand end to include the replacement
                    end = j
                    if indices[end]-indices[start] < dsol:
                        # new window is shorter than sol
                        sol = start, end
                        dsol = indices[sol[1]]-indices[sol[0]]
                elif indices[end]-indices[start] < dsol:
                    # the replacement is already inside the range, and moving
                    # the left side made the window smaller than sol
                    sol = start,end
                    dsol = indices[sol[1]]-indices[sol[0]]
                break # done with this pop
            else:
                # this match is left of our window
                pass
        else:
            # found no replacement, can't shrink left side anymore so we are
            # out of solutions
            break
    return (' '.join(doc[indices[sol[0]]:indices[sol[1]]+1]))
于 2016-03-09T22:33:07.287 回答
0

代码中的主要减速来自于您在不同单词之间寻找所有索引组合的事实。显然,这些组合中的大多数甚至都不符合最短运行的条件。这是一种应该运行得更快的算法:

  1. 查找所有搜索词的索引,按搜索词分隔(正如您所做的那样)
  2. 使用匹配最少的术语作为基础。
  3. 对于每一次出现的基本术语,找到每个其他术语中最接近的(请注意,这比计算每个组合的距离要快得多)。最近邻居的最大分布是候选运行的长度。
  4. 找到最短的候选运行并将其返回。

此版本使用字典推导和key参数 to min。但是,它不使用__builtins__模块之外的任何东西。下面的代码在 Python 2.7 和 3.5 下运行:

def answer(document, searchTerms):
    #build a list of the input document
    document = document.split()

    # construct list of indices of occurrences for each term
    indices = {w: [i for i,x in enumerate(document) if x == w] for w in searchTerms}

    # find the least frequent term and isolate it
    leastFrequent = min(indices.keys(), key=lambda x: len(indices[x]))
    loopIndex = indices[leastFrequent]
    del indices[leastFrequent]

    # for each element of leastFrequent, compute the nearest distance to each other item
    candidates = [None] * len(loopIndex)
    for index, element in enumerate(loopIndex):
        neighbors = [None] * len(indices)

        # find the distance to the nearest neighbor in each other list
        for ind, term in enumerate(indices):
            neighbors[ind] = min(indices[term], key=lambda x, e=element: abs(x - e))

        # the run length is the maximum of the maximum and element minus the minimum of the minimum and element
        start = min(min(neighbors), element)
        end = max(max(neighbors), element) + 1
        length = end - start
        candidates[index] = length, start, end

    # get the shortest candidate segment
    winner = min(candidates, key=lambda x: x[0])
    return ' '.join(document[winner[1]:winner[2]])

如果有s搜索词出现在(几何)平均k时间上,则该算法将在大约O(k * s * k) = O(s * k^2)时间内运行。的因素k来自循环结束element和内部的调用min。的因素k来自于循环term。通过以最不频繁的元素为基础,我们k显着减少了其中一项。特别是对于其中一项只出现一次的情况,保证在每一种可能的组合中,所以外层循环只运行一次。

为了比较,您的实现使用itertools.product,它产生s嵌套循环,每个循环都运行k迭代。这大约需要O(k^s)运行时间。

于 2016-03-07T17:49:45.850 回答