Попробуем написать функцию, которая поиском в глубину найдет все циклы, начиная от заданной вершины
IEnumerable<Stack<int>> FindAllCycles(int[,] edges, int currentV, HashSet<int> alreadyVisited, Stack<int> currentPath)
{
if (alreadyVisited.Contains(currentV))
{
var ret = new Stack<int>();
ret.Push(currentV);
foreach (var v in currentPath)
{
ret.Push(v);
// Крутим путь только до начала цикла
if (v == currentV) break;
}
yield return ret;
}
else
{
alreadyVisited.Add(currentV);
currentPath.Push(currentV);
for (int i = 0; i < edges.GetLength(1); i++)
if (currentV != i && edges[currentV, i] == 1)
foreach (var cycle in FindAllCycles(edges, i, alreadyVisited, currentPath)) yield return cycle;
alreadyVisited.Remove(currentV);
currentPath.Pop();
}
}
Далее, надо будет запустить эту функцию для каждой вершины и, по сути, получить все циклы, из полученных циклов вернуть цикл максимальной длины.
var vertices = new Dictionary<int, char>() { { 0, 'А' }, { 1, 'Б' }, { 2, 'В' }, { 3, 'Г' }, { 4, 'Д' } };
var edges = new int[,] {
{0, 1, 1, 0, 0},
{0, 0, 1, 0, 1},
{0, 0, 0, 0, 1},
{1, 1, 0, 0, 0},
{0, 0, 0, 1, 0},
};
var allCycles = vertices.Keys.SelectMany(x => FindAllCycles(edges, x, new HashSet<int>(), new Stack<int>()));
Stack<int> maxCycle = null;
foreach(var cycle in allCycles){
if (maxCycle == null || maxCycle.Count < cycle.Count)
maxCycle = cycle;
}
if (maxCycle == null)
Console.WriteLine("No cycles!");
else
Console.WriteLine(String.Join('-', maxCycle.Select(m => vertices[m])));
Вывод в консоль
А-Б-В-Д-Г-А
Сразу скажу, алгоритм далек от идеала и тут есть что улучшать, но он вроде работает на ваших данных. На большом графе с кучей ребер и кучей циклов со скоростью будет не важно.
Максимальный цикл в графе
- Подписаться на тему
- Сообщить другу
- Скачать/распечатать тему
|
|
|
|
Подскажите пожалуйста алгоритм решения задачи: дан неориентированный граф, нужно найти максимальный цикл в графе. |
|
Морской Ёж |
|
|
Senior Member Рейтинг (т): 17 |
Какая сложность? Можно back tracking’ом. Больше пока ничего на ум не приходит. |
|
kl |
|
|
Совершенно очевидно, что это NP-complete задача, ибо если бы за полиномиальное время можно было бы найти максимальный простой цикл, то мы бы автоматически решили бы и TSP (задачу коммивояжера). Просто путем проверки, гамильтонов ли цикл. |
shadeofgray |
|
|
Moderator Рейтинг (т): 30 |
Расшифровывая сказанное выше: перебор, перебор и только полный перебор всех циклов. Приведенная ссылка позволяет оптимизировать перебор, но ценой того, что находятся не самые длинные циклы, а просто «немного длинные» |
|
kl |
|
|
Цитата shadeofgray @ 25.04.06, 12:29 Расшифровывая сказанное выше: перебор, перебор и только полный перебор всех циклов. Приведенная ссылка позволяет оптимизировать перебор, но ценой того, что находятся не самые длинные циклы, а просто «немного длинные»
Ага, именно так, спасибо |
|
vek21 |
|
|
Цитата kl @ 25.04.06, 11:59 Просто путем проверки, гамильтонов ли цикл. А при чём здесь гамильтонов цикл? Гамильтонов цикл — это цикл, который проходит через все вершины графа, но не обязательно, что этот цикл максимальный. |
|
mo3r |
|
|
Цитата vek21 @ 26.04.06, 06:07 А при чём здесь гамильтонов цикл? Гамильтонов цикл — это цикл, который проходит через все вершины графа, но не обязательно, что этот цикл максимальный.
Скажем, так: Цитата vek21 @ 24.04.06, 20:20 Подскажите пожалуйста алгоритм решения задачи: дан неориентированный граф, нужно найти максимальный цикл в графе. Если граф эйлеров, то максимальным циклом, не проходящим по ребру дважды в одном направлении, будет эйлеров цикл. |
|
Морской Ёж |
|
|
Senior Member Рейтинг (т): 17 |
Цитата mo3r @ 26.04.06, 06:55 Гамильтонов цикл — это цикл, который проходит через все вершины графа, но не обязательно, что этот цикл максимальный. Я, конечно, не спорю, что гамильтонов цикл здесь не при чем, но есть небольшая поправка — гамильтонов цикл всегда минимален для данного графа. |
|
kl |
|
|
Цитата Arsuit @ 27.04.06, 13:08 Я, конечно, не спорю, что гамильтонов цикл здесь не при чем, но есть небольшая поправка — гамильтонов цикл всегда минимален для данного графа.
Гамильтонов цикл здесь очень даже причем. mo3r достаточно ясно показал, то о чем я говорил в первом посте, а именно, как задача нахождения гамильтонова цикла сводится к исходной задаче. Сводится по Куку. Отсюда автоматически следует, что либо вам удается показать, что P = NP, либо вы не сможете решить свою задачу точно и за приемлемое время. Вот и все. |
|
kl |
|
|
Цитата Arsuit @ 27.04.06, 13:08 гамильтонов цикл всегда минимален для данного графа. Это почему? |
|
Морской Ёж |
|
|
Senior Member Рейтинг (т): 17 |
по опредилению вроде. |
|
kl |
|
|
Цитата Arsuit @ 06.05.06, 12:31 по опредилению вроде. Прочитай определение еще разок. |
|
esperanto |
|
|
математик Рейтинг (т): 50 |
Цитата kl @ 25.04.06, 11:59 Совершенно очевидно, что это NP-complete задача, Судя по вашим словам, совершенно очевидно, что задача NP=HARD. Добавлено 07.05.06, 03:36 |
|
kl |
|
|
Да, я согласен, что термин NP-complete тут надо употреблять аккуратнее, потому что он строго определен только для decision problems (а TSP в классической формулировке таковой не является). Тем не менее ее можно перевести в класс decision problems. Но мне не хотелось вдаваться в эти детали, я просто имел в виду, что задача поиска максимального цикла — как минимум так же сложна как и TSP. И наоборот. |
|
KAV_Invariant |
|
|
Вот мне надо как-то было найти минимальный цикл в графе. Была у меня идея пробежаться по всем вершинам и для каждой вершины найти кратчайший путь из нее в нее же. Только вот я не знаю, как это сделать |
0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)
0 пользователей:
- Предыдущая тема
- Алгоритмы
- Следующая тема
[ Script execution time: 0,0460 ] [ 15 queries used ] [ Generated: 27.05.23, 22:25 GMT ]
- В этой теме 1 ответ, 2 участника, последнее обновление 1 год, 5 месяцев назад сделано .
-
Сообщения
-
-
Дан неориентированный граф. Написать программу, которая находит в графе максимальный цикл и выдает его в виде списка вершин. Если в графе нет циклов, функция должна сообщать об этом. Примерный граф прикреплю ниже.
-
Вот тут можно взять функцию, рассчитывающую все циклы в графе. При этом цикл возвращается в виде списка узлов. Все что вам остается сделать — найти список с максимальной длиной. Сделать это можно так:
longest_list(List, Longest):- member(Longest, List), length(Longest, MaxLen), + ( member(Other, List), length(Other, OtherLen), OtherLen > MaxLen ).Тут мы записали правило — список является самым длинным если не существует другого списка, длина которого больше.
Остается лишь вызвать готовые функции чтобы получить решение вашей задачи.
-
-
Автор
Сообщения
- Для ответа в этой теме необходимо авторизоваться.
Форум программистов Vingrad
|
Поиск: |
![]() ![]()
|
|
Опции темы |
| magicfa |
|
||
|
Новичок Профиль Репутация: нет
|
Никто не подскажет как в неориентированном графе можно найти цикл максимальной длинны, включающий заданную вершину? |
||
|
|||
| Silent |
|
||
|
Опытный Профиль Репутация: 1
|
Ну дык… поиск в глубину из этой вершины… |
||
|
|||
| SoWa |
|
||
Харекришна Профиль Репутация: 6
|
Действительно, это стандартный алгоритм на графах, описаный в любой книге по данной тематике. Вот, если не ошибаюсь, примеры: ——————— Всем добра |
||
|
|||
| maxdiver |
|
||
|
Опытный Профиль
Репутация: 16
|
SoWa Silent Не говоря уже о том, что на графах с существующим гамильтоновым циклом, задача нахождения длиннейшего цикла эквивалентна нахождению этого гамильтонова цикла, и вы предлагаете его дфсом искать |
||
|
|||
| Earnest |
|
||
Эксперт Профиль
Репутация: 7
|
maxdiver, насколько я помню, задача о поиски максимального цикла, в отличие от поиска минимального, не является тривиальной задачей, а является как раз np-полной. Говоря по-русски, пахнет полным перебором. Не всегда же есть гамильтонов цикл. ——————— … |
||
|
|||
| maxdiver |
|
||
|
Опытный Профиль
Репутация: 16
|
Earnest, ну так и я о том же Добавлено @ 17:37
Я подумал, что этой фразы достаточно, чтобы ни у кого не осталось сомнений в том, что задача ТС является NP-полной. (хотя сначала я сам чуть не поверил в алгоритм с дфсом Это сообщение отредактировал(а) maxdiver — 19.12.2008, 17:38 |
||
|
|||
| Earnest |
|
||
Эксперт Профиль
Репутация: 7
|
Ага, так бывает, когда заявляют с таким апломбом, а ты помнишь смутно… ——————— … |
||
|
|||
![]() ![]()
|
| Правила форума «Алгоритмы» | |
|
|
Форум «Алгоритмы» предназначен для обсуждения вопросов, связанных только с алгоритмами и структурами данных, без привязки к конкретному языку программирования и/или программному продукту.
Если Вам понравилась атмосфера форума, заходите к нам чаще! С уважением, maxim1000. |
| 0 Пользователей читают эту тему (0 Гостей и 0 Скрытых Пользователей) |
| 0 Пользователей: |
| « Предыдущая тема | Алгоритмы | Следующая тема » |
Поиск в ширину (англ. breadth-first search) — один из основных алгоритмов на графах, позволяющий находить все кратчайшие пути от заданной вершины и решать многие другие задачи.
Поиск в ширину также называют обходом — так же, как поиск в глубину и все другие обходы, он посещает все вершины графа по одному разу, только в другом порядке: по увеличению расстояния до начальной вершины.
#Описание алгоритма
На вход алгоритма подаётся невзвешенный граф и номер стартовой вершины $s$. Граф может быть как ориентированным, так и неориентированным — для алгоритма это не важно.
Основную идею алгоритма можно понимать как процесс «поджигания» графа: на нулевом шаге мы поджигаем вершину $s$, а на каждом следующем шаге огонь с каждой уже горящей вершины перекидывается на всех её соседей, в конечном счете поджигая весь граф.
Если моделировать этот процесс, то за каждую итерацию алгоритма будет происходить расширение «кольца огня» в ширину на единицу. Номер шага, на котором вершина $v$ начинает гореть, в точности равен длине её минимального пути из вершины $s$.
Моделировать это можно следующим образом. Создадим очередь, в которую будут помещаться горящие вершины, а также заведём булевый массив, в котором для каждой вершины будем отмечать, горит она или нет — или иными словами, была ли она уже посещена. Изначально в очередь помещается только вершина $s$, которая сразу помечается горящей.
Затем алгоритм представляет собой такой цикл: пока очередь не пуста, достать из её головы одну вершину $v$, просмотреть все рёбра, исходящие из этой вершины, и если какие-то из смежных вершин $u$ ещё не горят, поджечь их и поместить в конец очереди.
В итоге, когда очередь опустеет, мы по одному разу обойдём все достижимые из $s$ вершины, причём до каждой дойдём кратчайшим путём. Длины кратчайших путей можно посчитать, если завести для них отдельный массив $d$ и при добавлении в очередь пересчитывать по правилу $d_u = d_v + 1$. Также можно компактно сохранить дополнительную информацию для восстановления самих путей, заведя массив «предков», в котором для каждой вершины хранится номер вершины из которой мы в неё попали.
#Реализация
Если мы всё равно поддерживаем массив расстояний, то отдельный булевый массив с метками горящих вершин можно не создавать, а вместо этого просто присвоить изначальное расстояние всех вершин некоторым некоторым специальным значением (например, -1), которое будет сигнализировать а том, что эту вершину мы ещё не просмотрели.
vector<int> g[maxn];
void bfs(int s) {
queue<int> q;
q.push(s);
vector<int> d(n, -1), p(n);
d[s] = 0;
while (!q.empty()) {
int v = q.front();
q.pop();
for (int u : g[v]) {
if (d[u] == -1) {
q.push(u);
d[u] = d[v] + 1;
p[u] = v;
}
}
}
}
Теперь, чтобы восстановить кратчайший путь до какой-то вершины $v$, это можно сделать через массив p:
while (v != s) {
cout << v << endl;
v = p[v];
}
Обратим внимание, что путь выведется в обратном порядке.
#В неявных графах
Поиск в ширину часто применяется для поиска кратчайшего пути в неявно заданных графах.
В качестве конкретного примера, пусть у нас есть булева матрица размера $n times n$, в которой помечено, свободна ли клетка с координатами $(x, y)$, и требуется найти кратчайший путь от $(x_s, y_t)$ до $(x_y, y_t)$ при условии, что за один шаг можно перемещаться в свободную соседнюю по вертикали или горизонтали клетку.
Можно сконвертировать граф в явный формат: как-нибудь пронумеровать все ячейки (например по формуле $x cdot n + y$) и для каждой посмотреть на всех её соседей, добавляя ребро в $(x pm 1, y pm 1)$, если соответствующая клетка свободна.
Такой подход будет работать за оптимальное линейное время, однако с точки зрения реализации проще адаптировать не входные данные, а сам алгоритм обхода в глубину:
bool a[N][N]; // свободна ли клетка (x, y)
int d[N][N]; // кратчайшее расстояние до (x, y)
// чтобы немного упростить код
struct cell {
int x, y;
};
void bfs(cell s, cell t) {
queue<cell> q;
q.push(s);
memset(d, -1, sizeof d);
d[s.x][x.y] = 0;
while (!q.empty()) {
auto [x, y] = q.front();
q.pop();
// просматриваем всех соседей
for (auto [dx, dy] : {{-1, 0}, {+1, 0}, {0, -1}, {0, +1}}) {
// считаем координаты соседа
int _x = x + dx, _y = y + dy;
// и проверяем, что он свободен и не был посещен ранее
if (a[_x][_y] && d[_x][_y] == -1) {
d[_x][_y] = d[x][y] + 1;
q.push_back({_x, _y});
}
}
}
}
Перед запуском bfs следует убедиться, что не произойдет выход за пределы границ. Вместо того, чтобы добавлять проверки на _x < 0 || x >= n и т. п. при просмотре возможных соседей, удобно сделать следующий трюк: изначально создать матрицу a с размерностями на 2 больше, чем нужно, и на этапе чтения данных заполнять её в индексации не с нуля, а с единицы. Тогда границы матрицы всегда будут заполнены нулями (то есть помечены непроходимыми) и алгоритм никогда в них не зайдет.
#Применения и обобщения
Помимо нахождения расстояний в невзвешенном графе, обход в ширину можно использовать для многих задач, в которых работает обход в глубину, например для поиска компонент связности или проверки на двудольность.
Множественный BFS. Добавив в очередь изначально не одну, а несколько вершин, мы найдем для каждой вершины кратчайшее расстояние до одной из них.
Это полезно для задач, в которых нужно моделировать пожар, наводнение, извержение вулкана или подобные явления, в которых источник «волны» не один. Также так можно чуть быстрее находить кратчайший путь для конкретной пары вершин, запустив параллельно два обхода от каждой и остановив в тот момент, когда они встретятся.
Игры. С помощью BFS можно найти решение какой-либо задачи / игры за наименьшее число ходов, если каждое состояние системы можно представить вершиной графа, а переходы из одного состояния в другое — рёбрами графа.
В качестве (нетривиального) примера: собрать кубик Рубика за наименьшее число ходов.
Кратчайшие циклы. Чтобы найти кратчайший цикл в ориентированном невзвешенном графе, можно произвести поиск в ширину из каждой вершины. Как только в процессе обхода мы пытаемся пойти из текущей вершины по какому-то ребру в уже посещённую вершину, то это означает, что мы нашли кратчайший цикл для данной вершины, и останавливаем обход. Среди всех таких найденных циклов (по одному от каждого запуска обхода) выбираем кратчайший.
Такой алгоритм будет работать за $O(n^2)$: худшим случаем будет один большой цикл, в котором мы для каждой вершины пройдемся по всем остальным.
Ребра на кратчайшем пути. Мы можем за линейное время найти все рёбра, лежащие на каком-либо кратчайшем пути между заданной парой вершин $a$ и $b$. Для этого нужно запустить два поиска в ширину: из $a$ и из $b$.
Обозначим через $d_a$ и $d_b$ массивы кратчайших расстояний, получившиеся в результате этих обходов. Тогда каждое ребро $(u,v)$ можно проверить критерием
$$
d_a[u] + d_b[v] + 1 = d_a[b]
$$
Альтернативно, можно запустить один обход из $a$, и когда он дойдет до $b$, начать рекурсивно проходиться по всем обратным ребрам, ведущим в более близкие к $a$ вершины (то есть те, для которых $d[u] = d[v] — 1$), отдельно помечая их.
Аналогично можно найти все вершины на каком-либо кратчайшем пути.
0-1 BFS. Если веса некоторых ребер могут быть нулевыми, то кратчайшие пути искать не сильно сложнее.
Ключевое наблюдение: если от вершины $a$ до вершины $b$ можно дойти по пути, состоящему из нулевых рёбер, то кратчайшие расстояния от вершины $s$ до этих вершин совпадают.
Если в нашем графе оставить только $0$-рёбра, то он распадётся на компоненты связности, в каждой из которых ответ одинаковый. Если теперь вернуть единичные рёбра и сказать, что эти рёбра соединяют не вершины, а компоненты связности, то мы сведём задачу к обычному BFS.
Получается, запустив обход, мы можем при обработке вершины $v$, у которой есть нулевые ребра в непосещенные вершины, сразу пройтись по ним и добавить все вершины нулевой компоненты, проставив им такое же расстояние, как и у $v$.
Это можно сделать и напрямую, запустив BFS внутри BFS, однако можно заметить, что достаточно при посещении вершины просто добавлять всех её непосещенных соседей по нулевым ребрам в голову очереди, чтобы обработать их раньше, чем те, которые там уже есть. Это легко сделать, если очередь заменить деком:
vector<int> d(n, -1);
d[s] = 0;
deque<int> q;
q.push_back(s);
while (!q.empty()) {
int v = q.front();
q.pop_front();
for (auto [u, w] : g[v]) {
if (d[u] == -1) {
d[u] = d[v] + w;
if (w == 0)
q.push_front(u);
else
q.push_back(u);
}
}
}
Также вместо дека можно завести две очереди: одну для нулевых ребер, а другую для единичных, в внутри цикла while сначала просматривать первую, а только потом, когда она станет пустой, вторую. Этот подход уже можно обобщить.
1-k BFS. Теперь веса рёбер принимают значения от $1$ до некоторого небольшого $k$, и всё так же требуется найти кратчайшие расстояния от вершины $s$, но уже в плане суммарного веса.
Наблюдение: максимальное кратчайшее расстояние в графе равно $(n — 1) cdot k$.
Заведём для каждого расстояния $d$ очередь $q_d$, в которой будут храниться вершины, находящиеся на расстоянии $d$ от $s$ — плюс, возможно, некоторые вершины, до которых мы уже нашли путь длины $d$ от $s$, но для которых возможно существует более короткий путь. Нам потребуется $O((n — 1) cdot k)$ очередей.
Положим изначально вершину $s$ в $q_0$, а дальше будем брать вершину из наименьшего непустого списка и класть всех её непосещенных соседей в очередь с номером $d_v + w$ и релаксировать $d_u$, не забывая при этом, что кратчайшее расстояние до неё на самом деле может быть и меньше.
int d[maxn];
d[s] = 0;
queue<int> q[maxd];
q[0].push_back(s);
for (int dist = 0; dist < maxd; dist++) {
while (!q[dist].empty()) {
int v = q[dist].front();
q[dist].pop();
// если расстояние меньше и мы уже рассмотрели эту вершину,
// но она всё ещё лежит в более верхней очереди
if (d[v] > dist)
continue;
for (auto [u, w] : g[v]) {
if (d[u] < d[v] + w) {
d[u] = d[v] + w;
q[d[u]].push(u);
}
}
}
}
Сложность такого алгоритма будет $O(k n + m)$, поскольку каждую вершину мы можем прорелаксировать и добавить в другую очередь не более $k$ раз, а просматривать рёбра, исходящие из вершины мы будем только когда обработаем эту вершину в самый первый раз.
На самом деле, нам так много списков не нужно. Если каждое ребро имеет вес не более $k$, то в любой момент времени не более $k$ очередей будут непустыми. Мы можем завести только $k$ списков, и вместо добавления в $(d_v + w)$-ый список использовать $(d_v+w) bmod k$.
Заметим, что алгоритм также работает когда есть общее ограничение на длину пути, а не только на вес каждого ребра. Для более общего случая, когда веса ребер могут быть любыми неотрицательными, есть алгоритм Дейкстры, который мы разберем в следующей статье.




Я извиняюсь, у меня иногда возникают сложности с объяснением на русском того, что я на русском никогда не изучал… 














)


















