Попытка создать алгоритм для оптимального размещения башни в игре

Недавнее приложение я записал, что имел для использования многопоточности (хотя не неограниченное количество потоков) был тот, куда я должен был передать в нескольких направлениях более чем два протокола плюс контроль третьего ресурса для изменений. И библиотеки протокола потребовали, чтобы поток выполнил соответствующий цикл событий в, и когда те составлялись, было легко создать третий цикл для контроля ресурса. В дополнение к требованиям цикла событий сообщения, проходящие провода, имели строгие требования синхронизации, и одним циклом нельзя было рискнуть, блокируя другой, что-то, что было далее облегчено при помощи многоядерного ЦП (SPARC).

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

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

6
задан serg 27 October 2009 в 03:38
поделиться

7 ответов

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

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

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

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

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

Производительность неплохая. На моем ноутбуке с частотой 2,1 ГГц скомпилированная версия программы решает случай 4x4 за ~ 50 секунд, используя одно ядро. Я использую пример 5x5 прямо сейчас, чтобы посмотреть, сколько времени это займет. Поскольку функциональный код довольно легко распараллелить, я также собираюсь поэкспериментировать с параллельной обработкой. Есть распараллеленный компилятор 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 с, давая следующую доску:

5
ответ дан 8 December 2019 в 13:00
поделиться

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

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

4
ответ дан 8 December 2019 в 13:00
поделиться

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

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

3
ответ дан 8 December 2019 в 13:00
поделиться

Я думаю, вы захотите использовать алгоритм ветвей и границ, потому что я думаю, что придумать хорошую эвристику для реализации 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)

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

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

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

3
ответ дан 8 December 2019 в 13:00
поделиться

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

1
ответ дан 8 December 2019 в 13:00
поделиться

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

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

1
ответ дан 8 December 2019 в 13:00
поделиться

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

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

Похожие вопросы: