在优化我的应用程序性能的同时,我在几行(Python)代码中遇到了巨大的性能瓶颈。
我有N个令牌。每个令牌都有一个分配给它的值。一些令牌相互矛盾(例如,令牌8和12不能“共存”)。我的工作是找到k-best令牌组。一组标记的值只是其中标记值的总和。
Naïve算法(我已经实现了......):
真实世界数字 - 我需要来自一组20个令牌的前10个令牌组(我为此计算了1,000,000个排列(!)),缩小到3500个非矛盾的令牌组。我的笔记本电脑花了5秒钟......
我确信我只能通过生成非矛盾的令牌组来优化步骤1 + 2。
我也非常确定我能以某种方式神奇地在一次搜索中找到最佳的令牌组,并找到一种通过减少价值来遍历令牌组的方法,从而找到我想要的10个最好的... ..
我的实际代码:
all_possibilities = sum((list(itertools.combinations(token_list, i)) for i in xrange(len(token_list)+1)), [])
all_possibilities = [list(option) for option in all_possibilities if self._no_contradiction(option)]
all_possibilities = [(option, self._probability(option)) for option in all_possibilities]
all_possibilities.sort(key = lambda result: -result[1]) # sort by descending probability
请帮帮忙?
塔尔。
答案 0 :(得分:3)
步骤1 + 2的简单方法可能如下所示:首先,定义一个令牌列表和一个矛盾字典(每个键都是一个令牌,每个值都是一组令牌)。然后,对于每个令牌采取两个动作:
result
,并使用与当前添加的令牌相矛盾的令牌增加conflicting
设置result
(选择忽略它)并转到下一个标记。所以这是一个示例代码:
token_list = ['a', 'b', 'c']
contradictions = {
'a': set(['b']),
'b': set(['a']),
'c': set()
}
class Generator(object):
def __init__(self, token_list, contradictions):
self.list = token_list
self.contradictions = contradictions
self.max_start = len(self.list) - 1
def add_no(self, start, result, conflicting):
if start < self.max_start:
for g in self.gen(start + 1, result, conflicting):
yield g
else:
yield result[:]
def add_yes(self, token, start, result, conflicting):
result.append(token)
new_conflicting = conflicting | self.contradictions[token]
for g in self.add_no(start, result, new_conflicting):
yield g
result.pop()
def gen(self, start, result, conflicting):
token = self.list[start]
if token not in conflicting:
for g in self.add_yes(token, start, result, conflicting):
yield g
for g in self.add_no(start, result, conflicting):
yield g
def go(self):
return self.gen(0, [], set())
样本用法:
g = Generator(token_list, contradictions)
for x in g.go():
print x
这是一个递归算法,所以它不会超过几千个令牌(因为Python的堆栈限制),但你可以很容易地创建一个非递归算法。
答案 1 :(得分:3)
O(n (log n))
令牌O(n + m)
或n
解决方案及字符串长度m
您的问题与NP-complete clique问题的不同之处在于,您的“冲突”图表具有结构 - 即可以将其投影到1维(可以对其进行排序)。< / p>
这意味着你可以分而治之;毕竟,非重叠范围对彼此没有影响,因此不需要探索完整的状态空间。特别是,动态编程解决方案将起作用。
[start, end)
(即包含开始,独占结束)。按令牌结束对令牌列表进行排序,我们将迭代它们。现在,诀窍在缓存中 - 你需要尝试两个选项,这看起来像一个递归(指数)搜索,但它不一定是。
J
的最佳子集包含 token[J]
,则它不能包含与该令牌重叠的任何令牌 - 特别是,因为我们按{{排序} 1}},该列表中有一个最后一个令牌token.end
,K
和K < J
: 令牌token[K].end <= token[J].start
我们可以计算也是最好的子集(或者我们已经将它缓存了)。 K
,但最好的子集只是token[J]
。token[J-1]
和子集值token[-1]
的特殊情况token[-1].end = 0
都可以形成基本情况。由于您只需要为每个令牌索引执行一次此计算,因此该部分实际上是令牌数量的线性。然而,天真地(我推荐)对标记进行排序是O(n log(n))并且在给定字符串位置的情况下找到最后一个标记索引是O(log(n)) - 重复n次;所以总的运行时间是O(n log(n))。您可以通过观察您不需要对任意列表进行排序来将其减少为O(n) - 最大字符串位置是有限的,因此您可以通过索引字符串来进行排序,但几乎肯定不值得。类似地,尽管通过二进制搜索找到一个令牌是0
,但您可以通过对齐两个列表来实现此目的 - 一个在令牌端排序,另一个在令牌开始时排序 - 从而允许log n
实现。除非O(n + m)
能够真正变大,否则它是不值得的。
如果从字符串的前面迭代到结尾,因为所有查找都显示为“返回”,您可以完全删除递归,只需直接查找给定索引的结果,因为它必须已经计算过。< / p>
这个相当含糊的解释是否有帮助?它是动态编程的基本应用,它只是缓存的一个奇特的词;所以,如果你感到困惑,那就是你应该阅读的内容。
如果你想找到top-K最佳解决方案,你需要一个凌乱但可行的扩展,它将令牌的索引不是映射到单个最佳子集,而是映射到目前为止的最佳K子集 - 显然增加了计算成本和一些额外的代码。从本质上讲,不是选择 包含或不包含n
,而是选择集合并在每个令牌索引处修剪为k-best选项。如果直接实施,那就是token[J]
。
答案 2 :(得分:2)
这是一种可能的“启发式优化”方法和一个小样本:
import itertools
# tokens in decreasing order of value (must all be > 0)
toks = 12, 11, 8, 7, 6, 2, 1
# contradictions (dict highestvaltok -> set of incompatible ones)
cont = {12: set([11, 8, 7, 2]),
11: set([8, 7, 6]),
7: set([2]),
2: set([1]),
}
rec_calls = 0
def bestgroup(toks, contdict, arein=(), contset=()):
"""Recursively compute the highest-valued non-contradictory subset of toks."""
global rec_calls
toks = list(toks)
while toks:
# find the top token compatible w/the ones in `arein`
toptok = toks.pop(0)
if toptok in contset:
continue
# try to extend with and without this toptok
without_top = bestgroup(toks, contdict, arein, contset)
contset = set(contset).union(c for c in contdict.get(toptok, ()))
newarein = arein + (toptok,)
with_top = bestgroup(toks, contdict, newarein, contset)
rec_calls += 1
if sum(with_top) > sum(without_top):
return with_top
else:
return without_top
return arein
def noncongroups(toks, contdict):
"""Count possible, non-contradictory subsets of toks."""
tot = 0
for l in range(1, len(toks) + 1):
for c in itertools.combinations(toks, l):
if any(cont[k].intersection(c) for k in c if k in contdict): continue
tot += 1
return tot
print bestgroup(toks, cont)
print 'calls: %d (vs %d of %d)' % (rec_calls, noncongroups(toks, cont), 2**len(toks))
我相信这总是会产生尽可能多的递归调用,因为可行(非矛盾)子集存在,但尚未证明它(所以我只计算两者 - 当然noncongroups
无关通过解决方案,它只是检查行为属性; - )。
如果这对您的“实际用例”基准产生可接受的加速,那么进一步优化可能会引入alpha修剪(因此您可以停止沿着您知道非生产性路径的递归 - 这是降序的动机令牌;-)和递归消除(使用函数内的显式堆栈)。但是我希望保持第一个版本的简单,因此可以很容易地理解和验证(同样,我想到的进一步优化只会有点帮助,我怀疑 - 说,充其量,将典型的运行时间减半,如果甚至那么多。)
答案 3 :(得分:2)
获取所有非矛盾的令牌组的一种非常简单的方法:
#!/usr/bin/env python
token_list = ['a', 'b', 'c']
contradictions = {
'a': set(['b']),
'b': set(['a']),
'c': set()
}
result = []
while token_list:
token = token_list.pop()
new = [set([token])]
for r in result:
if token not in contradictions or not r & contradictions[token]:
new.append(r | set([token]))
result.extend(new)
print result
答案 4 :(得分:0)
以下解决方案生成所有最大的非矛盾子集,利用了从解决方案中省略元素的事实,除非它与解决方案中的另一个元素相矛盾。
在元素t不与任何其余元素相矛盾的情况下避免第二次递归的简单优化应该有助于在矛盾数量很少的情况下使该解决方案有效。
def solve(tokens, contradictions):
if not tokens:
yield set()
else:
tokens = set(tokens)
t = tokens.pop()
for solution in solve(tokens - contradictions[t], contradictions):
yield solution | set([t])
if contradictions[t] & tokens:
for solution in solve(tokens, contradictions):
if contradictions[t] & solution:
yield solution
此解决方案还演示了动态编程(也称为memoization)可能有助于进一步提高某些类型输入的解决方案的性能。