根据连续项目的相似性对双面项目列表进行排序

时间:2018-02-28 19:44:12

标签: python algorithm sorting

我正在寻找某种“Domino排序”算法,该算法根据后续项目的“切线”边的相似性对双面项目列表进行排序。

假设以下列表中的项目由2元组表示:

>>> items
[(0.72, 0.12),
 (0.11, 0.67),
 (0.74, 0.65),
 (0.32, 0.52),
 (0.82, 0.43),
 (0.94, 0.64),
 (0.39, 0.95),
 (0.01, 0.72),
 (0.49, 0.41),
 (0.27, 0.60)]

目标是对该列表进行排序,使得每两个后续项目(损失)的切线末端的平方差的总和最小:

>>> loss = sum(
...     (items[i][1] - items[i+1][0])**2
...     for i in range(len(items)-1)
... )

对于上面的例子,这可以通过处理所有可能的排列来计算,但对于包含更多项目的列表,这变得很快变得不可行(O(n!))。

如图所示,逐步选择最佳匹配的方法

def compute_loss(items):
    return sum((items[i][1] - items[i+1][0])**2 for i in range(len(items)-1))


def domino_sort(items):
    best_attempt = items
    best_score = compute_loss(best_attempt)
    for i in range(len(items)):
        copy = [x for x in items]
        attempt = [copy.pop(i)]
        for j in range(len(copy)):
            copy = sorted(copy, key=lambda x: abs(x[0] - attempt[-1][1]))
            attempt.append(copy.pop(0))
        score = compute_loss(attempt)
        if score < best_score:
            best_attempt = attempt
            best_score = score
    return best_attempt, best_score

在丢失0.1381时给出以下结果:

[(0.01, 0.72),
 (0.72, 0.12),
 (0.11, 0.67),
 (0.74, 0.65),
 (0.49, 0.41),
 (0.39, 0.95),
 (0.94, 0.64),
 (0.82, 0.43),
 (0.32, 0.52),
 (0.27, 0.6)]

然而,这不是最好的解决方案

[(0.01, 0.72),
 (0.82, 0.43),
 (0.27, 0.6),
 (0.49, 0.41),
 (0.32, 0.52),
 (0.39, 0.95),
 (0.94, 0.64),
 (0.72, 0.12),
 (0.11, 0.67),
 (0.74, 0.65)]

失去0.0842。显然,上述算法对前几个项目表现良好,但最后几个项目的差异变得如此之大,以至于它们主导了损失。

是否有任何算法可以在可接受的时间依赖性中执行此类排序(对于数百个项目的列表是可行的)?

如果不可能在O(n!)以内进行完全这种排序,那么是否有任何近似方法可能会得到一个好的分数(小损失)?

3 个答案:

答案 0 :(得分:2)

一般来说,这个问题是找到一个Hamiltonian path,其最小长度与着名的Travelling salesman problem (TSP)密切相关。并且它看起来不像这个问题的特殊情况,可以在多项式时间内解决。

有很多用于解决TSP的启发式算法和近似算法。 This wikipedia article可能是一个很好的起点。

答案 1 :(得分:1)

使用bisect的朴素方法稍微有效一点。
(实施荣誉:https://stackoverflow.com/a/12141511/6163736

# Domino Packing
from bisect import bisect_left
from pprint import pprint


def compute_loss(items):
    return sum((items[i][1] - items[i+1][0])**2 for i in range(len(items)-1))


def find_nearest(values, target):
    """
    Assumes values is sorted. Returns closest value to target.
    If two numbers are equally close, return the smallest number.
    """
    idx = bisect_left(values, target)
    if idx == 0:
        return 0
    if idx == len(values):
        return -1
    before = values[idx - 1]
    after = values[idx]
    if after - target < target - before:
        return idx      # after
    else:
        return idx - 1  # before


if __name__ == '__main__':

    dominos = [(0.72, 0.12),
               (0.11, 0.67),
               (0.74, 0.65),
               (0.32, 0.52),
               (0.82, 0.43),
               (0.94, 0.64),
               (0.39, 0.95),
               (0.01, 0.72),
               (0.49, 0.41),
               (0.27, 0.60)]

    dominos = sorted(dominos, key=lambda x: x[0])
    x_values, y_values = [list(l) for l in zip(*dominos)]
    packed = list()
    idx = 0

    for _ in range(len(dominos)):
        x = x_values[idx]
        y = y_values[idx]
        del x_values[idx]
        del y_values[idx]

        idx = find_nearest(x_values, y)
        packed.append((x, y))

    pprint(packed)
    print("loss :%f" % compute_loss(packed))

<强>输出

[(0.01, 0.72),
 (0.72, 0.12),
 (0.11, 0.67),
 (0.74, 0.65),
 (0.49, 0.41),
 (0.39, 0.95),
 (0.94, 0.64),
 (0.82, 0.43),
 (0.32, 0.52),
 (0.27, 0.6)]
loss :0.138100

答案 2 :(得分:0)

其他答案者已经讨论过理论问题。我试图改善你最近的未受访邻居&#34;算法

在我进入算法之前,请注意您显然可以sorted替换pop(0) + pop(min_index)

min_index, _ = min(enumerate(copy), key=lambda i_x: abs(i_x[1][0] - attempt[-1][1]))
attempt.append(copy.pop(min_index))

方法1:改进基本方法

我受到了一个非常简单的想法的指导:不是只考虑下一个多米诺骨牌的左侧,看它是否与当前序列的右侧匹配,为什么不在它的上面添加一个约束右边呢?

我试过这个:检查候选人右侧是否接近剩余的多米诺骨牌&#39;左边。我认为更容易找到下一个&#34;下一个&#34;多米诺骨牌,右侧接近剩余左侧的平均值。因此,我对您的代码进行了以下更改:

mean = sum(x[0] for x in copy)/len(copy)
copy = sorted(copy, key=lambda x: abs(x[0] - attempt[-1][1]) + abs(x[1]-mean)) # give a bonus for being close to the mean.

但这不是一个改进。 100个随机系列100个项目(所有值在0和1之间)的累计损失为:

  • 最近的未访问邻居:132.73
  • 最近未访问的邻居和右边接近意思是:259.13

经过一些调整,我试图将奖金转化为惩罚:

mean = sum(x[0] for x in copy)/len(copy)
copy = sorted(copy, key=lambda x: 2*abs(x[0] - attempt[-1][1]) - abs(x[1]-mean)) # note the 2 times and the minus

这次,有一个明显的改善:

  • 最近的未访问邻居:145.00
  • 最近未访问的邻居和右边远离平均值:93.65

但为什么呢?我做了一点研究。显然,原始算法在开始时表现更好,但新算法&#34;消耗&#34;大多数人(左右之间有很大的差距)因此最终表现得更好。

因此我专注于差距:

copy = sorted(copy, key=lambda x: 2*abs(x[0] - attempt[-1][1]) - abs(x[1]-x[0]))

这个想法很明确:在其他人之前消耗大的多米诺骨牌。这很好用:

  • 最近的未访问邻居:132.85
  • 最近的未访问的邻居和右边远离平均值:90.71
  • 最近的未受访邻居和大多米诺骨牌:79.51

方法2:改善给定序列

好的,现在是更复杂的启发式方法。我从the Lin–Kernighan heuristic获取灵感。我尝试构建满足以下条件的交换序列:一旦最后一次交换确实减少了一个交换的多米诺骨牌的局部损失,就停止序列。估计交换的每个序列都是最好的。

代码会比很长的解释更清晰:

def improve_sort(items, N=4):
    """Take every pair of dominos and try to build a sequence that will maybe reduce the loss.
    N is the threshold for the size of the subsequence"""
    ret = items
    ret = (items, compute_loss(items))
    for i in range(len(items)):
        for j in range(i+1, len(items)):
            # for every couple of indices
            r = try_to_find_better_swap_sequence(ret, [i, j], N)
            if r[1] < ret[1]:
                ret = r

    return ret

def try_to_find_better_swap_sequence(ret, indices, N):
    """Try to swap dominos until the local loss is greater than in the previous sequence"""
    stack = [(indices, ret[0])] # for an iterative DFS
    while stack:
        indices, items = stack.pop()

        # pop the last indices
        j = indices.pop()
        i = indices.pop()
        # create a copy and swap the i-th and the j-th element
        items2 = list(items)
        items2[i] = items[j]
        items2[j] = items[i]
        loss = compute_loss(items2)
        if loss < ret[1]:
            ret = (items2, loss)
        if len(indices) <= N-3: # at most N indices in the sequence
            # continue if there is at least one local improvement
            if local_loss(items2, i) < local_loss(items, i): # i was improved
                stack.extend((indices+[i,j,k], items2) for k in range(j+1, len(items2)))
            if local_loss(items2, j) < local_loss(items, j): # j was improved
                stack.extend((indices+[j,i,k], items2) for k in range(i+1, len(items2)))

    return ret

def local_loss(items, i):
    """This is the loss on the left and the right of the domino"""
    if i==0:
        return (items[i][1] - items[i+1][0])**2
    elif i==len(items)-1:
        return (items[i-1][1] - items[i][0])**2
    else:
        return (items[i-1][1] - items[i][0])**2+(items[i][1] - items[i+1][0])**2
  • 没有排序+改进排序:46.72
  • 最近的未受访邻居和大多米诺骨牌:78.37
  • 最近的未访问的邻居和大的多米诺骨牌+ improve_sort:46.68

结论

第二种方法仍然不是最理想的(在原始items上尝试)。它显然比第一个慢,但提供了更好的结果,甚至不需要预先排序。 (考虑使用shuffle来避免退化的情况。)

您还可以查看this。获得下一个可能的多米诺骨牌的方法是将剩余的多米诺骨牌洗牌多次,并将每次洗牌的损失加起来。最小累积损失可能会给你一个好的下一个多米诺骨牌。我没试过......