Как мы можем доказать индукцией, что бинарный поиск правильный?

Мне трудно понять, как индукция в сочетании с некоторым инвариантом может использоваться для доказательства правильности алгоритмов. А именно, как найден инвариант и когда используется индуктивная гипотеза - особенно для бинарного поиска? Я еще не смог найти интуитивный ответ, поэтому я надеялся, что кто-то может пролить свет на эту тему здесь.

4 ответов


предположим, что двоичный поиск определяется так:

def binary_search(a,b,e,x):
  n = e-b
  if n==0: return None
  m = b + int(n/2)
  if x<a[m]: return binary_search(a,b,m,x)
  if x>a[m]: return binary_search(a,m+1,e,x)
  return m

здесь

  • a-это массив значений -- assumed sorted
  • [b, e) - это диапазон от b до e, включая b, но не включая e, который мы ищем.
  • X-это значение, которое мы ищем

цель функции-вернуть i, где a[i]==x, если есть такое значение i, в противном случае не возвращайте None.

binary_search работает для диапазона нулевого размера:

  • Proof: если диапазон не содержит элементов, то n==0 и функция возвращает None, что является правильным.

предполагая, что binary_search работает для диапазона элементов любого размера от 0 до n, то двоичный поиск работает для диапазона элементов размера n+1.

  • доказательство:

    поскольку массив отсортирован, если x m. Это значит что нам только нужно искать диапазон [b, m). Диапазон [b,m) обязательно меньше диапазона [b, e), и мы предположили,что двоичный поиск работает для всех диапазонов размера меньше n+1, поэтому он будет работать для [b, m).

    Если x > a[m], то применяется аналогичная логика.

    Если x==a[m], то функция вернет m, что является правильным.


/** Return an index of x in a.
 *  Requires: a is sorted in ascending order, and x is found in the array a
 *  somewhere between indices left and right.
 */
int binsrch(int x, int[] a, int left, int right) {
  while (true) {
    int m = (left+right)/2;
    if (x == a[m]) return m;
    if (x < a[m])
    r = m−1;
    else
    l = m+1;
  }
}

пусть P (n)-утверждение, что binsrch работает правильно для входов, где right-left = n. Если мы можем доказать, что P(n) истинно для всех n, то мы знаем, что binsrch работает со всеми возможными аргументами.

Базовый Вариант. в случае, когда n=0, мы знаем left=right=m. Поскольку мы предположили, что функция будет вызываться только тогда, когда x находится между левым и правым, это должно быть так, что x = A[m], и поэтому функция вернет m, индекс x в массиве a.

Индуктивный Шаг. мы предполагаем, что binsrch работает до тех пор, пока left-right ≤ k. Наша цель-доказать, что он работает на входе, где left-right = k + 1. Есть три случая, где x = a[m], где x a[m].

    Case x = a[m]. Clearly the function works correctly.

    Case x < a[m]. We know because the array is sorted that x must be found between a[left] and a[m-1]. The n for the recursive call is n = m − 1 − left = ⌊(left+right)/2⌋ − 1 − left. (Note that ⌊x⌋ is the floor of x, which rounds it down toward negative infinity.) If left+right is odd, then n = (left + right − 1)/2 − 1 − left = (right − left)/2 − 1, which is definitely smaller than right−left. If left+right is even then n = (left + right)/2 − 1 − left = (right−left)/2, which is also smaller than k + 1 = right−left because right−left = k + 1 > 0. So the recursive call must be to a range of a that is between 0 and k cells, and must be correct by our induction hypothesis.

    Case x > a[m]. This is more or less symmetrical to the previous case. We need to show that r − (m + 1) ≤ right − left. We have r − (m + 1) − l = right − ⌊(left + right)/2⌋ − 1. If right+left is even, this is (right−left)/2 − 1, which is less than right−left. If right+left is odd, this is right− (left + right − 1)/2 − 1 = (right−left)/2 − 1/2, which is also less than right−left. Therefore, the recursive call is to a smaller range of the array and can be assumed to work correctly by the induction hypothesis. 

потому что во всех случаях индуктивный шаг работает, мы можем сделайте вывод, что binsrch (и его итеративный вариант) верны!

обратите внимание, что если бы мы допустили ошибку, кодируя случай x > a[m], и передали m как left вместо m+1 (легко сделать!), доказательство, которое мы только что построили, в этом случае потерпело бы неудачу. И на самом деле алгоритм может идти в бесконечный цикл, когда right = left + 1. Это показывает ценность тщательного индуктивного рассуждения.

ссылка : http://www.cs.cornell.edu/Courses/cs211/2006sp/Lectures/L06-Induction/binary_search.html


предположим, что отсортированный массив a[0...n]. Двоичный поиск работает рекурсивным разделением этого массива на три штук, средний элемент m, левой частью которого являются все элементы <= m (так как массив отсортирован по предположению) и правой частью которого являются все элементы >=m (опять же, потому, что массив отсортирован по предположению).

как сформулировать инвариант?

давайте сначала подумаем о как работает двоичный поиск. Если ключ (искомый элемент) равен k затем я сравниваю его со средним элементом m.

  1. если k = m, Я нашел свой товар (больше нечего делать)

  2. если k < m тогда я точно знаю, что если k появляется a, он не может появляться в правой части массива, потому что все элементы в той части массива >= m > k. Таким образом, он должен появиться (если это так) внутри левая часть массива.

  3. если k > m


вы должны доказать, что после каждого шага бинарного поиска arr[first] <= element <= arr[last]

из этого и завершения вы можете сделать вывод, что после завершения двоичного поиска arr[first] == element == arr[last]