【问题标题】:Flattening nested loops / decreasing complexity - complementary pairs counting algorithm展平嵌套循环/降低复杂性 - 互补对计数算法
【发布时间】:2012-02-09 18:45:57
【问题描述】:

我最近尝试在 Python 中解决一些任务,我发现解决方案似乎具有 O(n log n) 的复杂性,但我认为它对于某些输入来说效率非常低(例如第一个参数是0pairs 是很长的零列表)。

它还具有三个级别的for 循环。我相信它可以优化,但目前我无法对其进行更多优化,我可能只是缺少一些明显的东西;)

所以,基本上,问题如下:

给定整数列表 (values),函数需要返回满足以下条件的索引对的数量:

  • 让我们假设单个索引对是一个像 (index1, index2) 这样的元组,
  • 那么values[index1] == complementary_diff - values[index2] 为真,

示例: 如果将[1, 3, -4, 0, -3, 5] 之类的列表作为values1 作为complementary_diff,则该函数应返回4(这是以下索引对列表的长度:[(0, 3), (2, 5), (3, 0), (5, 2)])。

这是我目前所拥有的,它应该在大多数情况下都能完美运行,但是 - 正如我所说 - 在某些情况下它可能会运行得很慢,尽管它的复杂度接近 O(n log n) (看起来悲观复杂度是O(n^2))。

def complementary_pairs_number (complementary_diff, values):
    value_key = {} # dictionary storing indexes indexed by values
    for index, item in enumerate(values):
        try:
            value_key[item].append(index)
        except (KeyError,): # the item has not been found in value_key's keys
            value_key[item] = [index]
    key_pairs = set() # key pairs are unique by nature
    for pos_value in value_key: # iterate through keys of value_key dictionary
        sym_value = complementary_diff - pos_value
        if sym_value in value_key: # checks if the symmetric value has been found
            for i1 in value_key[pos_value]: # iterate through pos_values' indexes
                for i2 in value_key[sym_value]: # as above, through sym_values
                    # add indexes' pairs or ignore if already added to the set
                    key_pairs.add((i1, i2))
                    key_pairs.add((i2, i1))
    return len(key_pairs)

对于给定的示例,它的行为如下:

>>> complementary_pairs_number(1, [1, 3, -4, 0, -3, 5])
4

如果您看到代码如何“扁平化”或“简化”,请告诉我。

我不确定仅检查 complementary_diff == 0 等是否是最好的方法 - 如果您认为是,请告诉我。

编辑:我已经更正了这个例子(谢谢,unutbu!)。

【问题讨论】:

  • 如果有什么不清楚的地方或者你有什么问题,请问他们——也许我可以改进我的问题:)
  • 我认为您示例中的key_pairsset([(3, 0), (0, 3), (5, 2), (2, 5)])(注意5,而不是4)。是吗?
  • @unutbu:你说得对,谢谢!我已经编辑了问题。

标签: python algorithm loops nested complexity-theory


【解决方案1】:

我认为这将复杂性提高到O(n)

  • value_key.setdefault(item,[]).append(index) 比使用更快 try..except 块。它也比使用collections.defaultdict(list) 更快。 (我用 ipython %timeit 对此进行了测试。)
  • 原始代码访问每个解决方案两次。对于每个pos_valuevalue_key 中,有一个唯一的sym_valuepos_value。当sym_value也在时有解决方案 value_key。但是当我们遍历value_key 中的键时, pos_value 最终分配给sym_value 的值,即 使代码重复它已经完成的计算。这样你就可以 如果你能阻止pos_value 等于 老sym_value。我用seen = set() 实现了它以保持 看到sym_values 的轨迹。
  • 代码只关心len(key_pairs),而不关心key_pairs 本身。因此,而不是跟踪对(使用 set),我们可以简单地跟踪计数(使用num_pairs)。所以我们可以用

    替换两个内部的for循环
    num_pairs += 2*len(value_key[pos_value])*len(value_key[sym_value])
    

    或“唯一对角线”情况下的一半,pos_value == sym_value


def complementary_pairs_number(complementary_diff, values):
    value_key = {} # dictionary storing indexes indexed by values
    for index, item in enumerate(values):
        value_key.setdefault(item,[]).append(index)
    # print(value_key)
    num_pairs = 0
    seen = set()
    for pos_value in value_key: 
        if pos_value in seen: continue
        sym_value = complementary_diff - pos_value
        seen.add(sym_value)
        if sym_value in value_key: 
            # print(pos_value, sym_value, value_key[pos_value],value_key[sym_value])
            n = len(value_key[pos_value])*len(value_key[sym_value])
            if pos_value == sym_value:
                num_pairs += n
            else:
                num_pairs += 2*n
    return num_pairs

【讨论】:

  • 我相信这可能是一个靶心 :) 至少对于 complementary_diff=0values=[0,0,0] 似乎返回正确的值(请参阅此代码,您可以将其用于测试:ideone.com/8bZ2x )。我没有考虑 len*len :)
  • 我在您的代码中看不到任何错误:) 我将接受它,除非其他人会提供更好的解决方案。非常感谢!
  • 好的,感谢您出色的解决方案!您摆脱了嵌套循环并找到了一种简化代码的方法。我的问题是我不必要地存储了 key_pairs,然后不得不使用两个 for 循环来生成它们,而不是仅仅根据长度计算组合的数量。很遗憾我不能给你一个以上的支持,因为这个答案值得更多:)
【解决方案2】:

您可能想研究函数式编程习语,例如 reduce 等。

嵌套数组逻辑通常可以通过使用 reduce、map、reject 等函数来简化。

例如(在 javascript 中)查看下划线 js。我在 Python 方面不是很聪明,所以我不知道他们有哪些可用的库。

【讨论】:

  • 谢谢,你可能是对的,但是例如。 map() 没有解决问题,因为它仍然在循环。甚至建议在某些情况下使用列表推导/生成器表达式。但是reduce() 可能会以某种方式有用。再次感谢。
  • 我可能遗漏了一些明显的东西,也许这确实可以通过应用代数的一些理论来轻松解决,但我现在看不到它,那是很久以前的事了,如果你有任何提示,任何能指出我正确的方向,我将不胜感激! :)
  • 嗯,“代数”是指“使用已知规则降低复杂性的过程”#notenoughsleep #notenoughcoffee
  • 好的,我跟着。我仍然希望我错过了一些已知的规则。 #meetoonotenoughcoffee
【解决方案3】:

我认为(部分或全部)这些会有所帮助,但我不确定如何证明这一点。

1) 取值并将其减少为一组不同的值,记录每个元素的计数 (O(n))

2) 对结果数组进行排序。 (n log n)

3) 如果您可以分配大量内存,我猜您也许可以使用这些值填充一个稀疏数组 - 所以如果值的范围是 -100 : +100,则分配一个 [201] 和任何数组缩减集中存在的值会在大型稀疏数组中的值索引处弹出一个 1。

4) 任何你想检查它是否满足你的条件的值现在必须根据 x - y 关系查看稀疏数组中的索引,看看那里是否存在值。

5) 正如 unutbu 指出的那样,它是平凡对称的,所以如果 {a,b} 是一对,那么 {b,a} 也是。

【讨论】:

  • 谢谢,您可能为我指明了正确的方向。除了我相信当a==b(所以索引a指向同一个元素索引b指向),那么它应该只计算一次(我的意思是元素可以与自身成对,但不应该被对待与自己成对两次)。
【解决方案4】:

我认为您可以通过将代数部分从搜索中分离出来并使用更智能的数据结构来改进这一点。

  1. 遍历列表并从列表中每个项目的互补差异中减去。

    resultlist[index] = complementary_diff - originallist[index]
    

    您可以使用地图或简单循环。 -> 花费 O(n) 时间。

  2. 查看结果列表中的数字是否存在于原始列表中。

    • 在这里,对于一个简单的列表,您实际上会得到 O(n^2),因为您最终可以在结果列表中搜索每个项目的整个原始列表。

    • 但是,还有比这更聪明的方法来组织您的数据。如果您有原始列表排序,您的搜索时间减少到 O(nlogn + nlogn) = O(nlogn), nlogn 进行排序, 和 nlogn 用于每个元素的二进制搜索。

    • 如果您想变得更聪明,您可以将列表放入字典(或哈希表),然后这一步变为 O(n + n) = O (n), n 构建字典,1 * n 搜索字典中的每个元素。 (*编辑:* 由于您不能假设原始列表中每个值的唯一性。您可能需要计算每个值在原始列表中出现的次数。)

因此,现在您可以获得 O(n) 总运行时间。

使用您的示例:

1, [1, 3, -4, 0, -3, 5],
  1. 生成结果列表:

    >>> resultlist
    [0, -2, 5, 1, 4, -4].
    
  2. 现在我们搜索:

    • 将原始列表展平成字典。我选择使用原始列表的索引作为值,因为这似乎是您感兴趣的辅助数据。

      >>> original_table
      {(1,0), (3,1), (-4,2), (0,3), (-3,4), (5,5)}
      
    • 对于结果列表中的每个元素,在哈希表中搜索并制作元组:

      (resultlist_index, original_table[resultlist[resultlist_index]])
      

      这应该看起来像您的示例解决方案。

  3. 现在您只需找到结果元组列表的长度。

现在是代码:

example_diff = 1
example_values = [1, 3, -4, 0, -3, 5]
example2_diff = 1
example2_values = [1, 0, 1]

def complementary_pairs_number(complementary_diff, values):
    """
        Given an integer complement and a list of values count how many pairs
        of complementary pairs there are in the list.
    """
    print "Input:", complementary_diff, values
    # Step 1. Result list
    resultlist = [complementary_diff - value for value in values]
    print "Result List:", resultlist

    # Step 2. Flatten into dictionary
    original_table = {}
    for original_index in xrange(len(values)):
        if values[original_index] in original_table:
            original_table[values[original_index]].append(original_index)
        else:
            original_table[values[original_index]] = [original_index]
    print "Flattened dictionary:", original_table

    # Step 2.5 Search through dictionary and count up the resulting pairs.
    pair_count = 0
    for resultlist_index in xrange(len(resultlist)):
        if resultlist[resultlist_index] in original_table:
            pair_count += len(original_table[resultlist[resultlist_index]])
    print "Complementary Pair Count:", pair_count

    # (Optional) Step 2.5 Search through dictionary and create complementary pairs. Adds O(n^2) complexity.
    pairs = []
    for resultlist_index in xrange(len(resultlist)):
        if resultlist[resultlist_index] in original_table:
            pairs += [(resultlist_index, original_index) for original_index in
                original_table[resultlist[resultlist_index]]]
    print "Complementary Pair Indices:", pairs

    # Step 3
    return pair_count

if __name__ == "__main__":
    complementary_pairs_number(example_diff, example_values)
    complementary_pairs_number(example2_diff, example2_values)

输出:

$ python complementary.py
Input: 1 [1, 3, -4, 0, -3, 5]
Result List: [0, -2, 5, 1, 4, -4]
Flattened dictionary: {0: 3, 1: 0, 3: 1, 5: 5, -4: 2, -3: 4}
Complementary Pair Indices: [(0, 3), (2, 5), (3, 0), (5, 2)]
Input: 1 [1, 0, 1]
Result List: [0, 1, 0]
Flattened dictionary: {0: [1], 1: [0, 2]}
Complementary Pair Count: 4
Complementary Pair Indices: [(0, 1), (1, 0), (1, 2), (2, 1)]

谢谢!

【讨论】:

  • 感谢您的回答。我很高兴看到它被编码,因为我认为这个逻辑在某些地方可能会失败:) 当涉及到您的代码时:1)我使用的是哈希表(value_key),2)您的 original_table 似乎是一个集合,3)我不需要索引,如果它简化了任何事情,4)我不确定将原始列表展平到字典中是为了什么(你能解释一下吗?)。总之非常感谢! :)
  • 是的。我已将代码添加到原始答案中。关于你的问题 2) original_table 确实是一个哈希表(或字典)波浪括号({})表示python中的字典。 4) 将原始列表扁平化为字典是导致时间改进的原因,如步骤 2 的最后一个项目符号中所述。简而言之,字典比列表搜索要快得多。你能告诉我你认为逻辑可能在哪里失败吗?
  • 我相信它会失败,例如。如果输入中有两个相等的值(values 列表)。例如。对于1[1, 0, 1] 参数,函数应返回4[(0,1), (1,2), (1,0), (2,1)] 的长度),但您的函数返回3[(0,1), (1,2), (2,1)] 的长度)。它只是不是为了将相同的值放在输入中的不同索引处而设计的。我的代码中有两个级别的for 循环是由于输入列表中的值可能不是唯一的,因此假设它们是唯一的将帮助我大大简化我的脚本:) 无论如何,非常感谢:)跨度>
  • 啊,好吧。我知道了。在这种情况下,不关心实际的配对会有很大帮助,因为您可以在最后数数它们。我将在我的代码中反映更改。
【解决方案5】:

修改了@unutbu提供的解决方案:

问题可以归结为比较这两个字典:

  1. 价值观

  2. (complementary_diff - values[i]) 的预计算字典

    def complementary_pairs_number(complementary_diff, values):
        value_key = {} # dictionary storing indexes indexed by values
        for index, item in enumerate(values):
            value_key.setdefault(item,[]).append(index)
    
        answer_key = {} # dictionary storing indexes indexed by (complementary_diff - values)
        for index, item in enumerate(values):
            answer_key.setdefault((complementary_diff-item),[]).append(index)
    
        num_pairs = 0
        print(value_key)
        print(answer_key)
        for pos_value in value_key: 
            if pos_value in answer_key: 
                num_pairs+=len(value_key[pos_value])*len(answer_key[pos_value])
        return num_pairs
    

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-07-15
    • 1970-01-01
    • 1970-01-01
    • 2013-09-25
    • 1970-01-01
    相关资源
    最近更新 更多