Всем читающим эту статью здрасте. Сегодня я хотел бы поделиться с вами своей реализацией поиска в ширину (BFS) на C#.
Негативные комментарии, касающиеся моего говнокода и реализации поддерживаются. Настоятельно рекомендую вам высказаться по поводу всего. Я начинающий, поэтому думаю, что это даже к лучшему.
Для начала я хотел бы кратко ознакомить вас с принципом работы моего кода
Итак, изначально я создал класс под названием BFS:
class BFS{
}
Далее я начал создавать такие поля и свойства, как:
Начальное значение (StartValue)
Конечное значение (GoalValue)
Словарь с вершинами (Graph)
Очередь (Queue)
И проверенные значения (UsedValues)
class BFS
{
private string _startValue;
public string StartValue {
get { return _startValue; }
set { _startValue = value; }
}
private string _goalValue;
public string GoalValue
{
get { return _goalValue; }
set { _goalValue = value; }
}
private Dictionary<string, string[]> _graph = new Dictionary<string, string[]>();
public Dictionary<string, string[]> Graph
{
get { return _graph; }
set { _graph = value; }
}
private Queue<string> _queue = new Queue<string>();
public Queue<string> Queue
{
get { return _queue; }
set { _queue = value; }
}
private List<string> _usedValues = new List<string>();
public List<string> UsedValues
{
get { return _usedValues; }
set { _usedValues = value; }
}
}
Объявил конструктор класса:
public BFS(string startValue, string goalValue, Dictionary<string, string[]> graph)
{
StartValue = startValue;
GoalValue = goalValue;
Graph = graph;
}
Создал метод поиска кратчайшего пути:
public void Search()
{
Queue.Enqueue(StartValue);
while (Queue.Count != 0)
{
string node = Queue.Dequeue();
string[] vertites = Graph[node];
if (node == GoalValue)
{
return;
}
foreach (string vertite in vertites)
{
if (!UsedValues.Contains(vertite))
{
if (vertite == GoalValue)
{
if (Queue.Count > 0)
{
Queue.Dequeue();
}
Queue.Enqueue(node);
Queue.Enqueue(GoalValue);
return;
}
Queue.Enqueue(vertite);
}
}
UsedValues.Add(node);
}
}
И, наконец, метод вывода кратчайшего пути:
public void ShortestWay()
{
Search();
Console.Write($"{StartValue} ");
foreach (string value in Queue)
{
Console.Write($"--> {value} ");
}
}
За основу возьмем вот такой вот граф:
Кратчайший путь от A до B равен А -> P -> B
Запускаем программу, и получаем такой же вывод
На этом в принципе можно и заканчивать. Кто дочитал до конца, тому я сильно благодарен. Поддерживаю критику в сторону моего кода, приму все во внимание
Кто хочет скачать код, держите ссылку на GitHub.
Так кратчайшего пути или поиска в ширину? Это два разных алгоритма.
… так какая у вас алгоритмическая сложность получилась?
Если у дуг/рёбер нет весов либо веса одинаковые — то поиск в ширину находит кратчайшие пути.
Да я знаю, в общем-то. Но подменять одно другим без объяснений нехорошо.
Современный C# поддерживает краткую форму записи свойств на тот случай, когда в геттерах и сеттерах нет никакой логики (как у вас), пользуйтесь этим, чтобы уменьшить количество boilerplate кода.
Не надо делать свойства публичными, если это не является необходимым. Публичные свойства и методы образуют интерфейс класса (не в языковом смысле, а в смысле модели использования клиентским кодом), зачем наружу будет торчать то, что не должно модифицироваться снаружи? В вашем случае вообще все свойства стоит сделать приватными.
Если английский не очень хороший, имеет смысл включить спеллчекер в настройках среды разработки. Что означает слово "vertite" в принципе понятно из контекста, но вообще вершина это "vertex" / "vertices". Это может казаться мелочью, но в большом объёме кода подобные мелочи снижают читаемость кода.
Нужно ли разносить инициализацию и запуск поиска? Подумайте, как ваш класс может быть использован в клиентском коде.
Точно не нужно смешивать возврат результата работы алгоритма и вывод. Я понимаю, что это исключительно учебный код, но это очень плохая привычка. Код для BFS должен принимать на вход граф и необходимые параметры поиска, и возвращать результат. Вывод результата на экран - не его задача.
Добавлю ещё, что оба метода (search и shortestWay) публичные, но ничего не возвращают и при этом меняют внутреннее состояние. Можно их дёргать бесконечно и надеяться, что результат будет адекватным или хотя бы одинаковым
Прямо скажем, там вообще класс низачем не нужен. Один статический метод для поиска, который возвращает маршрут. Все эти свойства все равно нигде не используются.
Ваш способ хранения графа никуда не годится. Для начала, строки. Вы вообще понимаете, что строка — "медленный" тип данных? То же сравнение двух строк работает за ϴ(L), где L — длина строки. Причём эта самая длина строки равна Ω(log V), где V — количество вершин! Задумайтесь: вы заполучили множитель log V в вашу асимптотику только из-за того что выбрали хранение вершин как строк!
Ну, справедливости ради, в теории множитель log V будет при любом способе хранения, однако на практике именно в случае строк его невозможно игнорировать при малых V.
А ведь это далеко не единственная проблема строк, та же поддержка Юникода ещё и даёт приличных константный множитель при использовании компаратора по умолчанию...
Наконец, при использовании одних только строк ваш алгоритм является неуниверсальным. Если множество применений алгоритмов на графах — и некоторые из них требуют обозвать вершиной графа, к примеру, кортеж из нескольких элементов. Как вы будете их в строку кодировать — через запятую? Ну, это сработает, но это костыль. Медленный костыль, в силу медленных операциях над строками.
Самое простое что можно сделать для представления графа — это закодировать все вершины 32х-битными числами:
Почему 32 бита, а не 64? Потому что стандартные массивы в C# не могут содержать больше 231-1 элементов, и если вам когда-нибудь вдруг понадобится обрабатывать графы содержащие больше вершин — вам понадобится другой язык программирования.
Что же делать если нужно обрабатывать вершины, которые заданы не числами (а теми же строками)? Для начала, можно перекодировать их в числа:
Наконец, третий способ — аксиоматический. Просто объявляем свои ожидания от графа в виде интерфейса, и пусть тот, кто использует наш алгоритм, его реализует:
Достоинство такого способа — в возможности работы с графом без его материализации в памяти. Иногда это важно, к примеру при работе с такими штуками как прямое произведение графов (которое тоже является графом) это поможет хранить всего 2V вершин и 2E дуг вместо V2 и E2 соответственно.
Способы 2 и 3 можно комбинировать, требуя определённого интерфейса и предоставляя реализацию по умолчанию:
Теперь по поводу хранения остальных данных. Так вот: ваша ошибка в том, что вы в принципе начали их хранить!
Зачем вам в принципе класс BFS, хранящий внутреннее состояние алгоритма, когда локальные переменные работают куда лучше?
Ну куда же красивее получилось! И главное, нет такого количества нарушающих инкапсуляцию публичных свойств.
Наконец, самая важная придирка. У вас алгоритм неверный ответ выдаёт! Вы рассчитываете увидеть кратчайший путь в очереди, но в алгоритме поиска в ширину там никогда этого самого кратчайшего пути хранить не предполагалось!
Кажется, вы уже заметили что-то не так, из-за чего и добавили вот этот загадочный фрагмент кода:
Однако, это всего лишь костыль, работающий лишь на некоторых входных данных.
На самом деле, если вы хотите получить кратчайший путь — вам надо для каждой посещённой вершины хранить не просто признак посещения, а откуда вы в неё пришли:
Кстати, если выполнять обход графа не от start к goal, а от goal к start — при нахождении кратчайшего пути не понадобится делать Reverse.
спасибо что обьяснил в чем я не прав
Когда комментарии лучше статьи.
спасибо всем за подсказки!
Я понимаю что поиск в ширину это не уровень Хабра и реализован он криво, но самого-то автора зачем заминусовали?
Всё-таки это не маркетинговая шелуха, непроверенные слухи или перепост новости ещё и с кривым переводом, тут человек двигается в правильном направлении.
Поставил автору плюсик, хотя статье - минус.
Складывается впечатление, что статья получена методом генерации документации на основе бесполезных комментариев в коде. Основная тема статьи - алгоритм обхода графа. Вместо демонстрации бойлерплейта и описания порядка его добавления в код лучше было бы написать детальное объяснение самого алгоритма.
Алгоритмы на графах – Sedgewick, Robert. Algorithms, Part 5: Graph Algorithms.
Хорошая книга.