嗯,我还没有想到 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
在这种情况下,使用此成本函数似乎确实会产生更好的结果:
这可能是因为它为可行状态之间的矩形提供了更多的过渡路径。但是,如果您遇到困难,我会记住其他功能。