Попытка построить алгоритм для оптимального размещения башни в игре - PullRequest
6 голосов
/ 26 октября 2009

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

На xbox недавно вышла игра под названием "Tower Bloxx". Одной из частей игры является размещение разноцветных башен на поле наиболее оптимальным образом, чтобы максимально увеличить количество наиболее ценных башен. Я написал алгоритм, который бы определял наиболее эффективное размещение башен, но он не очень эффективен и в значительной степени просто перебор всех возможных комбинаций. Для поля 4х4 с 4 типами башен он решает его примерно за 1 час, для 5 типов башен потребуется около 40 часов, что слишком много.

Вот правила: Есть 5 типов башен, которые могут быть размещены на поле. Существует несколько типов полей, самый простой из которых - матрица 4х4, в других полях есть пробелы, которые вы не можете построить. Ваша цель - разместить на поле как можно больше наиболее ценных башен, чтобы максимизировать общую ценность башен на поле (предположим, что все башни построены одновременно, оборотов нет).

Типы башен (в порядке от меньшего к наиболее ценному):

  • Синий - можно разместить где угодно, значение = 10
  • Красный - может быть размещен только помимо синего, значение = 20
  • Зеленый - помещается помимо красного и синего, значение = 30
  • Желтый - кроме зеленого, красного и синего, значение = 40
  • Белый - кроме желтого, зеленого, красного и синего, значение = 100

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

Вот мой алгоритм для 4 башен на поле 4x4:

  1. Общее количество комбинаций = 4 ^ 16
  2. Переберите [1..4 ^ 16] и преобразуйте каждое число в строку base4, чтобы закодировать расположение башни, поэтому 4 ^ 16 = "3333 3333 3333 3333", который будет представлять наши типы башен (0 = синий ,. .., 3 = желтый)
  3. Преобразование строки размещения башни в матрицу.
  4. Для каждой башни в матрице проверяют ее соседей, и если любое из требований не выполняется, вся эта комбинация не выполняется.
  5. Поместите все правильные комбинации в массив, а затем отсортируйте этот массив как строки в лексикографическом порядке, чтобы найти наилучшую возможную комбинацию (сначала нужно отсортировать символы в строке).

Единственная оптимизация, которую я придумал, - пропускать комбинации, которые не содержат наиболее ценных башен. Это пропускает некоторую обработку, но я все еще перебираю все комбинации 4 ^ 16.

Есть мысли, как это можно улучшить? Примеры кода будут полезны, если в Java или PHP.

------- Обновление --------

После добавления большего количества недопустимых состояний (желтый не может быть построен по углам, белый не может быть построен по углам и по краям, поле должно содержать хотя бы одну башню каждого типа), понимая, что возможно построить только 1 белую башню. на поле 4x4 и оптимизации кода Java общее время было сокращено с 40 до ~ 16 часов. Возможно, многопоточность снизит его до 10 часов, но это, вероятно, предел грубой силы.

Ответы [ 7 ]

5 голосов
/ 29 октября 2009

Я нашел этот вопрос интригующим, и, поскольку я учу себя на Хаскеле, я решил попробовать свои силы в реализации решения на этом языке.

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

Мой алгоритм работает, начиная с «пустой» доски. Он помещает каждый возможный цвет башни в первый пустой слот и в каждом случае (каждый цвет) затем рекурсивно вызывает себя. Повторяющиеся вызовы пробуют каждый цвет во втором слоте, повторяя снова, пока доска не заполнится.

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

Как написан код, я генерирую список всех возможных решений, затем просматриваю список, чтобы найти лучшее. На самом деле, благодаря ленивой оценке Haskell, элементы списка генерируются так, как они нужны функции поиска, и, поскольку они больше не используются, они сразу становятся доступными для сборки мусора, так что даже для платы 5x5 использование памяти довольно мало (2 МБ).

Производительность довольно хорошая. На моем ноутбуке с частотой 2,1 ГГц скомпилированная версия программы решает задачу 4x4 за ~ 50 секунд, используя одно ядро. Сейчас я запускаю пример 5х5, чтобы посмотреть, сколько времени это займет. Поскольку функциональный код довольно легко распараллелить, я также собираюсь экспериментировать с параллельной обработкой. Существует параллельный компилятор Haskell, который будет распределять работу не только по нескольким ядрам, но и по нескольким компьютерам, и это очень распараллеливаемая проблема.

Вот мой код. Я понимаю, что вы указали Java или PHP, и Haskell совершенно другой. Если вы хотите поиграть с ним, вы можете изменить определение переменной «bnd» чуть выше дна, чтобы установить размер доски. Просто установите его в ((1,1), (x, y)), где x и y - количество столбцов и строк соответственно.

import Array
import Data.List

-- Enumeration of Tower types.  "Empty" isn't really a tower color,
-- but it allows boards to have empty cells
data Tower = Empty | Blue | Red | Green | Yellow | White
             deriving(Eq, Ord, Enum, Show)

type Location = (Int, Int)
type Board = Array Location Tower

-- towerScore omputes the score of a single tower
towerScore :: Tower -> Int
towerScore White = 100
towerScore t     = (fromEnum t) * 10

-- towerUpper computes the upper bound for a single tower
towerUpper :: Tower -> Int
towerUpper Empty = 100
towerUpper t = towerScore t

-- boardScore computes the score of a board
boardScore :: Board -> Int
boardScore b = sum [ towerScore (b!loc) | loc <- range (bounds b) ]

-- boardUpper computes the upper bound of the score of a board
boardUpper :: Board -> Int
boardUpper b = sum [ bestScore loc | loc <- range (bounds b) ]
    where
      bestScore l | tower == Empty = 
                      towerScore (head [ t | t <- colors, canPlace b l t ])
                  | otherwise = towerScore tower
                  where 
                    tower = b!l
                    colors = reverse (enumFromTo Empty White)

-- Compute the neighbor locations of the specified location
neighborLoc :: ((Int,Int),(Int,Int)) -> (Int,Int) -> [(Int,Int)]
neighborLoc bounds (col, row) = filter valid neighborLoc'
    where
      valid loc = inRange bounds loc
      neighborLoc' = [(col-1,row),(col+1,row),(col,row-1),(col,row+1)]

-- Array to store all of the neighbors of each location, so we don't
-- have to recalculate them repeatedly.
neighborArr = array bnd [(loc, neighborLoc bnd loc) | loc <- range bnd]

-- Get the contents of neighboring cells
neighborTowers :: Board -> Location -> [Tower]
neighborTowers board loc = [ board!l | l <- (neighborArr!loc) ]

-- The tower placement rule.  Yields a list of tower colors that must
-- be adjacent to a tower of the specified color.
requiredTowers :: Tower -> [Tower]
requiredTowers Empty  = []
requiredTowers Blue   = []
requiredTowers Red    = [Blue]
requiredTowers Green  = [Red, Blue]
requiredTowers Yellow = [Green, Red, Blue]
requiredTowers White  = [Yellow, Green, Red, Blue]

-- cellValid determines if a cell satisfies the rule.
cellValid :: Board -> Location -> Bool
cellValid board loc = null required ||
                      null needed   ||
                      (length needed <= length empties)
    where
      neighbors = neighborTowers board loc
      required  = requiredTowers (board!loc)
      needed    = required \\ neighbors
      empties   = filter (==Empty) neighbors

-- canPlace determines if 'tower' can be placed in 'cell' without
-- violating the rule.
canPlace :: Board -> Location -> Tower -> Bool
canPlace board loc tower =
    let b' = board // [(loc,tower)]
    in cellValid b' loc && and [ cellValid b' l | l <- neighborArr!loc ]

-- Generate a board full of empty cells       
cleanBoard :: Array Location Tower
cleanBoard = listArray bnd (replicate 80 Empty)

-- The heart of the algorithm, this function takes a partial board
-- (and a list of empty locations, just to avoid having to search for
-- them) and a score and returns the best board obtainable by filling
-- in the partial board
solutions :: Board -> [Location] -> Int -> Board
solutions b empties best | null empties = b
solutions b empties best = 
    fst (foldl' f (cleanBoard, best) [ b // [(l,t)] | t <- colors, canPlace b l t ])
    where
      f :: (Board, Int) -> Board -> (Board, Int)
      f (b1, best) b2  | boardUpper b2 <= best = (b1, best)
                       | otherwise = if newScore > lstScore
                                     then (new, max newScore best)
                                     else (b1, best)
                       where
                         lstScore = boardScore b1
                         new      = solutions b2 e' best
                         newScore = boardScore new
      l = head empties
      e' = tail empties

colors = reverse (enumFromTo Blue White)

-- showBoard converts a board to a printable string representation
showBoard :: Board -> String
showBoard board = unlines [ printRow row | row <- [minrow..maxrow] ]
    where
      ((mincol, minrow), (maxcol, maxrow)) = bounds board
      printRow row = unwords [ printCell col row | col <- [mincol..maxcol] ]
      printCell col row = take 1 (show (board!(col,row)))

-- Set 'bnd' to the size of the desired board.                          
bnd = ((1,1),(4,4))

-- Main function generates the solutions, finds the best and prints
-- it out, along with its score
main = do putStrLn (showBoard best); putStrLn (show (boardScore best))
       where
         s = solutions cleanBoard (range (bounds cleanBoard)) 0
         best = s

Также, пожалуйста, помните, что это моя первая нетривиальная программа на Haskell. Я уверен, что это можно сделать гораздо более элегантно и лаконично.

Обновление : Так как 5x5 с 5 цветами все еще занимал много времени (я ждал 12 часов, а он еще не закончился), я еще раз посмотрел, как использовать ограничение для удаления больше дерева поиска.

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

Это помогло некоторым, уменьшив доску 4x4x5 с 23 до 15 секунд. Чтобы еще больше улучшить его, я изменил функцию верхней границы, чтобы предположить, что каждый Пустой заполнен наилучшей возможной башней, соответствующей существующему непустому содержимому ячейки. Это очень помогло, сократив время 4x4x5 до 2 с.

Выполнение на 5x5x5 заняло 2600 с, давая следующую доску:

G B G R B
R B W Y G
Y G R B R
B W Y G Y
G R B R B

со счетом 730.

Я могу сделать еще одну модификацию, чтобы она нашла все доски с максимальным выигрышем, а не только одну.

4 голосов
/ 26 октября 2009

Если вы не хотите делать A *, используйте ответвление и ограниченный подход. Проблема должна быть относительно простой для кодирования, потому что ваши функции-значения хорошо определены. Я полагаю, вы должны быть в состоянии относительно легко обрезать огромные участки пространства поиска. Однако, поскольку ваше пространство поиска довольно большое, это может занять некоторое время. Есть только один способ узнать:)

Статья вики не самая лучшая в мире. Google может найти вам массу хороших примеров, деревьев и прочего для дальнейшей иллюстрации подхода.

3 голосов
/ 26 октября 2009

Я думаю, вы захотите использовать алгоритм ветвления и ограничения, потому что я думаю, что придумать хорошую эвристику для реализации A * будет сложно (но это только моя интуиция).

Псевдокод реализации ветвления и границы:

board = initial board with nothing on it, probably a 2D array

bestBoard = {}

function findBest(board)
  if no more pieces can be added to board then
     if score(board) > score(bestBoard) then
       bestBoard = board
     return
  else
    for each piece P we can legally add to board
      newBoard = board with piece P added
      //loose upper bound, could be improved
      if score(newBoard) + 100*number of blanks in newBoard > score(bestBoard)
        findBestHelper(newBoard)

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

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

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

3 голосов
/ 26 октября 2009

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

Могут быть и другие причудливые вещи, которые вы можете сделать, но это легко понять (надеюсь) улучшение вашего текущего решения.

1 голос
/ 27 октября 2009

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

  • Убедитесь, что вы не создаете экземпляры каких-либо объектов внутри цикла 4 ^ 16. Для JVM намного, намного дешевле переинициализировать существующий объект, чем создавать новый. Еще дешевле использовать массивы примитивов. :)
  • Если вы можете помочь, отойдите от классов коллекции. Они добавят много накладных расходов, которые вам, вероятно, не нужны.
  • Убедитесь, что вы не объединяете какие-либо строки. Используйте StringBuilder.
  • И, наконец, рассмотрите возможность переписать все это на языке C.
1 голос
/ 26 октября 2009

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

Линейное программирование для такой проблемы по существу сводится к наличию большого количества переменных (например, количества красных башен, присутствующих в ячейке (M, N)) и отношений между переменными (например, число белые башни в ячейке (M, N) должны быть меньше или равны количеству башен небелого цвета, которое имеет наименьшее такое число среди всех своих соседей). Писать линейную программу довольно сложно, но если вам нужно решение, которое работает за считанные секунды, это, вероятно, ваш лучший выбор.

1 голос
/ 26 октября 2009

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

...