Как обрабатывать «составные узлы» в графе, пройденном по алгоритму Дейкстры? - PullRequest
2 голосов
/ 04 февраля 2020

Я имею дело с конечным автоматом, который в настоящее время пересекается по алгоритму Дейкстры. Однако теперь мне нужно улучшить этот конечный автомат, чтобы он был «умнее» в плане определения маршрутов для учета некоторых побочных эффектов. По существу, некоторые пути не могут быть пройдены, если не выполнены определенные требования, даже если вы находитесь в правильном начальном состоянии для этого пути. Этим требованиям можно удовлетворить, пройдя сначала другие пути. Упрощенный пример того, что я пытаюсь рассмотреть, - это поездки между городами:

  • Вы можете путешествовать внутри страны без паспорта (только базовый c ID) (т.е. Philly -> NY C )
  • Как только вам нужно путешествовать за границей, вам нужен паспорт (NY C -> Париж)
  • Если у вас уже есть паспорт, вы можете путешествовать за границей (NY * 1027) * -> Париж)
  • Если нет, вам нужно сначала ехать домой, чтобы взять его (Нью-Йорк C -> Филадельфия -> Нью-Йорк C -> Париж)

Альтернативный пример (с которым я на самом деле имею дело) - это состояние веб-сайта и концепция входа в систему и выхода из нее.

Есть два подхода, о которых я думаю:

  • Составление состояний (т. Е. Наличие паспорта само по себе является вторичным состоянием, которое можно комбинировать с состояниями «местоположения»), похоже, что это добавило бы совершенно другое измерение в мой конечный автомат, и я не уверен, будет ли это сделать алгоритм беспорядком.
  • Условные пути, которые Они доступны только в том случае, если определенное свойство установлено во время нахождения в состоянии (несколько байесовский подход), это фактически сделает мои состояния «нечистыми», когда переход будет зависеть от свойств внутреннего состояния, поэтому я предпочитаю подход составления состояний.

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

Ответы [ 3 ]

2 голосов
/ 05 февраля 2020

Это можно решить с помощью Astar, действительно «дублируя» города, по-видимому, в 2 ^ n режиме (на практике это меньше, поскольку не все состояния будут исследованы).

Узел теперь является кортежем <city, ...flags>, где в этом случае flags - это простое логическое значение для представления, обладаем ли мы паспортом или нет.

Вместо того, чтобы в основном рассматривать соседей какого-то города C Теперь мы рассмотрим соседей кортежа T, которые являются соседями T.city, ограниченными некоторым правилом:

Если соседний город требует пропуск, T должен иметь пропуск в его флагах

Ниже Астара копия вставлена ​​из вики . Единственная адаптация:

при генерации соседей из какого-то узла, если узел имеет проход, то есть и соседи.

Уведомление в тестах (скопировано более или менее от Гая Кодера), прокомментировали два теста (которые провалились).

  • Первый, потому что Гаррисберг с паспортом переопределяет , в моем случае отсутствие пароля, указанного в качестве аргумента
  • Второй, потому что, как прокомментировано, я не ожидаю прихода "туда-сюда", если в этом нет необходимости.

Обратите внимание, что ребра не взвешены d(a,b) = 1 для существующих (a,b) но они могут / должны быть.

function h (node) { return 0 }
function d (a, b) { return 1 } // no weight but could be
const M = {
    harrisburg: [
      { c: 'philly', passRequired: false }
    ],
    nyc: [
      { c: 'philly', passRequired: false },
      { c: 'paris', passRequired: true }
    ],
    paris: [
      { c: 'nyc', passRequired: true }
    ],
    philly: [
      { c: 'harrisburg', passRequired: false },
      { c: 'nyc', passRequired: false }
    ]
}

const neighbours = node => {
    if (node.c === 'harrisburg') {
        return M[node.c].map(x => {
            return { c: x.c, hasPass: true }
        })
    }
    if (node.hasPass) {
        return M[node.c].map(x => Object.assign({ hasPass: true }, x))
    }
    return M[node.c].filter(x => !x.passRequired)
}
function id (node) { return node.c + !!node.hasPass }

//https://en.wikipedia.org/wiki/A*_search_algorithm
function reconstruct_path (cameFrom, current) {
  const total_path = [current]
  while(cameFrom.has(id(current))) {
    current = cameFrom.get(id(current))
    total_path.unshift(current)
  }
  return total_path
}


// A* finds a path from start to goal.
// h is the heuristic function. h(n) estimates the cost to reach goal from node n.
function A_Star(start, goal, h) {
  // The set of discovered nodes that may need to be (re-)expanded.
  // Initially, only the start node is known.
  const openSet = new Map([[id(start), start]])

  // For node n, cameFrom[n] is the node immediately preceding it on the cheapest path from start to n currently known.
  const cameFrom = new Map()

  // For node n, gScore[n] is the cost of the cheapest path from start to n currently known.
  const gScore = new Map()
  gScore.set(id(start), 0)

  // For node n, fScore[n] := gScore[n] + h(n).
  const fScore = new Map()
  fScore.set(id(start), h(start))

  while (openSet.size) {
    //current := the node in openSet having the lowest fScore[] value
    let current
    let bestScore = Number.MAX_SAFE_INTEGER
    for (let [nodeId, node] of openSet) {
      const score = fScore.get(nodeId)
      if (score < bestScore) {
        bestScore = score
        current = node
      }
    }
    
    if (current.c == goal.c) {
      return reconstruct_path(cameFrom, current)
    }
    openSet.delete(id(current))
    neighbours(current).forEach(neighbor => {
      const neighborId = id(neighbor)
      // d(current,neighbor) is the weight of the edge from current to neighbor
      // tentative_gScore is the distance from start to the neighbor through current
      const tentative_gScore = gScore.get(id(current)) + d(current, neighbor)
      if (!gScore.has(neighborId) || tentative_gScore < gScore.get(neighborId)) {
        // This path to neighbor is better than any previous one. Record it!
        cameFrom.set(neighborId, current)
        gScore.set(neighborId, tentative_gScore)
        fScore.set(neighborId, gScore.get(neighborId) + h(neighbor))
        if (!openSet.has(neighborId)){
          openSet.set(neighborId, neighbor)
        }
      }
    })
  }
  // Open set is empty but goal was never reached
  return false
}

function tests() {
  const assert = x => {
    if(!x){
      throw new Error(x)
    }
  }
  function travel_test_case_generator(from, to, initialPass, expect) {
    const res = A_Star({ c: from, hasPass: initialPass === 'yes'}, {c: to}, h).map(x => x.c)
    try {
    assert(res.length === expect.length)
    assert(res.every((x, i) => x === expect[i]))
    } catch (e) {
      console.log('failed', from, to, initialPass, res, expect)
      throw e
    }
    console.log('ok', `from ${from} to ${to} ${initialPass==='yes' ? 'with': 'without'} pass:`, res)
  }
  travel_test_case_generator( 'harrisburg' ,'harrisburg' ,'no'  ,['harrisburg'])
  travel_test_case_generator( 'harrisburg' ,'harrisburg' ,'yes' ,['harrisburg'])
  travel_test_case_generator( 'harrisburg' ,'philly'     ,'no'  ,['harrisburg', 'philly'])
  travel_test_case_generator( 'harrisburg' ,'philly'     ,'yes' ,['harrisburg', 'philly'])
  travel_test_case_generator( 'harrisburg' ,'nyc'        ,'no'  ,['harrisburg', 'philly', 'nyc'])
  travel_test_case_generator( 'harrisburg' ,'nyc'        ,'yes' ,['harrisburg', 'philly', 'nyc'])
  travel_test_case_generator( 'harrisburg' ,'paris'      ,'yes' ,['harrisburg', 'philly', 'nyc', 'paris'])
  // travel_test_case_generator( 'harrisburg' ,'paris'      ,'no'  ,['harrisburg', 'philly', 'nyc', 'philly', 'harrisburg', 'passport', 'philly', 'nyc', 'paris'])
  travel_test_case_generator( 'philly'     ,'philly'     ,'no'  ,['philly'])
  travel_test_case_generator( 'philly'     ,'philly'     ,'yes' ,['philly'])
  travel_test_case_generator( 'philly'     ,'nyc'        ,'no'  ,['philly', 'nyc'])
  travel_test_case_generator( 'philly'     ,'nyc'        ,'yes' ,['philly', 'nyc'])
  travel_test_case_generator( 'philly'     ,'paris'      ,'yes' ,['philly', 'nyc', 'paris'])
  // travel_test_case_generator( 'philly'     ,'paris'      ,'no'  ,['philly', 'nyc', 'philly', 'harrisburg', 'philly', 'nyc', 'paris'])
  travel_test_case_generator( 'nyc'        ,'paris'      ,'yes' ,['nyc', 'paris'])
  travel_test_case_generator( 'nyc'        ,'paris'      ,'no'  ,['nyc', 'philly', 'harrisburg', 'philly', 'nyc', 'paris'])
}
tests()
2 голосов
/ 05 февраля 2020

С учетом этих фактов

connection(philly,nyc,no).
connection(nyc,philly,no).
connection(philly,harrisburg,no).
connection(harrisburg,philly,no).
connection(paris,nyc,yes).
connection(nyc,paris,yes).
passport(harrisburg).

, где connection имеет аргументы from, to, passport needed

и эти контрольные примеры

:- begin_tests(travel).

travel_test_case_generator( harrisburg ,harrisburg ,no  ,[harrisburg]                                                        ).
travel_test_case_generator( harrisburg ,harrisburg ,yes ,[harrisburg]                                                        ).
travel_test_case_generator( harrisburg ,philly     ,no  ,[harrisburg,philly]                                                 ).
travel_test_case_generator( harrisburg ,philly     ,yes ,[harrisburg,philly]                                                 ).
travel_test_case_generator( harrisburg ,nyc        ,no  ,[harrisburg,philly,nyc]                                             ).
travel_test_case_generator( harrisburg ,nyc        ,yes ,[harrisburg,philly,nyc]                                             ).
travel_test_case_generator( harrisburg ,paris      ,yes ,[harrisburg,philly,nyc,paris]                                       ).
travel_test_case_generator( harrisburg ,paris      ,no  ,[harrisburg,philly,nyc,philly,harrisburg,passport,philly,nyc,paris] ).
travel_test_case_generator( philly     ,philly     ,no  ,[philly]                                                            ).
travel_test_case_generator( philly     ,philly     ,yes ,[philly]                                                            ).
travel_test_case_generator( philly     ,nyc        ,no  ,[philly,nyc]                                                        ).
travel_test_case_generator( philly     ,nyc        ,yes ,[philly,nyc]                                                        ).
travel_test_case_generator( philly     ,paris      ,yes ,[philly,nyc,paris]                                                  ).
travel_test_case_generator( philly     ,paris      ,no  ,[philly,nyc,philly,harrisburg,passport,philly,nyc,paris]            ).
travel_test_case_generator( nyc        ,paris      ,yes ,[nyc,paris]                                                         ).
travel_test_case_generator( nyc        ,paris      ,no  ,[nyc,philly,harrisburg,passport,philly,nyc,paris]                   ).

test(001,[forall(travel_test_case_generator(Start,End,Passport,Expected_route))]) :-
    route(Start,End,Passport,Route),

    assertion( Route == Expected_route ).

:- end_tests(travel).

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

route(Start,End,Passport,Route) :-
    route(Start,End,Passport,[],Route_reversed),
    reverse(Route_reversed,Route), !.

route(City,City,_,Route0,Route) :-
    visit(City,Route0,Route).

route(A,C,yes,Route0,Route) :-
    connection(A,B,_),
    \+ member(B,Route0),
    visit(A,Route0,Route1),
    route(B,C,yes,Route1,Route).

route(A,C,no,Route0,Route) :-
    connection(A,B,Need_passport),
    \+ member(B,Route0),
    (
        (
            Need_passport == yes,
            \+ member(passport,Route0)
        )
    ->
        (
            get_passport_shortest(A,Route1),
            route(B,C,yes,[],Route2),
            reverse(Route0,Route0_reversed),
            append([Route0_reversed,[A],Route1,Route2],Route_reversed),
            reverse(Route_reversed,Route)
        )
    ;
        (
            visit(A,Route0,Route1),
            route(B,C,no,Route1,Route)
        )
    ).

route_no(A,A,no,Route,Route).
route_no(A,C,no,Route0,Route) :-
    connection(A,B,no),
    \+ member(B,Route0),
    visit(B,Route0,Route1),
    route_no(B,C,no,Route1,Route).

get_passport(A,Route) :-
    passport(B),
    route_no(A,B,no,[],Route1),
    route_no(B,A,no,[],Route2),
    reverse(Route1,Route1_reversed),
    reverse(Route2,Route2_reversed),
    append([Route1_reversed,[passport],Route2_reversed],Route).

visit(City,Route0,Route) :-
    (
        Route0 = [City|_]
    ->
        Route = Route0
    ;
        Route = [City|Route0]
    ).

get_passport_shortest(A,Shortest_route) :-
    findall(Route,get_passport(A,Route),Routes),
    select_shortest(Routes,Shortest_route).

select_shortest([H|T],Result) :-
    length(H,Length),
    select_shortest(T,Length,H,Result).

select_shortest([],_Current_length,Result,Result).
select_shortest([Item|T],Current_length0,Current_shortest0,Result) :-
    length(Item,Item_length),
    (
        Item_length < Current_length0
    ->
        (
            Current_length = Item_length,
            Current_shortest = Item
        )
    ;
        (
            Current_length = Current_length0,
            Current_shortest = Current_shortest0
        )
    ),
    select_shortest(T,Current_length,Current_shortest,Result).

Когда тестовый пример запустите

?- make.
% c:/so_question_159 (posted) compiled 0.00 sec, 0 clauses
% PL-Unit: travel ................ done
% All 16 tests passed
true.

Весь тестовый проход.


Причина, по которой паспорт находится в Гаррисберге, а не в Филадельфии, заключается в том, что при тестировании кода код работал, когда паспорт находился в Филадельфии. , Затем, добавив Harrisburg и протестировав снова, проблема была обнаружена в коде и исправлена. Если изменить passport(harrisburg). на passport(philly)., код будет работать, но для этого потребуются дополнительные тестовые случаи.


Дополнительные вопросы размещены в комментариях и перенесены сюда.


Из гродзи

В ваших тестах строка (третья от конца) philly, paris, no, почему philly,nyc,philly, harrisbug..., когда вы можете просто сделать philly,harrisburg,philly..., чтобы получить паспорт? Это намеренная или какая-то незначительная ошибка?

Приятно видеть, что кто-то обращает внимание. Это не ошибка, и это был один из тестов, которые выявили ошибку, когда паспорт был в Гаррисберге. То, как я интерпретирую проблему, как указано в OP, случай путешествия - просто более понятная версия его реальной проблемы, связанной с динамическим c FSA с входом и выходом из системы. Так что зная, что вам нужен паспорт, неизвестно, пока вы не попытаетесь совершить поездку с nyc до paris. На данный момент вам нужен паспорт, если его нет в руке, и вам нужно вернуться к harrisbug, чтобы получить его.

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


Из OP

Что касается условий глубиной в несколько слоев, как ваш пример показывает это?

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


Из OP

Ваш пример с окном пароля пытается увидеть, как FSM обрабатывает ошибку пользователя?

Нет, я только посмотрел на ваши основные идеи в опубликованном вопросе.

Этот вопрос относится к ОП код , размещенным на GitHub


Ссылки на значение

Грамматика атрибутов ( Википедия )
Автоматическое планирование и планирование ( Википедия ) ( Пример пролога )
RosettaCode Алгоритм Дейкстры
Разрешение SLD
Выполнение таблицы (разрешение SLG)
Декларативное программирование - 3: logi c программирование и пролог

1 голос
/ 11 февраля 2020

То, что вы называете «составлением состояний», является обычным способом сделать это. Иногда это называется «наслоение графа». Он часто используется для решения проблем типа «кратчайший путь с ограничением».

Обычное описание будет выглядеть так:

Сделайте две копии вашего конечного автомата M 1 и M 2 , M 1 содержит только переходы, которые вы можете сделать без паспорта, M 2 содержит переходы, которые вы можете сделать с паспортом. Затем добавьте переход от M 1 к M 2 для каждого ар c, который приобретает вашего паспорта. Теперь найдите кратчайший путь от начального состояния в M 1 до целевого состояния в любой копии.

Это, как вы говорите, «добавление совершенно другого измерения». Если в исходном графе N вершин и M дополнительных состояний, результирующий граф имеет N * M вершин, так что это практично, только если N или M мало small * sh.

Вот неплохая лекция по технике: https://www.youtube.com/watch?v=OQ5jsbhAv_M&feature=youtu.be&t=47m7s

И вот некоторые другие ответы, которые я написал, используя ту же технику: { ссылка }

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...