Ошибка в Solidity и новый бенчмарк
Недавно произошло сразу два события, которые хорошо показывают, как меняется ландшафт безопасности смарт-контрактов: с одной стороны, нас подстерегают старые добрые ошибки в компиляторе, а с другой — на горизонте уже маячит искусственный интеллект, который учится взламывать и чинить код быстрее человека.
Команда Solidity раскрыла детали критической ошибки, которая пряталась в версиях компилятора с 0.8.28 по 0.8.33. Проблема проявлялась только при использовании IR-конвейера и была связана с коллизией имён вспомогательных функций. Если в коде одновременно использовалась очистка переменной временного хранилища и постоянного хранилища одного типа, компилятор путал инструкции. Вместо очистки временной ячейки он мог обнулить слот в постоянной памяти, что потенциально способно «сбросить» владельца контракта или нарушить логику блокировок. Ошибка уже исправлена в версии 0.8.34. Хотя в мейннете нашлось всего три уязвимых контракта, инцидент напоминает о том, как важно следить за обновлениями инструментов, особенно при использовании новых фич вроде transient storage.
Параллельно с этим исследователи безопасности представили бенчмарк EVMbench, созданный для тестирования ИИ-агентов. Учитывая, что под управлением смарт-контрактов находятся активы на сотни миллиардов долларов, важно понимать, насколько искусственный интеллект способен находить уязвимости или проводить атаки. В основе бенчмарка — 120 реальных багов из аудитов и соревнований. Тестирование показало, что современные модели уже более чем вдвое лучше справляются с задачей взлома контрактов по сравнению с версиями полугодовой давности. Однако задачи комплексного обнаружения всех проблем и аккуратного исправления кода без поломки функциональности пока даются ИИ сложнее. Разработчики выложили бенчмарк в открытый доступ и выделяют гранты на исследования в этой области, подчёркивая, что гонка вооружений между атакующими и защищающимися ИИ-агентами только начинается.
#news
Недавно произошло сразу два события, которые хорошо показывают, как меняется ландшафт безопасности смарт-контрактов: с одной стороны, нас подстерегают старые добрые ошибки в компиляторе, а с другой — на горизонте уже маячит искусственный интеллект, который учится взламывать и чинить код быстрее человека.
Команда Solidity раскрыла детали критической ошибки, которая пряталась в версиях компилятора с 0.8.28 по 0.8.33. Проблема проявлялась только при использовании IR-конвейера и была связана с коллизией имён вспомогательных функций. Если в коде одновременно использовалась очистка переменной временного хранилища и постоянного хранилища одного типа, компилятор путал инструкции. Вместо очистки временной ячейки он мог обнулить слот в постоянной памяти, что потенциально способно «сбросить» владельца контракта или нарушить логику блокировок. Ошибка уже исправлена в версии 0.8.34. Хотя в мейннете нашлось всего три уязвимых контракта, инцидент напоминает о том, как важно следить за обновлениями инструментов, особенно при использовании новых фич вроде transient storage.
Параллельно с этим исследователи безопасности представили бенчмарк EVMbench, созданный для тестирования ИИ-агентов. Учитывая, что под управлением смарт-контрактов находятся активы на сотни миллиардов долларов, важно понимать, насколько искусственный интеллект способен находить уязвимости или проводить атаки. В основе бенчмарка — 120 реальных багов из аудитов и соревнований. Тестирование показало, что современные модели уже более чем вдвое лучше справляются с задачей взлома контрактов по сравнению с версиями полугодовой давности. Однако задачи комплексного обнаружения всех проблем и аккуратного исправления кода без поломки функциональности пока даются ИИ сложнее. Разработчики выложили бенчмарк в открытый доступ и выделяют гранты на исследования в этой области, подчёркивая, что гонка вооружений между атакующими и защищающимися ИИ-агентами только начинается.
#news
👍6❤1
Что почитать?
Проснулся сегодня с явным нежеланием работать. На улице хоть и -2, но солнце светит уже по весеннему, и хочется просто отдыхать, а не фиксить баги и учить MatPlotLib...
Поэтому сегодня, в качестве оффтопа, решил поделиться с вами рекомендациями книг, которые читал или буду читать в ближайшее время.
1. System Design. Подготовка к сложному интервью. Алекс Сюй.
Первая книга, которую должны прочитать все вайбкодеры, да и просто начинающие разработчики. Здесь в понятной манере и простым языком объясняются базовые принципы архитектур компьютерных систем: какую базу данных выбрать, как работает кеш, что такое CDN, распределение трафика, нагрузка и организация серверной части. Очень достойная книга, чтобы начать разбираться в этих понятиях!
2. System Design. Машинное обучение. Али Аминиан, Алекс Сюй.
Еще одна книга от того же автора, но уже в соавторстве с Али Аминианом. Купил ее только потому что понравилась первая часть. Здесь также в простом формате раскрываются вопросы про машинное обучение, которые могут задавать на собеседовании: как выбирать модель, как обучать ее, рекомендация событий и данных для пользователей, организация поиска и т.д. В общем, я ожидаю, что она дополнит мои знания про ML. Еще не читал.
3. Прикладное машинное обучении и искусственный интеллект для инженеров. Джеф Просиз.
Вот это отличная книга для тех, кто уже познакомился с нейронками и хочет копнуть глубже с тем, как работают сети в своей основе. Тут раскрываются темы регрессионного анализа, классификационных моделей, опорные векторы, обучение с помощью keras/tensorflow, обнаружение лиц и распознавание изображений и т.д. Много примеров с кодом. Единственное что, может быть сложной в языке: если вы никогда ранее не сталкивались с определениями в машинном обучении, то придется много гуглить.
4. Байесовская статистика. Star Wars, резиновые уточки и многое другое.
Книга за которой долго гонялся. Не мог найти в РФ и ждал ее несколько недель из Китая. Еще не успел прочитать ее, только просмотрел несколько глав. Прекрасное, легкое и очень простое объяснение предсказательной статистики и вероятностных моделей. Как вы можете знать, что современные нейронные сети "под капотом" используют предсказательные алгоритмы для генерации токенов. Эта книга может помочь вам понять на простых примерах, как это все работает в сетях.
5. Алгоритмы искусственного интеллекта. Ришал Харбанс.
С использованием этой книги я позже буду делать посты про алгоритмы в нейронных сетях, когда мы закончим цикл про основные алгоритмы в программировании на канале. Еще не открывал ее даже, хочу погрузиться в нее чуть позже.
6. Высоконагруженные приложения. Мартин Клепман.
Еще одна книга, до которой не дошел, но очень жду. Очень много рекомендаций ее изучить видел в Твиттере. Тут рассказывается о подходе к работе над информационными системами, моделях данных и языках запросов, системах хранения данных, репликации, секционировании, проблемах и т.д.
Полагаю, что читать ее лучше после первой книги Алекса Сюя, чтобы складывалось общее понимание работы систем.
7. Поиск на основе искусственного интеллекта. Трей Грейнджер, Дуг Тернбул, Макс Ирвин.
Вот это отличная книга о том, как работает поиск. Читаю ее сейчас. Если вы когда-нибудь задумывались, почему поиск на сайте работает так "криво", или почему он выдает массу результатов подобных моим прошлым запросов - эта книга для вас. Детально разбираются системы алгоритмов поиска и рекомендаций, а также использование нейронных сетей для выдачи релевантных результатов. Книга огонь!
8. Конструкции. Джейсм Гордон. Это книгу давно забросил в корзину и все никак не мог купить ее. Пришлось просить в подарок на Новый Год. Единственное, о чем пожалел: почему я не купил ее раньше!
Проснулся сегодня с явным нежеланием работать. На улице хоть и -2, но солнце светит уже по весеннему, и хочется просто отдыхать, а не фиксить баги и учить MatPlotLib...
Поэтому сегодня, в качестве оффтопа, решил поделиться с вами рекомендациями книг, которые читал или буду читать в ближайшее время.
1. System Design. Подготовка к сложному интервью. Алекс Сюй.
Первая книга, которую должны прочитать все вайбкодеры, да и просто начинающие разработчики. Здесь в понятной манере и простым языком объясняются базовые принципы архитектур компьютерных систем: какую базу данных выбрать, как работает кеш, что такое CDN, распределение трафика, нагрузка и организация серверной части. Очень достойная книга, чтобы начать разбираться в этих понятиях!
2. System Design. Машинное обучение. Али Аминиан, Алекс Сюй.
Еще одна книга от того же автора, но уже в соавторстве с Али Аминианом. Купил ее только потому что понравилась первая часть. Здесь также в простом формате раскрываются вопросы про машинное обучение, которые могут задавать на собеседовании: как выбирать модель, как обучать ее, рекомендация событий и данных для пользователей, организация поиска и т.д. В общем, я ожидаю, что она дополнит мои знания про ML. Еще не читал.
3. Прикладное машинное обучении и искусственный интеллект для инженеров. Джеф Просиз.
Вот это отличная книга для тех, кто уже познакомился с нейронками и хочет копнуть глубже с тем, как работают сети в своей основе. Тут раскрываются темы регрессионного анализа, классификационных моделей, опорные векторы, обучение с помощью keras/tensorflow, обнаружение лиц и распознавание изображений и т.д. Много примеров с кодом. Единственное что, может быть сложной в языке: если вы никогда ранее не сталкивались с определениями в машинном обучении, то придется много гуглить.
4. Байесовская статистика. Star Wars, резиновые уточки и многое другое.
Книга за которой долго гонялся. Не мог найти в РФ и ждал ее несколько недель из Китая. Еще не успел прочитать ее, только просмотрел несколько глав. Прекрасное, легкое и очень простое объяснение предсказательной статистики и вероятностных моделей. Как вы можете знать, что современные нейронные сети "под капотом" используют предсказательные алгоритмы для генерации токенов. Эта книга может помочь вам понять на простых примерах, как это все работает в сетях.
5. Алгоритмы искусственного интеллекта. Ришал Харбанс.
С использованием этой книги я позже буду делать посты про алгоритмы в нейронных сетях, когда мы закончим цикл про основные алгоритмы в программировании на канале. Еще не открывал ее даже, хочу погрузиться в нее чуть позже.
6. Высоконагруженные приложения. Мартин Клепман.
Еще одна книга, до которой не дошел, но очень жду. Очень много рекомендаций ее изучить видел в Твиттере. Тут рассказывается о подходе к работе над информационными системами, моделях данных и языках запросов, системах хранения данных, репликации, секционировании, проблемах и т.д.
Полагаю, что читать ее лучше после первой книги Алекса Сюя, чтобы складывалось общее понимание работы систем.
7. Поиск на основе искусственного интеллекта. Трей Грейнджер, Дуг Тернбул, Макс Ирвин.
Вот это отличная книга о том, как работает поиск. Читаю ее сейчас. Если вы когда-нибудь задумывались, почему поиск на сайте работает так "криво", или почему он выдает массу результатов подобных моим прошлым запросов - эта книга для вас. Детально разбираются системы алгоритмов поиска и рекомендаций, а также использование нейронных сетей для выдачи релевантных результатов. Книга огонь!
8. Конструкции. Джейсм Гордон. Это книгу давно забросил в корзину и все никак не мог купить ее. Пришлось просить в подарок на Новый Год. Единственное, о чем пожалел: почему я не купил ее раньше!
❤6👍3
Эта книга не про IT, это больше популярное чтиво о том, как работает физика в конструкциях. Язык и объяснения невероятно простые. Если бы так физику преподавали в школе, я бы точно получал одни пятерки! Потрясающие графики и фотографии, рисунки и силуэты, описания базовых принципов растяжения и деформации и много другое! Просто зачитываюсь в свободное время и наслаждаюсь, как можно просто писать о таких сложных вещах!
Есть еще несколько других интересных книг, но расскажу о них позже, если вам зайдет такой формат в постах.
Так же буду рад получить от вас другие рекомендации, что еще моно почитать!
#books
Есть еще несколько других интересных книг, но расскажу о них позже, если вам зайдет такой формат в постах.
Так же буду рад получить от вас другие рекомендации, что еще моно почитать!
#books
👍9❤1🔥1
Алгоритмы. Обход в глубину (DFS)
После небольшой паузы мы продолжаем разбирать различные алгоритмы.
Обход в глубину (DFS) — это один из фундаментальных алгоритмов на графах, и его легче всего понять через аналогию с исследованием пещеры. Представь, что ты стоишь в системе туннелей с множеством разветвлений. Твоя стратегия заключается в том, чтобы выбрать первый попавшийся проход и идти по нему до тех пор, пока не упрешься в тупик. Только тогда ты возвращаешься назад к последнему перекрестку, где еще есть неисследованные пути, и пробуешь следующий туннель. Эта логика, где глубина проникновения важнее ширины охвата, и лежит в основе алгоритма DFS.
Рассмотрим небольшой граф. Предположим, у нас есть узлы, соединенные между собой: A связан с B и C; B связан с A, D и E; C связан с A и F; D связан только с B; E связан с B и F, а F, в свою очередь, связан с C и E. Если начать обход с узла A, DFS будет двигаться следующим образом: сначала мы попадаем в A, затем углубляемся в B, оттуда — в D. Достигнув D, мы понимаем, что это тупик (сосед B уже посещен), поэтому возвращаемся обратно в B и идем по следующему пути в E. Из E переходим в F, который тоже оказывается тупиком (все соседи посещены или ведут назад). После возврата в E и затем в B, мы наконец возвращаемся в A и идем в C, но обнаруживаем, что C уже был посещен через F. Итоговый порядок посещения узлов будет таким: A, B, D, E, F, C.
В этом заключается ключевое отличие DFS от другого известного алгоритма — поиска в ширину (BFS). BFS работает подобно кругам на воде: сначала исследуются все соседние узлы, затем их соседи, распространяясь равномерно во все стороны. DFS же напоминает нить Ариадны в лабиринте: он всегда идет до конца по выбранному пути и лишь затем возвращается.
Для реализации DFS можно использовать два основных подхода, которые делают одно и то же, но разными средствами: рекурсию и явный стек. Рекурсивный метод полагается на стек вызовов самой программы. Когда мы пишем функцию, которая вызывает саму себя для обработки соседей, язык программирования автоматически запоминает точку возврата. Однако здесь есть важная особенность Python: не стоит задавать значение аргумента
Пример 1
Чтобы лучше понять, как работает рекурсия, можно проследить её выполнение по шагам. Входя в узел A, мы помечаем его как посещенный и записываем в результат. Затем смотрим на соседа B, который еще не посещен, и вызываем функцию для B. Внутри вызова для B повторяется та же логика: добавляем B в путь и идем к его первому непосещенному соседу D. В D тупик, и функция возвращает результат обратно. Далее в B очередь доходит до следующего соседа — E. Из E попадаем в F, а из F — в C. Когда все пути исчерпаны, результаты склеиваются и возвращаются наверх.
Пример 2
Второй способ реализации — итеративный с использованием явного стека. Стек работает по принципу «последним пришел — первым вышел» (LIFO). Это как стопка тарелок: новую тарелку кладут сверху, и первую берут тоже сверху. В коде мы сами управляем этим стеком, помещая туда узлы для последующей обработки. Важный нюанс: чтобы порядок обхода совпадал с рекурсивной версией, соседей часто добавляют в обратном порядке. Если просто положить соседей в стек по порядку, то из-за механизма LIFO первым будет извлечен последний сосед, и обход пойдет в другую сторону.
Пример 3
После небольшой паузы мы продолжаем разбирать различные алгоритмы.
Обход в глубину (DFS) — это один из фундаментальных алгоритмов на графах, и его легче всего понять через аналогию с исследованием пещеры. Представь, что ты стоишь в системе туннелей с множеством разветвлений. Твоя стратегия заключается в том, чтобы выбрать первый попавшийся проход и идти по нему до тех пор, пока не упрешься в тупик. Только тогда ты возвращаешься назад к последнему перекрестку, где еще есть неисследованные пути, и пробуешь следующий туннель. Эта логика, где глубина проникновения важнее ширины охвата, и лежит в основе алгоритма DFS.
Рассмотрим небольшой граф. Предположим, у нас есть узлы, соединенные между собой: A связан с B и C; B связан с A, D и E; C связан с A и F; D связан только с B; E связан с B и F, а F, в свою очередь, связан с C и E. Если начать обход с узла A, DFS будет двигаться следующим образом: сначала мы попадаем в A, затем углубляемся в B, оттуда — в D. Достигнув D, мы понимаем, что это тупик (сосед B уже посещен), поэтому возвращаемся обратно в B и идем по следующему пути в E. Из E переходим в F, который тоже оказывается тупиком (все соседи посещены или ведут назад). После возврата в E и затем в B, мы наконец возвращаемся в A и идем в C, но обнаруживаем, что C уже был посещен через F. Итоговый порядок посещения узлов будет таким: A, B, D, E, F, C.
Граф:
A
/ \
B C
/ \ \
D E F
\ /
(F связан с E и C)
DFS от A пойдёт так:
A → B → D (тупик, назад) → E → F (тупик, назад) → назад → C (уже посещён через F)
Порядок посещения: A, B, D, E, F, C
В этом заключается ключевое отличие DFS от другого известного алгоритма — поиска в ширину (BFS). BFS работает подобно кругам на воде: сначала исследуются все соседние узлы, затем их соседи, распространяясь равномерно во все стороны. DFS же напоминает нить Ариадны в лабиринте: он всегда идет до конца по выбранному пути и лишь затем возвращается.
Граф: BFS (в ширину): DFS (в глубину):
A Уровень за уровнем Ветка за веткой
/ \ A A
B C B, C B, D, E, F, C
/ \ \ D, E, F
D E F
Для реализации DFS можно использовать два основных подхода, которые делают одно и то же, но разными средствами: рекурсию и явный стек. Рекурсивный метод полагается на стек вызовов самой программы. Когда мы пишем функцию, которая вызывает саму себя для обработки соседей, язык программирования автоматически запоминает точку возврата. Однако здесь есть важная особенность Python: не стоит задавать значение аргумента
visited по умолчанию как пустое множество (set()), так как изменяемые объекты создаются один раз и будут переиспользованы во всех вызовах функции. Вместо этого нужно создавать его внутри функции, если оно не передано.Пример 1
Чтобы лучше понять, как работает рекурсия, можно проследить её выполнение по шагам. Входя в узел A, мы помечаем его как посещенный и записываем в результат. Затем смотрим на соседа B, который еще не посещен, и вызываем функцию для B. Внутри вызова для B повторяется та же логика: добавляем B в путь и идем к его первому непосещенному соседу D. В D тупик, и функция возвращает результат обратно. Далее в B очередь доходит до следующего соседа — E. Из E попадаем в F, а из F — в C. Когда все пути исчерпаны, результаты склеиваются и возвращаются наверх.
Пример 2
Второй способ реализации — итеративный с использованием явного стека. Стек работает по принципу «последним пришел — первым вышел» (LIFO). Это как стопка тарелок: новую тарелку кладут сверху, и первую берут тоже сверху. В коде мы сами управляем этим стеком, помещая туда узлы для последующей обработки. Важный нюанс: чтобы порядок обхода совпадал с рекурсивной версией, соседей часто добавляют в обратном порядке. Если просто положить соседей в стек по порядку, то из-за механизма LIFO первым будет извлечен последний сосед, и обход пойдет в другую сторону.
Пример 3
Процесс итеративного обхода можно наглядно представить в виде таблицы, где видно, как меняется содержимое стека на каждом шаге. Начинаем со стеком, содержащим A. Извлекаем A, помечаем его и добавляем в результат, а затем кладем в стек его соседей C и B в обратном порядке (чтобы B оказался сверху). Затем извлекаем B, затем D, и так далее, пока стек не опустеет.
Этот код, по сути, является готовой реализацией итеративного DFS, где стек берет на себя работу, которую в рекурсивной версии выполняет стек вызовов. Каждая часть здесь важна: множество
Одна из классических задач, решаемых с помощью DFS, — поиск циклов в графе. Цикл — это ситуация, когда, двигаясь по графу, мы возвращаемся в узел, который уже встречали на текущем пути. Важно отличать просто посещенный узел от узла, находящегося в текущем стеке рекурсии. Ведь если мы видим уже посещенный узел, который не является частью текущего пути, это может быть просто пересечение, а не цикл. Поэтому в алгоритме используются два множества:
Алгоритм запускает проверку из каждой вершины, чтобы учесть возможную несвязность графа. Если в процессе обхода мы наталкиваемся на соседа, который уже есть в
Пример 4
Как именно это работает, можно увидеть на примере графа с циклом. Начиная с A, мы идем в B, затем в D. В узле D видим соседа B, который уже есть в
Пример 5
Еще одно важное практическое применение DFS — поиск пути в лабиринте. Здесь выбор между DFS и BFS зависит от конкретной задачи. Если нам нужно просто найти любой путь к выходу, DFS часто оказывается предпочтительнее. Он идет по одному пути до упора и, если лабиринт не слишком разветвлен, может найти выход довольно быстро. При этом DFS использует меньше памяти, так как хранит лишь текущий путь.
Пример 6
Однако если стоит задача найти кратчайший путь, DFS не подходит. Он может углубиться в длинную ветку и найти путь, который окажется далеко не самым коротким, в то время как существует более прямой маршрут. В такой ситуации BFS гарантированно найдет кратчайший путь, хотя и потребует больше памяти для хранения всех узлов текущего уровня.
Шаг | Стек (вершина →) | Берём | visited | result
----|--------------------|-------|-----------------|------------------
0 | [A] | A | {A} | [A]
1 | [C, B] | B | {A,B} | [A,B]
2 | [C, E, D] | D | {A,B,D} | [A,B,D]
3 | [C, E] | E | {A,B,D,E} | [A,B,D,E]
4 | [C, F] | F | {A,B,D,E,F} | [A,B,D,E,F]
5 | [C, E*, C] | C | {A,B,D,E,F,C} | [A,B,D,E,F,C]
(* E уже в visited — пропустим при извлечении)
Финальный результат: [A, B, D, E, F, C] ✓
Этот код, по сути, является готовой реализацией итеративного DFS, где стек берет на себя работу, которую в рекурсивной версии выполняет стек вызовов. Каждая часть здесь важна: множество
visited отслеживает уже обработанные узлы, список stack хранит узлы, которые предстоит обработать, а цикл while работает, пока есть что извлекать.Одна из классических задач, решаемых с помощью DFS, — поиск циклов в графе. Цикл — это ситуация, когда, двигаясь по графу, мы возвращаемся в узел, который уже встречали на текущем пути. Важно отличать просто посещенный узел от узла, находящегося в текущем стеке рекурсии. Ведь если мы видим уже посещенный узел, который не является частью текущего пути, это может быть просто пересечение, а не цикл. Поэтому в алгоритме используются два множества:
visited для всех узлов, которые когда-либо посещались, и rec_stack для узлов, в которых мы находимся прямо сейчас в процессе рекурсивного спуска.Граф БЕЗ цикла: Граф С циклом:
A A
/ \ / \
B C B C
| |\ /
D | X
|/ \
D E
Алгоритм запускает проверку из каждой вершины, чтобы учесть возможную несвязность графа. Если в процессе обхода мы наталкиваемся на соседа, который уже есть в
rec_stack, это означает обнаружение цикла.Пример 4
Как именно это работает, можно увидеть на примере графа с циклом. Начиная с A, мы идем в B, затем в D. В узле D видим соседа B, который уже есть в
rec_stack. Это и есть сигнал о наличии цикла.Пример 5
Еще одно важное практическое применение DFS — поиск пути в лабиринте. Здесь выбор между DFS и BFS зависит от конкретной задачи. Если нам нужно просто найти любой путь к выходу, DFS часто оказывается предпочтительнее. Он идет по одному пути до упора и, если лабиринт не слишком разветвлен, может найти выход довольно быстро. При этом DFS использует меньше памяти, так как хранит лишь текущий путь.
Пример 6
Однако если стоит задача найти кратчайший путь, DFS не подходит. Он может углубиться в длинную ветку и найти путь, который окажется далеко не самым коротким, в то время как существует более прямой маршрут. В такой ситуации BFS гарантированно найдет кратчайший путь, хотя и потребует больше памяти для хранения всех узлов текущего уровня.
Лабиринт: DFS может пойти так: BFS пойдёт так:
S S→A→C→E→EXIT S→A (длина 1)
/ \ (длина 4) S→B (длина 1)
A B Хотя существует S→A→C, S→B→EXIT
| | S→B→EXIT Найдёт S→B→EXIT
C EXIT (длина 2)! (длина 2) первым
\ /
E
|
EXIT
DFS нашёл путь длиной 4, хотя есть путь длиной 2.
BFS всегда находит кратчайший путь.
Сравнивая эти два подхода для лабиринта, можно выделить несколько критериев. Для задачи нахождения любого выхода оба алгоритма работают, но DFS требует меньше памяти. Для поиска кратчайшего пути BFS гарантирует результат, а DFS — нет. В глубоких лабиринтах рекурсивный DFS рискует переполнить стек, тогда как итеративный и BFS безопасны. В широких лабиринтах BFS может потребить много памяти, а DFS остается эффективным.
Что касается вычислительной сложности, то и для DFS, и для BFS она одинакова. Временная сложность составляет O(V + E), где V — количество вершин (узлов), а E — количество ребер. Это объясняется тем, что алгоритм посещает каждую вершину ровно один раз и просматривает каждое ребро также один раз. Пространственная сложность в худшем случае составляет O(V) — память нужна для хранения множества посещенных узлов и, в случае итеративной реализации, стека.
Подводя итог, можно сказать, что DFS — это алгоритм, который идет вглубь до упора, а затем возвращается. Его можно реализовать элегантно через рекурсию или более контролируемо через явный стек. Он применяется для поиска любого пути, обнаружения циклов, топологической сортировки и обхода деревьев, но не гарантирует нахождения кратчайшего маршрута.
#algorithm
Что касается вычислительной сложности, то и для DFS, и для BFS она одинакова. Временная сложность составляет O(V + E), где V — количество вершин (узлов), а E — количество ребер. Это объясняется тем, что алгоритм посещает каждую вершину ровно один раз и просматривает каждое ребро также один раз. Пространственная сложность в худшем случае составляет O(V) — память нужна для хранения множества посещенных узлов и, в случае итеративной реализации, стека.
Подводя итог, можно сказать, что DFS — это алгоритм, который идет вглубь до упора, а затем возвращается. Его можно реализовать элегантно через рекурсию или более контролируемо через явный стек. Он применяется для поиска любого пути, обнаружения циклов, топологической сортировки и обхода деревьев, но не гарантирует нахождения кратчайшего маршрута.
#algorithm
❤6👍2
Фиксы SoliditySet
Спасибо всем, кто сообщал мне о проблемах на платформе и подсказывал лучшие решения для тех или иных случаев. Сегодня был выполнен очередной фикс:
1. Были исправлены некоторые не работающие ссылки;
2. Обновлено отображение на мобильных экранах: немного удобнее использовать на планшетах;
3. К нумерации уроков были добавлены заголовки уроков;
4. Подсказки и ответы для модуля 5 (практика поиска уязвимостей) были скрыты под спойлером. (Тут я еще думаю как сделать это красивее...).
5. Исправлен порядок вывода некоторых уроков.
6. Откорректированы мелкие недочеты.
Спасибо, что помогаете сделать платформу лучше! В марте я планирую добавить несколько новых уроков, и если все пойдет как надо: новый раздел с переводами зарубежных статей.
#solidityset
Спасибо всем, кто сообщал мне о проблемах на платформе и подсказывал лучшие решения для тех или иных случаев. Сегодня был выполнен очередной фикс:
1. Были исправлены некоторые не работающие ссылки;
2. Обновлено отображение на мобильных экранах: немного удобнее использовать на планшетах;
3. К нумерации уроков были добавлены заголовки уроков;
4. Подсказки и ответы для модуля 5 (практика поиска уязвимостей) были скрыты под спойлером. (Тут я еще думаю как сделать это красивее...).
5. Исправлен порядок вывода некоторых уроков.
6. Откорректированы мелкие недочеты.
Спасибо, что помогаете сделать платформу лучше! В марте я планирую добавить несколько новых уроков, и если все пойдет как надо: новый раздел с переводами зарубежных статей.
#solidityset
👍9
Алгоритмы. Алгоритм Дейкстры
Рассматривая этот алгоритм, я все никак не мог привыкнуть читать его как Дейкстры. Каждый раз в голове крутился образ персонажа из Ведьмака - шпиона Диикстры. При этом на английском языке они пишутся одинаково - Dijkstra. Но забудем об этом и вернемся к нашему алгоритму.
Представьте карту городов, соединённых дорогами. Каждый город можно представить как вершину графа, каждая дорога — как ребро, а расстояние между городами — как вес ребра. Например, рассмотрим такой граф:
Если мы хотим добраться из A в D по кратчайшему пути, то на глаз ответ не всегда очевиден. Именно для решения таких задач и существует алгоритм Дейкстры, который находит кратчайшие пути от одной вершины до всех остальных во взвешенном графе с неотрицательными весами рёбер.
Прежде чем углубляться, стоит уточнить ключевые понятия. Граф — это набор точек (вершин), соединённых линиями (рёбрами). Вес ребра — это число, стоящее на ребре и обозначающее стоимость перехода (расстояние, время, цена и т.п.). Взвешенный граф — это граф, где у каждого ребра есть вес. Кратчайший путь — это маршрут с минимальной суммой весов рёбер. Алгоритм Дейкстры относится к классу жадных алгоритмов: на каждом шаге он выбирает локально лучший вариант, то есть вершину с наименьшим известным расстоянием от старта.
Идею алгоритма можно объяснить простыми словами: вы стоите в начальной точке и хотите узнать расстояния до всех остальных городов. Вы постепенно открываете для себя новые города, всегда переходя сначала в тот, до которого сейчас ближе всего. При этом, попадая в новый город, вы проверяете, не стали ли теперь известные пути до его соседей короче. Этот процесс повторяется, пока не будут обработаны все вершины. Иными словами, на каждом шаге мы идём в ближайший ещё не посещённый город и обновляем расстояния до его соседей.
Рассмотрим работу алгоритма на конкретном примере. Пусть дан граф:
Рёбра и их веса:
- A–B: 1
- A–C: 4
- B–C: 2
- B–D: 5
- C–D: 1
Начальная вершина — A. В начальный момент мы знаем расстояние только до самой себя (ноль), а до всех остальных вершин расстояние полагаем бесконечным. Состояние можно описать так:
Извлекаем из очереди вершину A с расстоянием 0. Рассматриваем её соседей: до B можно добраться за 0+1=1, что меньше бесконечности, поэтому обновляем расстояние до B. До C можно добраться за 0+4=4, обновляем C. После этого состояния становятся такими:
Теперь из очереди выбираем вершину с наименьшим расстоянием — это B (расстояние 1). Из B можно пойти к A (но расстояние 1+1=2 больше уже известного 0, поэтому пропускаем), к C (1+2=3, что меньше текущего 4, обновляем C до 3) и к D (1+5=6, обновляем D с бесконечности до 6). После этого:
Обратите внимание: в очереди теперь есть два элемента для вершины C: (3, C) и (4, C_old). Это старая запись, которая будет проигнорирована позже.
Следующая ближайшая вершина — C с расстоянием 3. Проверяем соседей: до A (3+4=7 > 0), до B (3+2=5 > 1), до D (3+1=4 < 6) — обновляем D до 4. После обновления:
Наконец, извлекаем D с расстоянием 4. Её соседи уже посещены или не дают улучшений. Алгоритм завершается. Итоговые расстояния:
- A = 0
- B = 1
- C = 3
- D = 4
Кратчайшие пути из A:
- A → A : 0
- A → B : A → B (вес 1)
- A → C : A → B → C (1 + 2 = 3)
- A → D : A → B → C → D (1 + 2 + 1 = 4)
Рассматривая этот алгоритм, я все никак не мог привыкнуть читать его как Дейкстры. Каждый раз в голове крутился образ персонажа из Ведьмака - шпиона Диикстры. При этом на английском языке они пишутся одинаково - Dijkstra. Но забудем об этом и вернемся к нашему алгоритму.
Представьте карту городов, соединённых дорогами. Каждый город можно представить как вершину графа, каждая дорога — как ребро, а расстояние между городами — как вес ребра. Например, рассмотрим такой граф:
A ──1── B
│ │
4 2
│ │
C ──────D
1
Если мы хотим добраться из A в D по кратчайшему пути, то на глаз ответ не всегда очевиден. Именно для решения таких задач и существует алгоритм Дейкстры, который находит кратчайшие пути от одной вершины до всех остальных во взвешенном графе с неотрицательными весами рёбер.
Прежде чем углубляться, стоит уточнить ключевые понятия. Граф — это набор точек (вершин), соединённых линиями (рёбрами). Вес ребра — это число, стоящее на ребре и обозначающее стоимость перехода (расстояние, время, цена и т.п.). Взвешенный граф — это граф, где у каждого ребра есть вес. Кратчайший путь — это маршрут с минимальной суммой весов рёбер. Алгоритм Дейкстры относится к классу жадных алгоритмов: на каждом шаге он выбирает локально лучший вариант, то есть вершину с наименьшим известным расстоянием от старта.
Идею алгоритма можно объяснить простыми словами: вы стоите в начальной точке и хотите узнать расстояния до всех остальных городов. Вы постепенно открываете для себя новые города, всегда переходя сначала в тот, до которого сейчас ближе всего. При этом, попадая в новый город, вы проверяете, не стали ли теперь известные пути до его соседей короче. Этот процесс повторяется, пока не будут обработаны все вершины. Иными словами, на каждом шаге мы идём в ближайший ещё не посещённый город и обновляем расстояния до его соседей.
Рассмотрим работу алгоритма на конкретном примере. Пусть дан граф:
A ──1── B
│ ╱ │
4 2 5
│ ╱ │
C ──1── D
Рёбра и их веса:
- A–B: 1
- A–C: 4
- B–C: 2
- B–D: 5
- C–D: 1
Начальная вершина — A. В начальный момент мы знаем расстояние только до самой себя (ноль), а до всех остальных вершин расстояние полагаем бесконечным. Состояние можно описать так:
Расстояния: A=0, B=∞, C=∞, D=∞
Посещены: {}
Очередь: [(0, A)]
Извлекаем из очереди вершину A с расстоянием 0. Рассматриваем её соседей: до B можно добраться за 0+1=1, что меньше бесконечности, поэтому обновляем расстояние до B. До C можно добраться за 0+4=4, обновляем C. После этого состояния становятся такими:
Расстояния: A=0, B=1, C=4, D=∞
Посещены: {A}
Очередь: [(1, B), (4, C)]
Теперь из очереди выбираем вершину с наименьшим расстоянием — это B (расстояние 1). Из B можно пойти к A (но расстояние 1+1=2 больше уже известного 0, поэтому пропускаем), к C (1+2=3, что меньше текущего 4, обновляем C до 3) и к D (1+5=6, обновляем D с бесконечности до 6). После этого:
Расстояния: A=0, B=1, C=3, D=6
Посещены: {A, B}
Очередь: [(3, C), (4, C_old), (6, D)]
Обратите внимание: в очереди теперь есть два элемента для вершины C: (3, C) и (4, C_old). Это старая запись, которая будет проигнорирована позже.
Следующая ближайшая вершина — C с расстоянием 3. Проверяем соседей: до A (3+4=7 > 0), до B (3+2=5 > 1), до D (3+1=4 < 6) — обновляем D до 4. После обновления:
Расстояния: A=0, B=1, C=3, D=4
Посещены: {A, B, C}
Очередь: [(4, D), (4, C_old), (6, D_old)]
Наконец, извлекаем D с расстоянием 4. Её соседи уже посещены или не дают улучшений. Алгоритм завершается. Итоговые расстояния:
- A = 0
- B = 1
- C = 3
- D = 4
Кратчайшие пути из A:
- A → A : 0
- A → B : A → B (вес 1)
- A → C : A → B → C (1 + 2 = 3)
- A → D : A → B → C → D (1 + 2 + 1 = 4)
Теперь разберём реализацию алгоритма на языке Python с использованием модуля heapq, который предоставляет приоритетную очередь. Приоритетная очередь отличается от обычной тем, что всегда возвращает элемент с наименьшим ключом (в нашем случае — расстоянием). Это как раз соответствует жадному выбору ближайшей вершины.
Поясним некоторые детали реализации. Строка
Важно понимать, почему используется именно приоритетная очередь. Если бы мы каждый раз искали минимум простым перебором, сложность стала бы квадратичной. Благодаря этому мы получаем эффективность: каждая операция извлечения минимума и вставки стоит O(log V), где V — число вершин.
Сложность алгоритма Дейкстры с приоритетной очередью составляет O((V + E) log V), где V — количество вершин, E — количество рёбер. Для большинства реальных задач, таких как прокладка маршрутов на карте или в сетях, это приемлемо быстро.
Однако у алгоритма Дейкстры есть важное ограничение: он работает только с неотрицательными весами рёбер. Почему это так? Дело в том, что алгоритм основан на гарантии: когда вершина извлекается из очереди с минимальным расстоянием, это расстояние уже окончательное и не может быть улучшено позже. Если бы существовали отрицательные рёбра, то путь через другую вершину, ещё не обработанную, мог бы дать меньшее расстояние, даже если текущая вершина уже «закрыта». Рассмотрим простой пример:
Дейкстра из A сначала установит расстояния: B=2, C=4. Затем извлечёт B (расстояние 2) и пометит его как финальное. При обработке B он обновит C: 2 + (−5) = −3, и это станет новым расстоянием до C. Однако теперь уже поздно пересматривать пути через другие вершины, потому что B уже обработан. В этом конкретном случае мы всё же получим правильный ответ, но в более сложных графах с отрицательными весами могут возникнуть ситуации, когда оптимальный путь требует пересмотра уже обработанных вершин, что невозможно в рамках алгоритма Дейкстры. Более того, отрицательные веса могут создавать циклы с отрицательной суммой, которые делают понятие кратчайшего пути вообще неопределённым (можно бесконечно уменьшать стоимость, обходя цикл).
Если в графе присутствуют отрицательные веса, следует использовать алгоритм Беллмана–Форда. Он не полагается на жадный выбор и не делает предположений о финальности расстояний. Вместо этого алгоритм выполняет V−1 проходов по всем рёбрам, каждый раз пытаясь улучшить расстояния. Вот его простая реализация:
import heapq # Модуль для приоритетной очереди (мин-куча)
def dijkstra(graph, start):
# 1. Инициализация: все расстояния = бесконечность
distances = {node: float("infinity") for node in graph}
distances[start] = 0 # До стартовой вершины — 0
# 2. Приоритетная очередь: (расстояние, вершина)
# heapq всегда отдаёт элемент с МИНИМАЛЬНЫМ расстоянием первым
priority_queue = [(0, start)]
while priority_queue:
# 3. Извлекаем вершину с наименьшим расстоянием
current_distance, current_node = heapq.heappop(priority_queue)
# 4. Устаревшая запись? Пропускаем
# (в очереди могут быть старые версии с большим расстоянием)
if current_distance > distances[current_node]:
continue
# 5. Обходим соседей
for neighbor, weight in graph[current_node].items():
distance = current_distance + weight
# 6. Нашли более короткий путь? Обновляем!
if distance < distances[neighbor]:
distances[neighbor] = distance
heapq.heappush(priority_queue, (distance, neighbor))
return distances
Поясним некоторые детали реализации. Строка
if current_distance > distances[current_node]: continue необходима для отбрасывания устаревших записей в очереди. Когда мы обновляем расстояние до вершины (например, C с 4 на 3), старая запись (4, C) остаётся в куче. Когда она позже извлекается, мы проверяем, что её расстояние больше текущего известного, и просто пропускаем её. Это экономит время и корректирует работу алгоритма.Важно понимать, почему используется именно приоритетная очередь. Если бы мы каждый раз искали минимум простым перебором, сложность стала бы квадратичной. Благодаря этому мы получаем эффективность: каждая операция извлечения минимума и вставки стоит O(log V), где V — число вершин.
Сложность алгоритма Дейкстры с приоритетной очередью составляет O((V + E) log V), где V — количество вершин, E — количество рёбер. Для большинства реальных задач, таких как прокладка маршрутов на карте или в сетях, это приемлемо быстро.
Однако у алгоритма Дейкстры есть важное ограничение: он работает только с неотрицательными весами рёбер. Почему это так? Дело в том, что алгоритм основан на гарантии: когда вершина извлекается из очереди с минимальным расстоянием, это расстояние уже окончательное и не может быть улучшено позже. Если бы существовали отрицательные рёбра, то путь через другую вершину, ещё не обработанную, мог бы дать меньшее расстояние, даже если текущая вершина уже «закрыта». Рассмотрим простой пример:
A ──2── B ──(−5)── C
A ─────────4───── C
Дейкстра из A сначала установит расстояния: B=2, C=4. Затем извлечёт B (расстояние 2) и пометит его как финальное. При обработке B он обновит C: 2 + (−5) = −3, и это станет новым расстоянием до C. Однако теперь уже поздно пересматривать пути через другие вершины, потому что B уже обработан. В этом конкретном случае мы всё же получим правильный ответ, но в более сложных графах с отрицательными весами могут возникнуть ситуации, когда оптимальный путь требует пересмотра уже обработанных вершин, что невозможно в рамках алгоритма Дейкстры. Более того, отрицательные веса могут создавать циклы с отрицательной суммой, которые делают понятие кратчайшего пути вообще неопределённым (можно бесконечно уменьшать стоимость, обходя цикл).
Если в графе присутствуют отрицательные веса, следует использовать алгоритм Беллмана–Форда. Он не полагается на жадный выбор и не делает предположений о финальности расстояний. Вместо этого алгоритм выполняет V−1 проходов по всем рёбрам, каждый раз пытаясь улучшить расстояния. Вот его простая реализация:
def bellman_ford(graph, start):
distances = {node: float("infinity") for node in graph}
distances[start] = 0
for _ in range(len(graph) - 1): # V-1 итераций
for node in graph:
for neighbor, weight in graph[node].items():
if distances[node] + weight < distances[neighbor]:
distances[neighbor] = distances[node] + weight
return distances
Сложность Беллмана–Форда выше — O(V·E), но он способен обнаруживать отрицательные циклы и, соответственно, определять, что задача не имеет решения. Сравним два подхода:
- Дейкстра: работает только с неотрицательными весами, быстрее (O((V+E) log V)).
- Беллман–Форд: работает с любыми весами, медленнее (O(V·E)), может обнаруживать отрицательные циклы.
В реальной жизни алгоритм Дейкстра широко применяется. Например, в GPS-навигаторах (Google Maps, Яндекс.Карты) города или перекрёстки — это вершины, дороги — рёбра, а весом может быть время в пути. Алгоритм находит маршрут с минимальным временем. В интернете протоколы маршрутизации, такие как OSPF, используют алгоритм Дейкстры для определения кратчайшего пути передачи пакетов данных (вершины — роутеры, рёбра — каналы связи, вес — задержка или пропускная способность). В игровых движках NPC (неигровые персонажи) находят путь по карте с препятствиями. В системах поиска авиабилетов вершинами выступают аэропорты, рёбрами — рейсы, а весом — стоимость или время, что позволяет находить самые дешёвые или быстрые маршруты с пересадками.
#algorithm
👍2