Бинарный поиск с: Двоичный (бинарный) поиск в массиве. С++
Бинарный поиск — Tinkoff Generation
Бинарный поиск — Tinkoff Generation
- Линейный поиск
- Бинарный поиск
- Бинарный поиск с вещественными числами
- Поиск максимума выпуклой функции: тернарный поиск, бинарный поиск
- Бинарный поиск по ответу
Линейный поиск
Мы считаем, что вы уже знаете линейный поиск, а именно умеете решать задачи такого типа:
- Проверить, есть ли в массиве число X
- Найти максимум в массиве
- Найти сумму чисел в массиве
- Найти первое четное число в массиве
Все такие задачи решаются с помощью одного прохода по массиву с помощью цикла for. Все такие алгоритмы работают за \(O(n)\). И даже можно понять, что быстрее, чем \(O(n)\) решить ни одну из этих задач не получится.
Задание
Убедитесь, что вы умеете решать эти задачи. Докажите, что быстрее, чем O(n) их решить в худшем случае нельзя.
Бинарный поиск
Однако иногда найти число X в массиве можно и быстрее! Для этого надо добавить условие на то, что массив отсортирован. Но давайте начнем не с этого.
Задание
Я загадал число X от 1 до 100. Вы можете спрашивать, больше ли мое число чем число T, я отвечаю “да” или “нет”. За сколько вопросов в худшем случае вы сможете найти число X? Как нужно действовать?
Решение и состоит в идее бинарного (двоичного) поиска — нужно первым вопросом спросить “число X больше, чем 50?”. После этого, если ответ “нет”, надо спросить “число X больше, чем 25”? И так далее, нужно уменьшать отрезок возможных значений в два раза каждый раз.
Почему нужно делить обязательно пополам? Почему бы не спросить “число X больше, чем 80?” первым же вопросом? Но если вдруг ответ “нет”, то мы останемся с 80 вариантами вместо 100. То есть деление отрезка ровно пополам гарантирует, что в худшем случае мы останемся не более чем с половиной вариантов.
Чтобы понять, как быстро это работает, введём новую математическую функцию. Логарифмом по основанию \(a\) от \(b\) будем называть число \(c\), такое что \(a ^ c = b\). Обозначается как \(\log_a b = c\). Чаще всего мы будем работать с двоичным логарифмом, то есть в какую степень \(c\) нужно возвести двойку, чтобы получить \(b\). Поэтому договоримся, что запись \(\log n\) означает двочный логарифм \(n\).
Теперь вернёмся к нашей задаче. Можно понять, что такой алгоритм работает как раз за \(O(\log n)\) вопросов (если число 100 на заменить абстрактную переменную \(n\)). Несложно убедиться, что именно логарифм раз нужно поделить число на два, чтобы получилось 1.
Общий принцип
А теперь представьте такую задачу: у вас есть массив, состоящий из некоторого количества подряд идущих нулей, за которыми следует какое-то количество подряд идущих единиц.
a = [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1]
n = len(a)
n
14
Вам дан массив, и вам нужно найти позицию первой единицы, то есть найти такое место, где заканчиваются нули, и начинаются единицы. Это можно сделать с помощью линейного поиска за один проход по массиву, но хочется сделать это быстрее.
Давайте обратимся к идее бинарного поиска. Посмотрим на элемент посередине массива. Если это нуль, то первую единицу стоит искать в правой половину массива, а если единица — то в левой.
Есть много способов писать бинарный поиск, и в его написании люди очень часто путаются. Очень удобно в данном случае воспользоваться инвариантом (это слово значит “постоянное свойство”):
Пусть переменная left
всегда указывает на \(0\) в массиве, а переменная right
всегда указывает на \(1\) в массиве.
Дальше мы будем переменные left
и right
постепенно сдвигать друг к другу, и в какой-то момент они станут соседними числами. Это и будет означать, что мы нашли место, где заканчиваются нули и начинаются единицы.
Чему равны left
и right
изначально, когда мы ничего про массив не знаем? Первая приходящая в голову идея — поставить их на \(0\) и \(n-1\) соответственно. Увы, в общем случае это может быть неверно, потому что a[0]
может быть единицей, а a[n-1]
может быть нулём. Правильнее сделать вот так:
left = -1
right = n
То есть изначально left
и right
указывают на несуществующие индексы. Но это нормально — например в массиве [1, 1, 1, 1]
в конце алгоритма как раз должно быть left == -1
, right == 0
.
Осталось нам написать цикл while
:
while right - left > 1:
middle = (left + right) // 2 # именно такая формула для среднего индекса между left и right
if a[middle] == 1:
right = middle # right всегда должна указывать на 1
else:
left = middle # left всегда должна указывать на 0
print left, right
print a[left], a[right]
8 9
0 1
Мы решили задачу для ноликов и единичек, но это легко обобщается на абсолютно любую задачу, где есть какое-то свойство, которое в начале массива не выполняется, а потом выполняется.
Например, если мы хотим найти, есть ли число \(X\) в отсортированном массиве, то мы просто представим, что \(0\) — это числа, меньшие \(X\), а \(1\) — это числа, большие или равные \(X\). Тогда достаточно найти первую “единицу” и проверить, равно ли это число \(X\).
a = [1, 3, 4, 10, 10, 10, 11, 80, 80, 81] # отсортированный массив
def bin_search(a, x):
n = len(a)
left = -1
right = n
while right - left > 1:
middle = (left + right) // 2
if a[middle] >= x: # практически единственная строка, которая меняется от задачи к задаче
right = middle
else:
left = middle
if right != n and a[right] == x: # ответ лежит в right
return True
else:
return False
print (bin_search(a, 1))
print (bin_search(a, 10))
print (bin_search(a, 20))
print (bin_search(a, 79))
print (bin_search(a, 80))
print (bin_search(a, 81)
True
True
False
False
True
True
Задание
Придумайте, как с помощью бинарного поиска решить такие задачи: * Найти первое число, равное X в отсортированном массиве, или вывести, что таких чисел нет * Найти последнее число, равное X в отсортированном массиве, или вывести, что таких чисел нет * Посчитать, сколько раз встречается число X в отсортированном массиве (в решении помогают два предыдущих пункта) * Дан массив чисел, первая часть состоит из нечетных чисел, а вторая — из четных. Найти индекс, начиная с которого все числа четные.
Все эти задачи решаются бинарным поиском за \(O(\log{n})\). Правда нужно понимать, что в чистом виде такую задачу решать двоичным поиском бессмысленно — ведь чтобы создать массив размера \(n\), уже необходимо потратить \(O(n)\) операций.
Поэтому зачастую такие задачи сформулированы таким образом:
Дан отсортированный массив размера \(n\). Нужно ответить на \(m\) запросов вида “встречается ли число \(x_i\) в массиве n”?
Задание
Найдите время работы, за которое решается эта задача?
.
.
.
.
.
.
.
.
.
.
Решение: Такая задача решается за \(O(n + m\log{n})\) — нужно создать массив за \(O(n)\) и \(m\) раз запустить бинарный поиск.
Задание
Решите 3 первые задачи в этом контесте:
https://informatics.msk.ru/moodle/mod/statements/view.php?id=33216
Бинарный поиск с вещественными числами
У нас все еще есть функция f(x), которая сначала равна 0, а потом равна 1, и мы хотим найти это место, где она меняет значение. Но теперь аргумент функции — вещественное число. Например: * \(f(x) = 1\), если \(x^2 > 2\) * \(f(x) = 0\), если \(x^2 \leq 2\)
Понятно, что при \(x = \sqrt 2\) \(f(x) = 0\), а при любом даже немного большем значении \(f(x) = 1\). Если мы научимся решать такую задачу, то мы научимся находить корень из двух!
Увы, возникает проблема: действительные числа хранятся в компьютере неточно
# известный пример
0.1 + 0.1 + 0.1
0.30000000000000004
Тем более не сможем найти точное значение \(\sqrt 2\), потому что это бесконечная непериодическая дробь. Так что давайте снова воспользуемся бинарным поиском, причем всегда \(f(left) = 0\), \(f(right) = 1\), и мы остановимся тогда, когда left
и right
будет очень-очень близко.
И тут снова возникает проблема. Помимо того, что бесконечную дробь в принципе невозможно точно хра
Бинарный поиск простыми словами — World-Hello.ru
Deprecated: Function create_function() is deprecated in /home/worldhel/public_html/wp-content/plugins/codecolorer/lib/geshi.php on line 4698
Алгоритмом называется набор инструкций для выполнения некоторой задачи.
Бинарный поиск — это алгоритм, на входе он получает отсортированный список элементов.
Таким образом, если элемент, который вы ищете, присутствует в списке, то бинарный поиск возвращает ту позицию, в которой он был найден.
В противном случае бинарный поиск возвращает null.
Рассмотрим пример того, как работает бинарный поиск. Сыграем в простую игру: я загадал число от 1 до 100.
Вы должны отгадать мое число, использовав как можно меньше попыток. При каждой попытке я буду давать один из трех ответов: «мало», «много» или «угадал».
Предположм, вы начинаете перебирать все варианты подряд:1,2,3,4
Вот как это будет выглядеть.
Это пример простого поиск. При каждой догадке исключается только одно число. Если я загадал число 99, то, чтобы добраться до него, потребуется 99 попыток!!
Бинарный поиск
Существует другой, более эффективный способ. Начнем с 50.
Слишком мало… но вы только что исключили половину чисел! Теперь вы знаете, что все числа 1-50 меньше загаданного. След. попытка: 75.
На этот раз перелет… Но вы снова исключили половину оставшихся чисел!
С бинарным поиском вы каждый раз загадываете число в середине диапозона и исключатете половину оставшихся чисел. Следующим будет число 63 (по середине между 50 и 75)
Так работает бинарный поиск. При бинарном поиске каждый раз исключается половина чисел.
Предположим, вы ищете слово в словаре с 240 000 словами. Как вы думаете, сколько попыток вам понадобится в худшем случае?
Простой поиск: | ____ | шагов |
Бинарный поиск: | ____ | шагов |
При простом поиске может потребоваться 240 000 попыток, если искомое слово находится на самом последней позиции в книге.
С каждым шагом бинарного поиска количество слов сокращается вдвое, пока не останется только одно слово.
Давайте напишем реализцию бинарного поиска на Python.
1 | def binary_search(list, item): # В переменных low и high хранятся границы той части списка, в которой выполняется поиск low = 0 high = len(list) — 1 # Пока эта часть не сократится до одного элемента… # Значение не существует # ТЕСТИРУЕМ! |
Упражнения:
- Имеется отсортированный список из 128 имен, и вы ищите в нем значение методом бинарного поиска. Какое максимальное количество проверок для этого может потребоваться?
- Предположим, размер списка увеличился вдвое (256 имен). Как изменится максимальное количество проверок?
Ответы:
- 7
- 8
Разбор задач. Бинпоиск_1 / Хабр
Здравствуйте, уважаемые хабровчане. Серия статей содержит разбор задач, которые дают в 8 классе на уроках информатики в Челябинском физико-математическом лицее №31. Немного истории… Наш лицей — одно из сильнейших учебных заведений России, который в рейтинге по конкурентоспособности выпускников занимает 5 место, уступив школам Москвы и Санкт-Петербурга. Ученики регулярно побеждают на олимпиадах всероссийского и международного уровня.
Данная статья лишена теории, она содержит только способы решения задач. Подробно про бинпоиск описано здесь.
Так вот, перейдем к задачам. Эти задачи подразумевают собой использование бинарного поиска, в том числе бинпоиска по ответам. Как мы знаем бинпоиск — это алгоритм поиска объектов по заданному признаку в множестве объектов. Применяем для отсортированных по возрастанию/убыванию списков. Всего 4 задачи, 2 из которых направлены на применение «алгоритма без изюминок».
Левый и правый двоичный поиск
В этой задаче сначала нужно считать длины первой и второй строки, затем каждую следующую строку записать в список. После мы берем каждый элемент второго списка и ищем для него правую и левую границу. Можно заметить, что если число отсутствует в списке, итоговые значения левой границы и правой границы в левом и правом бинпоиске равны.
n, m = map(int, input().split())
a = list(map(int, input().split()))
b = list(map(int, input().split()))
for x in b:
left = -1
right = len(a)
#левый бинпоиск, который ищет левую границу
while right - left > 1:
middle = (right + left) // 2
if a[middle] < x:
left = middle
else:
right = middle
left_1 = -1
right_1 = len(a)
#правый бинпоиск, который ищет правый границу
while right_1 - left_1 > 1:
middle = (right_1 + left_1) // 2
if a[middle] <= x:
left_1 = middle
else:
right_1 = middle
if left == left_1 and right == right_1:
print(0)
#возращение к следующей итерации с пропуском, что идет после него
continue
if right == left_1:
print(right + 1, right + 1)
else:
print(right + 1, left_1 + 1)
Приближенный двоичный поиск
Вот эта задачка с изюминкой! Тут надо так преобразовать поиск, чтобы выполнялось даже для чисел, которых нет в данном для поиска списке. Здесь мы также ищем середину в “граничном списке”, затем элемент, который посередине сравниваем с числом, если он меньше него, мы записываем в левую границу(который индекс) индекс середины + 1(то есть не включаем прошлую середину в граничный список), в ином случае в правую границу записываем индекс середины. Все это делаем, пока левая граница меньше правой. После нахождения left и right рассматриваем случай, когда числа нет в списке и расстояние до того, что левее меньше или равно, чем то, что правее. Поэтому выводим a[left — 1], в ином случае a[left].
n, m = map(int, input().split())
a = list(map(int, input().split()))
b = list(map(int, input().split()))
for x in b:
left = 0
right = n - 1
while left < right:
middle = (left + right) // 2
if a[middle] < x:
left = middle + 1
else:
right = middle
if left > 0 and a[left] != x and abs(a[left - 1] - x) <= abs(a[left] - x):
print(a[left - 1])
else:
print(a[left])
Квадратный корень и квадратный квадрат
Тадам! Задача на бинарный поиск по ответу. Для начала из библиотеки math подключим функцию sqrt, которая вычисляет квадратный корень, после определим для левой границы 0, а для правой 1e10, то есть 10 миллиардов, исходя из ограничений, которые указаны в условии. Далее как в типичном бинпоиске ищем середину, позже сравниваем. Но что тут интересного? Тут middle — это x в уравнении. Ввиду этого определяем значение уравнения и сверяем с реальным ответом(т.е. С). Ну и двигаем границы, до тех пор, пока разность границ не будет меньше или равна 10-ти миллиардным, это и есть точность измерения. Позже умножаем на миллион, округляем, переводим в int и вещественное деление на тот же миллион.
from math import sqrt
c = float(input())
left = 0
right = 1e10#1 c 10-тью нулями
while right - left > 10e-10:#10 нулей и 1
middle = (left + right) / 2
if middle * middle + sqrt(middle) >= c:
right = middle
else:
left = middle
#умножаем на 10e6, округляем, преобразуем в int, делим на 10e6
print(int(round(left*10e6))/10e6)
Очень Легкая Задача
Разбор на эту задачу уже есть, поэтому приложу только код.
n, x, y = map(int, input().split())
min1 = min(x, y)
if n == 1:
print(min1)
else:
left = 0
right = n * (x + y - min1 + 1)
while right - left > 1:
middle = (right + left) // 2
if n - 1 <= middle // x + middle // y:
right = middle
else:
left = middle
print(min1 + left + 1)
Для закрепления материала можно порешать задачи отсюда.
Только 10% программистов способны написать двоичный поиск / Хабр
Дональд Кнут (известный тем, что его книги никто не читает) пишет, что хотя первый двоичный поиск был опубликован в 1946 году, первый двоичный поиск без багов был опубликован только в 1962.
Алгоритм двоичного поиска похож на то, как мы ищем слово в словаре. Открываем словарь посередине, смотрим в какой из половин будет нужное нам слово. Допустим, в первой. Открываем первую часть посередине, продолжаем половинить, пока не найдем нужное слово.
С массивами так: есть упорядоченный массив, берем число из середины массива, сравниваем с искомым. Если оно оказалось больше, значит искомое число в первой половине массива, если меньше — во второй. Продолжаем делить оставшуюся половину, когда находим нужное число возвращаем его индекс, если не находим возвращаем null.
В статье утверждалось, что только 10% программистов могут решить эту задачу. Да не может быть! Вот лохи, подумал я, зарядил Firebug, каких-то 5 минут и… нерабочая версия готова. Еще одна итерация, и еще, и еще. В сумме полтора часа, и в конечном решении все равно 2 ошибки. Стыдно как!
Если вы никогда не писали двоичный поиск, я предлагаю вам написать этот алгоритм на любимом языке и выложить его в комменты без тестирования. Любой хороший программист сходу напишет этот поистине детский алгоритм. Потратьте столько времени, сколько нужно. В комменте укажите язык, затраченное время, и ссылку на ваш труд на pastie.org.
На Википедии правильное решение.
Маленький гостинец для тех, кому все это кажется банальным. Почти все реализации двоичного поиска и сортировки слиянием содержат ошибки. Это говорит человек, написавший двоичный поиск для JDK.
Спойлер.
Распространенные ошибки:
— не работает с массивом из 0/1/2 элементов
— не находит первый или последний элемент
— некорректно работает, если элемента в массиве нет
— некорректно работает, если в массиве есть повторяющиеся элементы
— обращение к элементами за пределами массива
— козырная, которая была в JDK, переполнение целого при вычислении среднего индекса
P.S.
Кто-то скажет, что эта функция уже есть в стандартной библиотеке.
Это так, согласен. Но это не значит, что от решения таких задач нет толку. Смотрите, если все простые задачи уже решены за нас в стандартной библиотеке, значит нам остались только более сложные задачи, которых там нет. Как мы будем решать эти более сложные задачи, если мы не умеем решить даже простую задачу из стандартной библиотеки?
Я вам скажу как. Очень плохо. Я делал проверку кода, когда работал в команде. Многие программисты не могли родить простейший код даже с 20 попытки! Они привыкли сидеть на готовом, и не способны написать что-то сложнее композиции foo(bar()), а если и напишут, то реализация будет медленная, путанная, и с ошибками.
Двоичный поиск в графах / Хабр
Двоичный поиск — один из самых базовых известных мне алгоритмов. Имея отсортированный список сравнимых элементов и целевой элемент, двоичный поиск смотрит в середину списка и сравнивает значение с целевым элементом. Если цель больше, мы повторяем с меньшей половиной списка, и наоборот.
При каждом сравнении алгоритм двоичного поиска разбиваем пространство поиска пополам. Благодаря этому всегда будет не более сравнений со временем выполнения . Красиво, эффективно, полезно.
Но всегда можно посмотреть под другим углом.
Что, если попробовать выполнить двоичный поиск по графу? Большинство алгоритмов поиска по графам, такие как поиск в ширину или поиск в глубину, требуют линейного времени и были придуманы довольно умными людьми. Поэтому если двоичный поиск по графу будет иметь какой-то смысл, то он должен использовать больше информации, чем та, к которой имеют доступ обычные алгоритмы поиска.
Для двоичного поиска по списку этой информацией является факт отсортированности списка, и для управления поиском мы можем сравнивать числа с искомым элементом. Но на самом деле важнейшая информация не связана со сравнимостью элементов. Она заключается в том, что мы можем избавляться на каждом этапе от половины пространства поиска. Этап «сравнение с целевым значением» можно воспринимать как чёрный ящик, отвечающий на запросы типа «То ли это значение, которое я искал?» ответами «Да» или «Нет, но посмотри лучше вот здесь»
Если ответы на наши запросы достаточно полезны, то есть они позволяют нам разделять на каждом этапе большие объёмы пространства поиска, то, похоже, мы получили хороший алгоритм. На самом деле, существует естественная модель графов, определённая в статье Эмамджоме-Заде, Кемпе и Сингала 2015 году следующим образом.
Мы получаем в качестве входных данных неориентированный взвешенный граф с весами при . Мы видим весь граф и можем задавать вопросы в форме «Является ли вершина нужной нам?» Ответами могут двух варианта:
- Да (мы победили!)
- Нет, но является ребром из на кратчайшем пути от до нужной вершины.
Наша цель — найти нужную вершину за минимальное количество запросов.
Очевидно, что это работает только если является связанным графом, но небольшие вариации изложенного в статье будут работать и для несвязанных графов. (В общем случае это неверно для ориентированных графов.)
Когда граф является линией, то задача «редуцируется» до двоичного поиска в том смысле, что используется та же базовая идея, что и в двоичном поиске: начинаем с середины графа, и ребро, которое мы получаем в ответ на запрос, говорит нам, в какой половине графа продолжать поиск.
И если мы сделаем этот пример всего немного более сложным, то обобщение станет очевидным:
Здесь мы снова начинаем с «центральной вершины» и ответ на наш запрос избавит нас от одной из двух половин. Но как тогда нам выбрать следующую вершину, ведь теперь у нас нет линейного порядка? Это должно быть очевидно — выбрать «центральную вершину» любой половины, в которой мы оказались. Этот выбор можно формализировать в правило, которое будет работать даже при отсутствии очевидной симметрии, и как оказывается, этот выбор всегда будет правильным.
Определение: медианой взвешенного графа относительно подмножества вершин является вершина (необязательно находящаяся в ), которая минимизирует сумму расстояний между вершинами в >. Более формально, она минимизирует
где — сумма весов рёбер вдоль кратчайшего пути от до .
Обобщение двоичного поиска до такой модели с запросами на графе приводит нас к следующему алгоритму, ограничивающему пространство поиска, запрашивая на каждом шаге медиану.
Алгоритм: двоичный поиск в графах. В качестве входных данных используется граф .
- Начинаем с множества кандидатов .
- Пока мы не нашли нужную вершину и :
- Выводим единственную оставшуюся вершину в
И в самом деле, как мы скоро увидим, реализация на Python будет почти столь же проста. Самая важная часть работы заключается в вычислении медианы и множества . Обе эти операции являются небольшими разновидностями алгоритма Дейкстры для вычисления кратчайших путей.
Теорема, которая проста и отлично сформулирована Эмамджоме-Заде с коллегами (всего около половины страницы на стр. 5) заключается в том, что алгоритму требуется всего запросов — столько же, сколько и в двоичном поиске.
Прежде чем мы углубимся в реализацию, стоит рассмотреть одну хитрость. Даже несмотря на то, что у нас гарантированно будет не больше запросов, из-за нашей реализации алгоритма Дейкстры, у нас точно не получится алгоритм логарифмического времени. Что в такой ситуации будет нам полезно?
Здесь мы воспользуется «теорией» — создадим вымышленную задачу, для которой позже найдём применение (что достаточно успешно используется в информатике). В этом случае мы будем обращаться с механизмом запросов как с чёрным ящиком. Естественно будет представить, что запросы затратны по времени и являются ресурсом, который нужно оптимизировать. Например, как авторы написали в дополнительной статье, граф может быть множеством группировок множества данных, а при запросах требуется вмешательство человека, смотрящего на данные и сообщающего, какой класер должен быть разделён, или какие два кластера должны быть объединены. Разумеется, граф оказывается слишком большим для обработки при группировании, поэтому алгоритм поиска медианы должен быть определён неявно. Но самое важное ясно: иногда запрос является самой затратной частью алгоритма.
Ну ладно, давайте всё это реализуем! Полный код, как всегда находится на Github.
Реализуем алгоритм
Мы начнём с небольшой вариации алгоритма Дейкстры. Здесь нам задана в качестве входных данных «начальная» точка, а в качестве выходных данных мы создаём список всех кратчайших путей до всех возможных конечных вершин.
Мы начнём с голой структуры данных графа.
from collections import defaultdict
from collections import namedtuple
Edge = namedtuple('Edge', ('source', 'target', 'weight'))
class Graph:
# Голая реализация взвешенного неориентированного графа
def __init__(self, vertices, edges=tuple()):
self.vertices = vertices
self.incident_edges = defaultdict(list)
for edge in edges:
self.add_edge(
edge[0],
edge[1],
1 if len(edge) == 2 else edge[2] # дополнительный вес
)
def add_edge(self, u, v, weight=1):
self.incident_edges[u].append(Edge(u, v, weight))
self.incident_edges[v].append(Edge(v, u, weight))
def edge(self, u, v):
return [e for e in self.incident_edges[u] if e.target == v][0]
Теперь, поскольку бо́льшая часть работы в алгоритме Дейкстры заключается в отслеживании информации, накапливаемой при поиске по графу, мы определим «выходную» структуру данных — словарь с весами рёбер вместе с обратными указателями на обнаруженные кратчайшие пути.
class DijkstraOutput:
def __init__(self, graph, start):
self.start = start
self.graph = graph
# наименьшее расстояние от начала до конечной точки v
self.distance_from_start = {v: math.inf for v in graph.vertices}
self.distance_from_start[start] = 0
# список предшествующих рёбер для каждой конечной точки
# для отслеживания списка возможного множества кратчайших путей
self.predecessor_edges = {v: [] for v in graph.vertices}
def found_shorter_path(self, vertex, edge, new_distance):
# обновление решения новым найденным более коротким путём
self.distance_from_start[vertex] = new_distance
if new_distance < self.distance_from_start[vertex]:
self.predecessor_edges[vertex] = [edge]
else: # "ничья" для нескольких кратчайших путей
self.predecessor_edges[vertex].append(edge)
def path_to_destination_contains_edge(self, destination, edge):
predecessors = self.predecessor_edges[destination]
if edge in predecessors:
return True
return any(self.path_to_destination_contains_edge(e.source, edge)
for e in predecessors)
def sum_of_distances(self, subset=None):
subset = subset or self.graph.vertices
return sum(self.distance_from_start[x] for x in subset)
Настоящий алгоритм Дейкстры затем выполняет поиск в ширину (управляемый очередью с приоритетами) по , обновляя метаданные при нахождении более коротких путей.
def single_source_shortest_paths(graph, start):
'''
Вычисляем кратчайшие пути и расстояния от начальной вершины до всех
возможных конечных вершин. Возвращаем экземпляр DijkstraOutput.
'''
output = DijkstraOutput(graph, start)
visit_queue = [(0, start)]
while len(visit_queue) > 0:
priority, current = heapq.heappop(visit_queue)
for incident_edge in graph.incident_edges[current]:
v = incident_edge.target
weight = incident_edge.weight
distance_from_current = output.distance_from_start[current] + weight
if distance_from_current <= output.distance_from_start[v]:
output.found_shorter_path(v, incident_edge, distance_from_current)
heapq.heappush(visit_queue, (distance_from_current, v))
return output
Наконец, мы реализуем подпрограммы нахождения медианы и вычисления :
def possible_targets(graph, start, edge):
'''
Имея заданный неориентированный граф G = (V,E), входную вершину v в V и ребро e,
инцидентное v, вычисляем множество вершин w, такое, что e находится на кратчайшем
пути от v к w.
'''
dijkstra_output = dijkstra.single_source_shortest_paths(graph, start)
return set(v for v in graph.vertices
if dijkstra_output.path_to_destination_contains_edge(v, edge))
def find_median(graph, vertices):
'''
Вычисляем в качестве output вершину во входном графе, которая минимизирует сумму
расстояний к входному множеству вершин
'''
best_dijkstra_run = min(
(single_source_shortest_paths(graph, v) for v in graph.vertices),
key=lambda run: run.sum_of_distances(vertices)
)
return best_dijkstra_run.start
А теперь ядро алгоритма
QueryResult = namedtuple('QueryResult', ('found_target', 'feedback_edge'))
def binary_search(graph, query):
'''
Находим нужный узел в графе с помощью запросов вида "Является ли x нужной вершиной?"
и даём ответ - или "Мы нашли нужный узел!", или "Вот ребро на кратчайшем пути к нужному узлу".
'''
candidate_nodes = set(x for x in graph.vertices) # копируем
while len(candidate_nodes) > 1:
median = find_median(graph, candidate_nodes)
query_result = query(median)
if query_result.found_target:
return median
else:
edge = query_result.feedback_edge
legal_targets = possible_targets(graph, median, edge)
candidate_nodes = candidate_nodes.intersection(legal_targets)
return candidate_nodes.pop()
Вот пример выполнения скрипта на образце графа, который мы использовали выше в посте:
'''
Граф выглядит как дерево с равномерными весами
a k
b j
cfghi
d l
e m
'''
G = Graph(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
'j', 'k', 'l', 'm'],
[
('a', 'b'),
('b', 'c'),
('c', 'd'),
('d', 'e'),
('c', 'f'),
('f', 'g'),
('g', 'h'),
('h', 'i'),
('i', 'j'),
('j', 'k'),
('i', 'l'),
('l', 'm'),
])
def simple_query(v):
ans = input("Является ли '%s' нужной вершиной? [y/N] " % v)
if ans and ans.lower()[0] == 'y':
return QueryResult(True, None)
else:
print("Введите вершину на кратчайшем пути между"
" '%s' и нужной вершиной. Граф: " % v)
for w in G.incident_edges:
print("%s: %s" % (w, G.incident_edges[w]))
target = None
while target not in G.vertices:
target = input("Введите соседнюю вершину '%s': " % v)
return QueryResult(
False,
G.edge(v, target)
)
output = binary_search(G, simple_query)
print("Найдена нужная вершина: %s" % output)
Функция запроса просто выводит напоминание о структуре графа и просит пользователя ответить (да/нет) и сообщить соответствующее ребро, если в случае отрицательного ответа.
Пример выполнения:
Является ли 'g' нужной вершиной? [y/N] n
Введите вершину на кратчайшем пути между 'g' и нужной вершиной. Граф:
e: [Edge(source='e', target='d', weight=1)]
i: [Edge(source='i', target='h', weight=1), Edge(source='i', target='j', weight=1), Edge(source='i', target='l', weight=1)]
g: [Edge(source='g', target='f', weight=1), Edge(source='g', target='h', weight=1)]
l: [Edge(source='l', target='i', weight=1), Edge(source='l', target='m', weight=1)]
k: [Edge(source='k', target='j', weight=1)]
j: [Edge(source='j', target='i', weight=1), Edge(source='j', target='k', weight=1)]
c: [Edge(source='c', target='b', weight=1), Edge(source='c', target='d', weight=1), Edge(source='c', target='f', weight=1)]
f: [Edge(source='f', target='c', weight=1), Edge(source='f', target='g', weight=1)]
m: [Edge(source='m', target='l', weight=1)]
d: [Edge(source='d', target='c', weight=1), Edge(source='d', target='e', weight=1)]
h: [Edge(source='h', target='g', weight=1), Edge(source='h', target='i', weight=1)]
b: [Edge(source='b', target='a', weight=1), Edge(source='b', target='c', weight=1)]
a: [Edge(source='a', target='b', weight=1)]
Введите соседнюю вершину 'g': f
Является ли 'c' нужной вершиной? [y/N] n
Введите вершину на кратчайшем пути между 'c' и нужной вершиной. Граф:
[...]
Введите соседнюю вершину 'c': d
Является ли 'd' нужной вершиной? [y/N] n
Введите вершину на кратчайшем пути между 'd' и нужной вершиной. Граф:
[...]
Введите соседнюю вершину 'd': e
Найдена нужная вершина: e
Вероятностная история
Реализованный нами в этом посте двоичный поиск довольно минималистичен. На самом деле, более интересной частью работы Эмамджоме-Заде с коллегами является часть, в которой ответ на запрос может быть неверным с неизвестной вероятностью.
В таком случае может быть множество кратчайших путей, являющихся правильным ответом на запрос, в дополнение ко всем неверным ответам. В частности, это исключает стратегию передачи одного и того же запроса несколько раз и выбора самого популярного ответа. Если частота ошибки равна 1/3, и к нужной вершине есть два кратчайших пути, то мы можем получить ситуацию, в которой мы с равной частотой увидим три ответа и не сможем выяснить, какой из них лжив.
Вместо этого Эмамджоме-Заде с соавторами использует технику, основанную на алгоритме обновления мультипликативных весов (он снова в деле!). Каждый запрос даёт мультипликативное увеличение (или уменьшение) для множества узлов, являющихся постоянными нужными вершинами при условии, что ответ на запрос верен. Есть ещё некоторые детали и постобработка для избежания невероятных результатов, но в целом основная идея такова. Её реализация будет отличным упражнением для читателей, заинтересованных в глубоком изучении этой недавней статьи (или в том, чтобы поразмять свои математические мышцы)
Но если смотреть ещё глубже, то эта модель «запроса и получения совета о том, как улучшить результат» является классической моделью обучения, впервые формально изученной Даной Энглуин. В её модели проектируется алгоритм для обучения классификатора. Допустимыми запросами являются запросы о принадлежности и об эквивалентности. Принадлежность — это, в сущности, вопрос «Какая метка у этого элемента?», а запрос эквивалентности имеет вид «Верный ли это классификатор?» Если ответ «нет», то передаётся пример с неправильной меткой.
Это отличается от обычного предположения машинного обучения, потому что алгоритм обучения должен создать пример, о котором он хочет получить больше информации, а не просто полагаться на случайно сгенерированное подмножество данных. Цель заключается в минимизации количества запросов, пока алгоритм точно не обучится нужной гипотезе. И на самом деле — как мы увидели в этом посте, если у нас будет немного лишнего времени на изучение пространства задачи, то мы можем создать запросы, получающие достаточно много информации.
Представленная здесь нами модель двоичного поиска по графам является естественным аналогом запроса эквивалентности в задаче поиска: вместо контрпримера с неверной меткой мы получаем «толчок» в правильном направлении к цели. Довольно неплохо!
Здесь мы можем пойти различными путями: (1) реализовать версию алгоритма с мультипликативными весами, (2) применить эту технику к задаче ранжирования или группирования, или (3) подробнее рассмотреть теоретические модели обучения типа принадлежности и эквивалентности.
Использование бинарного поиска для оптимизации запроса на выборку данных / Хабр
Введение
Сейчас очень популярна тем оптимизации работы с различными СУБД. На многочисленных форумах ведутся дискуссии о «самой лучшей СУБД в мире», но часто все это перетекает в необоснованные выкрики о том, что «я познал смысл жизни и понял, что самое лучшее хранилище данных — Х».
Да, несомненно, сейчас мы можем наблюдать активное развитие NoSQL решений, которые позволяют делать многое. Но данная статья не о них. Так вышло, что я сменил работу и в нагрузку мне достался один очень интересный проект на связке php+MySQL. В нем есть много хороших решений, но он писался без расчёта на большую аудиторию. За несколько лет существования количество активных пользователей начало приближаться к числам с 7 нулями. Так как проект представляет из себя подобие социальной сети с игровыми элементами, то таблица с пользователями оказалась не самой «тяжёлой» из всех. В наследство мне достались таблицы с десятками миллионов вещей пользователей, личных сообщений, биллинговыми записями и т. п. Проект начали рефакторить, разбивать на несколько серверов и достигли значительных результатов. Сейчас все стабильно.
Но недавно мне на почту прислали новую задачу. Суть заключалась в сборе статистики. Проанализировав требования я понял, что для выполнения достаточно написать один единственный запрос, выполняющий 3 INNER JOIN’а на таблицы, размеры которых впечатляли. Каждая таблица в среднем содержала 40 миллионов записей. Получается, что временная таблица состояла бы из 4*4*4*10^21 = 64*10^21 записей. Это колоссальная цифра. И загружать СУБД таким запросом для сбора статистики — непозволительная роскошь.
Далее, собственно, я и хочу представить решение данной абстрактной задачи, которое пришло мне в голову, когда я вспоминал занятия по информатике на первом курсе университета.
(в проекте используется СУБД MySQL, но алгоритм не имеет каких-либо специфических особенностей)
Что такое бинарный поиск
Думаю, что многие из вас писали лабораторные работы, посвящённые поиску элемента в массиве с использование бинарного алгоритма. Постараюсь кратко изложить его суть.
Пусть у нас имеется отсортированный массив из n элементов:
Первый элемент массива = 1
Последний элемент массива = n
Нам необходимо найти индекс элемента со значением f.
Каждый шаг заключается в том, что мы вычисляем середину массива:
Середина массива = round(Первый элемент + Последний элемент)/2
Затем вычисляем значение этого элемента и проверяем больше или меньше полученное значение в сравнении с искомым f. Диапазон поиска уменьшается в 2 раза:
Если <значение середины> > f, то
Последний элемент массива = значение середины
Иначе
Первый элемент массива = значение середины
Данные шаги повторяются пока не наступит одно из условий:
- Модуль разности значений середины и f меньше некоторого эпсилон(где эпсилон, некоторая погрешность)
- Количество итераций превысило значение log2(количество элементов в массиве)
Думаю, суть ясна. Таким образом мы значительно ускоряем поиск нужного элемента за счёт сокращения диапазонов поиска, но жертвуем некоторой точностью вычислений (для статистики это не критично, если пару элементов из миллионов не будут учтены. В противном случае необходимо сделать эпсилон нулевым и завершать поиск только после достижения последнего уровня дерева).
Переходим к практике
Итак, предположим, что нам необходимо сделать INNER JOIN на 3 таблицы, а затем задать условие «столбец х в диапазоне между 10 и 20». Причём столбец х не имеет индекса. Это будет очень долго выполнятся. Тут и приходит на помощь это простой способ.
Берем таблицу с этим самым столбцом и применяем на ней бинарный поиск, для поиска диапазона первичных ключей, удовлетворяющих условию 10<=x<=20. Учитывая, что для подобной выборки мы будем использовать только индексы, то очень скоро мы получим нужную пару значений.
Стоит сказать, что бинарный поиск используется для нахождения одного элемента, а не диапазона, но никто не мешает нам найти первый элемент со значением 10 и последний элемент со значением 20. Их первичные ключи и буду ограничениями диапазонов.
С этим диапазоном мы идём снова к нашему запросу, но теперь вместо условия WHERE x >= 10 AND x <= 20 мы пишем WHERE id_x BETWEEN min_id_x AND max_id_x, где min_id_x и max_id_x — значение нижнего и верхнего краёв диапазона, удовлетворяющего условию.
Что мы получаем: теперь мы делаем выборку не по некому столбцу х, а по первичному ключу. Время потраченное на обход одной таблицы сэкономлено. Аналогичную процедуру можно провести с другими условиями в запросе.
Приводить здесь код не вижу смысла, т. к. код бинарного поиска можно найти на википедии, а сам запрос — ничего сверхъестественного.
Выводы
Данный алгоритм позволяет перенести условия с полей без индексов на первичные ключи, что значительно ускоряет работу запроса. Но метод нельзя считать панацеей.
Во-первых, трудно подготовить универсальное решение для всех запросов. В любом случае будет необходимо учитывать детали реализации той или иной таблицы и, как следствие, каждый раз тратить время на оптимизацию.
Во-вторых, данной способ подходит не для всех решений, т. к. строки в таблице должны быть отсортированы в некотором порядке.
Двоичный поиск — это… Что такое Двоичный поиск?
Двоичный (бинарный) поиск (также известен как метод деления пополам и дихотомия) — классический алгоритм поиска элемента в отсортированном массиве (векторе), использующий дробление массива на половины. Используется в информатике, вычислительной математике и математическом программировании.
Частным случаем двоичного поиска является метод бисекции, который применяется для поиска корней заданной непрерывной функции на заданном отрезке.
Поиск элемента в отсортированном массиве
Пример кода на языке программирования Cи для поиска элемента x
в массиве a[n]
, отсортированного в возрастающем порядке:
size_t first = 0; /* Номер первого элемента в массиве */ size_t last = n; /* Номер элемента в массиве, СЛЕДУЮЩЕГО ЗА последним */ /* Если просматриваемый участок непустой, first<last */ size_t mid; if (n == 0) { /* массив пуст */ } else if (a[0] > x) { /* не найдено; если вам надо вставить его со сдвигом - то в позицию 0 */ } else if (a[n - 1] < x) { /* не найдено; если вам надо вставить его со сдвигом - то в позицию n */ } while (first < last) { /* ВНИМАНИЕ! В отличие от более простого (first+last)/2, этот код стоек к переполнениям. Если first и last знаковые, возможен код (unsigned)(first+last) >> 1. */ mid = first + (last - first) / 2; if (x <= a[mid]) { last = mid; } else { first = mid + 1; } } /* Если условный оператор if(n==0) и т.д. в начале опущен - значит, тут раскомментировать! */ if (/* last<n &&*/ a[last] == x) { /* Искомый элемент найден. last - искомый индекс */ } else { /* Искомый элемент не найден. Но если вам вдруг надо его вставить со сдвигом, то его место - last. */ }
Несмотря на то, что код достаточно прост, в нём есть несколько ловушек.
- Что будет, если
first
иlast
по отдельности умещаются в свой тип, аfirst+last
— нет? - Будет ли работать на пустом массиве (
n=0
)? - Способен ли код находить отсутствующие значения? У некоторых программистов написанный «с листа» двоичный поиск в такой ситуации зацикливается — и они этого не осознают, пока тестирование не даст ошибку.
- Иногда требуется, чтобы, если
x
в цепочке существует в нескольких экземплярах, находило не любой, а обязательно первый (как вариант: последний; следующий за последним). Данная версия кода в такой ситуации находит первый из равных.
Учёный Йон Бентли утверждает, что 90% студентов, разрабатывая двоичный поиск, забывают учесть какое-либо из этих требований. И даже в код, написанный самим Йоном и ходивший из книги в книгу, вкралась ошибка: код не стоек к переполнениям[1].
Приложения
Практические приложения метода двоичного поиска разнообразны:
- Широкое распространение в информатике применительно к поиску в структурах данных. Например, поиск в массивах данных осуществляется по ключу, присвоенному каждому из элементов массива (в простейшем случае сам элемент является ключом).
- Также его применяют в качестве численного метода для нахождения приближённого решения уравнений.
- Метод бисекции используется для поиска численных решений уравнений.
- Метод используется для нахождения экстремума целевой функции и в этом случае является методом условной одномерной оптимизации. Когда функция имеет вещественный аргумент, найти решение с точностью до можно за время . Когда аргумент дискретен, и изначально лежит на отрезке длины N, поиск решения займёт времени. Наконец, для поиска экстремума, скажем для определённости минимума, на очередном шаге отбрасывается тот из концов рассматриваемого отрезка, значение в котором максимально.
См. также
Ссылки
Примечания
Литература
- Ананий В. Левитин. Глава 4. Метод декомпозиции: Бинарный поиск // Алгоритмы: введение в разработку и анализ = Introduction to The Design and Analysis of Algorithms. — М.: «Вильямс», 2006. — С. 180-183. — ISBN 5-8459-0987-2
- Амосов А. А., Дубинский Ю. А., Копченова Н. П. Вычислительные методы для инженеров. — М.: Мир, 1998.
- Бахвалов Н. С., Жидков Н. П., Кобельков Г. Г. Численные методы. — 8-е изд. — М.: Лаборатория Базовых Знаний, 2000.
- Вирт Н. Алгоритмы + структуры данных = программы. — М.: «Мир», 1985. — С. 28.
- Волков Е. А. Численные методы. — М.: Физматлит, 2003.
- Гилл Ф., Мюррей У., Райт М. Практическая оптимизация. Пер. с англ. — М.: Мир, 1985.
- Кормен, Т., Лейзерсон, Ч., Ривест, Р., Штайн, К. Алгоритмы: построение и анализ = Introduction to Algorithms / Под ред. И. В. Красикова. — 2-е изд. — М.: Вильямс, 2005. — 1296 с. — ISBN 5-8459-0857-4
- Корн Г., Корн Т. Справочник по математике для научных работников и инженеров. — М.: Наука, 1970. — С. 575-576.
- Коршунов Ю. М., Коршунов Ю. М. Математические основы кибернетики. — Энергоатомиздат, 1972.
- Максимов Ю. А., Филлиповская Е. А. Алгоритмы решения задач нелинейного программирования. — М.: МИФИ, 1982.
- Роберт Седжвик. Фундаментальные алгоритмы на C. Анализ/Структуры данных/Сортировка/Поиск = Algorithms in C. Fundamentals/Data Structures/Sorting/Searching. — СПб.: ДиаСофтЮП, 2003. — С. 672. — ISBN 5-93772-081-4
java — Использование двоичного поиска с отсортированным массивом с дубликатами
Переполнение стека
- Около
Продукты
- Для команд
Переполнение стека
Общественные вопросы и ответыПереполнение стека для команд
Где разработчики и технологи делятся частными знаниями с коллегамиВакансии
Программирование и связанные с ним технические возможности карьерного ростаТалант
Нанимайте технических специалистов и создавайте свой бренд работодателяРеклама
Обратитесь к разработчикам и технологам со всего мира- О компании
.Алгоритм
— поиск нескольких записей с помощью двоичного поиска
Переполнение стека
- Около
Продукты
- Для команд
Переполнение стека
Общественные вопросы и ответыПереполнение стека для команд
Где разработчики и технологи делятся частными знаниями с коллегамиВакансии
Программирование и связанные с ним технические возможности карьерного ростаТалант
Нанимайте технических специалистов и создавайте свой бренд работодателяРеклама
Обратитесь к разработчикам и технологам со всего мира- О компании
.Алгоритм
— можем ли мы использовать двоичный поиск с несортированным массивом?
Переполнение стека
- Около
Продукты
- Для команд
Переполнение стека
Общественные вопросы и ответыПереполнение стека для команд
Где разработчики и технологи делятся частными знаниями с коллегамиВакансии
Программирование и связанные с ним технические возможности карьерного ростаТалант
Нанимайте технических специалистов и создавайте свой бренд работодателяРеклама
Обратитесь к разработчикам и технологам со всего мира
.