===
Имеется N вершин, между ними есть ребра (длину ребер мы задаем сами целым числом). По ребрам ездят точки, которые могут ехать в обе стороны. При нахождении двух точек в одной вершине происходит столкновение. Соответственно, если точки едут по одному ребру навстречу друг другу, тоже произойдет столкновение. Движение точек задано списком вершин, через которые они проходят. Скорости точек равны единице. При
достижении конечной вершины точка исчезает. По заданной конфигурации сети и точек
определить, будет ли столкновение.===
Попробуем написать решение этой задачи на Haskell.
Давайте сначала проанализируем условие. Если принять, что все ребра имеют целочисленную длину, а скорости точек равны единице, то понятно, что столкновения точек возможны либо в целочисленных узлах ребер, либо ровно посередине между ними (если точки на текущем шаге находились в соседних узлах). Для упрощения логики программы мы пойдем на следующую хитрость. Вместо того, чтобы сделать временной шаг
для эмуляции взаимодействия точек равным половине временнОй единицы скорости точки, мы увеличим во внутреннем представлении входных данных в 2 раза длину всех ребер, а временной шаг оставим равным единице. Таким образом, мы получаем то же самое корректное представление задачи.
Итак, давайте начнем с описания типов данных.
type Vertex = Int
type Path = [Vertex]
type Idx = Int
type Len = Int
type Position = Int
type Timestep = Int
data Direction = Up | Down  deriving (Eq)
data Edge = Edge Vertex Vertex Len deriving (Eq)
     
type State = [(Position, Len, Direction,Edge)] 
type Collision = (Point, Point, Timestep)
data Point = Point Idx Path State 
Vertex - это просто целочисленная величина, представляющая собой номер/индекс вершины во входном графе.
Path - список вершин во входном графе, представляющий собой маршрут перемещения некоторой точки.
Idx, Len и Position - числа, представляющие собой, соответственно, номер/индекс перемещающейся точки, целочисленную длину ребра и целочисленное положение точки на этом ребре в некоторый момент времени.
Timestep - целочисленная текущая отметка времени, которая на каждой итерации процесса эмуляции будет увеличиваться на единицу. За это время точка будет проходить по ребру
некоторую его часть между двумя соседними целочисленными значениями.
Direction - направление перемещения точки по ребру (Up - от первой точки в описании ребра ко второй, Down - в обратном направлении).
Edge - тип описания ребра, который задается двумя вершинами и его длиной.
State - состояние точки в текущий момент, которое представляет собой список еще не пройденных точкой ребер, где для каждого ребра задано текущее положение точки в данный момент, увеличенная вдвое длина ребра (см. начало статьи), направление движения точки по ребру и, собственно, само ребро. В каждой момент времени текущим является первый элемент списка. Как только точка прошла одно из ребер на своем пути, первый элемент списка, представляющий данное ребро, будет удаляться.
Collision - тип пересечения точек, представляющий собой тройку значений - 2 точки, которые участвуют в коллизии, и временная отметка, на которой эта коллизия произошла.
Теперь опишем некоторые входные данные.
graph :: [Edge]
graph = [
 Edge 2 1 3,
 Edge 2 3 3,
 Edge 2 4 3,
 Edge 2 5 6,
 Edge 2 6 7
 ]
points :: [(Idx, Path)]
points = [
 (1,[1,2,3]),
 (2,[3,2,1]),   
 (3,[4,2,1]),
 (4,[5,2,6]),
 (5,[6,2,5])      
 ] 
Далее, опишем 2 функции, которые по входным данным создают список наших точек.
makePoints :: [(Idx, Path)] -> [Edge] -> [Point]
makePoints l gr =
 map (\(i,vs) -> Point i vs (makeStartState vs gr)) l
makeStartState :: [Vertex] -> [Edge] -> State
makeStartState vs gr =    
 map (\(v1,v2) -> makeEdge (v1,v2) $ eqE (v1,v2))
 $ zip vs $ tail vs
 where
  eqE (v1,v2) = head $ filter (eqVertEdge (v1,v2)) gr
  eqVertEdge (v1,v2) (Edge ev1 ev2 l) 
          | v1 == ev1 && v2 == ev2 = True
          | v2 == ev1 && v1 == ev2 = True          
          | otherwise = False
  makeEdge (v1,v2) e@(Edge ev1 ev2 l)
   | ev1 == v1 = (0,    dblL, Up, e)
   | otherwise = (dblL, dblL, Down, e)
   where dblL = l * 2 
данным по точкам и самому графу. Ключевая функция в ней - это генерация начальных состояний каждой точки, которую выполняет функция makeStartState. В ней мы сначала для пути точки генерируем список пар вершин, затем для каждой пары находим соответствующее ребро в описании графа (функция eqE), а затем в makeEdge генерируем для текущего ребра элемент списка состояния точки, включающий определение начальной позиции на ребре (с учетом его удвоенной длины), удвоенную длину ребра, направление движения по нему и, собственно, само ребро).
Теперь посмотрим на функцию, которая для каждой точки в списке выполняет очередной шаг.
makeStep :: [Point] -> [Point]
makeStep pts = 
 map procP pts
 where 
  procP p@(Point i path (hd:tl)) =
   let (pos, len, dir, edge) = hd in 
   case dir of
    Up ->  Point i path ((pos + 1, len, dir, edge) : tl)
    Down -> Point i path ((pos - 1, len, dir, edge) : tl)
Если направление движения вниз (Down) - уменьшаем на 1.
Теперь определим основную функцию, которая запускает, собственно, эмуляцию нашего процесса.
emulate :: [Point] -> ([Point],[Collision])
emulate pts  = 
 let emulate' cnt pts colls = 
  let newPts = makeStep pts in
  let newColls = checkCollisions newPts cnt in
   if null pts then (pts,concat $ reverse colls)
  else emulate' (cnt + 1) 
          (processPoints $ delCollPoints newPts newColls) 
         (newColls : colls)
 in 
  emulate' 1 pts []  
Функция прекращает работать, если полученный список точек пустой (нам нечего обрабатывать дальше). Если же нет - мы запускаем процесс эмуляции для следующего шага.
Теперь напишем функцию генерации коллизий для списка точек в его некотором состоянии.
checkCollisions :: [Point] -> Timestep -> [Collision]  
checkCollisions pts ts = 
    filter isIntersect $ [ (p1,p2,ts) | 
                p1@(Point i1 pt1 st1) <- pts, 
                p2@(Point i2 pt2 st2) <- pts, 
                i1 < i2 ]
    where
 getVert st@(p,l,d, Edge v1 v2 _) 
  | d == Up && p == l = [v2]
  | d == Down && p == 0 = [v1]
  | otherwise = [] 
 chkEdges st1@(p1,l1,d1,e1) st2@(p2,l2,d2,e2)
  | e1 == e2 && p1 == p2 = True
  | otherwise = False
 chkVers st1@(p1,l1,d1,e1) st2@(p2,l2,d2,e2)
  | v1 /= [] && v2 /= [] && head v1 == head v2 = [head v1]
  | otherwise = []
  where
   v1 = getVert st1 
   v2 = getVert st2 
 isIntersect (p1@(Point _ _ (hd1:tl1)), 
       p2@(Point _ _ (hd2:tl2)), _) = 
        (not $ null $ chkVers hd1 hd2) || chkEdges hd1 hd2 
исключения дублирующихся значений при генерации троек.
Далее опишем функцию, которая удаляет из списка точек те, которые были задействованы в коллизиях на текущем шаге:
delCollPoints :: [Point] -> [Collision] -> [Point]
delCollPoints pts colls = filter (not . collPoint) pts
    where
  collPoint (Point i _ _) = 
         elem i 
   $ concatMap 
    (\(Point i1 _ _, Point i2 _ _, _ ) -> [i1,i2]) 
    colls
Следующая функция решает задачу обработки точек для обновления их списков состояний:
processPoints :: [Point] -> [Point]
processPoints pts =
    filter notEmptyState $ map chkPath pts
    where
 chkPath p@(Point i pt (hd:tl))  
  | pos == 0 && dir == Down = Point i pt tl
  | pos == len && dir == Up = Point i pt tl   
  | otherwise = p
  where 
   (pos,len,dir,_) = hd
 notEmptyState (Point _ _ st) = st /= []
Дополнительно мы проверяем, что если все ребра пройдены (список состояний пуст),
мы удаляем заданную точку из списка точек.
Ну и заключительная часть: функция сериализации состояния и функция main, запускающая
работу программы.
showCollision :: Collision -> String
showCollision coll@(p1,p2,ts) = 
    "Collision between points " ++ 
    showPoint p1 ++ " and "  ++ showPoint p2 ++
    " at time moment: " ++ show (fromIntegral ts / 2)
    where
 showPoint p@(Point i pt st) = 
             "(Idx = " ++ show i ++ 
             " State = " ++ showState st ++ ")"
        showState (h@(p,l,d,e):t) = 
  "Pos: " ++ show (fromIntegral p / 2) ++
                                    " edge: " ++ showEdge e
        showEdge e@(Edge v1 v2 l) = show v1 ++ "-" ++ show v2
main :: IO ()
main =  mapM_ (putStrLn . showCollision) 
  $ snd $ emulate $ makePoints points graph
Collision between points (Idx = 1 State = Pos: 0.0 edge: 2-1) 
and (Idx = 2 State = Pos: 0.0 edge: 2-3) at time moment: 3.0
Collision between points (Idx = 1 State = Pos: 0.0 edge: 2-1) 
and (Idx = 3 State = Pos: 0.0 edge: 2-4) at time moment: 3.0
Collision between points (Idx = 2 State = Pos: 0.0 edge: 2-3) 
and (Idx = 3 State = Pos: 0.0 edge: 2-4) at time moment: 3.0
Collision between points (Idx = 4 State = Pos: 0.5 edge: 2-6) 
and (Idx = 5 State = Pos: 0.5 edge: 2-6) at time moment: 6.5
оставшиеся (4 и 5) столкнутся на шаге 13 (временнАя отметка - 6.5) на ребре 2-6
на расстоянии 0.5 от вершины 2.
Вот, собственно, и всё. Надеюсь, вам было интересно.
В заключение добавлю, что на днях вышла новая версия Haskell Platform - 2011.4.0.0.
Комментариев нет:
Отправить комментарий