Алгоритм поиска решения головоломки

Я пытаюсь сделать игру, в которой игрок должен найти свой путь от начала до конца на игровой доске.

Как вы видите, это игровое поле содержит множество красных круглых препятствий. Чтобы выиграть игру, игрок должен удалить минимальное количество препятствий. Итак, мой вопрос: как мне программно узнать минимальное количество препятствий, которые нужно удалить, чтобы освободить путь? Свободным путем будет считаться пространство между кругами, не перекрывающимися и не соприкасающимися.

Итак, что мне действительно нужно, так это минимальное количество кругов для удаления, мне не нужен фактический путь. Есть простой способ сделать это?

И чтобы дополнить понимание этой игровой доски, все круги имеют одинаковый радиус, и он ограничен черными линиями.

Также не обязательно двигаться по прямой.


person Cheesebaron    schedule 28.10.2010    source источник
comment
Вам не нужно добавлять комментарии. Вы можете просто отредактировать свой вопрос.   -  person Marcelo Cantos    schedule 28.10.2010
comment
Нужно ли двигаться по прямой?   -  person Shamim Hafiz    schedule 28.10.2010
comment
Я думаю, что могу знать, как вы могли бы проверить, есть ли вообще путь между началом и концом, но не знаю (по крайней мере, пока), как вычислить наименьшее количество кругов, которые нужно удалить. Будет ли это вообще полезно?   -  person fingerprint211b    schedule 28.10.2010
comment
Не обязательно двигаться по прямой.   -  person Cheesebaron    schedule 28.10.2010
comment
+1 за интересный вопрос, хорошо проиллюстрированный!   -  person Paul Dixon    schedule 28.10.2010
comment
Мне нравится этот вопрос! (Ответ для этой таблицы равен 3, но я не уверен, как это решить с помощью компьютера)   -  person warren    schedule 28.10.2010
comment
Это очень похоже на задачу, которую я поставил перед классом «Алгоритмы 2», который вы сейчас проходите в Техническом университете Дании.   -  person utdiscant    schedule 11.11.2010


Ответы (5)


Это maximum flow проблема теории графов.

Предположим, что каждый круг является узлом в графе. Дополнительно ввести 2 специальных узла: TOP и BOTTOM. Соедините окружности с этими узлами, если они пересекаются со стороной ВЕРХ/НИЗ. Соедините узлы, соответствующие кругам, друг с другом, если круги пересекаются.

Теперь вам нужно найти минимальный разрез на этом графике, имея ВЕРХ в качестве источника и НИЗ в качестве стока или наоборот. Вы можете использовать Max-flow_min-cut_theorem, чтобы решить эту проблему. В нем говорится, что Minimum-cut problem эквивалентно проблеме максимального потока. Вы можете найти подробную информацию о том, как решить Max-Flow problem, на TopCoder.

Поскольку мы можем пройти через каждый узел только один раз, мы должны преобразовать узлы в направленное ребро с пропускной способностью, равной единице с входным узлом и выходным узлом для каждого круга. Алгоритм максимального потока решит задачу на полученном графе и учтет тот факт, что мы удаляем круги, а не соединения между кругами. Всегда лучше решить эту проблему, чтобы удалить узел в графе, а не ребро, поскольку мы всегда можем удалить любое ребро, удалив узел. Дополнительное удаление узла может привести к удалению более одного ребра.

Кстати, похожую проблему можно найти на Uva Online Judge. Рекомендуется попробовать решить эту задачу на судье, тогда вы будете уверены в правильности своего решения.

person Leonid    schedule 28.10.2010
comment
Но круги не являются узлами. Они могут перекрываться немоделируемым образом. Рассмотрим игровое поле, покрытое кругами так, что каждая точка поля имеет по крайней мере два круга поверх него. Как бы вы смоделировали такой беспорядок с помощью графа? - person Dialecticus; 28.10.2010
comment
Если круг пересекается с другим кругом или лежит внутри него, то между узлами, соответствующими этим кругам на графе, существует двунаправленное ребро. Если есть 2 огромных круга, покрывающих все поле, то они будут как соединены с ВЕРХНИМИ и НИЗНИМИ узлами, так и сами между собой. Нам пришлось бы удалить оба круга, поскольку они оба подключены к приемнику и источнику в первую очередь (т.е. максимальный поток на графике равен 2, поэтому ответ). Я бы посоветовал вам прочитать статью на TopCoder и попытаться самостоятельно min-cut-problem вывести данную проблему. - person Leonid; 28.10.2010
comment
Хорошо, но еще одно. Теорема говорит об обрезании ребер, а не об удалении узлов. Не должны ли мы тогда моделировать круги как ребра графа? - person Dialecticus; 28.10.2010
comment
Это хороший момент. Но узлы в графе можно заменить и на ребра пропускной способности 1. Здесь также можно применить понятие вместимости заметок, т. е. мы не проходим через один и тот же узел более одного раза. - person Leonid; 28.10.2010
comment
Я думаю, что это правильный ответ на эту проблему, я попытаюсь применить этот алгоритм к моей проблеме. - person Cheesebaron; 28.10.2010

Пытаясь визуализировать написанное Леонидом, я сделал следующую схему.

альтернативный текст

person Svante    schedule 28.10.2010

Для перевода графа может работать что-то вроде этого.

Сделайте стену (синие линии) между двумя кругами, если они пересекаются. Не забудьте также добавить верхнюю и нижнюю границу. Это создает пару регионов. Это будут все узлы графа.

Далее нам нужно найти ребра, стоимость перехода от одного узла к другому. Возьмем два соседних региона (общие стены). Затем методом грубой силы или любым другим хитроумным методом определите стоимость перехода из этого региона в другой. Если вы удаляете круг, это означает, что вы удаляете все стены, ведущие к этому кругу. Если это превращает две области в одну область, стоимость ребра равна 1. Если вам нужно удалить два круга (все стены, которые у них есть), чтобы объединить две области, стоимость равна 2. И т. д.

Некоторые края (зеленые) нарисованы. Мы должны перейти из начального региона в конечный регион. Теперь у вас есть ежедневный взвешенный график.

Я думаю, что это можно значительно улучшить, но я оставлю это в качестве упражнения =)

В этом случае минимум 3.

Внимание, картинка нарисована от руки, я забыл несколько стен, ребер и областей. Только для иллюстрации. альтернативный текст

person Ishtar    schedule 28.10.2010
comment
Что в этом глупого? Окружности — это ребра, области — это узлы. Имеет смысл для меня. - person Ishtar; 28.10.2010
comment
Стены глупые. У вас нет постоянных затрат на их пересечение. - person Alin Purcaru; 28.10.2010
comment
Я должен немного прояснить это, я думаю. Я никогда не говорю, что пересечение стен требует постоянных затрат (синие линии). Однако у каждой зеленой линии есть стоимость, сколько кругов мне нужно удалить, чтобы попасть туда. - person Ishtar; 28.10.2010
comment
Тогда зеленые линии глупы :). Существует бесконечное количество возможностей выбрать эти линии. - person Alin Purcaru; 28.10.2010
comment
@ Алин - Надеюсь, теперь это более понятно. Зеленых линий не бесконечное количество, вам нужно только посмотреть на две области, которые имеют хотя бы одну общую стену. Я забыл 3 зеленые линии на рисунке (маленький треугольник внизу, посередине) - person Ishtar; 28.10.2010
comment
Я думаю, вам не хватает синей линии в верхнем левом углу, рядом (чуть правее) большой зеленой линии соединения; и отсутствуют многие другие в правом нижнем углу. - person Denilson Sá Maia; 28.10.2010
comment
@ Denilson Sá - Вы правы, извините. (Также не хватает нескольких в центре) Я мог бы отредактировать их обратно, но это просто создает все больше и больше линий, я хотел использовать изображение, чтобы проиллюстрировать способ превращения его в график. Компьютер лучше найдет все регионы. - person Ishtar; 28.10.2010
comment
Это нормально. Я просто случайно сначала посмотрел на одну из пропущенных строк, и меня это очень смутило. :) - person Denilson Sá Maia; 28.10.2010
comment
максимальный поток проще, но в любом случае хорошая работа, иштар. ознакомьтесь с двойными графами: en.wikipedia.org/wiki/Dual_graph - person robert king; 21.03.2012

Хорошо, поэтому я решил сделать визуализацию этого в pygame.

Это оказалось намного сложнее, чем я ожидал.

Идея, как и в других предложениях, заключается в использовании Max Flow. Узкое место в потоке от истока к стоку находится там, где поток наиболее плотный. Если мы разрежем график пополам в этом плотном узком месте (т. е. в точке min-cut), то получим минимальное количество кругов. Так получается, что макспоток = мин-рез.

вот шаги, которые я предпринял:

  1. Создайте мир pygame, в котором я могу случайным образом генерировать круги

  2. Сделать функцию для обработки всех столкновений между кругами:

Это включало сортировку кругов по координате x. Теперь, чтобы найти все столкновения Circle[0], я продолжаю двигаться по массиву, проверяя столкновения, пока не найду круг, значение x которого более чем на 2 * радиус больше, чем значение x круга [0], затем я могу вытолкнуть круг [0 ] и повторите процесс..

  1. Создайте исходные и приемные узлы, определите, к каким кругам они должны подключаться.

Шаги 4-5 выполняются в "функции findflow()"

  1. Создайте базовый неориентированный граф networkX, представляющий круги с узлами. Соединяющие узлы только в том случае, если их соответствующие окружности сталкиваются.

  2. И вот здесь начинаются сложности... Я создаю новый ориентированный граф из моего неориентированного графа. Поскольку мне нужно было проработать поток через круги (то есть узлы), а не через края, мне нужно разделить каждый узел на два узла с ребром между ними.

    Допустим, у меня есть узел X, соединенный с узлом Y (Y‹->X) (на исходном графике).

    Я меняю X на Xa и Xb, так что Xa соединяется с Xb (Xa->Xb). Также Y меняется на (Ya->Yb).

    Мне также нужно добавить (Yb->Xa) и (Xb->Ya), чтобы представить исходную связь между X и Y.

Все ребра в неориентированном графе имеют мощность = 1 (например, круг можно пересечь только один раз).

  1. Теперь я применяю алгоритм networkx.ford_fulkerson() к моему новому ориентированному графу. Это находит мне flowValue и flowGraph.

значение потока является целым числом. Это минимальная отсечка (или максимальный поток от источника к стоку). Это ответ, который мы искали. Он представляет собой минимальное количество кругов, которые нам необходимо удалить.

Бонусное задание:

Я подумал... зачем останавливаться на этом, иметь количество кругов, которые нужно удалить, это хорошо, но я хочу знать, какие именно круги мне нужно удалить. Для этого мне нужно выяснить, где на самом деле происходит минимальное отсечение на потоковом графике. Мне удалось выяснить, как это сделать, однако в моей реализации где-то есть ошибка, поэтому иногда она выбирает разрез немного неправильно и поэтому получает неправильные круги.

Чтобы найти минимальный разрез, мы будем использовать потоковую диаграмму, созданную на шаге 6. Идея состоит в том, что узким местом этого графика будет минимальная отсечка. если мы попытаемся течь от источника к стоку, мы застрянем в горлышке бутылки, так как все края вокруг узкого места будут на максимальной мощности. Поэтому мы просто используем DFS (Поиск в глубину), чтобы спуститься как можно дальше. DFS разрешено перемещаться только по ребрам, которые не имеют максимальной пропускной способности в потоковом графе. (например, их поток равен 0, а не 1). Используя DFS из источника, я записал все узлы, которые я мог видеть, сохраняя их в self.seen. Теперь, после DFS, для всех видимых узлов я проверяю, имеет ли узел максимальную пропускную способность по сравнению с узлом, который не был замечен в DFS. Все такие узлы находятся на мин-разрезе.

Вот изображение одной из симуляций, которые я запускал:

симуляция

и, удалив круги, я залил его краской (возможно, вам придется немного увеличить масштаб, чтобы увидеть, что между кругами действительно есть зазор):

simulation_with_circles_removed

Обучение:

Скорость нормальная даже на питоне, пробегает 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)
person robert king    schedule 23.03.2012

Один из вариантов — сначала удалить круги с наибольшим количеством перекрытий или касаний. После каждого удаления проверяйте, является ли это решением, если не продолжаете удаление.

var circle;
circle = findMostOverlapCircle();
while(circle != null) {
    circle.remove();
    circle = findMostOverlapCircle();
}
person goenning    schedule 28.10.2010
comment
Я думаю, что удаление кругов с минимальным количеством перекрытий даст лучшее решение. Попробуйте применить свое решение на примере, который он предоставил. - person Alin Purcaru; 28.10.2010