寻找拼图解决方案的算法

时间:2010-10-28 09:50:07

标签: algorithm path collision-detection collision

我正在尝试制作一款游戏,玩家必须在游戏板上从头到尾找到自己的方式。 ![游戏板] [1]

如你所见,这个游戏板包含一堆红色的圆形障碍物。为了赢得比赛,玩家必须移除最少量的障碍物。所以我的问题是,我如何以编程方式找出要移除的最小障碍物,以释放路径?自由路径将被视为空间之间的空间,圆圈不重叠且不接触。

所以我真正需要的是要移除的最小圆圈量,我不需要实际的路径。有一个简单的方法吗?

为了补充对这个游戏板的理解,每个圆圈都有相同的半径,并受到黑线的限制。

也没有必要直线移动。

5 个答案:

答案 0 :(得分:17)

这是一个图论maximum flow问题。

假设每个圆都是图中的一个节点。另外引入了2个特殊节点:TOPBOTTOM。如果这些节点与TOP / BOTTOM侧相交,则将这些节点连接起来。如果圆相交,则将与圆相对应的节点相互连接。

现在您需要在此图表中找到最小切割,将 TOP作为源并将 BOTTOM作为接收器,反之亦然。您可以使用Max-flow_min-cut_theorem来解决它。它说的是Minimum-cut problem与Max-flow问题等价。您可以在TopCoder上找到有关如何解决Max-Flow problem的详细信息。

由于我们只能通过每个节点一次,我们应该将节点转换为容量的有向边,其中每个节点具有节点内和节点外。 max-flow算法将解决结果图上的问题,并考虑到我们正在删除圆而不是圆之间的连接这一事实。对于此问题,删除图形中的节点而不是边缘总是更好的决定,因为我们总是可以通过删除节点来删除任何边缘。另外删除节点可能导致删除多个边缘。

顺便说一下,在Uva Online Judge上可以找到类似的问题。尝试在法官身上解决这个问题是个好主意,然后你就会确定你的解决方案是正确的。

答案 1 :(得分:13)

在试图想象列昂尼德所写的内容时,我制作了以下图表。

alt text

答案 2 :(得分:6)

对于图表翻译,这样的事情可能有用。

如果两个圆重叠,则在两个圆之间做一个墙(蓝线)。不要忘记添加顶部和底部边框。这会产生几个区域。这些将是图表的所有节点。

接下来,我们必须找到边缘,从一个节点到另一个节点的成本。拿两个相邻区域(共用一堵墙)。然后通过蛮力,或者你想出的聪明方法,确定从这个区域移动到另一个区域的成本。如果删除圆圈,则表示删除了转到该圆圈的所有墙壁。如果这使得两个区域成为一个区域,则边缘的成本为1.如果需要移除两个圆(它们具有的所有墙)以组合这两个区域,则成本为2.等等。

绘制了一些边缘(绿色)。我们必须从起始区域到结束区域。你现在有一个日常加权图。

我认为这可以改进很多,但我把它留作练习=)

在这种情况下,最小值为3。

警告,图片是手工绘制的,我确实忘记了几个墙壁,边缘和区域。仅用于说明目的。 alt text

答案 3 :(得分:3)

好吧,所以我决定在pygame中对此进行可视化。

事实证明,这比我预期的要强很多。

其他建议中的想法是使用最大流量。从源到汇的流动瓶颈是流量最密集的地方。如果我们在这个密集的瓶颈上将图形切成两半(即 min-cut ),那么我们有最小的圆圈数。 它发生在maxflow = min-cut。

以下是我采取的步骤:

  1. 创建pygame世界,我可以随机生成圆圈

  2. 设定功能以计算圆圈之间的所有碰撞:

  3. 这涉及按x坐标对圆圈进行排序。现在找到Circle [0]的所有碰撞,我继续沿着阵列测试进行碰撞,直到找到一个圆圈,其中sx值大于2 *半径大于circle [0]' sx值,然后我可以弹出圆圈[0]并重复这个过程..

    1. 创建源节点和汇聚节点,找出需要连接的圈子。
    2. 步骤4-5在" findflow()功能"

      中执行
      1. 创建表示带节点的圆的基本无向网络X图。仅在相应的圆圈碰撞时才连接节点。

      2. 这就是它开始变得艰难的地方..我从我的无向图中创建一个新的有向图。因为我需要通过圆(即节点)而不是边缘来计算流量,我需要将每个节点分成两个节点,边缘介于两者之间。

        假设我将节点X连接到节点Y(Y< - > X)(在原始图中)。

        我将X更改为Xa和Xb,以便Xa连接到Xb(Xa> Xb) Y也变为(Ya-> Yb)。

        我还需要添加(Yb-> Xa)和(Xb-> Ya)来表示X和Y之间的原始连接。

      3. 无向图中的所有边都被赋予容量= 1 (例如,您只能跨过一次圆圈)

        1. 我现在在我的新有向图上应用networkx。 ford_fulkerson()算法。这找到了 flowValue和flowGraph。
        2. flowValue是一个整数。它是最小切割(或从源到下沉的最大流量)。 这是我们一直在寻找的答案。它代表我们需要删除的最小圈数。

          奖金分配:

          我想...为什么要停在这里,要删除的圈数很好,但我想知道我需要删除的确切圆圈。要做到这一点,我需要找出在flowGraph上实际发生最小切割的位置。我设法弄清楚如何做到这一点,但是我的实现在某个地方有一个错误,所以它有时会选择稍微错误的错误,所以得到错误的圈子。

          要找到最小切割,我们将使用步骤6中生成的flowGraph。这个想法是这个图的瓶颈将是最小的。如果我们尝试从源头到水槽的流动,我们将卡在瓶颈上,因为瓶颈周围的所有边缘都将达到最大容量。因此,我们只需使用DFS(深度优先搜索)尽可能向下流动。 DFS只允许在流程图中沿最大容量的边缘移动。 (例如,他们的流量为0而不是1)。使用源代码中的DFS,我记下了我可以看到的所有节点,将它们存储在self.seen中。现在在DFS之后,对于所见的所有节点,我检查节点是否具有到达DFS中未见的节点的最大容量边缘。所有这些节点都在最小切割上。

          以下是我跑过的一个模拟图片:

          simulation

          并且删除了圆圈后,我用油漆填充(你可能需要放大一点以确定圆圈之间确实存在间隙):

          simulation_with_circles_removed

          学习:

          即使在python中速度也可以,运行1000圈即可。

          这比我想象的要难,而且我仍然有一个错误,试图使用DFS来找到原始的圆圈。 (如果有人可以帮助找到那个很棒的错误。)

          一开始代码很优雅,虽然我不断添加黑客来改变可视化等。

          工作代码(除了DFS中的轻微偶然错误):

          __author__ = 'Robert'
          import pygame
          import networkx
          
          class CirclesThing():
              def __init__(self,width,height,number_of_circles):
                  self.removecircles = False #display removable circles as green.
                  self.width = width
                  self.height = height
          
                  self.number_of_circles = number_of_circles
                  self.radius = 40
          
                  from random import randint
                  self.circles = sorted(set((randint(self.radius,width-self.radius),randint(2*self.radius,height-2*self.radius)) for i in range(self.number_of_circles)))
          
                  self.sink = (self.width/2, self.height-10)
                  self.source = (self.width/2, 10)
          
                  self.flowValue,self.flowGraph = self.find_flow()
          
                  self.seen = set()
                  self.seen.add(self.source)
                  self.dfs(self.flowGraph,self.source)
          
                  self.removable_circles = set()
                  for node1 in self.flowGraph:
                      if node1 not in self.seen or node1==self.source:
                          continue
                      for node2 in self.flowGraph[node1]:
                          if self.flowGraph[node1][node2]==1:
                              if node2 not in self.seen:
                                  self.removable_circles.add(node1[0])
          
          
              def find_flow(self):
                  "finds the max flow from source to sink and returns the amount, along with the flow graph"
                  G = networkx.Graph()
                  for node1,node2 in self.get_connections_to_source_sink()+self.intersect_circles():
                      G.add_edge(node1,node2,capacity=1)
          
                  G2 = networkx.DiGraph()
                  for node in G:
                      if node not in (self.source,self.sink):
                          G2.add_edge((node,'a'),(node,'b'),capacity=1) #each node is split into two new nodes. We add the edge between the two new nodes flowing from a to b.
          
                  for edge in G.edges_iter():
                      if self.source in edge or self.sink in edge:
                          continue #add these edges later
                      node1,node2 = edge
                      G2.add_edge((node1,'b'),(node2,'a'),capacity=1) #if we flow through a circle (from node1a to node1b) we need to be able to flow from node1b to all node1's children
                      G2.add_edge((node2,'b'),(node1,'a'),capactiy=1) #similarly for node2..
          
                  for node in G[self.source]:
                      G2.add_edge(self.source,(node,'a'))
                  for node in G[self.sink]:
                      G2.add_edge((node,'b'),self.sink)
          
                  flowValue, flowGraph = networkx.ford_fulkerson(G2,self.source,self.sink)
          
                  return flowValue, flowGraph
          
          
              def dfs(self,g,v):
                  "depth first search from source of flowGraph. Don't explore any nodes that are at maximum capacity. (this means we can't explore past the min cut!)"
                  for node in g[v]:
                      if node not in self.seen:
                          self.seen.add(node)
                          if g[v][node]!=1 or v==self.source:
                              self.dfs(g,node)
          
              def display(self):
                  self.draw_circles()
                  self.draw_circles(circle_radius=5, circle_colour=(255,0,0))
                  if not self.removecircles:
                      lines = self.intersect_circles()
                      self.draw_lines(lines)
                  self.draw_source_sink()
          
              def draw_circles(self,circle_radius=None,circle_colour=(0,0,255),circles=None):
                  if circle_radius is None:
                      circle_radius = self.radius
                  if circles is None:
                      circles = self.circles
          
                  circle_thickness = 2
                  for pos in circles:
                      cc = circle_colour if pos not in self.removable_circles else (100,200,0) #change colour of removable circles
                      ct = circle_thickness if pos not in self.removable_circles else 4 #thicken removable circles
                      if pos not in self.removable_circles or not self.removecircles:
                          pygame.draw.circle(screen, cc, pos, circle_radius, ct)
          
              def intersect_circles(self):
                  colliding_circles = []
                  for i in range(len(self.circles)-1):
                      for j in range(i+1,len(self.circles)):
                          x1,y1 = self.circles[i]
                          x2,y2 = self.circles[j]
                          if x2-x1>2*self.radius+5: #add 5 to make a more obvious gap visually
                              break #can't collide anymore.
                          if (x2-x1)**2 + (y2-y1)**2 <= (2*self.radius)**2+5:
                              colliding_circles.append(((x1,y1),(x2,y2)))
                  return colliding_circles
          
              def draw_lines(self,lines,line_colour=(255, 0, 0)):
                  for point_pair in lines:
                      point1,point2 = point_pair
                      try:
                          tot = self.flowGraph[(point1,'b')][(point2,'a')] + self.flowGraph[(point2,'b')][(point1,'a')] #hack, does anything flow between the two circles?
                      except KeyError:
                          tot = 0
                      thickness = 1 if tot==0 else 3
                      lc = line_colour if tot==0 else (0,90,90)
                      pygame.draw.line(screen, lc, point1, point2, thickness)
          
              def draw_source_sink(self):
                  self.draw_circles(circles=(self.sink,self.source),circle_radius=15,circle_colour=(0,255,0))
          
                  bottom_line = ((0,self.height-3*self.radius),(self.width,self.height-3*self.radius))
                  top_line = ((0,3*self.radius),(self.width,3*self.radius))
          
                  self.draw_lines([top_line, bottom_line],line_colour=(60,60,60))
          
                  if not self.removecircles:
                      self.draw_lines(self.get_connections_to_source_sink(),line_colour=(0,255,0))
          
              def get_connections_to_source_sink(self):
                  connections = []
                  for x,y in self.circles:
                      if y<4*self.radius:
                          connections.append((self.source,(x,y)))
                      elif y>height-4*self.radius:
                          connections.append((self.sink,(x,y)))
                  return connections
          
              def get_caption(self):
                  return "flow %s, circles removes %s" %(self.flowValue,len(self.removable_circles))
          
          
          
          time_per_simulation = 5 #5 seconds
          width, height = 1400, 600
          background_colour = (255,255,255)
          screen = pygame.display.set_mode((width, height))
          
          screen.fill(background_colour)
          from pygame.locals import USEREVENT
          pygame.time.set_timer(USEREVENT+1,time_per_simulation*1000)
          
          simulations = 0
          simulations_max = 20
          
          running = True
          while running:
              for event in pygame.event.get():
                  if event.type == pygame.QUIT:
                      running = False
                  if event.type == USEREVENT+1:
                      if simulations % 2 ==0:
                          world = CirclesThing(width,height,120) #new world
                      else:
                          world.removecircles = True #current world without green circles
          
                      screen.fill(background_colour)
                      world.display()
                      pygame.display.set_caption(world.get_caption())
                      pygame.display.flip()
          
                      if simulations>=2*simulations_max:
                          running = False
                      simulations+=1
          
                      if False:
                          pygame.image.save(screen,'sim%s.bmp'%simulations)
          

答案 4 :(得分:1)

一种选择是先删除重叠或触摸次数最多的圈子。在您删除每个之后,检查它是否是解决方案,如果不是继续删除。

var circle;
circle = findMostOverlapCircle();
while(circle != null) {
    circle.remove();
    circle = findMostOverlapCircle();
}