Búsqueda binaria
La búsqueda binaria (binary search) es un método que permite buscar algo más rápido al partir el intervalo de búsqueda en dos. Su aplicación más común es buscar valores en arreglos ordenados; no obstante, la idea de partir el intervalo es crucial en muchas otras tareas típicas.
Búsqueda en arreglos ordenados
El problema más típico que conduce a la búsqueda binaria es el siguiente. Dado un arreglo ordenado , hay que comprobar si está presente en la secuencia. La solución más simple sería revisar cada elemento uno por uno y compararlo con (una llamada búsqueda lineal). Este enfoque funciona en , pero no aprovecha el hecho de que el arreglo está ordenado.
Búsqueda binaria del valor en un arreglo.
La imagen de AlwaysAngry se distribuye bajo la licencia CC BY-SA 4.0 .
Supongamos ahora que conocemos dos índices tales que . Como el arreglo está ordenado, podemos deducir que o bien aparece entre o bien no aparece en el arreglo en absoluto. Si elegimos un índice arbitrario tal que y comprobamos si es menor o mayor que , tenemos dos casos posibles:
- . En este caso, reducimos el problema de a ;
- . En este caso, reducimos el problema de a .
Cuando no es posible elegir , es decir, cuando , comparamos directamente con y . En caso contrario, querríamos elegir de modo que reduzca el segmento activo a un solo elemento lo más rápido posible en el peor caso.
En el peor caso siempre reduciremos al segmento más grande entre y . Así, en el peor caso la reducción sería de a . Para minimizar este valor, debemos elegir , y entonces
En otras palabras, desde la perspectiva del peor caso es óptimo elegir siempre en el medio de y partirlo por la mitad. Así, el segmento activo se reduce a la mitad en cada paso hasta que queda de tamaño . Por lo tanto, si el proceso necesita pasos, al final reduce la diferencia entre y de a , lo que nos da la ecuación .
Tomando en ambos lados, obtenemos .
Un número logarítmico de pasos es drásticamente mejor que el de la búsqueda lineal. Por ejemplo, para harían falta aproximadamente un millón de operaciones con búsqueda lineal, pero solo alrededor de operaciones con la búsqueda binaria.
Lower bound y upper bound
A menudo conviene hallar la posición del primer elemento que es mayor o igual que (llamada lower bound o cota inferior de en el arreglo) o la posición del primer elemento que es estrictamente mayor que (llamada upper bound o cota superior de ), en lugar de la posición exacta del elemento.
Juntas, la lower bound y la upper bound producen un semiintervalo, posiblemente vacío, de los elementos del arreglo que son iguales a . Para comprobar si está presente en el arreglo basta hallar su lower bound y verificar si el elemento correspondiente es igual a .
Implementación
La explicación anterior da una descripción aproximada del algoritmo. Para los detalles de implementación necesitamos ser más precisos.
Mantendremos un par tal que . Esto significa que el intervalo activo de búsqueda es . Usamos un semiintervalo en lugar de un segmento porque resulta requerir menos trabajo con casos borde.
Cuando , de las definiciones anteriores se deduce que es la upper bound de . Es conveniente inicializar con el índice past-the-end (uno más allá del final), es decir , y con el índice anterior al comienzo, es decir . Esto está bien mientras nunca evaluemos ni de forma directa en el algoritmo, tratándolos formalmente como y .
Por último, para concretar el valor de que elegimos, nos quedaremos con .
Entonces la implementación podría verse así:
... // un arreglo ordenado está guardado como a[0], a[1], ..., a[n-1]
int l = -1, r = n;
while (r - l > 1) {
int m = (l + r) / 2;
if (k < a[m]) {
r = m; // a[l] <= k < a[m] <= a[r]
} else {
l = m; // a[l] <= a[m] <= k < a[r]
}
}Durante la ejecución del algoritmo nunca evaluamos ni , ya que . Al final, será el índice del último elemento que no es mayor que (o si no existe tal elemento) y será el índice del primer elemento mayor que (o si no existe tal elemento).
Nota. Calcular m como m = (r + l) / 2 puede provocar desbordamiento si l y r son dos enteros positivos, y este error vivió unos 9 años en el JDK, como se describe en el blogpost . Algunos enfoques alternativos incluyen, por ejemplo, escribir m = l + (r - l) / 2, que siempre funciona para enteros positivos l y r, pero aún puede desbordar si l es un número negativo. Si se usa C++20, ofrece una solución alternativa en la forma m = std::midpoint(l, r), que siempre funciona correctamente.
Búsqueda sobre un predicado arbitrario
Sea una función booleana definida en que es monótona creciente, es decir
La búsqueda binaria, tal como se describió arriba, halla la partición del arreglo según el predicado , que guarda el valor booleano de la expresión . Es posible usar un predicado monótono arbitrario en lugar de . Resulta particularmente útil cuando el cálculo de requiere demasiado tiempo como para computarlo para cada valor posible. En otras palabras, la búsqueda binaria halla el único índice tal que y si existe tal punto de transición, o nos da si o si .
Demostración de corrección suponiendo que existe un punto de transición, es decir y : la implementación mantiene el invariante del bucle . Cuando , la elección de implica que siempre decrece. El bucle termina cuando , lo que nos da el punto de transición deseado.
... // f(i) es una función booleana tal que f(0) <= ... <= f(n-1)
int l = -1, r = n;
while (r - l > 1) {
int m = (l + r) / 2;
if (f(m)) {
r = m; // 0 = f(l) < f(m) = 1
} else {
l = m; // 0 = f(m) < f(r) = 1
}
}Búsqueda binaria sobre la respuesta
Esta situación ocurre a menudo cuando se nos pide calcular algún valor, pero solo somos capaces de comprobar si ese valor es al menos . Por ejemplo, se nos da un arreglo y se nos pide hallar el máximo promedio entero (floored average)
entre todos los pares posibles tales que . Una de las formas simples de resolver este problema es comprobar si la respuesta es al menos , es decir, si existe un par tal que se cumple lo siguiente:
De forma equivalente, se reescribe como
de modo que ahora hay que comprobar si existe un subarreglo del nuevo arreglo de longitud al menos con suma no negativa, lo cual se puede hacer con algunas sumas de prefijos.
Búsqueda continua
Sea una función real continua en un segmento .
Sin pérdida de generalidad, supongamos que . Del teorema del valor intermedio se sigue que para cualquier existe tal que . Nótese que, a diferencia de los párrafos anteriores, no se exige que la función sea monótona.
El valor puede aproximarse hasta en tiempo para cualquier valor específico de . La idea es esencialmente la misma: si tomamos , podremos reducir el intervalo de búsqueda a o a según si es mayor que . Un ejemplo común aquí sería hallar raíces de polinomios de grado impar.
Por ejemplo, sea . Entonces y cuando y . Esto significa que siempre es posible hallar un suficientemente pequeño y un suficientemente grande tales que y . Entonces, con búsqueda binaria es posible hallar un intervalo arbitrariamente pequeño que contenga un tal que .
Búsqueda con potencias de 2
Otra forma destacable de hacer búsqueda binaria es, en lugar de mantener un segmento activo, mantener el puntero actual y la potencia actual . El puntero empieza en y luego, en cada iteración, se evalúa el predicado en el punto . Si el predicado sigue siendo , el puntero avanza de a ; en caso contrario se queda igual, y después la potencia se disminuye en .
Este paradigma se usa mucho en tareas sobre árboles, como hallar el ancestro común más bajo (LCA) de dos vértices o hallar un ancestro de un vértice específico que tenga cierta altura. También se puede adaptar, por ejemplo, para hallar el -ésimo elemento no nulo en un Árbol de Fenwick.
Problemas para practicar
- LeetCode - Find First and Last Position of Element in Sorted Array
- LeetCode - Search Insert Position
- LeetCode - First Bad Version
- LeetCode - Valid Perfect Square
- LeetCode - Find Peak Element
- LeetCode - Search in Rotated Sorted Array
- LeetCode - Find Right Interval
- Codeforces - Interesting Drink
- Codeforces - Magic Powder - 1
- Codeforces - Another Problem on Strings
- Codeforces - Frodo and pillows
- Codeforces - GukiZ hates Boxes
- Codeforces - Enduring Exodus
- Codeforces - Chip ‘n Dale Rescue Rangers
- Codeforces - Points on Line