6  Algoritmos de intersección y unión de conjuntos en el modelo de comparación

Objetivo

Analizar el rendimiento de algoritmos de unión e intersección de conjuntos representados como listas ordenadas parametrizando los algoritmos con los algoritmos internos de búsqueda, tamaño de los conjuntos y la distribución de los elementos, bajo un enfoque experimental midiendo los costos en términos del tiempo de ejecución y el uso de memoria.

6.1 Problema

Cómo se vió en Capítulos anteriores, un conjunto es una colección de elementos donde no hay repetición. El uso de conjuntos es fundamental para un gran número de problemas. En particular, en este capítulo representaremos conjuntos como arreglos ordenados de números enteros; esto para posicionarlo dentro de un dominio de aplicación objetivo, que es la Recuperación de Información, como parte de la representación de una la matriz dispersa muy grande, llamada índice invertido.

Estaremos resolviendo los problemas de intersección y unión de conjuntos. Demaine, López-Ortiz, y Munro (2000) demuestra que el costo y procedimiento de las intersecciones y uniones de conjuntos representados como arreglos ordenados, es básicamente el mismo; ya que requieren determinar la misma información. Claramente, colectar los datos para la unión y la intersección, requieren diferentes esfuerzos.

6.1.1 Costo del problema

En general, dados dos conjuntos \(A[1..m] = \{a_1 < a_2 < \cdots < a_m \}\) y \(B[1..n] = \{b_1 < b_2 < \cdots < b_n \}\), el costo de unión es \[\log{m+n \choose m},\] ver Hwang y Lin (1971).

De manera más detallada, supongamos que \(A \cap B = \emptyset\), esto es, el conjunto de salida será de tamaño \(m+n\). De manera similar al razonamiento que se utilizó para el problema de ordenamiento, el problema puede verse como todas las posibles instancias de ordenes o permutaciones de tamaño \(n+m\); removiendo la necesidad de los ordenes parciales, esto es \({n+m \choose m}\) posibles instancias de tamaño \(n+m\), generadas por dos conjuntos de tamaño \(n\) y \(m\). Dado que estamos en un modelo basado en comparaciones, y dado el mejor algoritmo \(s\) puede dividir el espacio de posibles ordenes en 2, por tanto, dicho algoritmo necesitará \[\log_2 {n+m \choose m}\] comparaciones para resolver la unión de cualquier par de conjuntos de tamaño \(m\) y \(n\).

Usando la aproximación de Stirling para coefficientes binomiales de MacKay (2003), el costo se convierte en: \[\log{m+n \choose m} = n\log{\frac{m+n}{n}} + m \log\frac{n+m}{m} \]

Recuerde que \(\log_2 x = \frac{\log_e x}{\log e}\).

6.2 Algoritmos

Se puede observar que si \(m \approx n\), entonces el costo se convierte en \(O(m + n)\), esto es, lo más eficiente sería tomar el siguiente algoritmo:


function merge2!(C, A, B)
    i = j = 1
    m, n = length(A), length(B)
    
    @inbounds while i <= m ||  j <= n
        a, b = A[i], B[j]
        if a == b
            push!(C, a)
            i += 1
            j += 1
        elseif a < b
            push!(C, a)
            i += 1
        else
            push!(C, b)
            j += 1
        end
    end

    C
end
merge2! (generic function with 1 method)

6.2.1 Ejercicio

  • Escriba y pruebe el algoritmo de intersección de merge para \(n \approx m\).

6.3 Algoritmos para arreglos de tamaño muy diferente

Si \(m \ll n\), el costo tenderá a \(O(m \log n)\), por lo que se pueden realizar \(m\) búsquedas binarias directas para localizar la posición de inserción en \(B\).

lista A B a1 1 a2 2 a3 3 a4 4 a5 5 a6 6 a7 7 a8 8 a9 9 a10 10 a11 11 a12 12 a13 13 a15 15 a16 16 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 10 10 11 11 12 12 13 13 15 15 16 16
Figura 6.1: Dos listas alineadas donde los nodos sombreados son elementos de los conjuntos.

Se hace notar que \(A\) y \(B\) estan ordenados, y por lo tanto, localizar \(A[i]\) en \(B[j]\) significa que \(B[j-1] < A[i]\), por lo que intentar localizar \(A[i+1]\) puede comenzar en \(B[j+1]\). A continuación se muestra el código de un algoritmo de intersección usando algoritmos de búsqueda con memoria de la posición anterior.

function intsearch!(C, A, B, algosearch=doublingsearch)
    if length(B) < length(A)
        A, B = B, A
    end

    p = 1
    for (i, a) in enumerate(A)
        p = algosearch(B, a, p)
        p > length(B) && break
        if a == B[p]
            p += 1
            push!(C, a)
        end
    end

    C
end

Hwang y Lin (1971) propone otro algoritmo que funciona para casos similares:

  1. Divide \(B\) en bloques de tamaño \(m\), define un arreglo virtual \(B'[1..n/m]\) donde \(B'[i] = B[i \cdot m]\)
  2. Se búsca la posición de inserción \(p\) de cada \(a \in A\) en \(B'\), costando \(\log n/m\) para cada búsqueda.
  3. Después se localiza dentro del \(B\) en el \(p\)-ésimo bloque, i.e., \(B[(p-1)m + 1 .. p\cdot m]\), por la posición de inserción del bloque, con un costo de \(\log m\).

Entonces, se obtiene un costo de \(O(m \log{n/m} + m \log m)\); esto es equivalente en el peor caso a búsquedas directas, i.e., las posiciones de inserción de \(a \in A\) se encuentran distribuidas de manera uniforme en \(B\). Sin embargo, es posible mejorar si se descartan bloques en el paso 1. Esto es, si se hay concentración de elementos de \(A\) en bloques de \(B\). Para esto, es necesario un análisis de costo promedio, el cual se muestra en el artículo.

Incluso cuando hay concentración, podemos recordar la \((i-1)\) posición de inserción para iniciar la \(i\)-ésima búsqueda, y sacar provecho de posiciones esperadas cercanas de la posición inicial de búsqueda, i.e., podemos utilizar algoritmos de búsqueda adaptables para mejorar el desempeño.

6.3.1 Algoritmo de Baeza Yates

Baeza-Yates (2004) propone un algoritmo eficiente para intersecciones de dos conjuntos. El algoritmo tiene una estrategía dividir para vencer:

  1. Se toma la mediana \(M\) de \(A\) y se busca en \(B\) obteniendo su posición de inserción \(p\).
  2. El problema entonces se divide en 3 subproblemas: \[\begin{align} C_< &= \{A[1..M-1] \cap B[1..p-e]\} \\ C_= &= \{A[M]\} \cap \{B[p]\} \\ C_> &= \{A[M+1..m] \cap B[p+e..n]\} \\ \end{align}\] donde \(e=1\) si \(A[M] = B[p]\) y \(e=0\) cuando \(A[M] \not= B[p]\).
  3. La unión de estos tres conjuntos es la solución \(C_< \cup C_= \cup C_>\).
  4. El problema \(C_=\) es trivial, y \(C_<\) y \(C_>\) se implementan recurriendo, ajustando los rangos de trabajo.

A continuación se muestra el código en Julia, usando los algoritmos de búsqueda del Cap. 5.

# Adaptado de https://github.com/sadit/Intersections.jl

1function baezayates!(output, A, B, findpos::Function=binarysearch)
    baezayates!(output, A, 1, length(A), B, 1, length(B), findpos)
end

function baezayates!(output, A, a_sp::Int, a_ep::Int, B, b_sp::Int, b_ep::Int, findpos::Function)
    (a_ep < a_sp || b_ep < b_sp) && return output
    imedian = ceil(Int, (a_ep + a_sp) / 2)
    median = A[imedian]
    ## our findpos returns n + 1 when median is larger than B[end]
2    medpos = min(findpos(B, median, b_sp), b_ep)
    
    matches = median == B[medpos] 
3    baezayates!(output, A, a_sp, imedian - 1, B, b_sp, medpos - matches, findpos)
4    matches && push!(output, median)
5    baezayates!(output, A, imedian + 1, a_ep, B, medpos + matches, b_ep, findpos)
    output
end
1
Punto de entrada.
2
Búsqueda de la posición de inserción de la mediana de \(A\) en \(B\).
3
Recurrencia para el problema \(C_<\).
4
Añadir al resultado el valor de la mediana si es que se encontró en \(B\); es importante que este paso este entre las recurrencias para que output sea un arreglo ordenado.
5
Recurencia para el problema \(C_>\).

El algoritmo de Baeza Yates es óptimo en el peor caso y es capaz de aprovechar casos donde \(C_<\) o \(C_>\) se convierten en triviales, lo cual da muy buenos casos en algunas distribuciones.

6.3.2 Ejercicios

  1. Implemente la unión con el algoritmo de Baeza Yates.

6.4 Operaciones con tres o más conjuntos

Los algoritmos y costos hasta ahora revisados se cumplen para dos conjuntos; se mencionaron diferentes algoritmos, algunos de ellos especializados por características como las proporciones de los conjuntos de entrada.

En particular, es importante hacer notar que ni el problema ni las aplicaciones estan limitadas a dos conjuntos, y por tanto, es importante algoritmos y estrategías para resolver \(\bigcup_i A_i\) así como \(\bigcap_i A_i\).

6.4.1 Algoritmo SvS

Dado \(C = A \cap B\) es un hecho que \(|C| \leq min \{|A|, |B|\}\). Recordando, que hay maneras relativamente simples y eficientes de resolver la intersección cuando \(m \ll n\); por tanto, cuando tenemos más de dos conjuntos podemos aplicar la estrategía Small vs Small (SvS), que consisten en intersectar los \(k\) conjuntos por pares intersectando el par de arreglos más pequeños cada vez.

# Adaptado de https://github.com/sadit/Intersections.jl

function svs(L::Vector{T}, in2::Function=baezayates!) where T
    prev, curr = eltype(T)[], eltype(T)[]
    sort!(L, by=length, rev=true)
    curr = pop!(L)

    while length(L) > 0
        empty!(prev)
        isize = in2(prev, curr, pop!(L))
        isize == 0 && return prev
        prev, curr = curr, prev
    end

    curr
end

6.4.2 Algoritmo de Barbay y Kenyon

Existe otra familia de algoritmos, basados en búsquedas adaptativas que pueden llegar a mejorar el desempeño bajo cierto tipo de entradas. Demaine, López-Ortiz, y Ian Munro (2001), Barbay, López-Ortiz, y Lu (2006), y Barbay et al. (2010) muestran algoritmos de intersección basados en búsqueda adaptables para aprovechar instancias simples. Estos estudios se basan en contribuciones teóricas de los mismos autores: Demaine, López-Ortiz, y Munro (2000), Demaine, López-Ortiz, y Ian Munro (2001), Barbay y Kenyon (2002) y Baeza-Yates (2004).

El algoritmo de Barbay, López-Ortiz, y Lu (2006) trabaja sobre los \(k\) conjuntos de entrada, representados como arreglos ordenados de números enteros. Es un algoritmo simple pero poderoso: hace uso de búsquedas adaptivas con memoria para guardar las posiciones donde se avanza, de tal forma que no se recalculen posiciones. Las diferentes estrategias para revisar los conjuntos pueden dar diferentes desempeños, como se valida en Barbay et al. (2010), donde además de hacer una gran variedad de experimentos sobre diferentes algoritmos de búsqueda, se introducen variantes en el orden de acceso de cada conjunto.

A continuación se muestra el código del algoritmo base:

# Adaptado de https://github.com/sadit/Intersections.jl

function bk!(output, L::AbstractVector, findpos::Function=doublingsearch)
    P = ones(Int, length(L))
    bk!(output, L, P, findpos)
end
 
1function bk!(output, L, P, findpos::Function=doublingsearch)
2    n = length(L)
3    el = L[1][1]
4    c = 0

    @inbounds while true
        for i in eachindex(P)
5            P[i] = findpos(L[i], el, P[i])
            P[i] > length(L[i]) && return output
            pval = L[i][P[i]]
            if pval == el
                c += 1
6                if c == n
                    push!(output, el)
7                    c = 0
                    P[i] += 1
                    P[i] > length(L[i]) && return output
                    el = L[i][P[i]]
                end
            else
                c = 0
                el = pval
            end
        end
    end

    output
end
1
El algoritmo de Barbay & Kenyon recibe: i) output el conjunto de salida. ii) L la lista de conjuntos (representados como arreglos ordenados). iii) P arreglo de posiciones actuales para cada arreglo. iv) findpos` función de búsqueda.
2
Número de conjuntos en L.
3
el es el elemento siendo búscado en todos los arreglos.
4
c número de listas que contienen el.
5
Búscando la posición de inserción de el en L[i], comenzando en P[i].
6
Esta igualdad implica que hay interección.
7
Reiniciando el y c y actualizando P[i].

De manera particular, Barbay et al. (2010) presentan un estudio experimental sobre los algoritmos presentados en el área durante la decada de 2000 a 2010, dichos algoritmos se parametrizaron de maneras que nos permiten aprender diferentes características de cada uno de ellos, dependiendo de los algoritmos de búsqueda que usan, la arquitectura computacional donde se evalúa, y el número de conjuntos siendo procesados.

6.5 Recursos audio-visuales de la unidad

Parte 1: Algoritmos de intersección (y unión) de listas ordenadas

Parte 2: Algoritmos de intersección y algunas aplicaciones

6.6 Actividades

Implementación y comparación de diferentes algoritmos de intersección de conjuntos.

Lea cuidadosamente las instrucciones y desarrolle las actividades. Entregue el reporte correspondiente en tiempo.