网格上二维正方形的非分离矩形边缘覆盖

时间:2017-01-26 12:11:23

标签: algorithm optimization geometry mathematical-optimization

即使标题听起来很复杂,我的实际问题也不应该太难建模。但是,我还没有找到一个好的算法来执行以下操作:

我想在网格上覆盖一组具有固定数字 n 矩形的正方形。这些矩形可能会重叠,它们只需要覆盖我的形状的外边缘。

为什么不是暴力?

sqare m x m 网格上不同矩形的数量

(m^2 (1 + m)^2)/4

因此,蛮力方法必须尝试的组合数量为

O((m^4)^n)

10 x 10 网格的 27,680,640,625 组合,只有 3 矩形。

实施例

上面有一些正方形的初始网格可能如下所示:

Grid with squares to be covered

n = 1 :使用单个矩形覆盖此形状的最佳方法是:

Grid with squares covered by a single rectangle

n = 2 :使用两个矩形可以减少覆盖空方块的数量:

Grid with squares covered by two rectangles

(请注意,中心现在由两个矩形覆盖)

有效封面

我正在寻找一种解决方案,它至少覆盖了所有属于外边缘的正方形,即所有填充的正方形,它们在网格宽度上共享一个空方块。

所有不属于形状外边缘的正方形可能会或可能不会被覆盖,覆盖矩形可能相交也可能不相交。

目标函数

给定固定数量的覆盖矩形 n ,我想覆盖所有填充的正方形,但最小化覆盖的空方格的数量。这意味着中心的空方块不应计入必须最小化的目标函数(我也可以在应用算法之前填充所有空洞,而不会产生差异)。

我的例子的目标函数的值是:

n  |  target function
---|-----------------
1  |  11
2  |   3

附加约束

请注意,原始方块组可以不连接,并且未连接的子形状的数量甚至可能超过覆盖矩形的数量。

替代说明

为了简化问题,您还可以处理输入数据的转换版本:

enter image description here

然后目标是覆盖所有蓝色方块并使用可能相交的 n 矩形最小化覆盖白色方块的数量

3 个答案:

答案 0 :(得分:2)

不是一个完整的解决方案,而是一些(保留最佳保留条件)减少规则:

  1. 如果您想要一个完全没有覆盖白色方块的解决方案,那么您可以安全地合并任何相邻的相同行或列对。这是因为对于较小的合并问题的任何有效解决方案都不能覆盖任何白方,可以通过"拉伸"来扩展到解决原始问题的解决方案。每个合并线上的每个矩形按照与合并相反的顺序执行 - 这不会导致任何未覆盖的白色方块被覆盖,任何蓝色方块被覆盖,或者更改所需的矩形数量。取决于"曲线如何"你的原始图像,这可以大大减少输入问题的大小。 (即使对于覆盖白色方块的解决方案,您仍然可以应用此策略 - 但是"扩展的"解决方案可能会覆盖比原始解决方案更多的白色方块。仍可用作启发式方法。)
  2. 您可以通过将已经放置的矩形(无论它们原来是蓝色还是白色)覆盖所有细胞粉红色来表示任何部分解决方案;粉红色细胞是可以被(进一步)矩形覆盖的细胞,不需要任何费用,但不需要覆盖。如果您正在寻找一个完全没有覆盖白色方块的解决方案,那么您可以应用强化形式的规则1来缩小实例:不仅可以像以前那样合并相同的相邻行和列对,您可以先根据以下规则将一些粉红色单元格更改为蓝色,这可能会使更多合并发生。两个相邻列的规则是:如果第1列中的每个白色单元格在第2列中也是白色,反之亦然,那么在包含一个粉红色和一个蓝色单元格的每一行中,您可以将粉红色单元格更改为蓝色。 (理由:一些非白细胞覆盖的矩形最终必须覆盖蓝色细胞;这个矩形也可以拉伸以覆盖粉红色细胞,而不覆盖任何新的白细胞。)例如:

    WW         WW         W
    BB         BB         B
    BP   -->   BB   -->   B
    PP         PP         P
    PB         BB         B
    
  3. 您永远不要认为矩形是一个矩形的正确子矩形,不包含白色单元格。

  4. 一些进一步的想法:

    只需将图像调整为较小的尺寸,其中新高度是原始高度的整数因子,类似于宽度,如果原始图像中相应的单元格块中的任何单元格为蓝色,则单元格为蓝色,应该给出一个更容易解决的好的近似子问题。 (如果需要,用原始图像填充白色单元格。)解决了这个较小的问题并将解决方案重新扩展到原始大小后,您可以从一些矩形的边缘修剪更多的行或列。

答案 1 :(得分: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'])

以下是我在上面的示例代码中指定的十次运行的算法进度的显示,作为迭代次数的函数(我添加了一些抖动,因此您可以看到所有行):

Convergence by iteration

你会注意到大多数(8/10)的跑步在早期8点发现了最小值。同样地,在6/10运行中,最小值为5,其中大部分都是在早期这样做的。这表明,运行许多较短的搜索而不是一些长搜索可能会更好。选择合适的长度和运行次数将是一个实验问题。

请注意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

在这种情况下,使用此成本函数确实可以产生更好的结果:

Convergence by iteration with new cost function

这可能是因为它为可行状态之间的矩形提供了更多的转换路径。但如果遇到困难,我会记住其他功能。

答案 2 :(得分:0)

我有一个不同的问题,我想提议:

说你有三个孤立的方块有哪些可能性:

一个覆盖所有三个

的矩形

两个矩形,有3种可能性覆盖2 + 1

和三个覆盖每个

的矩形

所以订单是Sum_i n_choose_i

远小于您的订单

多项式在任何情况下都是n而不是指数。

然后你可以减少你的解决方案(顺便说一下:这是更好的矩形或更小的空单元,但你可以覆盖它)