【问题标题】:Divide the list into three lists such that their sum are close to each other将列表分成三个列表,使它们的总和彼此接近
【发布时间】:2017-05-04 11:06:05
【问题描述】:

假设我有一个数字 S = [6, 2, 1, 7, 4, 3, 9, 5, 3, 1] 的数组。我想分成三个数组。这些数组中数字的顺序和项目的数量无关紧要。

假设 A1、A2 和 A3 是子数组。我想最小化函数

f(x) = ( SUM(A1) - SUM(S) / 3 )^2 / 3 +
       ( SUM(A2) - SUM(S) / 3 )^2 / 3 +
       ( SUM(A3) - SUM(S) / 3 )^2 / 3
  • 我不需要最优解;我只需要足够好的解决方案。
  • 我不想要太慢的算法。我可以用一些速度换取更好的结果,但我不能换得太多。
  • S 的长度在 10 到 30 之间。

为什么

为什么我需要解决这个问题?我想把盒子很好地排列成三列,这样每列的总高度不会相差太大。

我尝试了什么

我的第一直觉是使用贪婪。结果还不错,但并不能确保最佳解决方案。有没有更好的办法?

s = [6, 2, 1, 7, 4, 3, 9, 5, 3, 1]
s = sorted(s, reverse=True)

a = [[], [], []]
sum_a = [0, 0, 0]

for x in s:
    i = sum_a.index(min(sum_a))
    sum_a[i] += x
    a[i].append(x)

print(a)

【问题讨论】:

  • 我的大脑还没有完全启动,但我正在尝试确定这是否本质上是 NP 难的knapsack problem
  • 但是“结果还不错,不是保证最优解。”听起来他在寻找最优解。
  • 您的问题是子集和问题的推广,但是有一个伪多项式时间算法和简单(非常好的)近似。
  • 顺便说一下,sorted(s, reverse=True) 无效,因为sorted 返回一个新列表。
  • 这篇关于多路数分区的论文可能会让您感兴趣(特别是第 4 节,序列数分区 (SNP)):aaai.org/ocs/index.php/IJCAI/IJCAI-09/paper/viewFile/625/705

标签: python algorithm


【解决方案1】:

我们可以研究您找到的解决方案在替换找到的列表之间的元素方面的稳定性。下面我放置了我的代码。如果我们通过替换使目标函数变得更好,我们会保留找到的列表并进一步希望我们能通过另一个替换使函数再次变得更好。作为起点,我们可以采取您的解决方案。最终结果将类似于局部最小值。

from copy import deepcopy

s = [6, 2, 1, 7, 4, 3, 9, 5, 3, 1]
s = sorted(s, reverse=True)

a = [[], [], []]
sum_a = [0, 0, 0]

for x in s:
    i = sum_a.index(min(sum_a))
    sum_a[i] += x
    a[i].append(x)


def f(a):
    return ((sum(a[0]) - sum(s) / 3.0)**2 + (sum(a[1]) - sum(s) / 3.0)**2 + (sum(a[2]) - sum(s) / 3.0)**2) / 3


fa = f(a)

while True:
    modified = False

    # placing
    for i_from, i_to in [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)]:
        for j in range(len(a[i_from])):
            a_new = deepcopy(a)
            a_new[i_to].append(a_new[i_from][j])
            del a_new[i_from][j]
            fa_new = f(a_new)
            if fa_new < fa:
                a = a_new
                fa = fa_new
                modified = True
                break
        if modified:
            break

    # replacing
    for i_from, i_to in [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)]:
        for j_from in range(len(a[i_from])):
            for j_to in range(len(a[i_to])):
                a_new = deepcopy(a)
                a_new[i_to].append(a_new[i_from][j_from])
                a_new[i_from].append(a_new[i_to][j_to])
                del a_new[i_from][j_from]
                del a_new[i_to][j_to]
                fa_new = f(a_new)
                if fa_new < fa:
                    a = a_new
                    fa = fa_new
                    modified = True
                    break
            if modified:
                break
        if modified:
            break

    if not modified:
        break

print(a, f(a)) # [[9, 3, 1, 1], [7, 4, 3], [6, 5, 2]] 0.2222222222222222222

有趣的是,即使我们以任意 a 开头,这种方法也能很好地工作:

from copy import deepcopy

s = [6, 2, 1, 7, 4, 3, 9, 5, 3, 1]

def f(a):
    return ((sum(a[0]) - sum(s) / 3.0)**2 + (sum(a[1]) - sum(s) / 3.0)**2 + (sum(a[2]) - sum(s) / 3.0)**2) / 3


a = [s, [], []]
fa = f(a)

while True:
    modified = False

    # placing
    for i_from, i_to in [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)]:
        for j in range(len(a[i_from])):
            a_new = deepcopy(a)
            a_new[i_to].append(a_new[i_from][j])
            del a_new[i_from][j]
            fa_new = f(a_new)
            if fa_new < fa:
                a = a_new
                fa = fa_new
                modified = True
                break
        if modified:
            break

    # replacing
    for i_from, i_to in [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)]:
        for j_from in range(len(a[i_from])):
            for j_to in range(len(a[i_to])):
                a_new = deepcopy(a)
                a_new[i_to].append(a_new[i_from][j_from])
                a_new[i_from].append(a_new[i_to][j_to])
                del a_new[i_from][j_from]
                del a_new[i_to][j_to]
                fa_new = f(a_new)
                if fa_new < fa:
                    a = a_new
                    fa = fa_new
                    modified = True
                    break
            if modified:
                break
        if modified:
            break

    if not modified:
        break

print(a, f(a)) # [[3, 9, 2], [6, 7], [4, 3, 1, 1, 5]] 0.2222222222222222222

它提供了不同的结果但函数的值相同。

【讨论】:

  • 有趣,我会尝试理解代码并查看结果。感谢您花时间写这么长的回复。
【解决方案2】:

正如我在问题的评论中提到的,这是直接的动态编程方法。 s = range(1, 30) 只需不到 1 秒的时间,并提供优化的解决方案。

如果你知道Memoization,我认为代码是不言自明的。

s = range(1, 30)
# s = [6, 2, 1, 7, 4, 3, 9, 5, 3, 1]
n = len(s)

memory = {}
best_f = pow(sum(s), 3)
best_state = None

def search(state, pre_state):
    global memory, best_f, best_state    
    s1, s2, s3, i = state
    f = s1 * s1 + s2 * s2 + s3 * s3
    if state in memory or f >= best_f:
        return
    memory[state] = pre_state
    if i == n:
        best_f = f
        best_state = state
    else:
        search((s1 + s[i], s2, s3, i + 1), state)
        search((s1, s2 + s[i], s3, i + 1), state)
        search((s1, s2, s3 + s[i], i + 1), state)

search((0, 0, 0, 0), None)

a = [[], [], []]
state = best_state
while state[3] > 0:
    pre_state = memory[state]
    for j in range(3):
        if state[j] != pre_state[j]:
            a[j].append(s[pre_state[3]])
    state = pre_state

print a
print best_f, best_state, map(sum, a)

【讨论】:

  • 感谢回答这个问题。我会检查和比较不同的策略。感谢您抽出宝贵的时间来回答这个问题。是的,我确实知道记忆:D
  • @invisal 很高兴我能帮上忙。
  • 我不使用全局变量,而是将该代码放在一个函数中并在search 中使用nonlocal,这样可以避免毒害全局命名空间...
  • 您的代码对s = [random.randint(0,10) for r in xrange(30)] 很满意。但即使数字范围小到random.randint(0,100),代码也不再那么开心了……
  • @Bakuriu 是的,那会更好。此代码仅用于演示算法。
【解决方案3】:

正如您所说,您不介意非最佳解决方案,但我会重新使用您的初始功能,并添加一种方法来为您的初始列表找到良好的起始安排s

你的初始函数:

def pigeon_hole(s):
    a = [[], [], []]
    sum_a = [0, 0, 0]
    for x in s:
        i = sum_a.index(min(sum_a))
        sum_a[i] += x
        a[i].append(x)
    return map(sum, a)

这是一种为您的列表找到合理的初始排序的方法,它通过按排序和反向排序顺序创建列表的轮换来工作。一旦列表被归类,通过最小化标准偏差来找到最佳轮换:

def rotate(l):
    l = sorted(l)
    lr = l[::-1]
    rotation = [np.roll(l, i) for i in range(len(l))] + [np.roll(lr, i) for i in range(len(l))]
    blocks = [pigeon_hole(i) for i in rotation]
    return rotation[np.argmin(np.std(blocks, axis=1))]  # the best rotation

import random
print pigeon_hole(rotate([random.randint(0, 20) for i in range(20)]))

# Testing with some random numbers, these are the sums of the three sub lists
>>> [64, 63, 63]

虽然这可以进一步优化,但对于 20 个数字只需 0.0013 秒。使用a = rotate(range(1, 30)) 与@Mo Tao 的答案进行快速比较

# This method
a = rotate(range(1, 30))
>>> [[29, 24, 23, 18, 17, 12, 11, 6, 5], [28, 25, 22, 19, 16, 13, 10, 7, 4, 1], [27, 26, 21, 20, 15, 14, 9, 8, 3, 2]]
map(sum, a)
# Sum's to [145, 145, 145] in 0.002s

# Mo Tao's method
>>> [[25, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1], [29, 26, 20, 19, 18, 17, 16], [28, 27, 24, 23, 22, 21]]
# Sum's to [145, 145, 145] in 1.095s

这种方法似乎在许多情况下也能找到最佳解决方案,尽管这可能不适用于所有情况。使用包含 30 个数字的列表与莫涛的答案测试此实现 500 次,并比较子列表的总和是否相同:

c = 0
for i in range(500):
    r = [random.randint(1, 10) for j in range(30)]
    res = pigeon_hole(rotate(r))
    d, e = sorted(res), sorted(tao(r))  # Comparing this to the optimal solution by Mo Tao
    if all([k == kk] for k, kk in zip(d, e)):
        c += 1
    memory = {}
    best_f = pow(sum(s), 3)
    best_state = None

>>> 500 # (they do)

我想我会在这里提供一个更优化版本的函数更新:

def rotate2(l):
    # Calculate an acceptable minimum stdev of the pigeon holed list
    if sum(l) % 3 == 0:
        std = 0
    else:
        std = np.std([0, 0, 1])

    l = sorted(l, reverse=True)
    best_rotation = None
    best_std = 100

    for i in range(len(l)):
        rotation = np.roll(l, i)
        sd = np.std(pigeon_hole(rotation))

        if sd == std:  
            return rotation  # If a min stdev if found 

        elif sd < best_std:
            best_std = sd
            best_rotation = rotation

    return best_rotation

主要的变化是一旦找到合适的轮换,对良好排序的搜索就会停止。也只搜索反向排序的列表,这似乎不会改变结果。用

计时
print timeit.timeit("rotate2([random.randint(1, 10) for i in range(30)])", "from __main__ import rotate2, random", number=1000) / 1000.

导致大幅加速。在我目前的计算机上,rotate 大约需要 1.84 毫秒,rotate2 大约需要 0.13 毫秒,因此速度提高了 14 倍。为了比较 גלעד ברקן 的实现在我的机器上花费了大约 0.99 毫秒。

【讨论】:

  • 不确定你在运行什么,但这里是 repl.it (repl.it/Euz5) 我的结果:--- 0.0004673004150390625 seconds --- | rotate2: --- 0.0009853839874267578 seconds --- 另外,KK3 这个函数本身就很酷,哈哈。干杯。
【解决方案4】:

我不得不说,你的贪心函数确实产生了很好的结果,但如果输入量很大,比如超过 100,就会变得非常慢。

但是,您已经说过您的输入大小固定在10,30 范围内。所以贪心的方案其实挺好的。与其一开始就变得太贪心。我建议先变得有点懒,最后变得贪心。

这是一个修改后的函数lazy

def lazy(s):
    k = (len(s)//3-2)*3 #slice limit

    s.sort(reverse=True)
    #Perform limited extended slicing
    a = [s[1:k:3],s[2:k:3],s[:k:3]]

    sum_a = list(map(sum,a))
    for x in s[k:]:
        i = sum_a.index(min(sum_a))
        sum_a[i] += x
        a[i].append(x)
    return a

它的作用是首先按降序对输入进行排序,并将项目一个接一个地填充到三个子列表中,直到剩下大约 6 个项目。(您可以更改此限制并进行测试,但对于大小 10-30我认为这是最好的)

完成后,只需继续使用贪心方法即可。平均而言,这种方法比贪心解决方案花费的时间更少且更准确。

这是大小与时间的折线图 -

尺寸与准确性 -

准确度是最终子列表和原始列表均值的标准差。因为您希望列以几乎相似的高度而不是(原始列表的平均值)高度堆叠。

此外,项目值的范围在3-15 之间,因此总和在100-150 左右,正如您提到的那样。

这些是测试功能 -

def test_accuracy():
    rsd = lambda s:round(math.sqrt(sum([(sum(s)//3-y)**2 for y in s])/3),4)
    sm = lambda s:list(map(sum,s))

    N=[i for i in range(10,30)]
    ST=[]
    MT=[]
    for n in N:
        case = [r(3,15) for x in range(n)]

        ST.append(rsd(sm(lazy(case))))
        MT.append(rsd(sm(pigeon(case))))

    strace = go.Scatter(x=N,y=ST,name='Lazy pigeon')
    mtrace = go.Scatter(x=N,y=MT,name='Pigeon')
    data = [strace,mtrace]

    layout = go.Layout(
    title='Uniform distribution in 3 sublists',
    xaxis=dict(title='List size',),
    yaxis=dict(title='Accuracy - Standard deviation',))
    fig = go.Figure(data=data, layout=layout)

    plotly.offline.plot(fig,filename='N vs A2.html')

def test_timings():
    N=[i for i in range(10,30)]
    ST=[]
    MT=[]
    for n in N:
        case = [r(3,15) for x in range(n)]           
        start=time.clock()
        lazy(case)
        ST.append(time.clock()-start)
        start=time.clock()
        pigeon(case)
        MT.append(time.clock()-start)

    strace = go.Scatter(x=N,y=ST,name='Lazy pigeon')
    mtrace = go.Scatter(x=N,y=MT,name='Pigeon')
    data = [strace,mtrace]

    layout = go.Layout(
    title='Uniform distribution in 3 sublists',
    xaxis=dict(title='List size',),
    yaxis=dict(title='Time (seconds)',))

    fig = go.Figure(data=data, layout=layout)

    plotly.offline.plot(fig,filename='N vs T2.html')

这是完整的file

编辑 -

我测试了 kezzos 答案的准确性,它的表现非常好。偏差始终保持在 0.8 以下。

100 次运行的平均标准差。

懒鸽鸽旋转 1.10668795 1.1573573 0.54776425

在速度的情况下,旋转函数比较的顺序是相当高的。但是,除非您想重复运行该函数,否则 10^-3 是可以的。

懒鸽鸽旋转 5.384013e-05 5.930269e-05 0.004980

这是比较所有三个函数的准确性的条形图。 -

总而言之,如果您对速度感到满意,kezzos 解决方案是最好的。

plotly 的 HTML 文件 - versus time,versus accuracythe bar chart

【讨论】:

  • 不知道为什么这被否决了。绝对不是最优的,但 OP 并不是在寻找最优的。我建议进行一项修改,以循环索引以选择元素0, 3+1, 6+2, 9, 12+1, 15+2, ...,而不仅仅是三的倍数。您将获得更好的传播,而几乎不增加复杂性。
  • @MadPhysicist 感谢您的建议。我还将使用切片与鸽洞以及此处显示的其他一些方法发布精度图,并尝试将它们与速度进行比较。
  • 只是出于好奇,您在我的方法中发现了什么样的错误?
  • @kezzos 现在已修复。我正在使用 python 3.5,其中 map 返回一个地图对象而不是一个列表。
  • 请也测试一下我的
【解决方案5】:

这是我对 Korf's1 序号分区 (SNP) 的疯狂实现,但它只使用 Karmarkar–Karp 而不是 Complete Karmarkar–Karp 进行双向分区(我包含了一个未使用的、有点不满意的 @987654322 @ - 也许有人建议让它更有效率?)。 在第一个子集上,它设置了下限和上限。请参阅参考文章。我确信可以做出更有效的实现。编辑 MAX_ITERATIONS 以获得更好的结果而不是更长的等待:)

顺便说一句,KK3 函数(Karmarkar–Karp 对三路分区的扩展,用于计算第一个下限)本身看起来还不错。

from random import randint
from collections import Counter
from bisect import insort
from time import time

def KK3(s):
  s = list(map(lambda x: (x,0,0,[],[],[x]),sorted(s)))

  while len(s) > 1:
    large = s.pop()
    small = s.pop()
    combined = sorted([large[0] + small[2], large[1] + small[1],
large[2] + small[0]],reverse=True)
    combined = list(map(lambda x: x - combined[2],combined))
    combined = combined + sorted((large[3] + small[5], large[4] +
small[4], large[5] + small[3]),key = sum)
    insort(s,tuple(combined))

  return s

#s = [6, 2, 1, 7, 4, 3, 9, 5, 3, 1]

s = [randint(0,100) for r in range(0,30)]

# global variables
s = sorted(s,reverse=True)
sum_s = sum(s)
upper_bound = sum_s // 3
lower_bound = sum(KK3(s)[0][3])
best = (sum_s,([],[],[]))
iterations = 0
MAX_ITERATIONS = 10000

def partition(i, accum):
  global lower_bound, best, iterations
  sum_accum = sum(accum)

  if sum_accum > upper_bound or iterations > MAX_ITERATIONS:
    return

  iterations = iterations + 1

  if sum_accum >= lower_bound:
    rest = KK(diff(s,accum))[0]
    new_diff = sum(rest[1]) - sum_accum

    if new_diff < best[0]:
      best = (new_diff,(accum,rest[1],rest[2]))
      lower_bound = (sum_s - 2 * new_diff) // 3
      print("lower_bound: " + str(lower_bound))

  if not best[0] in [0,1] and i < len(s) - 1 and sum(accum) + sum(s[i
+ 1:]) > lower_bound:
    _accum = accum[:]
    partition(i + 1, _accum + [s[i]])
    partition(i + 1, accum)

def diff(l1,l2):
  return list((Counter(l1) - Counter(l2)).elements())

def KK(s):
  s = list(map(lambda x: (x,[x],[]),sorted(s)))

  while len(s) > 1:
    large = s.pop()
    small = s.pop()
    insort(s,(large[0] - small[0],large[1] + small[2],large[2] + small[1]))

  return s


print(s)
start_time = time()
partition(0,[])
print(best)
print("iterations: " + str(iterations))
print("--- %s seconds ---" % (time() - start_time))

1Richard E. Korf,加州大学洛杉矶分校计算机科学系多路数分区; aaai.org/ocs/index.php/IJCAI/IJCAI-09/paper/viewFile/625/705

【讨论】:

    猜你喜欢
    • 2011-05-27
    • 2015-01-28
    • 2014-04-26
    • 1970-01-01
    • 2017-02-25
    • 1970-01-01
    • 2019-07-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多