将 n 个可变高度图像拟合到 3 个(相似长度)列布局中

Posted

技术标签:

【中文标题】将 n 个可变高度图像拟合到 3 个(相似长度)列布局中【英文标题】:fitting n variable height images into 3 (similar length) column layout 【发布时间】:2011-08-03 08:21:00 【问题描述】:

我希望创建一个类似于piccsy.com 的 3 列布局。给定许多宽度相同但高度不同的图像,有什么算法可以对它们进行排序以使列长的差异最小?最好使用 Python 或 javascript...

非常感谢您提前提供的帮助!

马丁

【问题讨论】:

您正在寻找一种打包算法。特别是二维矩形条形包装算法。希望这会有所帮助。 但所有矩形的宽度相同这一事实使它更简单,不是吗? @Robin:是的,这只是普通的 Bin Packing(参见***)。但是 Bin Packing 是 NP 完全的,因此找到最佳解决方案可能无法满足您的需求。 clintp 提出的 First Fit Decreasing 算法可能足以满足您的需求。如果您需要更多,请查看元启发式等。 不,正如我在回答中所说,这实际上是与 Bin Packing 密切相关的问题,可以描述为离线 makepan 最小化,或多处理器调度问题。不同之处在于,装箱涉及可变数量的箱和固定的箱尺寸,而这个问题恰恰相反:实际上它是固定数量的箱和可变的箱尺寸。 @Robin 很好,这不是一个确切的 Bin 打包问题。 【参考方案1】:

多少张图片?

如果您限制最大页面大小,并且有一个最小图片高度值,您可以计算每页的最大图像数量。在评估任何解决方案时都需要这个。

我认为您提供的链接上有 27 张图片。

以下使用 Robin Green 之前提到的 first_fit 算法,但随后通过贪婪交换对此进行了改进。

交换例程找到离平均列高最远的列,然后系统地在其一张图片和另一列中的第一张图片之间寻找交换,以最大限度地减少与平均值的最大偏差。

我使用了 30 张图片的随机样本,高度在 5 到 50 个“单位”之间。就我而言,收敛速度很快,并且在 first_fit 算法上得到了显着改进。

代码(Python 3.2:

def first_fit(items, bincount=3):
    items = sorted(items, reverse=1) # New - improves first fit.
    bins     = [[] for c in range(bincount)]
    binsizes = [0] * bincount
    for item in items:
        minbinindex = binsizes.index(min(binsizes))
        bins[minbinindex].append(item)
        binsizes[minbinindex] += item
    average = sum(binsizes) / float(bincount)
    maxdeviation = max(abs(average - bs) for bs in binsizes)

    return bins, binsizes, average, maxdeviation

def swap1(columns, colsize, average, margin=0):
    'See if you can do a swap to smooth the heights'
    colcount = len(columns)
    maxdeviation, i_a = max((abs(average - cs), i)
                              for i,cs in enumerate(colsize))
    col_a = columns[i_a]
    for pic_a in set(col_a): # use set as if same height then only do once
        for i_b, col_b in enumerate(columns):
            if i_a != i_b: # Not same column
                for pic_b in set(col_b):
                    if (abs(pic_a - pic_b) > margin): # Not same heights
                        # new heights if swapped
                        new_a = colsize[i_a] - pic_a + pic_b
                        new_b = colsize[i_b] - pic_b + pic_a
                        if all(abs(average - new) < maxdeviation
                               for new in (new_a, new_b)):
                            # Better to swap (in-place)
                            colsize[i_a] = new_a
                            colsize[i_b] = new_b
                            columns[i_a].remove(pic_a)
                            columns[i_a].append(pic_b)
                            columns[i_b].remove(pic_b)
                            columns[i_b].append(pic_a)
                            maxdeviation = max(abs(average - cs)
                                               for cs in colsize)
                            return True, maxdeviation
    return False, maxdeviation

def printit(columns, colsize, average, maxdeviation):
    print('columns')
    pp(columns)
    print('colsize:', colsize)
    print('average, maxdeviation:', average, maxdeviation)
    print('deviations:', [abs(average - cs) for cs in colsize])
    print()


if __name__ == '__main__':
    ## Some data
    #import random
    #heights = [random.randint(5, 50) for i in range(30)]
    ## Here's some from the above, but 'fixed'.
    from pprint import pprint as pp

    heights = [45, 7, 46, 34, 12, 12, 34, 19, 17, 41,
               28, 9, 37, 32, 30, 44, 17, 16, 44, 7,
               23, 30, 36, 5, 40, 20, 28, 42, 8, 38]

    columns, colsize, average, maxdeviation = first_fit(heights)
    printit(columns, colsize, average, maxdeviation)
    while 1:
        swapped, maxdeviation = swap1(columns, colsize, average, maxdeviation)
        printit(columns, colsize, average, maxdeviation)
        if not swapped:
            break
        #input('Paused: ')

输出:

columns
[[45, 12, 17, 28, 32, 17, 44, 5, 40, 8, 38],
 [7, 34, 12, 19, 41, 30, 16, 7, 23, 36, 42],
 [46, 34, 9, 37, 44, 30, 20, 28]]
colsize: [286, 267, 248]
average, maxdeviation: 267.0 19.0
deviations: [19.0, 0.0, 19.0]

columns
[[45, 12, 17, 28, 17, 44, 5, 40, 8, 38, 9],
 [7, 34, 12, 19, 41, 30, 16, 7, 23, 36, 42],
 [46, 34, 37, 44, 30, 20, 28, 32]]
colsize: [263, 267, 271]
average, maxdeviation: 267.0 4.0
deviations: [4.0, 0.0, 4.0]

columns
[[45, 12, 17, 17, 44, 5, 40, 8, 38, 9, 34],
 [7, 34, 12, 19, 41, 30, 16, 7, 23, 36, 42],
 [46, 37, 44, 30, 20, 28, 32, 28]]
colsize: [269, 267, 265]
average, maxdeviation: 267.0 2.0
deviations: [2.0, 0.0, 2.0]

columns
[[45, 12, 17, 17, 44, 5, 8, 38, 9, 34, 37],
 [7, 34, 12, 19, 41, 30, 16, 7, 23, 36, 42],
 [46, 44, 30, 20, 28, 32, 28, 40]]
colsize: [266, 267, 268]
average, maxdeviation: 267.0 1.0
deviations: [1.0, 0.0, 1.0]

columns
[[45, 12, 17, 17, 44, 5, 8, 38, 9, 34, 37],
 [7, 34, 12, 19, 41, 30, 16, 7, 23, 36, 42],
 [46, 44, 30, 20, 28, 32, 28, 40]]
colsize: [266, 267, 268]
average, maxdeviation: 267.0 1.0
deviations: [1.0, 0.0, 1.0]

好问题。


这是我在下面的单独评论中提到的反向排序信息。

>>> h = sorted(heights, reverse=1)
>>> h
[46, 45, 44, 44, 42, 41, 40, 38, 37, 36, 34, 34, 32, 30, 30, 28, 28, 23, 20, 19, 17, 17, 16, 12, 12, 9, 8, 7, 7, 5]
>>> columns, colsize, average, maxdeviation = first_fit(h)
>>> printit(columns, colsize, average, maxdeviation)
columns
[[46, 41, 40, 34, 30, 28, 19, 12, 12, 5],
 [45, 42, 38, 36, 30, 28, 17, 16, 8, 7],
 [44, 44, 37, 34, 32, 23, 20, 17, 9, 7]]
colsize: [267, 267, 267]
average, maxdeviation: 267.0 0.0
deviations: [0.0, 0.0, 0.0]

如果您有反向排序,附加到上述代码底部的额外代码(在 'if name == ... 中)将对随机数据进行额外试验:

for trial in range(2,11):
    print('\n## Trial %i' % trial)
    heights = [random.randint(5, 50) for i in range(random.randint(5, 50))]
    print('Pictures:',len(heights))
    columns, colsize, average, maxdeviation = first_fit(heights)
    print('average %7.3f' % average, '\nmaxdeviation:')
    print('%5.2f%% = %6.3f' % ((maxdeviation * 100. / average), maxdeviation))
    swapcount = 0
    while maxdeviation:
        swapped, maxdeviation = swap1(columns, colsize, average, maxdeviation)
        if not swapped:
            break
        print('%5.2f%% = %6.3f' % ((maxdeviation * 100. / average), maxdeviation))
        swapcount += 1
    print('swaps:', swapcount)

额外的输出显示了交换的效果:

## Trial 2
Pictures: 11
average  72.000 
maxdeviation:
 9.72% =  7.000
swaps: 0

## Trial 3
Pictures: 14
average 118.667 
maxdeviation:
 6.46% =  7.667
 4.78% =  5.667
 3.09% =  3.667
 0.56% =  0.667
swaps: 3

## Trial 4
Pictures: 46
average 470.333 
maxdeviation:
 0.57% =  2.667
 0.35% =  1.667
 0.14% =  0.667
swaps: 2

## Trial 5
Pictures: 40
average 388.667 
maxdeviation:
 0.43% =  1.667
 0.17% =  0.667
swaps: 1

## Trial 6
Pictures: 5
average  44.000 
maxdeviation:
 4.55% =  2.000
swaps: 0

## Trial 7
Pictures: 30
average 295.000 
maxdeviation:
 0.34% =  1.000
swaps: 0

## Trial 8
Pictures: 43
average 413.000 
maxdeviation:
 0.97% =  4.000
 0.73% =  3.000
 0.48% =  2.000
swaps: 2

## Trial 9
Pictures: 33
average 342.000 
maxdeviation:
 0.29% =  1.000
swaps: 0

## Trial 10
Pictures: 26
average 233.333 
maxdeviation:
 2.29% =  5.333
 1.86% =  4.333
 1.43% =  3.333
 1.00% =  2.333
 0.57% =  1.333
swaps: 4

【讨论】:

您可以通过允许图片从一列移动到另一列以及交换来改进上述 swap1 功能。就目前而言,一列中的图片数量始终与首次拟合算法找到的数量相同。如果有一张特别高的图片,那么这可能会扭曲事情。 我刚刚发现,如果你在反向排序的高度上使用 first_fit,你会得到一个完美的分布。我怀疑这将永远是这种情况。也许我们应该总是对图片高度进行反向排序? 只是从蹩脚的***文章转到developerfusion.com/article/5540/bin-packing,我认为从他们的例子中我所说的 first_fit 可能更好地称为最坏的_fit。【参考方案2】:

这是离线makepan最小化问题,我认为它相当于多处理器调度问题。你有图像而不是工作,而不是工作持续时间你有图像高度,但这是完全相同的问题。 (它涉及空间而不是时间这一事实并不重要。)因此(大约)解决其中任何一个问题的任何算法都可以。

【讨论】:

是的。这是一个算法的链接:columbia.edu/~cs2035/courses/ieor6400.F07/hs.pdf【参考方案3】:

这是一种算法(称为First Fit Decreasing),可以在合理的时间内为您提供非常紧凑的安排。可能有更好的算法,但这太简单了。

    按从高到低的顺序对图像进行排序。 拍摄第一张图像,并将其放在最短的列中。 (如果多列高度相同(且最短),请选择任意一列。) 重复第 2 步,直到没有图像。

完成后,如果您不喜欢从最高到最低的外观,您可以根据自己的选择重新排列每列中的元素。

【讨论】:

感谢 clint,我已经尝试过您的解决方案,但在某些情况下结果并不令人满意,例如如果在您添加 20 张图片后列的长度已经非常相似,并且只有一张要添加,那么最后一张会非常明显地突出.. 这是一个 NP-hard(或者我相信甚至是 NP-Complete)问题,可以找到绝对最佳的包装安排。给定少量元素,您当然可以暴力破解最佳解决方案。只需获取图像的所有排列,分成 3 个桶,计算高度,保存最适合的分区/排列。【参考方案4】:

这是一个:

 // Create initial solution
 <run First Fit Decreasing algorithm first>
 // Calculate "error", i.e. maximum height difference
 // after running FFD
 err = (maximum_height - minimum_height)
 minerr = err

 // Run simple greedy optimization and random search
 repeat for a number of steps: // e.g. 1000 steps
    <find any two random images a and b from two different columns such that
     swapping a and b decreases the error>
    if <found>:
         swap a and b
         err = (maximum_height - minimum_height)
         if (err < minerr):
              <store as best solution so far> // X
    else:
         swap two random images from two columns
         err = (maximum_height - minimum_height)

 <output the best solution stored on line marked with X>

【讨论】:

以上是关于将 n 个可变高度图像拟合到 3 个(相似长度)列布局中的主要内容,如果未能解决你的问题,请参考以下文章

将 svg 拟合到 angular.js 中的背景图像 div

在 Java 中使用 hasNext 创建可变长度的自动换行

SQL Server常见数据类型介绍

Java可变长度参数

如何将字体大小绑定到可变网格大小

获取pyspark数组类型列的最后n个元素