【问题标题】:What is an efficient way to find the minimum sum of multiple dictionary values, given keys of mutually exclusive integers?给定互斥整数的键,找到多个字典值的最小和的有效方法是什么?
【发布时间】:2019-02-04 02:16:33
【问题描述】:

我有一个字典,其中的键由整数 0 到 n 的所有 len(4) 组合组成,所有值都是浮点数(表示由另一个函数计算的成本) .

例如:

cost_dict = {(0,1,2,3): 6.23, (0,1,2,4): 7.89,
            ...
            (14,15,16,17): 2.57}

我想有效地找到 m 个互斥键(即键不共享它们的任何整数),其值总和为最低数字(因此,找到最低的总成本)。也就是说,我不只是想要字典的 m 最小值,我想要总和为最小值的 m 互斥值。 (或者没有达到绝对最小值,我不介意一些非常接近的高效)。

所以在上面的例子中,对于 m = 3,也许:

cost_dict[(0,3,5,11)]
>1.1 
cost_dict[(2,6,7,13)]
>0.24
cost_dict[(4,10,14,15)]
>3.91

... 可能是该字典中所有相互独占的键中其值总和为可能值最低的键。

dict 中最小的三个值可能是这样的:

cost_dict[(0,3,7,13)]
>0.5
cost_dict[(2,6,7,13)]
>0.24
cost_dict[(4,6,14,15)]
>0.8

但鉴于这些键中的整数互斥,这是不正确的。


有可能比 O(n**m) 时间做得更好吗?也就是说,对于 m 级别,我可以将每个项目与键与第一个不相交的每个其他项目相加(这需要键是冻结集而不是元组)。鉴于字典的长度可以达到 10,000,这相当慢。

似乎对我解决此问题的早期版本有所帮助的是创建所有可能的键组合的列表,这很耗时,但考虑到我需要找到大量的最低成本,可能会更有效次。

【问题讨论】:

  • 您的所有示例键都在增加,这是否表明您的问题? (4, 3, 2, 1) 是有效的密钥吗?键是否有序,例如(1, 2, 3, 4) 与 (4, 3, 2, 1) 是不同的键吗? “互斥键”是指键元组不能共享任何值吗?如有必要,您是否可以选择使用不同的数据结构来计算成本?
  • 如果字典包含所有 len(4) 组合并且由于字典中的键必须是唯一的,那么您对“互斥值”的担忧似乎是理所当然的。此外,您可能不需要创建所有可能键的单独列表,因为它们是该字典的 keys()
  • @Alexander Reynolds - 键中整数的顺序无关紧要。所以整数 1-4 只有一个键。我已经成功了(1,2,3,4),但如果有帮助的话,它可能是别的东西(比如frozenset)。感谢您指出语法,已修复。如果这样更有效,我愿意使用不同的数据结构。
  • @martineau - 我所说的“互斥”是键不共享整数。因此 (1,2,3,4) 和 (1,2,4,5) 为 False,但 (1,2,3,4) 和 (5,6,7,8) 为 True。跨度>
  • 什么是n?值是否密集?

标签: python performance combinations


【解决方案1】:

我尝试了三种不同的方法来解决这个问题——优化的蛮力、动态编程方法和贪心算法。前两个无法处理n > 17 的输入,但生成了最优解,因此我可以使用它们来验证贪心方法的平均性能。我将首先从动态规划方法开始,然后描述贪婪的方法。

动态编程

首先,请注意,如果我们确定(1, 2, 3, 4)(5, 6, 7, 8) 的总和小于(3, 4, 5, 6)(1, 2, 7, 8),那么您的最佳解决方案绝对不能同时包含(3, 4, 5, 6)(1, 2, 7, 8)-因为您可以将它们换成前者,并且金额较小。扩展这个逻辑,(a, b, c, d)(e, f, g, h) 的最佳组合将导致所有x0, x1, x2, x3, x4, x5, x6, x7 组合的和最小,因此我们可以排除所有其他组合。

利用这些知识,我们可以通过暴力破解x0, x1, x2, x3 的所有组合的总和,将集合[0, n) 中的所有x0, x1, x2, x3, x4, x5, x6, x7 组合映射到它们的最小总和。然后,我们可以使用这些映射从x0, x1, x2, x3, x4, x5, x6, x7x0, x1, x2, x3 对中重复x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11 的过程。我们重复这个过程,直到我们获得x0, x1 ... x_(4*m-1) 的所有最小总和,然后我们对其进行迭代以找到最小总和。

def dp_solve(const_dict, n, m):

    lookup = {comb:(comb,) for comb in const_dict.keys()}

    keys = set(range(n))
    for size in range(8, 4 * m + 1, 4):
        for key_total in combinations(keys, size):
            key_set = set(key_total)
            min_keys = (key_total[:4], key_total[4:])
            min_val = const_dict[min_keys[0]] + const_dict[min_keys[1]]

            key1, key2 = min(zip(combinations(key_total, 4), reversed(list(combinations(key_total, size - 4)))), key=lambda x:const_dict[x[0]]+const_dict[x[1]])

            k = tuple(sorted(x for x in key1 + key2))
            const_dict[k] = const_dict[key1] + const_dict[key2]
            lookup[k] = lookup[key1] + lookup[key2]

    key, val = min(((key, val) for key, val in const_dict.items() if len(key) == 4 * m), key=lambda x: x[1])
    return lookup[key], val

诚然,这个实现相当粗糙,因为我不断地进行微优化,希望能够让它足够快,而不必切换到贪婪的方法。

贪婪

这可能是您关心的,因为它可以快速处理相当大的输入,并且非常准确。

首先为部分和构建一个列表,然后通过增加值开始迭代字典中的元素。对于每个元素,找到所有不与其键产生任何冲突的部分和,并将它们“组合”成一个新的部分和,并附加到列表中。在这样做的过程中,您构建了一个最小部分和列表,该列表可以从字典中的最小 k 值创建。为了加快这一切,我使用哈希集来快速检查哪些部分和包含相同的键对。

在“快速”贪婪方法中,您将在找到密钥长度为 4 * m(或等效地为 m 4 元组)的部分总和时中止。根据我的经验,这通常会产生相当好的结果,但如果需要,我想添加一些逻辑以使其更准确。为此,我添加了两个因素-

  • extra_runs - 它规定了在中断之前需要多少额外的迭代来寻找更好的解决方案
  • check_factor - 指定当前搜索“深度”的倍数,以向前扫描 single 新整数,该整数会在当前状态下创建更好的解决方案。这与上面的不同之处在于它不会“保留”每个检查的新整数 - 它只会快速求和以查看它是否创建了一个新的最小值。这大大加快了速度,但代价是其他 m - 1 4 元组必须已经存在于其中一个部分和中。

结合起来,这些检查似乎总能找到真正的最小总和,代价是运行时间延长了大约 5 倍(尽管仍然相当快)。要禁用它们,只需为这两个因素传递 0

def greedy_solve(const_dict, n, m, extra_runs=10, check_factor=2):
    pairs = sorted(const_dict.items(), key=lambda x: x[1])

    lookup = [set([]) for _ in range(n)]
    nset = set([])

    min_sums = []
    min_key, min_val = None, None
    for i, (pkey, pval) in enumerate(pairs):
        valid = set(nset)
        for x in pkey:
            valid -= lookup[x]
            lookup[x].add(len(min_sums))
        
        nset.add(len(min_sums))
        min_sums.append(((pkey,), pval))

        for x in pkey:
            lookup[x].update(range(len(min_sums), len(min_sums) + len(valid)))
        for idx in valid:
            comb, val = min_sums[idx]
            for key in comb:
                for x in key:
                    lookup[x].add(len(min_sums))
            nset.add(len(min_sums))
            min_sums.append((comb + (pkey,), val + pval))
            if len(comb) == m - 1 and (not min_key or min_val > val + pval):
                min_key, min_val = min_sums[-1]
        
        if min_key:
            if not extra_runs: break
            extra_runs -= 1

    for pkey, pval in pairs[:int(check_factor*i)]:
        valid = set(nset)
        for x in pkey:
            valid -= lookup[x]
        
        for idx in valid:
            comb, val = min_sums[idx]
            if len(comb) < m - 1:
                nset.remove(idx)
            elif min_val > val + pval:
                min_key, min_val = comb + (pkey,), val + pval
    return min_key, min_val

我针对n &lt; 36m &lt; 9 进行了测试,它似乎运行得相当快(最坏的情况下只需几秒钟即可完成)。我想它应该很快适用于您的情况12 &lt;= n &lt;= 24

【讨论】:

  • 我可以验证这会产生与我当前的蛮力每种组合方法相同的输出。所以谢谢!
  • 您不会碰巧知道如何向贪婪求解中添加参数:“键必须包含集合 {x} 中的整数”?例如,您可能希望返回的键同时包含 4 和 7(尽管不一定在同一个键中)。 (我尝试了一种骇人听闻的方法,即简单地将非常高的负成本添加到其键包含需要包含的整数的值的所有实例中,但这会使算法由于某种原因运行得非常慢)。
  • 您可以在两个 if 语句中添加另一个条件来分配 min_key, min_val,检查 all(x in tuple(k for c in comb for k in c) + pkey for x in required_keys) 或类似的东西。
  • 这似乎适用于第一个 if 语句,但不适用于第二个(当 check_factor >0 时)。但最大的问题是时间 - 对于相对较长的所需密钥(例如,10 个,n=3 和 m=22),即使没有检查因素,也可能需要一分钟或更长时间才能返回答案。
  • @Plato'sCave 到底是什么意思不起作用?因为它返回一个无效的答案,一个不包含所需整数的答案,或者超时?我可以花一点时间看看是否可以找到任何优化,但如果 all(...) 不起作用,则需要先进行调整。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-03-02
  • 1970-01-01
  • 1970-01-01
  • 2011-03-10
  • 1970-01-01
  • 2010-12-06
  • 2011-01-15
相关资源
最近更新 更多