将 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