Обход пространства состояний игры: больше поиска приводит к плохим результатам

Я публикую этот вопрос из codereview поскольку я обнаружил, что он не отвечает.

Эта проблема доступна на hackerrank ai. Я не прошу решений, а пытаюсь найти, что не так с моей стратегией или кодом.

Я пытаюсь решить проблему, которая, как мне кажется, TSP on a 2-D grid. Итак, я пытаюсь получить лучший результат, который я могу. Однако заглядывание вперед на 1 шаг дает лучшие результаты, чем заглядывание вперед на 2 шага.

Проблема в том, что я должен очистить грязные блоки на 2-D сетке за минимальное количество движений либо UP, DOWN, LEFT, RIGHT, CLEAN.

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

Короче говоря, мне нужно сделать только next_move в моем процессе.

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

Чтобы посмотреть вперед на 1 шаг, я бы сделал: для каждой грязной ячейки и нашел ближайшую грязную ячейку к взятой грязной ячейке. Для шага 2 для каждой грязной ячейки выполните поиск шага 1 и найдите лучший ход. Аналогично для нескольких шагов.

Тем не менее, я получаю более высокий балл, когда выполняю поиск только в 1 шаг, но меньше баллов за поиск в 2 шага. оценка рассчитывается по (200 - steps_taken). Итак, я думаю, что что-то не так в моем коде/стратегии.

Формат ввода:

b представляет бота в сетке. - — чистая ячейка. d - грязная ячейка.

Первая строка — пара целых чисел позиции бота. Это делает b в сетке избыточным. Если бот в данный момент стоит в грязной ячейке, d будет присутствовать в этой ячейке сетки.

Вторая строка - размерность сетки.

Третий вход представляет собой сетку в виде строк. См. пример ввода ниже.

Мой код на Haskell:

module Main where
import Data.List 
import Data.Function (on)
import Data.Ord

-- slits up a string 
-- ** only used in IO. 
split sep = takeWhile (not . null) . unfoldr (Just . span (/= sep) . dropWhile (== sep))
-- ** only used in IO
getList :: Int -> IO [String]
getList n = if n==0 then return [] else do i <- getLine; is <- getList(n-1); return (i:is)

-- find positions of all dirty cells in the board
getAllDirtyCells :: (Int, Int) -> [String] -> [(Int, Int)]
getAllDirtyCells (h, w) board = [(x, y) | x <- [0..(h-1)], y <- [0..(w - 1)]
                               , ((board !! x) !! y) == 'd']

-- finally get the direction to print ;
-- first argument is my-position and second arg is next-position.
getDir :: (Int, Int) -> (Int, Int) -> String
getDir (x, y) (a, b) | a == x && y == b = "CLEAN"
                     | a < x = "UP"
                     | x == a && y < b = "RIGHT"
                     | x == a = "LEFT"
                     | otherwise = "DOWN"

-- only used in IO for converting strin gto coordinate.
getPos :: String -> (Int, Int)
getPos pos = let a = map (\x -> read x :: Int) (words pos)
             in ((a !! 0) , (a !! 1))


-- manhattan Distance :  sum of difference of x and y coordinates
manhattanDis :: (Int, Int) -> (Int, Int) -> Int
manhattanDis (a, b) (x, y) = (abs (a - x) + (abs (b - y)))

-- sort the positions from (botX, botY) position on manhattan-distance.
-- does not returns the cost.
getSortedPos :: (Int, Int) -> [(Int, Int)] -> [(Int, Int)]
getSortedPos (botX, botY) points = map (\x -> (snd x)) $ 
                                   sortBy (comparing fst)  -- compare on the basis of cost.
                                              [(cost, (a, b)) | 
                                                     (a, b) <- points, 
                                                     cost <- [manhattanDis (a,b) (botX, botY)]]
-- exclude the point `v` from the list `p`
excludePoint :: (Ord a) => [a] -> a -> [a]
excludePoint [] _ = []
excludePoint p v = [x | x <- p , x /= v]

-- playGame uses the nearest-node-policy. 
-- we start playing game when we are not going more deep. 
-- more about that in findBestMove
-- game is to reduce the nodes to one node with the total cost ;
-- reduction : take the next shortest node from the current-node.
playGame :: (Int, Int) -> [(Int, Int)] -> [(Int, Int)]
playGame pos [] = [pos]
playGame startPos points = let nextPos = (head (getSortedPos startPos points))
                           in (nextPos : playGame nextPos (excludePoint points nextPos))

-- sum up cost of all the points as they occur.
findCost :: [(Int, Int)] -> Int
findCost seq = sum $ map (\x -> (manhattanDis (fst x) (snd x))) $ zip seq (tail seq)

-- find the position which gives the smallest overall cost.
smallestCostMove :: [(Int, (Int, Int))] -> (Int, (Int, Int))
smallestCostMove [] = (0, (100, 100))
smallestCostMove [x] = x
smallestCostMove (x:y:xs) | (fst x) <= (fst y) = smallestCostMove (x : xs)
                          | otherwise = smallestCostMove (y : xs)                      

-- This is actual move-finder. It does the lookups upto `level` deep.
-- from startpoint, take each point and think it as starting pos and play the game with it.
-- this helps us in looking up one step.
-- when level is 0, just use basic `playGame` strategy. 
findBestMove :: (Int, Int) -> [(Int, Int)] -> Int -> (Int, (Int, Int))
findBestMove startPos  points level 
                                    -- returns the move that takes the smallest cost i.e. total distances.
                                    | level == 0 = smallestCostMove $ 
                                                     -- return pair of (cost-with-node-x-playGame, x)
                                                     map (\x -> (findCost (startPos : (x : (playGame x (excludePoint points x)))), 
                                                                x)) 
                                                         points
                                    | otherwise  = smallestCostMove $ 
                                                     map (\x -> 
                                                           -- return pair of (cost-with-node-x, x)
                                                            ( (findCost (startPos : [x])) + 
                                                              -- findBestMove returns the pair of (cost, next-move-from-x)
                                                              (fst (findBestMove x (excludePoint points x) (level - 1))),
                                                             x)) 
                                                         points

-- next_move is our entry point. go only 2 level deep for now, as it can be time-expensive.
next_move :: (Int, Int) -> (Int, Int) -> [String] ->  String
next_move pos dim board = let boardPoints = (getAllDirtyCells dim board)
                              numPoints = (length boardPoints)
                              -- ** Important : This is my question :
                              -- change the below `deep` to 1 for better results. 
                              deep = if (numPoints > 3) 
                                     then 2 
                                     else if (numPoints == 1) 
                                          then 1 
                                          else (numPoints - 1)                                
                          in if pos `elem` boardPoints 
                             then getDir pos pos
                             else getDir pos $ snd $ findBestMove pos boardPoints deep


main :: IO()
main = do
    -- Take input
   b <- getLine
   i <- getLine
   -- bot contains (Int, Int) : my-coordinates. like (0,0)
   let botPos = (read $ head s::Int,read $ head $ tail s::Int) where s = split (' ') b
   -- dimOfBoard contains dimension of board like (5,5)
   let dimOfBoard = (read $ head s::Int, read $ head $ tail s::Int) where s = split (' ') i
   board <- getList (fst dimOfBoard)
   putStrLn $ next_move botPos dimOfBoard board

Я контролирую, как deep я могу работать с переменной deep.

Образец платы:

0 0
5 5
b---d
-d--d
--dd-
--d--
----d

Ответов может быть три:

Вывод :

RIGHT or DOWN or LEFT

Важно: снова будет вызываться новый процесс с new board и my bot new position, пока я не очистю все грязные ячейки.

Что я делаю неправильно ?


person Ashish Negi    schedule 25.02.2015    source источник
comment
Если вы считаете, что разместили что-то не в том месте, попросите модераторов переместить это. Кросс-постинг не приветствуется, потому что он разделяет ответы.   -  person Doval    schedule 25.02.2015
comment
Совет, который я могу дать, — сначала очистить код. У вас много лишних скобок ((read (head s)) :: Int можно заменить на read $ head s, это не шепелявость, и сигнатура типа избыточна). Вы должны преобразовать свои направления в тип данных вместо использования строк. Ваш код выходит за правую часть страницы, немного разбейте его. Введите несколько новых функций в необходимых where блоках, чтобы сделать ваш код более понятным. Это поможет вам понять ваш код точно так же, как и другим. Если вы лучше понимаете свой код, вам будет легче решать проблемы.   -  person bheklilr    schedule 25.02.2015
comment
Можете ли вы предоставить образец платы?   -  person ErikR    schedule 25.02.2015
comment
А можно конкретный пример что не работает?   -  person ErikR    schedule 25.02.2015
comment
Следуя совету bheklilr, вы также должны использовать тип данных, например data Square=Clean|Dirty. И задокументируйте, что делает каждая функция. И объясните где-нибудь, как представлена ​​доска и почему вы выбрали именно это представление. И ссылку на первоисточник проблемы. И объясните проблему лучше. В нынешнем виде ваш код практически нечитаем.   -  person dfeuer    schedule 26.02.2015
comment
@bheklir Спасибо за ваши предложения. Я новичок в хаскеле. Я прокомментировал и отрефакторил код, насколько это было в моих силах. Пожалуйста, посмотрите.   -  person Ashish Negi    schedule 26.02.2015
comment
@ user5402 Я дал один образец платы. Поскольку это онлайн-проблема, у меня нет точной платы. Также нет правильных/неправильных ответов. просто лучшие ответы на эту проблему.   -  person Ashish Negi    schedule 26.02.2015


Ответы (1)


После большой работы я нашел пример, в котором лучший путь, определенный findBestMove на уровне 1, возвращает худший путь, чем при вызове с уровнем, установленным на 0:

 points = [(6,8),(9,7),(9,4),(4,10),(4,6),(7,10),(5,7),(2,4),(8,8),(6,5)]
 start: (1,10)

  level = 0:
    cost: 31
    path: [(1,10),(4,10),(7,10),(5,7),(6,8),(8,8),(9,7),(9,4),(6,5),(4,6),(2,4)]

  level = 1:
    cost: 34
    path: [(1,10),(2,4),(6,5),(6,8),(5,7),(4,6),(4,10),(7,10),(8,8),(9,7),(9,4)]

Проблема в том, что playGame исследует только один из лучших возможных ходов. Я обнаружил, что ваш алгоритм становится более стабильным, если вы исследуете все наилучшие возможные ходы следующим образом:

 greedy start [] = 0
 greedy start points =
   let sorted@((d0,_):_) = sort [ (dist start x, x) | x <- points ]
       nexts = map snd $ takeWhile (\(d,_) -> d == d0) sorted
   in d0 + minimum [ greedy n (delete n points)  | n <- nexts ]

Здесь greedy объединяет findCost и playGame. При просмотре только первого хода в отсортированном списке playGame зависит от алгоритма сортировки и порядка точек.

Вы также можете написать bestMove так:

 bestMove _ start [] = (0,start)
 bestMove depth start points
   | depth == 0 = minimum [ (d0+d,x) | x <- points,
                              let d0 = dist start x,
                              let d = greedy x (delete x points) ]
   | otherwise  = minimum [ (d0+d,x) | x <- points,
                              let d0 = dist start x,
                              let (d,_) = bestMove (depth-1) x (delete x points  ) ]

и это более четко подчеркивает симметрию между двумя случаями.

Вот код, который я использовал для поиска и отображения наилучшего пути для указанной выше доски: http://lpaste.net/121294 Чтобы использовать его, просто поместите свой код в модуль с именем Ashish.

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

person ErikR    schedule 27.02.2015
comment
я получаю *Main> findBestMove (1,10) [(6,8),(9,7),(9,4),(4,10),(4,6),(7,10),(5,7),(2,4),(8,8),(6,5)] 0 (35,(4,10)) *Main> findBestMove (1,10) [(6,8),(9,7),(9,4),(4,10),(4,6),(7,10),(5,7),(2,4),(8,8),(6,5)] 1 (34,(2,4)) *Main> на ghci. т.е. стоимость 35 для уровня 0 и 34 для уровня 1. - person Ashish Negi; 03.03.2015
comment
Тем не менее, вы правы в том, что выбор только первой случайной (зависящей от сортировки) точки приводит к плохим результатам. - person Ashish Negi; 03.03.2015