网格上二维正方形的非分离矩形边缘覆盖
Posted
技术标签:
【中文标题】网格上二维正方形的非分离矩形边缘覆盖【英文标题】:Non-Disjunct Rectangle Edge Covering for 2D Squares on a Grid 【发布时间】:2017-06-11 21:47:36 【问题描述】:尽管标题听起来很复杂,但我的实际问题不应该太难建模。但是,我还没有找到一个好的算法来执行以下操作:
我想用固定数量的 n 个矩形覆盖网格上的一组正方形。这些矩形可能会重叠,它们只需要覆盖我的形状的外边缘。
为什么不用蛮力?
一个正方形m x m网格上不同矩形的数量是
.
因此,蛮力方法必须尝试的组合数量在
这将是 10 x 10 网格的 27,680,640,625 个组合,并且只有 3 个矩形。
示例
带有一些正方形的初始网格可能如下所示:
n = 1:用一个矩形覆盖这个形状的最佳方法是:
n = 2:使用两个矩形可以减少被覆盖的空方块的数量,如下所示:
(注意中心现在被两个矩形覆盖)
有效封面
我正在寻找一种解决方案,它至少涵盖作为外边缘一部分的所有正方形,即所有在网格宽度上共享一个边缘的填充正方形和一个空正方形。
所有不属于形状外边缘的正方形可能被覆盖,也可能不被覆盖,覆盖的矩形可能相交也可能不相交。
目标函数
给定固定数量的覆盖矩形n,我想覆盖所有填充的正方形,但尽量减少覆盖的空正方形的数量在形状之外。这意味着中心的空方格不应计入必须最小化的目标函数(我也可以在应用算法之前填充所有空洞而不会产生影响)。
因此,我的示例的目标函数的值是:
n | target function
---|-----------------
1 | 11
2 | 3
其他约束
请注意,原始正方形集可能不连通,未连通子形状的数量甚至可能超过覆盖矩形的数量。
备用说明
为了简化问题,您也可以只处理输入数据的转换版本:
那么目标是覆盖所有蓝色方块并使用可能相交的 n 个矩形来最小化覆盖的白色方块的数量。
【问题讨论】:
你的所有目标集是否都有对称线,就像这个? @Richard 不,他们没有。 这让我想起了切削库存和顶点覆盖问题,这两个问题都在 NP 中,这意味着您需要一个启发式、近似方案或类似的方案。我今天花了几个小时研究 MIP 风格的解决方案,但我仍在学习这些技术,最终得到了混乱的非线性。我已经更改了标签,试图吸引更擅长这类事情的人。 @Richard 我知道许多常见的顶点和边缘覆盖问题都是 NP。但是,有些极端情况在 P 中有算法。虽然我真的不确定我的具体问题...... 【参考方案1】:嗯,我还没有想到 P 级的解决方案,但我确实想到这个问题可能是随机解决方案的一个很好的候选者。
值得注意的是,有一个易于定义的可行起点:只需将所有覆盖矩形设置为目标方块的边界框的范围即可。
从这个初始状态,可以通过减少覆盖矩形的边界之一并检查所有目标方块是否仍然被覆盖来生成新的有效状态。
此外,任何两个状态之间的路径都可能很短(每个矩形都可以在 O(√n) 时间内缩减到其适当的尺寸,其中 n是边界框中的方块数),这意味着它很容易在搜索空间中移动。尽管这附带了一个警告,即一些可能的解决方案被一条返回初始状态的狭窄路径隔开,这意味着重新运行我们即将开发的算法几次可能是好的。
鉴于上述情况,simulated annealing 是解决问题的一种可能方法。下面的 Python 脚本实现了它:
#!/usr/bin/env python3
import random
import numpy as np
import copy
import math
import scipy
import scipy.optimize
#Generate a grid
class Grid:
def __init__(self,grid_array):
self.grid = np.array(grid_array)
self.width = len(self.grid[0]) #Use inclusive coordinates
self.height = len(self.grid) #Use inclusive coordinates
#Convert into a list of cells
self.cells =
for y in range(len(self.grid)):
for x in range(len(self.grid[y])):
self.cells[(x,y)] = self.grid[y][x]
#Find all cells which are border cells (the ones we need covered)
self.borders = []
for c in self.cells:
for dx in [-1,0,1]: #Loop through neighbors
for dy in [-1,0,1]:
n = (c[0]+dx,c[1]+dy) #This is the neighbor
if self.cells[c]==1 and self.cells.get(n, 1)==0: #See if this cell has a neighbor with value 0. Use default return to simplify code
self.borders.append(c)
#Ensure grid contains only valid target cells
self.grid = np.zeros((self.height,self.width))
for b in self.borders:
self.grid[b[1],b[0]] = 1
self.ntarget = np.sum(self.grid)
def copy(self):
return self.grid.copy()
#A state is valid if the bounds of each rectangle are inside the bounding box of
#the target squares and all the target squares are covered.
def ValidState(rects):
#Check bounds
if not (np.all(0<=rects[0::4]) and np.all(rects[0::4]<g.width)): #x
return False
if not (np.all(0<=rects[1::4]) and np.all(rects[1::4]<g.height)): #y
return False
if not (np.all(0<=rects[2::4]) and np.all(rects[2::4]<=g.width)): #w
return False
if not (np.all(0<=rects[3::4]) and np.all(rects[3::4]<=g.height)): #h
return False
fullmask = np.zeros((g.height,g.width))
for r in range(0,len(rects),4):
fullmask[rects[r+1]:rects[r+3],rects[r+0]:rects[r+2]] = 1
return np.sum(fullmask * g.grid)==g.ntarget
#Mutate a randomly chosen bound of a rectangle. Keep trying this until we find a
#mutation that leads to a valid state.
def MutateRects(rects):
current_state = rects.copy()
while True:
rects = current_state.copy()
c = random.randint(0,len(rects)-1)
rects[c] += random.randint(-1,1)
if ValidState(rects):
return rects
#Determine the score of a state. The score is the sum of the number of times
#each empty space is covered by a rectangle. The best solutions will minimize
#this count.
def EvaluateState(rects):
score = 0
invgrid = -(g.grid-1) #Turn zeros into ones, and ones into zeros
for r in range(0,len(rects),4):
mask = np.zeros((g.height,g.width))
mask[rects[r+1]:rects[r+3],rects[r+0]:rects[r+2]] = 1
score += np.sum(mask * invgrid)
return score
#Print the list of rectangles (useful for showing output)
def PrintRects(rects):
for r in range(0,len(rects),4):
mask = np.zeros((g.height,g.width))
mask[rects[r+1]:rects[r+3],rects[r+0]:rects[r+2]] = 1
print(mask)
#Input grid is here
gridi = [[0,0,1,0,0],
[0,1,1,1,0],
[1,1,0,1,1],
[0,1,1,1,0],
[0,1,0,1,0]]
g = Grid(gridi)
#Number of rectangles we wish to solve with
rect_count = 2
#A rectangle is defined as going from (x,y)-(w,h) where (w,h) is an upper bound
#on the array coordinates. This allows efficient manipulation of rectangles as
#numpy arrays
rects = []
for r in range(rect_count):
rects += [0,0,g.width,g.height]
rects = np.array(rects)
#Might want to run a few times since the initial state is something of a
#bottleneck on moving around the search space
sols = []
for i in range(10):
#Use simulated annealing to solve the problem
sols.append(scipy.optimize.basinhopping(
func = EvaluateState,
take_step = MutateRects,
x0 = rects,
disp = True,
niter = 3000
))
#Get a minimum solution and display it
PrintRects(min(sols, key=lambda x: x['lowest_optimization_result']['fun'])['x'])
这是我在上面的示例代码中指定的十次运行的算法进度显示为迭代次数的函数(我添加了一些抖动,以便您可以看到所有行):
您会注意到,大多数 (8/10) 的运行在 8 点很早就找到了最小值。同样,在 5 处找到最小值的 6/10 运行中,它们中的大多数在早期就这样做了。这表明运行许多较短的搜索而不是一些较长的搜索可能会更好。选择合适的运行长度和运行次数将是一个实验问题。
请注意,EvaluateState
每次会在一个空方块被矩形覆盖时添加点。这抑制了冗余覆盖,这可能是找到解决方案所必需的,或者可能导致更快地找到解决方案。成本函数包含这种东西是很常见的。尝试直接询问您想要什么的成本函数很容易 - 只需替换 EvaluateState
如下:
#Determine the score of a state. The score is the sum of the number of times
#each empty space is covered by a rectangle. The best solutions will minimize
#this count.
def EvaluateState(rects):
score = 0
invgrid = -(g.grid-1) #Turn zeros into ones, and ones into zeros
mask = np.zeros((g.height,g.width))
for r in range(0,len(rects),4):
mask[rects[r+1]:rects[r+3],rects[r+0]:rects[r+2]] = 1
score += np.sum(mask * invgrid)
return score
在这种情况下,使用此成本函数似乎确实会产生更好的结果:
这可能是因为它为可行状态之间的矩形提供了更多的过渡路径。但是,如果您遇到困难,我会记住其他功能。
【讨论】:
这似乎是一个精心设计的启发式算法。对于更大的问题,我将不得不对其进行测试,但这在大多数情况下应该会给出“足够好”的结果。我考虑过实现遗传算法,但实际上模拟退火似乎更适合这个问题。 谢谢,@MaximilianKöstler。我添加了一个图表,您可能会对它显示启发式的进度感兴趣。我也考虑过遗传算法,但决定反对它们,因为(a)如果包括交叉,它们在寻找全局最小值方面变得不那么熟练,(b)交叉可能更频繁地产生不可行的状态,以及(c)如果只是使用突变,其他算法,例如这个,更容易实现。我很想知道这对你有什么影响:请回来报告! :-) 您能否将图表的(我假设的)matplotlib 代码添加到解决方案中?有评价就好了。 哦,我刚刚看到您的EvaluateState
函数将每个空白区域的覆盖矩形数相加。我实际上只是希望将(由任意数量的矩形)覆盖的空白空间的 total count 最小化,但我可以自己解决这个问题。
@MaximilianKöstler:我实际上在 gnuplot 中构建了图表,但是构建 matplotlib 代码来执行您的建议应该很容易。【参考方案2】:
我有一个不同的问题想提出:
假设你有三个孤立的方块,有哪些可能性:
一个矩形覆盖所有三个
两个矩形,覆盖 2 +1 的 3 种可能性
三个矩形分别覆盖一个
所以顺序是 Sum_in_choose_i
比您的订单小很多
在任何情况下都是多项式,而不是指数。
然后你可以减少你的解决方案(顺便说一句,这是冲突的:哪个更好,更少的矩形或更少的空单元格,但你可以覆盖它)
【讨论】:
首先,关于解决方案的最后一行:我的目标是为任意但固定数量的矩形 n 获得最佳覆盖(关于我的目标函数)。所以 n 在优化过程中不是变量。它是预先选择的。其次,您的解决方案不正确。如果您使用组合数学,则需要 Stirling number of the second kind。对于 100 平方米和三个大约 10^47 的矩形。【参考方案3】:不是一个完整的解决方案,而是一些(在某些条件下保持最优性)减少规则:
-
如果您想要一个完全没有白色方块被覆盖的解决方案,那么您可以安全地合并任何相邻的相同行或列对。这是因为对于不覆盖任何白色正方形的较小的合并问题的任何有效解决方案都可以通过以执行合并的相反顺序“拉伸”每个合并线上的每个矩形来扩展为原始问题的解决方案 - 这赢了不要导致任何未覆盖的白色正方形被覆盖,任何蓝色正方形被覆盖,或更改所需的矩形数量。根据原始图像的“曲线”程度,这可以大大减少输入问题的大小。 (即使对于覆盖白色方块的解决方案,您仍然可以应用此策略 - 但“扩展”解决方案可能会比原始解决方案覆盖更多的白色方块。仍然可以用作启发式方法。)
您可以通过将已放置的矩形(无论它们最初是蓝色还是白色)覆盖的所有单元格变为粉红色来表示任何部分解决方案;粉红色单元格是可以免费被(进一步)矩形覆盖但不需要覆盖的单元格。如果您正在寻找一个完全没有白色方块被覆盖的解决方案,那么您可以应用规则 1 的强化形式来缩小实例:您不仅可以像以前一样合并相同的相邻行和列对,您还可以首先根据以下规则将一些粉红色的单元格更改为蓝色,这可能会使更多的合并发生。相邻两列的规则是:如果第 1 列中的每个白色单元格在第 2 列中也是白色的,反之亦然,则在包含一个粉色和一个蓝色单元格的每一行中,您可以将粉色单元格更改为蓝色。 (理由:一些非白色单元格覆盖的矩形最终必须覆盖蓝色单元格;这个矩形也可以被拉伸以覆盖粉色单元格,而不覆盖任何新的白色单元格。)示例:
WW WW W
BB BB B
BP --> BB --> B
PP PP P
PB BB B
您永远不需要考虑一个矩形,它是不覆盖白色单元格的矩形的正确子矩形。
一些进一步的想法:
只需将图像大小调整为更小的尺寸,其中新高度是原始高度的整数因子,宽度也是如此,如果原始图像中相应单元格块中的任何单元格为蓝色,则单元格为蓝色,应该给出一个更容易解决的好近似子问题。 (如有必要,用白色单元格填充原始图像。)在解决了这个较小的问题并将解决方案重新扩展到原始大小之后,您可能能够从某些矩形的边缘修剪更多的行或列。
【讨论】:
感谢您的意见!我同意你建议的简化。但是,我不认为仅仅像这样降低复杂性最终会导致一个计算上可行的问题。即使我可以消除 99.999% 的所有要考虑的情况,仍然需要十亿年才能找到比我给出的示例稍大的问题的最佳解决方案。如果没有针对相关数学问题的“智能”解决方案,我将不得不使用启发式方法。非常感谢您的建议。 不客气 :) 如果在您选择的每个决策点,我认为在无覆盖白方块条件下使用分支定界以获得精确解决方案可能会走得很远可以被最少数量的避免白细胞的最大矩形覆盖的蓝色细胞(可能有几个这样的细胞;选择任何一个),然后尝试依次放置这些最大矩形中的每一个。此外,我现在已经说服自己合并相邻的相同行或列可以保持最优性即使我们被允许覆盖一些白色单元格。 事实上,B&B 甚至适用于覆盖一些白色正方形的解决方案——而不是只考虑不覆盖白色正方形的最大矩形,您需要考虑更大的一组矩形 (a)不包含在任何更大的仅蓝色和粉红色的矩形中,并且 (b) 不会导致到目前为止部分解决方案中覆盖的白色单元的总数超过现有(迄今为止最好的)完整解决方案中的数量.以上是关于网格上二维正方形的非分离矩形边缘覆盖的主要内容,如果未能解决你的问题,请参考以下文章
dlib,python:如何使整个头部(不仅是脸)变成正方形(不是矩形)?