【问题标题】:Fastest Algorithm to Find the Minimum Sum of Absolute Differences through List Rotation通过列表旋转找到最小绝对差和的最快算法
【发布时间】:2020-10-20 00:31:21
【问题描述】:

通过从左到右旋转 2 个列表,找到两个列表中每个对应项目之间差异的绝对值的最小可能总和,因为它们是 相同 长度。

轮换样本:

List [0, 1, 2, 3, 4, 5] rotated to the left = [1, 2, 3, 4, 5, 0]

List [0, 1, 2, 3, 4, 5] rotated to the right= [5, 0, 1, 2, 3, 4]

绝对差之和:

List 1 = [1, 2, 3, 4]
List 2 = [5, 6, 7, 8]

Sum of Abs. Diff. = |1-5| + |2-6| + |3-7| + |4-8| = 16

再一次,对于任意长度的列表和整数值,任务是通过简单地向任一或两个列表的左/右旋转来寻找可能的最小总和。

我对旋转和获取绝对差的最小和没有任何问题。我只想知道更聪明的方法,因为我的算法会检查所有可能的组合,这很慢。

这是我的蛮力方法:

list1 = [45, 21, 64, 33, 49]
list2 = [90, 12, 77, 52, 28]
choices = []                # Put all possible sums into a list to find the minimum value.
for j in range(len(list1)):  # List1 does a full rotation
    total = 0
    for k in range(len(list1)):
        total += abs(list1[k] - list2[k])
    list1.append(list1.pop(0))
    choices.append(total)
print(min(choices))

什么是更聪明的方法?我也会欣赏更短的代码和时间复杂度。

我设法通过应用生成器使其更快。感谢@kuriboh 的想法!但是由于我还是生成器实现的新手,所以只想知道这是否是实现它以降低时间复杂度的最佳方法,尤其是对于我的循环。 我们还能比这个配置更快吗?

list1 = [45, 21, 64, 33, 49]
list2 = [90, 12, 77, 52, 28]
choices = []
l = len(list1)
for j in range(l):
    total = sum([abs(int(list1[k])-int(list2[k])) for k in range(l)])
    list1.append(list1.pop(0))
    choices.append(total)
print(min(choices))

【问题讨论】:

  • 提示:使用列表中的差异 bewteen 元素。例如,您上面的 list1 的差异为 [-24, 43, -31, 16, -4]。使用 list2 差异查找“适合”模式。请注意,仅当您的列表非常大时,该改进才有效;否则,模式匹配的开销会耗尽您的 O(N^2) 算法的收益。
  • @Prune 我不太确定我是否完全理解您的评论,为什么我们会在list1 中得到相邻整数的差异?
  • 这个问题是你自己造成的,或者你为什么不能告诉我们它来自哪里?
  • @Prune 我不知道如何利用它,想看看它:-)
  • @muw 我的list2 只是您列表的一个排列。使用 list2 时,最佳旋转是 [52, 28, 90, 12, 77],它给出了 [-24, 62, -78, 65, -25] 的差异列表。该差异列表使 Prune 的想法看起来很有希望。我的目标是以创建更具挑战性的测试用例的方式置换list2

标签: python algorithm discrete-mathematics


【解决方案1】:

由于 Python 将负索引视为从右端开始计数,因此您可以将 list1 减去(list2 移位 k)的绝对值相加,其中 0 ≤ k

sum(abs(list1[i] - list2[i - k]) for i in range(len(list1)))

如果你想要所有这些值中的最小值

length = len(list1)
min(sum(abs(list1[i] - list2[i - k]) for i in range(length))
    for k in range(length))

这段代码仍然是 O(n^2),但是推送和弹出的次数要少得多。

我实在想不出任何办法让算法比 O(n^2) 更快。

【讨论】:

  • 减号是用- :-P 完成的。而且我很确定这些索引减法比 pop+append 花费更多的时间。
  • @superbrain 索引操作绝对不会比 pop 和 append 花费更多的时间。 pop(0) 调用是 O(n),因为它正在删除列表的第一个元素。
  • @EvilTak 他们肯定会。总体而言,pop 只进行了 n 次,而 index 操作则进行了 n^2 次。所以他们在做同样的“工作量”,但流行音乐在更低的水平上做得更快。
  • @superbrain 通常我会不同意你的观点,但鉴于这是 Python,你有可能是对的。仍然很难相信n 额外减法所产生的开销超过了memcpy 用于流行音乐的成本。我肯定会推荐 OP 配置这两种解决方案,并选择最适合他们的解决方案。无论如何,这个答案确实消除了冗余的 choices 列表,在每次迭代中附加都会产生相当多的开销。
  • @user3386109 我希望你看到了!编辑摘要滥用是完全值得的。在看到您的编辑摘要后,您不会相信我能够通过另一个潜在的编辑回复您的兴奋...
【解决方案2】:

您的原始答案和弗兰克接受的答案的优化组合:

min(list1.append(list1.pop(0)) or
    sum(abs(x - y) for x, y in zip(list1, list2))
    for _ in list1)

像那样进行轮换有点脏,但是,嘿,您要的是“最快”:-)

使用长度为 1000 的列表进行基准测试:

    original     Frank_Yellin   superb_rain  
     127 ms         164 ms         125 ms    
     140 ms         170 ms         117 ms    
     134 ms         166 ms         116 ms    
     124 ms         161 ms         126 ms    
     135 ms         164 ms         126 ms    

基准代码:

from timeit import repeat
from random import shuffle

def original(list1, list2):
    choices = []                # Put all possible sums into a list to find the minimum value.
    for j in range(len(list1)):  # List1 does a full rotation
        total = 0
        for k in range(len(list1)):
            total += abs(list1[k] - list2[k])
        list1.append(list1.pop(0))
        choices.append(total)
    return min(choices)

def Frank_Yellin(list1, list2):
    length = len(list1)
    return min(sum(abs(list1[i] - list2[i - k]) for i in range(length))
    for k in range(length))

def superb_rain(list1, list2):
    return min(list1.append(list1.pop(0)) or
               sum(abs(x - y) for x, y in zip(list1, list2))
               for _ in list1)

funcs = [
    (10, original),
    (10, Frank_Yellin),
    (10, superb_rain),
    ]

list1 = list(range(1000))
list2 = list1.copy()
shuffle(list2)

for _, f in funcs:
    print(f(list1, list2))

for _, f in funcs:
    print(f.__name__.center(15), end='')
print()

for _ in range(5):
    for number, f in funcs:
        t = min(repeat(lambda: f(list1, list2), number=number)) / number
        print('%8d ms    ' % (t * 1e3), end='')
    print()

【讨论】:

  • 喜欢这个。我认为我的代码已经过优化。我有点错过了每次迭代都将列表类型转换为整数的事实。你做的更好。干得好!
  • @muw 哈,我什至没有意识到你这样做了。那有什么意义呢?顺便说一句,我认为David's answer 是最有前途也是唯一有趣的。
  • 老实说我还不太了解他的流程,但如果你觉得他的想法比你的好,那我会选择他的答案。
  • @muw 我也不明白他的解决方案,但这个想法很好,测试让我信服,基准测试表明它非常快(我将发布另一个答案)。
【解决方案3】:

我还没有破解完整的问题,但是在输入值都是 01 (或任何两个不同的值,或任何 O(1) 不同的值,但我们会需要另一个想法才能走得更远),我们可以通过应用快速卷积得到O(n log n)-time 算法。

这个想法是将所有绝对差的总和计算为List1 * reverse(1 - List2) + (1 - List1) * reverse(List2),其中1 - List 表示逐点执行该操作,* 表示循环卷积(可使用一对 FFT 及时计算O(n log n)) .这里循环卷积的定义是

             n-1
             __
             \
(f * g)(i) = /_  f(j) g((i - j) mod n).
             j=0

List1 代替freverse(1 - List2) 代替g,我们得到

                                  n-1
                                  __
                                  \
(List1 * reverse(1 - List2))(i) = /_ List1(j) (1 - List2((n-1-(i-j)) mod n))
                                  j=0

                                  n-1
                                  __
                                  \
                                = /_ List1(j) (1 - List2((j-(i+1)) mod n)).
                                  j=0

乘积List1(j) (1 - List2((j-(i+1)) mod n))1 当且仅当List1(j) = 1List2((j-(i+1)) mod n) = 0,否则0。因此,卷积的i 值计算List1 具有1 偏移i+1 的位置的数量,循环到List2 具有0 的左侧。其他卷积计数0s 对应于1s。鉴于我们的输入限制,这是绝对差的总和。

代码:

import numpy


def convolve_circularly(a1, a2):
    return numpy.round(numpy.abs(numpy.fft.ifft(numpy.fft.fft(a1) * numpy.fft.fft(a2))))


def min_sum_abs_diff(a1, a2):
    a1 = numpy.array(a1)
    a2 = numpy.array(a2)[::-1]
    return numpy.min(convolve_circularly(a1, 1 - a2) + convolve_circularly(1 - a1, a2))


def slow_min_sum_abs_diff(a1, a2):
    return min(
        sum(abs(a1[i] - a2[i - k]) for i in range(len(a1))) for k in range(len(a2))
    )


def main():
    n = 100
    for r in range(100000):
        a1 = numpy.random.randint(2, size=n)
        a2 = numpy.random.randint(2, size=n)
        r = min_sum_abs_diff(a1, a2)
        slow_r = slow_min_sum_abs_diff(a1, a2)
        if r != slow_r:
            print(a1, a2, r, slow_r)
            break


if __name__ == "__main__":
    main()

【讨论】:

  • 尝试一个更简单的问题的好主意。可惜我不懂数学。 Python 实现会简单吗?不知道如何处理你的方程式。很高兴看到它,然后可以对其进行测试。
  • @superbrain 我可以尝试使用 NumPy 一起破解一个。
  • 太好了,谢谢。看起来很酷,而且确实非常快(我在new answer 中做了一些基准测试)。
【解决方案4】:

David Eisenstat's solution 的基准测试和我提供的两个 NumPy 解决方案。

500 个随机整数 0 或 1:

  189.62414 ms  slow_min_sum_abs_diff          # Like Frank's
   49.75403 ms  less_slow_min_sum_abs_diff     # My NumPy
   10.13092 ms  lesser_slow_min_sum_abs_diff   # My Numpy
    0.85030 ms  min_sum_abs_diff               # David's
    0.27434 ms  array_conversion               # for comparison

1000 个随机整数 0 或 1:

  857.02381 ms  slow_min_sum_abs_diff
  100.26820 ms  less_slow_min_sum_abs_diff
   28.55692 ms  lesser_slow_min_sum_abs_diff
    1.67077 ms  min_sum_abs_diff
    0.49301 ms  array_conversion

从 -106 到 106 的 1000 个随机整数(没有 David's,因为它不是为此而设计的,并且会产生错误的结果):

  829.18451 ms  slow_min_sum_abs_diff
   89.97418 ms  less_slow_min_sum_abs_diff
   22.69516 ms  lesser_slow_min_sum_abs_diff

我不明白 David 的解决方案,但他自己的验证令人信服,我自己做了更多,将每个更快的解决方案与下一个较慢的解决方案进行比较(这实际上是我编写 NumPy 解决方案的原因,所以我可以用更大的输入):

passed: less_slow_min_sum_abs_diff (220 tests with n=100)
passed: lesser_slow_min_sum_abs_diff (89 tests with n=300)
passed: min_sum_abs_diff (146 tests with n=1000)
passed: min_sum_abs_diff (5 tests with n=10000)

在 repl.it 的 Python 3.8.2 64 位上完成的基准测试。

代码:

import numpy
from timeit import repeat, default_timer as timer


def array_conversion(a1, a2):
    a1 = numpy.array(a1)
    a2 = numpy.array(a2)


def convolve_circularly(a1, a2):
    return numpy.round(numpy.abs(numpy.fft.ifft(numpy.fft.fft(a1) * numpy.fft.fft(a2))))


def min_sum_abs_diff(a1, a2):
    a1 = numpy.array(a1)
    a2 = numpy.array(a2)[::-1]
    return numpy.min(convolve_circularly(a1, 1 - a2) + convolve_circularly(1 - a1, a2))


def slow_min_sum_abs_diff(a1, a2):
    return min(
        sum(abs(a1[i] - a2[i - k]) for i in range(len(a1))) for k in range(len(a2))
    )


def less_slow_min_sum_abs_diff(a1, a2):
    a1 = numpy.array(a1)
    a2 = numpy.array(a2)
    return min(
        numpy.abs(a1 - numpy.roll(a2, k)).sum()
        for k in range(len(a2))
    )


def lesser_slow_min_sum_abs_diff(a1, a2):
    n = len(a2)
    a1 = numpy.array(a1)
    a2 = numpy.concatenate((a2, a2))
    return min(
        numpy.abs(a1 - a2[k:k+n]).sum()
        for k in range(n)
    )


def random_arrays(n):
    a1 = numpy.random.randint(2, size=n).tolist()
    a2 = numpy.random.randint(2, size=n).tolist()
    return a1, a2


def verify(candidate, reference, n, timelimit=1):
    t0 = timer()
    count = 0
    while timer() - t0 < timelimit:
        a1_orig, a2_orig = random_arrays(n)
        a1, a2 = a1_orig.copy(), a2_orig.copy()
        expect = reference(a1, a2)
        assert a1 == a1_orig and a2 == a2_orig
        result = candidate(a1, a2)
        assert a1 == a1_orig and a2 == a2_orig
        if result != expect:
            print('wrong:')
            print('  expected', expect, 'by', reference.__name__)
            print('  got', result, 'by', candidate.__name__)
            print('  a1:', a1)
            print('  a2:', a2)
            break
        count += 1
    else:
        print('passed:', candidate.__name__, f'({count} tests with {n=})')


def main():
    if 1:
        verify(less_slow_min_sum_abs_diff, slow_min_sum_abs_diff, 100)
        verify(lesser_slow_min_sum_abs_diff, less_slow_min_sum_abs_diff, 300)
        verify(min_sum_abs_diff, lesser_slow_min_sum_abs_diff, 1000)
        verify(min_sum_abs_diff, lesser_slow_min_sum_abs_diff, 10000)
        print()

    funcs = [
        (10, slow_min_sum_abs_diff),
        (100, less_slow_min_sum_abs_diff),
        (100, lesser_slow_min_sum_abs_diff),
        (1000, min_sum_abs_diff),
        (1000, array_conversion),
    ]

    a1, a2 = random_arrays(1000)

    for _ in range(3):
        for number, func in funcs:
            t = min(repeat(lambda: func(a1, a2), number=number)) / number
            print('%11.5f ms ' % (t * 1e3), func.__name__)
        print()


if __name__ == "__main__":
    main()
    print('done')

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-19
    • 1970-01-01
    • 2016-03-07
    • 1970-01-01
    相关资源
    最近更新 更多