找到“最好的”完整子图

时间:2010-06-28 13:37:51

标签: python graph

在优化我的应用程序性能的同时,我在几行(Python)代码中遇到了巨大的性能瓶颈。

我有N个令牌。每个令牌都有一个分配给它的值。一些令牌相互矛盾(例如,令牌8和12不能“共存”)。我的工作是找到k-best令牌组。一组标记的值只是其中标记值的总和。

Naïve算法(我已经实现了......):

  1. 找到令牌的所有2 ^ N个令牌组排列
  2. 消除其中存在矛盾的令牌组
  3. 计算所有剩余令牌组的值
  4. 按值排序令牌组
  5. 选择前K个令牌组
  6. 真实世界数字 - 我需要来自一组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
    

    请帮帮忙?

    塔尔。

5 个答案:

答案 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>

这意味着你可以分而治之;毕竟,非重叠范围对彼此没有影响,因此不需要探索完整的状态空间。特别是,动态编程解决方案将起作用。

算法概要

  1. 假设令牌的位置表示为[start, end)(即包含开始,独占结束)。按令牌结束对令牌列表进行排序,我们将迭代它们。
  2. 您将扩展这些令牌的子集。这些令牌集将具有结束(如果子集在子集结束之前开始,则不能将令牌添加到子集中)和累积值。标记子集的结尾是子集中所有标记的末尾的最大值。
  3. 您将要保持从索引到已排序的标记数组的映射(例如,通过哈希表或数组),并将所有内容处理到最终但非冲突标记的子集。这意味着存储在索引J的映射中的最佳子集必须只能包含索引小于或等于J的标记
  4. 在每个步骤中,您将计算某个位置J的最佳子集,然后可能发生以下三种情况之一:您可能已经在映射中缓存了此计算(简单),或者最佳子集包括该项目J,或者最好的子集不包括项目J.如果你没有缓存它,你只能通过尝试这两个选项找到包括或排除J的最佳子集。
  5. 现在,诀窍在缓存中 - 你需要尝试两个选项,这看起来像一个递归(指数)搜索,但它不一定是。

    • 如果索引J 的最佳子集包含 token[J],则它不能包含与该令牌重叠的任何令牌 - 特别是,因为我们按{{排序} 1}},该列表中有一个最后一个令牌token.endKK < 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>

    这个相当含糊的解释是否有帮助?它是动态编程的基本应用,它只是缓存的一个奇特的词;所以,如果你感到困惑,那就是你应该阅读的内容。

    将其扩展到顶级k-best解决方案

    如果你想找到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)可能有助于进一步提高某些类型输入的解决方案的性能。