Автор оригинала: FreeCodeCapm Team.
Сачин Малхотра
Вы знаете сумму глобального воздушного движения в 2017 году? Вы знаете, что рост был для воздушного движения за последние несколько лет? Ну, давайте посмотрим на некоторую статистику.
Согласно Международная организация гражданской авиации (ИКАО), рекорд 4,1 млрд. Пассажиров проводились авиационной отраслью по запланированным услугам в 2017 году. И количество рейсов выросло до 37 миллионов во всем мире в 2017 году.
Это много пассажиров, и многие полеты занимают воздушное пространство ежедневно по всему миру. Поскольку все по всему миру сотни и тысячи рейсов.
У каждого полета есть источник и пункт назначения самостоятельно и стандартная цена экономики, связанная с ней. Давайте оставим причудливые билеты бизнес-класса и дополнительную ногу и что нет!
В таком сценарии это слишком запутано, чтобы выбрать, какой полет был бы лучшим, если мы хотим идти от одного места в другое.
Давайте посмотрим количество вариантов полета Студентнование (Обеспечивает скидки для студентов?) Отдает мне Лос-Анджелес в Нью-Дели.
119 общих рейсов предлагается. Затем на сайте появляется всплывающее окно, говоря, что есть и другие веб-сайты, которые могут быть предложены подобные рейсы по более дешевым ценам. ?
Так много веб-сайтов и неисчисных рейсов только для одного источника и назначения.
Как разработчик, если я хочу решить эту проблему, я бы построил систему для эффективного решения следующих запросов:
- Общее количество доступных направлений доступно (с максимальным количеством остановок) из моего текущего местоположения, а также перечисляет эти места назначения. Нужно держать свои варианты открытыми, когда они хотят путешествовать?
- Это известный факт (IMO), что маршрут с несколькими остановками имеет более дешевую альтернативу прямым рейсами. Таким образом, учитывая источник и пункт назначения, мы можем захотеть найти маршруты как минимум на 2 или 3 остановки.
- Самое главное: что самый дешевый маршрут от данного источника до данного пункта назначения?
- А также…. Мы придем к этому в конце?
Как вы можете догадаться, возможно, было бы потенциально тысячи рейсов в качестве выпуска первых двух запросов. Но мы можем, безусловно, мы можем уменьшить это, предоставляя некоторые другие критерии для уменьшения размера вывода. Для объема этой статьи позвольте нам сосредоточиться на этих оригинальных запросах.
Моделирование летной сети как график
От заголовка этой статьи довольно ясно, что графики где-то будут вовлечены, не так ли?
Моделирование этой проблемы, как проблема обхода графика значительно упрощает его и делает проблему гораздо более увлеченной. Итак, как первый шаг, давайте определим наш график.
Мы моделируем воздушный трафик как:
- направленный
- возможно циклическое
- взвешенный
- лес. G (V, E)
Направленный Поскольку каждый полет будет иметь назначенный источник и место назначения. Они несут много смысла.
Циклический Потому что очень можно следовать кучу рейсов, начиная с данного местоположения и заканчиваясь в том же месте.
Взвешенные Поскольку у каждого рейса есть стоимость, связанная с ними, которая будет билет на билет на этой статье эконом-класса.
И, наконец, а лес Потому что у нас могут быть несколько подключенных компонентов. Нет необходимости, чтобы все города в мире были какая-то сеть полета между ними. Итак, график может быть отключен и, следовательно, лес.
Вершины, V Были бы местами по всему миру везде, где бы там работали рабочие аэропорты.
Края, Е Было бы представитель всех рейсов, составляющих воздушный трафик. Край от U ->
; v Просто означает, что у вас есть направленный рейс из места/нет D
E U T
o v.
Теперь, когда у нас есть идея о том, как моделировать полетую сеть как график, давайте перейдем и решите первый общий запрос, который может иметь пользователь.
Общее количество пунктов назначения доступно
Кто не любит путешествовать?
Как кто-то, кто любит изучать разные места, вы бы хотели узнать, что все направления доступны из вашего местного аэропорта. Опять же, здесь будут дополнительные критерии, чтобы уменьшить результаты этого запроса. Но держать вещи простыми, мы просто попытаемся найти все места, доступные из нашего местного аэропорта.
Теперь, когда у нас есть четко определенный график, мы можем применить алгоритмы обхода для его обработки.
Начал с данной точки, мы можем использовать либо Ширина первого поиска (BFS) или Глубина первый поиск (DFS) Чтобы исследовать график или местоположения, доступные из начального местоположения в пределах максимального количества остановок. Поскольку эта статья о том, что эта статья о том, что в ширине первого алгоритма поиска, давайте посмотрим, как мы можем использовать знаменитые BFS для достижения этой задачи.
Мы инициализируем очередь BFS с заданным местоположением в качестве отправной точки. Затем мы выполняем ширину первого обхода, и продолжайте идти, пока очередь не будет пустой или до тех пор, пока максимальное количество остановок не было исчерпано.
Примечание: Если вы не знакомы с широким первым поиском или первым поиском глубины, я бы порекомендовал пройти Эта статья перед продолжением.
Давайте посмотрим на код, чтобы инициализировать нашу структуру данных графика. Нам также необходимо посмотреть, как ALGORITH BFS будет в конечном итоге дать нам все направления, доступные из данного источника.
Теперь, когда у нас хорошая идея о том, как график должен быть инициализирован, давайте посмотрим на код для алгоритма BFS.
Выполнение BFS
На город Лос-Анджелес даст нам следующие пункты назначения, которые доступны:
{'Chicago', 'France', 'Ireland', 'Italy', 'Japan', 'New Delhi', 'Norway'}
Это было просто, не так ли?
Мы рассмотрим, как мы можем ограничить BFS на максимальное количество остановок позже в статье.
В случае, если у нас есть влагальная сеть полета, которую мы бы имели в производственном сценарии, то мы не хотим исследовать все достижимыми направлениями из данной отправной точки.
Это использование случая, если полетный сеть очень маленькая или относится только к нескольким регионам в Соединенных Штатах.
Но для большой сети более реалистичный случай использования будет находить все маршруты полета с несколькими остановками. Давайте посмотрим на эту проблему еще более подробно и посмотрим, как мы можем решить ее.
Маршруты с несколькими остановками
Это хорошо известный факт, что чаще всего, чем нет, для данного источника и назначения, многократная поездка дешевле, чем прямой, без остановки полета.
Много раз мы предпочитаем прямой рейс, чтобы избежать укладки. Также потому, что многократные рейсы, как правило, занимают много времени – которые у нас нет.
Однако, если у вас нет срочных сроков, и вы хотите сэкономить несколько баксов (и комфортно с Multi-Stop Manager – это много авиакомпаний), тогда вы можете на самом деле принести пользу от чего-то вроде этого.
Кроме того, вы можете пройти через некоторые из самых красивых мест в мире с некоторыми из самых передовых аэропортов, которые вы можете наслаждаться. Итак, это достаточно мотивации, как это.
С точки зрения графической модели, о которой мы говорили, учитывая источник и пункт назначения, нам нужно придумать маршруты с 2 или более остановками для данного источника и назначения.
Как конечный пользователь, мы могли бы не захотеть видеть рейсы в этом порядке для этого запроса:
[A, C, D, B], 2 stops, $X[A, E, D, C, F, W, G, T, B], 8 stops, $M[A, R, E, G, B], 3 stops, $K[A, Z, X, C, V, B, N, S, D, F, G, H, B, 11 stops, $P
Я знаю. Никто в своих нужных умах не хотел бы поехать на маршрут полета с 11 остановками. Но точка я пытаюсь сделать, это то, что конечный пользователь хотел бы симметрию. Это означает, что они хотели бы увидеть все рейсы с 2 остановками первыми, затем все рейсы с 3 остановками и так далее до тех пор, пока не будет, скажем, 5 остановок.
Но все маршруты полета с одинаковым количеством остановок между между ними должны отображаться вместе. Это требование, которое нам нужно удовлетворить.
Давайте посмотрим на то, как мы можем решить это. Итак, учитывая график летных сетей, источник S
и пункт назначения D
Мы должны выполнить прохождение порядка уровня и сообщить маршруты по маршрутам S ->
; D с по меньшей мере 2 и не более 5 останавливаются между ними. Это означает, что мы должны выполнить прохождение порядка уровня до глубины 7 от начала нет D
e s.
Посмотрите на код для решения этой проблемы:
Это может быть не лучший способ пойти на решение этой проблемы в масштабе – самое большое ограничение памяти было бы связано с узлами, которые в настоящее время присутствуют в очереди.
Поскольку каждый узел или местоположение могут иметь тысячи рейсов к другим направлениям в мире, очередь может быть жужоковым, если мы храним реальные данные полета, как это. Это просто для демонстрации одного из примесей случаев алгоритма широта первого поиска алгоритма.
Теперь давайте просто сосредоточимся на прохождении и посмотрим на то, как это сделано. Обход алгоритма простой, как есть. Однако вся космическая сложность обхода порядка уровня поступает из элементов в очереди и размером каждого элемента.
Есть несколько способов реализации алгоритма. Кроме того, каждый из них варьируется в зависимости от максимальной памяти, потребляемой в любой момент времени элементами в очереди.
Мы хотим увидеть максимальную память, потребляемую очередью в любой момент во время прохождения порядка уровня. До этого давайте построим случайную сеть полетов со случайными ценами.
Теперь давайте посмотрим на реализацию обхода на уровне заказа.
Это вышеизложенное является самым простым и наиболее простым реализацией алгоритма обхода порядка уровня.
С каждым узлом мы добавляем в очередь, мы также храним информацию о уровне, и мы нажимаем кортеж (Узел, уровень)
в очередь. Поэтому каждый раз, когда мы попметим элемент из очереди, у нас есть информация о уровне, прикрепленная к самому узлу.
Информация о уровне для нашего использования в нашем использовании будет означать количество остановок от источника в это место в этом месте на маршруте полета.
Оказывается, мы можем сделать лучше, насколько связано с памятью программы. Давайте посмотрим на несколько лучший подход к прохождению ордеров уровня.
Идея здесь состоит в том, что мы не храним какую-либо дополнительную информацию с узлами, которые вытесняются в очередь. Мы используем Нет
объект, чтобы отметить конец данного уровня. Мы не знаем размера любого уровня до руки, кроме первого уровня, который просто имеет наш Источник
узел.
Итак, мы начинаем очередь с [Источник, нет]
И мы продолжаем всплывающие элементы. Каждый раз, когда мы сталкиваемся с Нет
Элемент, мы знаем, что уровень закончился, и начался новый. Мы нажимаем другой Нет
отметить конец этого нового уровня.
Рассмотрим очень простой график здесь, а затем высушите пробежку через график.
**************************************************** LEVEL 0 beginslevel = 0, queue = [A, None]level = 0, pop, A, push, B, C, queue = [None, B, C]pop None ******************************************* LEVEL 1 beginspush Nonelevel = 1, queue = [B, C, None]level = 1, pop, B, push, C, D, F, queue = [C, None, C, D, F]level = 1, pop, C, push, D, D (lol!), queue = [None, C, D, F, D, D]pop None ******************************************* LEVEL 2 beginspush Nonelevel = 2, queue = [C, D, F, D, D, None] .... and so on
Я надеюсь, что это суммирует алгоритм довольно хорошо. Это, безусловно, является аккуратным трюком, чтобы сделать обход порядка уровня, отслеживать уровни, и не сталкивайтесь с слишком большим количеством проблемы с памятью. Это, безусловно, уменьшает след памяти кода.
Не садоваться сейчас, думая, что это отличное улучшение.
Это, но вы должны задавать два вопроса:
- Насколько это большое улучшение?
- Можем ли мы сделать лучше?
Я отвечу оба этих вопроса сейчас, начиная с второго вопроса. Ответ на это да!
Мы можем сделать лучше здесь и полностью покончить с необходимостью Нет
в очереди. Мотивация для этого подхода происходит от самого предыдущего подхода.
Если вы внимательно посмотрите на сухое бегание выше, вы можете увидеть, что каждый раз, когда мы поплым Нет
Один уровень закончен, а другой готов к обработке. Важно то, что целый следующий уровень существует в очереди во время поп действия Нет
Отказ Мы можем использовать эту идею рассмотрения размера очереди в логику прохождения.
Вот псевдо-код для этого улучшенного алгоритма:
queue = Queue()queue.push(S)level = 0while queue is not empty { size = queue.size() // size represents the number of elements in the current level for i in 1..size { element = queue.pop() // Process element here // Perform a series of queue.push() operations here
level += 1
И вот код для того же.
Псевдо-код самостоятельно поясняется. Мы, по сути, покончим с необходимостью дополнительной Нет
элемент на уровень и вместо этого используете размер очереди, чтобы изменить уровни. Это также приведет к улучшению последнего метода, но сколько?
Посмотрите на следующую ноутбук Jupyter, чтобы увидеть разницу в памяти между тремя методами.
- Мы отслеживаем максимальный размер очереди в любое время, учитывая сумму размеров отдельных элементов очереди.
- Согласно документации Python,
sys.getsizeof
Возвращает размер указателя объекта или ссылки в байтах. Итак, мы спасли почти 4,4 кб пространства(20224 - 15800 байтов)
Переключаясь наНет
Метод из метода обхода оригинального уровня уровня. Это просто экономия памяти для этого случайного примера, и мы пошли только до 5-го уровня в обходу. - Конечный метод дает только улучшение 16 байтов над
Нет
метод. Это потому, что мы избавились всего за 4Нет
Объекты, которые использовались для отмены 4 уровней (кроме первого), которые мы обработали. Размер каждого указателя (указатель на объект) – 4 байта в Python в 32-битной системе.
Теперь, когда у нас есть все эти интересные многосторонние маршруты из нашего источника к нашему пункту назначения и высокоэффективные алгоритмы обхода на уровне, чтобы решить ее, мы можем посмотреть на более прибыльную проблему для решения наших собственных BFS.
Какой самый дешевый маршрут из моего источника до данного пункта назначения? Это то, что все будут мгновенно заинтересованы. Я имею в виду, кто не хочет спасать несколько баксов?
Кратчайший путь от данного источника до назначения
Там не так много описания, чтобы дать заявление о проблеме. Нам просто нужно найти кратчайший путь и сделать конечный пользователь счастливым.
Алгоритмично, учитывая взвешенный направленный график, нам нужно найти кратчайший путь от источника до назначения. Самый короткий или самый дешевый будет один и то же самое с точки зрения вида алгоритма.
Мы не будем описывать возможное решение BFS к этой проблеме, потому что такое решение будет неактивно. Давайте посмотрим на график ниже, чтобы понять, почему это так.
Мы говорим, что BFS является алгоритмом для использования, если мы хотим найти кратчайший путь в Непраректированный, невосстановленный график Отказ Претензия на BFS заключается в том, что в первый раз узел обнаружен во время обхода, это расстояние от источника даст нам кратчайший путь.
То же самое нельзя сказать для взвешенного графика. Рассмотрим график выше. Если сказать, что мы должны были найти кратчайший путь от узла А
к B
В неопряченной версии графика, то кратчайший путь будет прямой связь между A и B. Итак, кратчайший путь будет длиной 1
А БФС правильно найдет это для нас.
Однако мы имеем дело с взвешенным графом здесь. Итак, первое открытие узла во время обхода не гарантирует кратчайший путь для этого узла. Например, на диаграмме выше, узел B
будет обнаружен изначально изначально, потому что это сосед А
и стоимость, связанная с этим путем (край в этом случае) будет 25
Отказ Но это не самый короткий путь. Самый короткий путь – A -> M -> E
-> B o F
Длина 10.
Ширина первого поиска не имеет способа узнать, если определенное открытие узла даст нам кратчайший путь к этому узлу. Итак, единственный возможный способ для BFS (или DFS) находить кратчайший путь в взвешенном графике – это поиск всего графа и продолжать запись минимального расстояния от источника до вершины назначения.
Это решение невозможно осуществимо для огромной сети, например, нашей летной сети, которая потенциально бы тысячи узлов.
Мы не пойдем в детали того, как мы можем решить это. Это не имеет возможности для этой статьи.
Что, если я сказал вам, что BFS – это просто алгоритм правильного, чтобы найти кратчайший путь в взвешенном графике с небольшим ограничением ?
Ограничены кратчайшие пути
Поскольку график, который у нас будет для полета сети, соревноваю, мы знаем, что исследуя его полностью не имеет возможности.
Рассмотрим проблему кратчайших путей от перспективы клиента. Когда вы хотите забронировать рейс, это следующие варианты, которые вы идеально рассмотрите:
- Это не должно быть слишком длинным полетом.
- Это должно быть под вашим бюджетом (очень важно).
- Это может иметь несколько остановок, но не более
К
гдеК
может варьироваться от человека к человеку. - Наконец у нас есть личные предпочтения, которые включают в себя такие вещи, как доступом в лаундж, качество продуктов питания, локальные места и средняя нога.
Важным моментом рассмотрения здесь является третьим выше: он может иметь несколько остановок, но не более К
где К
может варьироваться от человека к человеку.
Клиент хочет, чтобы самый дешевый маршрут рейсов, но они также не хотят сказать, что 20 останавливается между их источником и назначением. Клиент может быть в порядке с максимум 3 остановки или в крайних случаях, может быть, даже 4 – но не более того.
Мы хотели бы приложение, которое вы найдете самый дешевый рейс из На большинстве K останавливается для данного источника и назначения.
Этот вопрос от LeetCode был основной мотивацией для меня, чтобы написать эту статью. Я настоятельно рекомендую пройти через вопрос один раз и не только полагаться на снимок выше.
«Почему BFS работает здесь?» можно спросить. «Это также взвешенный график и той же причиной провала BFS, которые мы обсуждали в предыдущем разделе, должны применяться здесь». НЕТ!
Количество уровней, которые поиск будет ограничен значением К
в вопросе или в описании, предусмотренном в начале раздела. Таким образом, по сути, мы бы пытались найти кратчайший путь, но нам не придется исследовать весь график как таковой. Мы просто доберемся до уровня К
Отказ
Из реального сценария, стоимость К
будет до 5 для любого вменяемого путешественника?
Давайте посмотрим на псевдокод для алгоритма:
def bfs(source, destination, K): min_cost = dictionary representing min cost under K stops for each node reachable from source.
set min_cost of source to be 0
Q = queue() Q.push(source) stops = 0 while Q is not empty {
size = Q.size for i in range 1..size { element = Q.pop()
if element == destination then continue
for neighbor in adjacency list of element { if stops == K and neighbor != destination then continue
if min_cost of neighbor improves, update and add back to the queue. } } stops ++ }
Это снова – это обход на уровне порядка, и используемый здесь подход, это тот, который использует размер очереди на каждом уровне. Давайте посмотрим на комментарию версию кода, чтобы решить эту проблему.
По сути, мы отслеживаем минимальное расстояние каждого узла из данного источника. Минимальное расстояние для источника будет 0 и + INF для всех остальных изначально.
Всякий раз, когда мы сталкиваемся с узлом, мы проверяем, может ли текущая длина минимальной длины пути или нет. Если это может быть улучшено, это означает, что мы нашли альтернативный путь от источника к этой вершине с более дешевыми стоить – более дешевый маршрут полетов до этой вершины. Мы снова введем в очередь эту вершину, так что местоположения и узлы, доступные от этой вершины, обновляются (могут быть или не могут быть).
Ключевая вещь это:
# No need to update the minimum cost if we have already exhausted our K stops. if stops == K and neighbor != dst: continue
Поэтому мы только что выскочили узел, представленный элемент
в коде и Сосед
может быть либо пункт назначения, либо случайным другим узлом. Если мы уже исчерпали наши К
останавливается с элемент
Быть Kth
Стоп, то мы не должны обрабатывать и обновлять (возможно) минимальное расстояние для Сосед
Отказ Это нарушит наши максимум К
останавливает состояние в этом случае.
Как оказывается, я решал эту проблему изначально с помощью динамического программирования и потребовалось около 165 мс, чтобы запустить на платформе LeetCode. Я бежал с использованием BFS, и он быстро сломал на 45 мс, чтобы выполнить. Достаточно мотивация, чтобы написать эту статью для вас, ребята.
Я надеюсь, что вы смогли получить преимущество от этой статьи по широту первого поиска и некоторых его приложений. Основное внимание было продемонстрировать свое применение на короткие пути в весовой графике под некоторыми ограничениями?,