找出与三个最小数字相加的排列

时间:2016-03-31 14:29:04

标签: python algorithm python-3.x

昨天我问了同样的事情,但发现找到正确的句子来描述我的问题很困难,所以我删除了它。但是又来了。

我们说我们有3个列表:

list1 = [1, 2]
list2 = [2, 3]
list3 = [1]

让我们说我想找到这些列表的3个排列,当它们加在一起时,它会产生尽可能小的数字。所以在这里,我们想要的排列是:

1,2,1

2,2,1

1,3,1

因为每个排列上的数字总和会产生尽可能小的数字。

2,3,1-

不会成为解决方案的一部分,因为总和大于其他三个,因此,不是三个最小的一部分。

当然,使用itertools并列出所有排列,并在每个排列上添加数字将是最明显的解决方案,但我想知道是否有更高效的算法?考虑到它应该能够获得1000个列表。

注意:如果列表的数量是N,那么我需要找到N个排列。因此,如果有3个列表,我会找到3个最小的排列。

预备:

- 前提条件的一部分是所有这些列表都已排序。

- 所有列表中的元素数量为2N-1,用于处理只有一个列表具有多个元素的情况。

- 所有列表都从最小的列表中排序。

2 个答案:

答案 0 :(得分:2)

由于列表是排序的,每个列表中的最小元素是第一个,其总和给出了“最小和置换”。挑选除第一个元素之外的任何元素将增加总和值。

我们首先计算元素 i 与每个列表的第一个元素之间的差异。例如,对于列表[1, 3, 4, 8][3, 9, 12, 15],这些差异分别为[2, 3, 7][6, 9, 12]。我们将它们分开cost_lists,因为稍后会需要它们。但是在cost_global中,我们将它们汇总在一起,并按升序对它们进行排序,我们找到了一个解决方案,其中对于所有列表,我们选择最小值。为了跟踪哪个元素从哪个列表中给出下一个最小和,我们将差值与它来自的列表的索引以及该列表中的哪个元素进行分组。

但是,这不是一个完整的方法。例如,从两个列表中获取下一个值可能比从一个列表中获取下一个值的成本更低。因此,我们必须搜索k = 2,3,...,N的组合的乘积。这样做通常会导致N ** N复杂度,但我们可以采取一些非常好的捷径。

从上面的部分解决方案中,我们列出了最低成本。由于我们只需要前N个最小和,我们检查第N个排列的成本值是什么(阈值)。因此,当我们搜索一组两个下一个值时,如果它超过我们当前的阈值,我们可以安全地忽略它们的总和。由于列表中的差值按升序排列,一旦我们越过阈值,我们就可以立即退出循环。类似地,如果我们在k = 2的阈值内没有找到任何新的组合,那么寻找k> 1是没有意义的。 2.考虑到最可能的最小和成本将是单个非最小值或几个小成本的结果(除非大多数列表在顺序值之间存在巨大差异),我们必然会相当快地退出这些循环。我实现这个目标的代码相当丑陋,但它实际上和

一样
for k in xrange(2, len(lists)):
    for comb in itertools.combinations(cost_lists, k):
        for group in itertools.product(*comb):
            if sum(g[0] for g in group) <= threshold:
                cost_global.append(group)

除非我们保证不会找到任何结果,否则我们会立即退出循环,以免我们毫无意义地转移超过阈值的无数组合/产品。

def filter_cost(cost_lists, threshold):
    cost = [[i for i in ilist if i[0] <= threshold] for ilist in cost_lists]
    # the algorithm requires that we remove any lists that have become empty
    return [ilist for ilist in cost if ilist]    

def _combi(cost_lists, k, start, depth, subtotal, threshold):
    if depth == k:
        for i in xrange(start, len(cost_lists)):
            for value in cost_lists[i]:
                if value[0] + subtotal > threshold:
                    break
                yield (value,)
    else:
        for i in xrange(start, len(cost_lists)):
            for value in cost_lists[i]:
                if value[0] + subtotal > threshold:
                    break
                for c in _combi(cost_lists, k, i+1, depth+1,
                                value[0]+subtotal, threshold):
                    yield (value,) + c

def combinations_product(cost_lists, k, threshold):
    for i in xrange(len(cost_lists)-k+1):
        for value in cost_lists[i]:
            if value[0] > threshold:
                break
            for comb in _combi(cost_lists, k, i+1, 2, value[0], threshold):
                temp = (value,) + comb
                cost, ilists, ith_items = zip(*temp)
                yield sum(cost), ilists, ith_items

def find_smallest_sum_permutations(lists):
    minima = [min(x) for x in lists]

    cost_local = []
    cost_global = []
    for i, ilist in enumerate(lists):
        if len(ilist) > 1:
            first = ilist[0]
            diff = [(num-first, i, j) for j, num in enumerate(ilist[1:], 1)]
            cost_local.append(diff)
            cost_global.extend(diff)
    cost_global.sort()

    threshold_index = len(lists) - 2
    cost_threshold = cost_global[threshold_index][0]
    cost_local = filter_cost(cost_local, cost_threshold)
    for k in xrange(2, len(lists)):
        group_combinations = tuple(combinations_product(cost_local, k,
                                                        cost_threshold))
        if group_combinations:
            cost_global.extend(group_combinations)
            cost_global.sort()
            cost_threshold = cost_global[threshold_index][0]
            cost_local = filter_cost(cost_local, cost_threshold)
        else:
            break

    permutations = [minima]
    for k in xrange(N-1):
        _, ilist, ith_item = cost_global[k]
        if type(ilist) == int:
            permutation = [minima[i]
                           if i != ilist else lists[ilist][ith_item]
                           for i in xrange(N)]
        else:
            # multiple nonminimal values combination
            mapping = dict(zip(ilist, ith_item))
            permutation = [minima[i]
                           if i not in mapping else lists[i][mapping[i]]
                           for i in xrange(N)]
        permutations.append(permutation)
    return permutations

<强>实施例

问题中的示例。

>>> lists = [
    [1, 2],
    [2, 3],
    [1],
]
>>> for p in find_smallest_sum_permutations(lists):
...     print p, sum(p)

[1, 2, 1] 4
[2, 2, 1] 5
[1, 3, 1] 5

我用随机列表生成的示例。

>>> import random
>>> N = 5
>>> random.seed(1024)
>>> lists = [sorted(random.sample(range(10*N), 2*N-1)) for _ in xrange(N)]
>>> for p in find_smallest_sum_permutations(lists):
...     print p, sum(p)

[4, 4, 1, 6, 0] 15
[4, 6, 1, 6, 0] 17
[4, 4, 3, 6, 0] 17
[4, 4, 1, 6, 4] 19
[4, 6, 3, 6, 0] 19

user2357112的示例,它在我之前的迭代中发现了一个明显的错误。

>>> lists = [
    [1, 2, 30, 40],
    [1, 2, 30, 40],
    [10, 20, 30, 40],
    [10, 20, 30, 40],
]
>>> for p in find_smallest_sum_permutations(lists):
...     print p, sum(p)

[1, 1, 10, 10] 22
[2, 1, 10, 10] 23
[1, 2, 10, 10] 23
[2, 2, 10, 10] 24

答案 1 :(得分:2)

诀窍是只生成可能需要的组合,并将它们存储在堆中。你拔出的每一个都是你还没见过的最小的一个。事实上,这个组合被拉出来告诉你,有新的组合可能也很小。

有关如何使用堆的信息,请参阅this。我们还需要用于生成组合的代码。有了它,这里有用于获取任何列表列表的第一个n组合的工作代码:

import heapq

# Helper class for storing combinations.
class ListSelector:
    def __init__(self, lists, indexes):
        self.lists = lists
        self.indexes = indexes

    def value(self):
        answer = 0
        for i in range(0, len(self.lists)):
            answer = answer + self.lists[i][self.indexes[i]]
        return answer

    def values(self):
       return [self.lists[i][self.indexes[i]] for i in range(0, len(self.lists))]

    # These are the next combinations.  We are willing to increment any
    # leading 0, or the first non-zero value.  This will provide one and
    # only one path to each possible combination.
    def next_selectors(self):
        lists = self.lists
        indexes = self.indexes
        selectors = []
        for i in range(0, len(lists)):
            if len(lists[i]) <= indexes[i] + 1:
                if 0 == indexes[i]:
                    continue
                else:
                    break
            new_indexes = [
                indexes[j] + (0 if j != i else 1)
                    for j in range(0, len(lists))]
            selectors.append(ListSelector(lists, new_indexes))
            if 0 < indexes[i]:
                break
        return selectors


# This will just return an iterator over all combinations, from smallest
# to largest.  It does NOT generate them until needed.    
def combinations(lists):
    sel = ListSelector(lists, [0 for _ in range(len(lists))])
    upcoming = [(sel.value(), sel)]
    while len(upcoming):
        value, sel = heapq.heappop(upcoming)
        yield sel
        for next_sel in sel.next_selectors():
            heapq.heappush(upcoming, (next_sel.value(), next_sel))


# This just gets the first n of them.  (It will return less if less.)
def smallest_n_combinations(n, lists):
    i = 0
    for sel in combinations(lists):
        yield sel
        i = i + 1
        if i == n:
            break

# Example usage
lists = [
    [1, 2, 5],
    [2, 3, 4],
    [1]]

for sel in smallest_n_combinations(3, lists):
    print(sel.value(), sel.values(), sel.indexes)

(对于一长串列表,这可以更有效,例如缓存ListSelector中的值并逐步计算新的值。)