Saltar a contenido

Myers Bit-Vector

## Introducción y fundamentos previos El enfoque clásico para el problema de la búsqueda aproximada (buscar el patron _P_ en un texto _T_ con no mas de _k_ errores) se basa en la programación dinámica, que construye una matriz D de tamaño *(M+1)*×*(N+1)* (donde _M_ es la longitud de _P_ y _N_ es la longitud de _T_), resultando en una complejidad temporal de _O(M×N)_. Si bien es robusto, este costo es prohibitivo para textos extensos. El algoritmo de Eugene Myers, publicado en 1999, introdujo un paradigma diferente. Utiliza paralelismo a nivel de bits para codificar el estado de la computación en los registros de la CPU. En lugar de calcular explícitamente cada celda de la matriz _D_, el algoritmo de Myers representa columnas enteras de la matriz (o sus diferencias) como vectores de bits y las actualiza en tiempo constante (para _M≤W_) o _O(M/W)_ (donde _W_ es el tamaño de palabra del procesador), logrando una aceleración masiva. ### Solución tradicional con programación dinámica El enfoque tradicional para la búsqueda aproximada se basa en el algoritmo de Wagner-Fischer, que calcula la distancia de Levenshtein (o distancia de edición). Resumidamente, utiliza la matriz de programación dinámica _D_ de tamaño *(M+1)*×*(N+1)* mencionada previamente. El valor en cada celda _D[i,j]_ representa el costo (distancia de edición) mínimo para encontrar una coincidencia entre los primeros i caracteres del patrón (_P[1..i]_) y una subcadena del texto que termina en _T[j]_. ### Ejemplo de uso (Patrón "MAR" vs texto "PAR") **Caso base:** La primera fila y primera columna se rellenan de la siguiente forma: _D[0,j]=0_, _D[i,0]=i_ **Siguientes elementos:** Para el resto de elementos d[i,j] en la matriz, se utiliza el siguiente criterio, fila por fila: $$ d[i,j] = min\begin{cases} D[i-1,j]+1 \\ D[i,j-1]+1 \\ D[i-1,j-1] + costo\_sust \end{cases} $$ donde: $$ costo\_sust=\begin{cases} 0, \text{ si } P[i]=T[j] \\ 1, \text{ si } P[i]\neq T[j] \end{cases} $$ ### Ejemplo ilustrado ![ejemplo](images/myers_aprox/example1.jpg) Si queremos por ejemplo, las coincidencias de la palabra "PAR" con no mas de un solo error en la palabra "MAR", bastaría con revisar los elementos de la ultima fila que tengan un número menor o igual a uno. Lo que significa que si la palabra PAR termina en ese elemento, entonces se cumplirá con el criterio. Como se ha mencionado previamente, el principal problema es la complejidad. Para encontrar las coincidencias, debe calcular el valor de cada una de las M×N celdas de la matriz. El cálculo de cada celda toma un tiempo constante _O(1)_, pero el número total de celdas es el producto de ambas longitudes, resultando en una complejidad temporal de _O(M×N)_. Esto es problemático debido a que el costo es multiplicativo. Si el texto es muy largo (ej. = 1.000 millones, como un genoma) y el patrón es moderadamente largo (ej. = 100), el número de operaciones es prohibitivo (100 × 1.000 millones = 100 mil millones de operaciones). El rendimiento se degrada linealmente con la longitud del patrón. La solución que propone el algoritmo de Myers implica una reducción completa del calculo de la matriz. El algoritmo no calcula los valores _D[i,j]_ directamente. En su lugar, explota la propiedad de que los valores en celdas adyacentes de la matriz _D_ difieren en una cantidad pequeña. ### Preliminares Antes de describir el algoritmo, se definen las **operaciones binarias** fundamentales utilizadas en las ecuaciones: | Símbolo | Nombre | Descripción | Ejemplo | | ---------- | ----------------------------- | -------------------------------------------------------------------- | ------------------------- | | `∨` | OR bit a bit | Devuelve 1 si al menos un bit de las dos posiciones comparadas es 1. | `1010 ∨ 1100 = 1110` | | `∧` | AND bit a bit | Devuelve 1 solo si ambos bits son 1. | `1010 ∧ 1100 = 1000` | | `¬` | NOT bit a bit | Invierte cada bit (0→1, 1→0). | `¬1010 = 0101` | | `⊕` | XOR bit a bit | Devuelve 1 si los bits son diferentes. | `1010 ⊕ 1100 = 0110` | | `+` | Suma binaria | Opera con acarreo entre bits (como una suma entera). | `0111 + 0001 = 1000` | | `<<` | Desplazamiento a la izquierda | Mueve todos los bits una posición hacia los más significativos. | `0011 << 1 = 0110` | | `bit_m(X)` | Bit más significativo | Bit en la posición del último carácter del patrón. | Si `X = 101`, `bit₃(X)=1` | ## Definición del algoritmo El algoritmo propone su solución partiendo de la siguiente premisa; la diferencia entre entradas adyacentes en la matriz de programación dinámica $D[i,j]$ ($\Delta$) es siempre un valor $\in \{-1, 0, 1\}$ Así, se definen principalmente tres tipos de propiedades: - Adyacencia horizontal $\Delta h_{i,j}=D_{i,j}-D_{i,j-1}\in \{-1,0,+1\}$ - Adyacencia vertical $\Delta v_{i,j}=D_{i,j}-D_{i-1,j}\in \{-1,0,+1\}$ - Diagonal $\Delta d_{i,j}=D_{i,j}-D_{i-1,j-1}\in \{0,+1\}$ A través de estas propiedades, se definen los siguientes vectores de bits o máscaras: - Delta vertical positivo: $VP_{ij}\equiv (\Delta v_{i,j}=+1$) - Delta vertical negativo: $VN_{ij}\equiv (\Delta v_{i,j}=-1$) - Delta horizontal positivo: $HP_{ij}\equiv (\Delta h_{i,j}=+1$) - Delta horizontal negativo: $HN_{ij}\equiv (\Delta h_{i,j}=-1$) - Delta diagonal cero: $D0_{ij}\equiv (\Delta d_{i,j}=0$) Estas cinco máscaras codifican completamente la evolución de la matriz $D[i,j]$ a través de relaciones booleanas entre sí. A continuación se detallaran cuales son estas relaciones y su significado. Las siguientes equivalencias definen cómo se relacionan las deltas verticales, horizontales y diagonales entre posiciones adyacentes de la matriz, Estas fórmulas son el núcleo lógico del algoritmo y permiten calcular toda una columna mediante operaciones de bits. ### Relación entre `HN`, `VP` y `D0` $$HN_{i,j} \Leftrightarrow VP_{i,j-1} \land D0_{i,j}$$ Si el valor de una celda es menor que el de su izquierda (Δh = −1), entonces la celda anterior en la misma fila debió haber tenido Δv = +1 y su diagonal inmediata no cambió (D0 = 1). ### Relación entre `VN`, `HP` y `D0` $$VN_{i,j} \Leftrightarrow HP_{i-1,j} \land D0_{i,j}$$ Una disminución vertical (Δv = −1) ocurre cuando arriba hay una celda con incremento horizontal (Δh = +1) y la diagonal actual es igual (D0 = 1). ### Relación entre `HP`, `VN`, `VP` y `D0` $$HP_{i,j} \Leftrightarrow VN_{i,j-1} \lor \neg(VP_{i,j-1} \lor D0_{i,j})$$ Hay un aumento horizontal (Δh = +1) si: - la celda anterior tenía Δv = −1, o - la celda anterior **no tenía ni** Δv = +1 **ni** D0 = 1 (es decir, si el valor a la izquierda era menor). ### Relación entre `VP`, `HN`, `HP` y `D0` $$VP_{i,j} \Leftrightarrow HN_{i-1,j} \lor \neg(HP_{i-1,j} \lor D0_{i,j})$$ Se produce un aumento vertical (Δv = +1) si: - la celda de arriba tenía Δh = −1, o - la celda de arriba no tenía ni Δh = +1 ni D0 = 1. ### Relación entre `D0`, coincidencias y deltas $$D0_{i,j} \Leftrightarrow (p_i = t_j) \lor VN_{i,j-1} \lor HN_{i-1,j}$$ Dos celdas consecutivas tienen el mismo valor (Δd = 0) si los caracteres coinciden ($p_i = t_j$), o el valor se propagó desde la izquierda con Δh = −1, o desde arriba con Δv = −1. ### Codificación de caracteres Adicionalmente, el algoritmo propone una codificación para cada carácter $c\in P$ del patrón $P$ de largo $m$: Cada codificación tendrá $m$ valores binarios que se determinarán, de derecha a izquierda de la siguiente forma para cada $i\in \{1,...m\}$: - 1, si $c_i=p_i$ - 0, si $c_i \neq p_i$ Ejemplo con el patrón MAR: - $M\rightarrow 001$ - $A\rightarrow 010$ - $R\rightarrow 100$ A esta codificación se le conoce como máscara de coincidencia ($Eq[t_j]$). Notar que si existe algún carácter en el texto pero no en el patrón, entonces su máscara de coincidencia es un vector de ceros de tamaño $m$. ### ¿Como utilizamos esta información para calcular equivalentemente la distancia de edición? #### Resolución de dependencias Como puede observarse en las relaciones anteriores, las variables `VP`, `VN`, `HP`, `HN` y `D0` están interdependientes. Por ejemplo, `D0` depende de `HN`, que depende de `VP`, y esta última depende nuevamente de `D0`. Para romper este ciclo y permitir el cálculo directo de toda una columna, Myers propuso una formulación cerrada para `D0`. Sea: - `X` el vector que marca coincidencias o propagaciones negativas desde la izquierda: $$ X = Eq[t_j] \lor VN_{i,j-1} $$ - `Y` el vector que marca incrementos verticales desde la posición anterior: $$ Y = VP_{i,j-1} $$ El valor de `D0` puede calcularse **bit a bit** como: $$ D0 = (((X \land Y) + Y) \oplus Y) \lor X $$ Esta ecuación constituye el **núcleo algebraico del algoritmo de Myers**, y se basa en el hecho de que la operación de suma binaria `(+)` propaga los bits de acarreo hacia arriba, simulando así la propagación de coincidencias diagonales en la matriz de programación dinámica. En términos intuitivos: - `X` representa las coincidencias o “correcciones” que reducen la distancia. - `Y` representa los posibles incrementos acumulados en la columna anterior. - La suma `(X ∧ Y) + Y` permite extender coincidencias en secuencia a través de bits adyacentes, de la misma manera que una cadena de ceros se propaga en una resta binaria. ### Cálculo de los vectores auxiliares Una vez obtenido `D0`, el resto de los vectores puede actualizarse en una sola pasada mediante operaciones lógicas a nivel de bit: $$ \begin{aligned} HP &= VN \lor \neg(VP \lor D0) \\ HN &= VP \land D0 \\ VP' &= HN \lor \neg(HP \lor X) \\ VN' &= HP \land X \end{aligned} $$ Cada línea de este conjunto de ecuaciones describe cómo los cambios horizontales y verticales se propagan hacia la siguiente columna (`VP'`, `VN'`). Así, cada paso del algoritmo avanza una columna completa en el texto `T`, manteniendo el estado del patrón `P` comprimido en una palabra de bits. ### Actualización del puntaje de edición Una vez que se actualizan `VP` y `VN`, el algoritmo puede determinar la diferencia en el valor de la última fila (`D[M,j]`), correspondiente al costo total acumulado hasta el carácter `t_j`. Para ello, se utiliza el bit más significativo (correspondiente a `p_M`) de los vectores `HP` y `HN`: $$ \text{si } HP[M] = 1 \Rightarrow \text{score} \leftarrow \text{score} + 1 $$ $$ \text{si } HN[M] = 1 \Rightarrow \text{score} \leftarrow \text{score} - 1 $$ Este puntaje (`score`) representa la **distancia de edición** entre el patrón `P` y el prefijo del texto que termina en `t_j`. Así, los pasos de ejecución del algoritmo son los siguientes: #### Paso 1: Inicialización Entradas del algoritmo: - Patron P[i,..m] - Texto T[1,...n] - tolerancia $k$
Para cada carácter c ∈ Σ:
    Eq[c] ← vector binario de longitud m
    Para i = 1 hasta m:
        Eq[c][i] = 1  si  P[i] = c
        Eq[c][i] = 0  en caso contrario
    VP ← (1...1)_m     // todos los bits en 1
    VN ← (0...0)_m     // todos los bits en 0
    score ← m           // distancia inicial = longitud del patrón
#### Paso 2: Búsqueda
Para cada carácter del texto $T[j]$ (con j desde 1 hasta n):
    Eqj ← Eq[T[j]]
    X ← Eqj ∨ VN
    D0 ← (((X ∧ VP) + VP) ⊕ VP) ∨ X
    HN ← VP ∧ D0
    HP ← VN ∨ ¬(VP ∨ D0)
    si bit_m(HP) = 1  → score ← score + 1
    si bit_m(HN) = 1  → score ← score - 1
    VP' ← (HN << 1) ∨ ¬((HP << 1) ∨ X)
    VN' ← (HP << 1) ∧ X
    VP ← VP'
    VN ← VN'
    si score ≤ k → agregar j a lista de coincidencias
### Ejemplo ilustrado (Búsqueda de patrón "MAR" en el texto "PAR") ![ejemplo](images/myers_aprox/example2.jpg) ## Implementación del algoritmo
#include <bits/stdc++.h>
using namespace std;

using u64 = uint64_t;
static constexpr int W = 64;

string bits_to_string(u64 x, int m) {
    string s;
    for (int i = m - 1; i >= 0; --i)
        s.push_back((x >> i) & 1ULL ? '1' : '0');
    return s;
}

inline pair<u64, int> shl_with_carry(u64 x, int carry_in) {
    int carry_out = (int)((x >> (W - 1)) & 1ULL);
    u64 res = (x << 1) | (u64)carry_in;
    return {res, carry_out};
}

vector<u64> build_masks(int m) {
    int nb = (m + W - 1) / W;
    vector<u64> mask(nb, ~0ULL);
    if (m % W) {
        int lastbits = m % W;
        mask[nb - 1] = (1ULL << lastbits) - 1ULL;
    }
    return mask;
}

vector<vector<u64>> build_Eq(const string &pattern) {
    int m = (int)pattern.size();
    int nb = (m + W - 1) / W;
    vector<vector<u64>> Eq(256, vector<u64>(nb, 0ULL));
    for (int i = 0; i < m; ++i) {
        unsigned char c = (unsigned char)pattern[i];
        int b = i / W;
        int pos = i % W;
        Eq[c][b] |= (1ULL << pos);
    }
    return Eq;
}

vector<int> myers_search_debug(const string &text, const string &pattern, int k) {
    int n = (int)text.size();
    int m = (int)pattern.size();
    int nb = (m + W - 1) / W;

    auto mask = build_masks(m);
    auto Eq = build_Eq(pattern);

    // === Paso 1: Inicialización ===
    vector<u64> VP(nb), VN(nb);
    for (int b = 0; b < nb; ++b) {
        VP[b] = mask[b]; // VP ← (1...1)
        VN[b] = 0ULL;    // VN ← (0...0)
    }

    int score = m;
    vector<int> occ;

    vector<u64> HP(nb), HN(nb), D0(nb), Eqj(nb), X(nb);
    vector<u64> VP_next(nb), VN_next(nb);

    cout << "=== Algoritmo Myers Bit-Vector ===\n";
    cout << "Patrón: \"" << pattern << "\" (" << m << " chars)\n";
    cout << "Texto : \"" << text << "\" (" << n << " chars)\n\n";

    // === Paso 2: Iteración principal ===
    for (int j = 0; j < n; ++j) {
        unsigned char tc = (unsigned char)text[j];

        cout << "Iteración " << j + 1 << "\n";
        cout << string(70, '-') << "\n";

        for (int b = 0; b < nb; ++b) {
            Eqj[b] = Eq[tc][b];

            // === 1. Calcular X y D0 ===
            X[b] = Eqj[b] | VN[b];
            u64 tmp = X[b] & VP[b];
            u64 add = tmp + VP[b];
            D0[b] = (add ^ VP[b]) | X[b];
            D0[b] &= mask[b];

            // === 2. Calcular HN y HP ===
            HN[b] = VP[b] & D0[b];
            HP[b] = VN[b] | (~(VP[b] | D0[b]) & mask[b]);
        }

        // === 3. Actualizar el puntaje ===
        int last_block = nb - 1;
        int last_pos = (m - 1) % W;
        int bit_hp = (int)((HP[last_block] >> last_pos) & 1ULL);
        int bit_hn = (int)((HN[last_block] >> last_pos) & 1ULL);
        if (bit_hp) score++;
        else if (bit_hn) score--;

        // === 4. Mostrar información ===
        int bits_to_show = (m < 64) ? m : 64;
        cout << "Codificación del carácter '" << text[j] << "' (Eq): "
             << bits_to_string(Eqj[0], bits_to_show) << "\n";
        cout << "D0  = " << bits_to_string(D0[0], bits_to_show) << "\n";
        cout << "HN  = " << bits_to_string(HN[0], bits_to_show) << "\n";
        cout << "HP  = " << bits_to_string(HP[0], bits_to_show) << "\n";
        cout << "VN  = " << bits_to_string(VN[0], bits_to_show) << "\n";
        cout << "VP  = " << bits_to_string(VP[0], bits_to_show) << "\n";
        cout << "Score actual = " << score << "\n";

        // === 5. Calcular VN' y VP' ===
        int carry_in_hp = 0, carry_in_hn = 0;
        for (int b = 0; b < nb; ++b) {
            auto [HP_shift, c1] = shl_with_carry(HP[b], carry_in_hp);
            auto [HN_shift, c2] = shl_with_carry(HN[b], carry_in_hn);
            carry_in_hp = c1;
            carry_in_hn = c2;

            VN_next[b] = (HP_shift & X[b]) & mask[b];
            VP_next[b] = (HN_shift | (~(HP_shift | X[b]) & mask[b])) & mask[b];
        }

        // === 6. Actualizar los vectores ===
        VN = VN_next;
        VP = VP_next;

        // === 7. Registrar coincidencias si aplica ===
        if (score <= k) occ.push_back(j);

        cout << "------------------------------------------\n\n";
    }

    // === Resultado final ===
    cout << "\n=== Resultado ===\n";
    cout << "Score final: " << score << "\n";
    cout << "Coincidencias (índices finales): ";
    for (int p : occ)
        cout << p << " ";
    cout << "\n";

    return occ;
}
## Aplicaciones del algoritmo Una de las aplicaciones más relevantes del algoritmo de Myers es la identificación de variaciones genéticas y mutaciones puntuales en secuencias de ADN. El genoma humano contiene aproximadamente 3 mil millones de pares de bases, y la búsqueda eficiente de patrones con errores es fundamental en genómica. **Ejemplo:** Consideremos la búsqueda de la secuencia del gen BRCA1 (implicado en cáncer de mama) con hasta 2 mutaciones permitidas:
Patrón (BRCA1 - fragmento): ATGCTAGTCG
Secuencia en genoma: ...ATGCCAGTCG...ATGCGAGTCG...
En la primera posición encontramos una mutación (C→C en lugar de T en la posición 4), y en la segunda encontramos dos mutaciones (C→G en la posición 4 y A→A en la posición 5). Con el algoritmo Myers, estas variaciones se identifican en tiempo lineal O(n) en lugar de O(m×n). ## Referencias 1. **Eugene W. Myers.** _A Fast Bit-Vector Algorithm for Approximate String Matching._ _Journal of the ACM (JACM)_, Vol. 46, No. 3, 1999, pp. 395–415. DOI: [10.1145/316542.316550](https://doi.org/10.1145/316542.316550) 2. **Eugene W. Myers.** _Bit-Parallel string matching— Verification Presentation._ Documento técnico complementario Presentación [https://www.mi.fu-berlin.de/wiki/pub/ABI/RnaSeqP4/myers-bitvector-verification.pdf](https://www.mi.fu-berlin.de/wiki/pub/ABI/RnaSeqP4/myers-bitvector-verification.pdf)