Я хочу написать алгоритм поиска кратчайшего (наименьшей стоимости) пути от фиксированного источника к фиксированному узлу назначения во взвешенном ориентированном графе с неотрицательными весами (может иметь циклы).
Следующий алгоритм DFS + DP делает вышеописанное.
(cost_to_dst + edgecost_to_neighbour)
dp
, чтобы нам не приходилось вычислять ее снова для этого узла.V
(количество всех узлов), поскольку максимальная длина кратчайшего пути от SRC до DST может быть V.int dfs(int u, int stops = V + 1, int &dst, vector<vector<pair<int, int>>> &adj, vector<int> &dp)
{
if (u == dst) // reached dst
return 0;
if (stops == 0) // can't go on anymore
return INT_MAX;
if (dp[u]!= -1)
return dp[u];
int res = INT_MAX;
for(auto &[v, edgecost]: adj[u])
{
int vdstcost = dfs(v, stops - 1, dst, adj, dp); // min cost from v to dst
if (vdstcost != INT_MAX)
res = min(res, vdstcost + edgecost);
}
return dp[u] = res;
}
Какова будет временная сложность этого кода с точки зрения E (количество ребер) и V (количество узлов)?
Это логически правильно? т. е. даст ли этот алгоритм правильный ответ для каждого сценария в рамках ограничений?
Четвертый момент введения ограничения глубины V в рекурсивные вызовы затрудняет анализ временной сложности. Но я уверен, что это будет где-то между O(V+E)
и O(VE)
.
Представьте себе путь --> A --->...----> B ---->...----> A. В таком случае рекурсивные вызовы будут зацикливаться до тех пор, пока глубина (V) не будет исчерпана. Таким образом, TC не может быть просто O(V+E)
ДП также необходимо рассмотреть вопрос «нет». осталось остановок, как указано в ответе ниже. код ниже обновлен для использования 2D DP
int dfs(int u, int stops, int dst, vector<vector<pair<int, int>>> &adj, vector<vector<int>> &dp)
{
if (u == dst) // reached dst
return 0;
if (stops == 0) // can't go on anymore
return INT_MAX;
if (dp[u][stops]!= -1)
return dp[u][stops];
int res = INT_MAX;
for(auto &[v, edgecost]: adj[u])
{
int vdstcost = dfs(v, stops - 1, dst, adj, dp); // min cost from v to dst
if (vdstcost != INT_MAX)
res = min(res, vdstcost + edgecost);
}
return dp[u][stops] = res;
}
Если я правильно понимаю вопрос, вы намерены иметь работающий алгоритм, который полагается только на запоминание и обход в глубину, без понятия посещенных/непосещенных или приоритетных очередей. Но это не всегда работает:
Указанный вами алгоритм не всегда находит путь, поскольку он может хранить INT_MAX
расстояния в dp[u]
, когда на самом деле существует путь от u
до цели:
u
. Результат зависит от количества оставшихся остановок. Возможно, имея всего две остановки, невозможно достичь цели, но с пятью путь есть. Поэтому вам нужно запомнить результаты для каждого узла и каждой остановки.Не критическая проблема, но:
stops
может быть 𝑉−1, поскольку вы уже передаете ему узел в качестве аргумента, который считается за единицу, оставляя вам только 𝑉−1 для дальнейшего расширения (т. е. число ребер, которым нужно следовать).Как только вышеизложенное исправлено, мы можем рассмотреть этот «плохой» случай:
У нас есть узлы 𝑛, каждый из которых имеет два исходящих ребра, одно к самому себе и одно к «следующему» узлу, и где все веса равны 1. Пусть 𝑣1 будет источником, а 𝑣𝑛 целью. Предположим, что в худшем случае в первую очередь всегда посещаются ребра цикла. Тогда, если мы на мгновение проигнорируем мемоизацию, DFS, начинающаяся с 𝑣1, будет пробовать эти пути, все длины 𝑉−1 (т. е. с 𝑉 узлами):
Только при последней попытке цель будет найдена и все узлы получат небесконечное присвоение в массиве dp
.
Если мы рассмотрим мемоизацию для обрезки дерева, то в узле 𝑣1 мы будем иметь 𝑛 разные значения stops
, которые могут возникнуть в этом узле. Для 𝑣2 у нас будет на одну возможность меньше для stops
, ... и т. д., вплоть до For 𝑣𝑛, где stops
не может быть ничем иным, как 0. Итак, чтобы записать все эти dp
записи (т. е. в худшем случае), нам понадобится 𝑛 + 𝑛−1 + 𝑛−2 + ... + 1 посещение. Все остальные посещения узла, поступающие через альтернативный край к тому же узлу, выиграют от мемоизации. Таким образом, они рассчитаны на одинаковое количество операций.
Таким образом, мы получаем сложность этого сценария O(𝐸²).
Если вы ограничитесь графами без циклов, то вы можете провести аналогичные рассуждения с графом, который имеет четное количество узлов 𝑛 и где узлы в паре нечет-чет ссылаются друг на друга, например:
В Википедии перечислены некоторые популярные алгоритмы поиска кратчайшего пути в графе. Хотя алгоритм Дейкстры может решить проблему поиска кратчайших путей от заданной вершины ко всем вершинам графа, его можно использовать и для одного пункта назначения, куда он просто возвращается, когда эта вершина найдена. Его временная сложность в худшем случае равна O(𝐸 + 𝑉log𝑉).
«Тогда DFS, начинающаяся с 𝑣1, будет пробовать эти пути длиной 𝑉−1». Это не то, что произойдет. Алгоритм просто не найдет правильный ответ. Он не продвинется дальше v2.
Верно. В начале я добавил пункт, чтобы выделить проблему в их коде, которую необходимо исправить, прежде чем можно будет использовать этот анализ.
Зависит от. Ваше изменение может сработать, а может и не сработать, но если вы исправите его правильно, сложность тоже будет устранена. Только не посещайте один и тот же узел на одном и том же пути-кандидате дважды (отметьте его при входе в функцию, снимите отметку при выходе, не обрабатывайте, если отмечено).
Даже если бы мы добавили в их алгоритм концепцию посещений, сложность в худшем случае все равно была бы экспоненциальной (но для других графов, чем я предоставил). У меня сложилось впечатление, что целью автора вопроса было представить работающий алгоритм, отличающийся от более известных алгоритмов. То, что это не работает, сводится к легко исправимой оплошности при их запоминании.
Я не уверен, как в этом случае сложность может быть экспоненциальной, можете ли вы показать пример графика?
Я ошибаюсь в этом. При посещенной маркировке временная сложность тогда равна O(𝐸).
Во-первых, спасибо огромное за такое подробное объяснение @trincot. Во-вторых, @n.m.couldbeanAI правильно указал, что мой код не найдет правильный ответ - исправление @trincot использования [u, stops] в качестве ключей для dp должно исправить мой код. В-третьих, мы не можем использовать посещаемый массив, как предложил @n.m.couldbeanAI. Это также приведет к тому, что алгоритм будет давать неверные ответы, поскольку пути с более низкой стоимостью могут быть пропущены. Пытался объяснить [здесь]. Lasty @ trincot, как вы создавали эти диаграммы?
Спасибо за хороший и развернутый комментарий. Схемы я создал в PowerPoint.
@trincot Было бы очень полезно, если бы вы могли дать мне анализ худшего случая для обновленного кода, используя [u, stop] в качестве ключей для DP. Используя этот псевдокод, я вижу, что в вашем примере 2^n не пройдено несколько путей. Но не могу точно определить, насколько точно сократится временная сложность.
Я обновил свой ответ, чтобы сосредоточиться на этом случае. Мне пришлось исправить свое предыдущее решение исключить мемоизацию из анализа, поэтому теперь я включил ее, и она выходит на уровне O(𝐸²) — что то же самое, что O(𝑉²) для этого конкретного примера плохого случая.
«Это также приведет к тому, что алгоритм будет давать неправильные ответы, поскольку пути с более низкой стоимостью могут быть пропущены». Нет, ваше объяснение неправильно использует посещенный массив. Вам необходимо отметить u
перед входом в цикл for
и снять отметку после выхода из цикла. Вы проверяете, посещается ли v
в цикле. График без циклов никогда не вызовет эту проверку.
«Это логически правильно?» Нет, ваш алгоритм застрянет в графовом цикле и никогда не найдет ответа.