O Rabin-Karp realmente precisa que eu me importe em aplicar uma operação mod Q nos hashes de rolamento?

8

Eu tenho lido sobre o algoritmo Rabin Karp e fiquei pensando qual é o grande problema em manter nossos valores de hashes rotativos limitados por um valor Q?

Eu pensava que, como nossa representação de número inteiro no computador típico é de 2 complementos, na verdade é exatamente equivalente a limitar todas as nossas operações sobre os hashes rotativos por 2 ^ 31, de modo que, em outras palavras, eu simplesmente não deveria me importar. Além disso, quanto menor o limite ou o hash, mais colisões teríamos, portanto, um Q maior seria igual ao desempenho aprimorado!

Eu tentei codificar uma implementação simples (Java):

public static int rabinKarp(String text, String pattern) {
    if (text.length() < pattern.length()) {
        return -1;
    } else {
        int patternHash = 0;
        int textHash = 0;
        int pow = 1;

        // preprocessing the pattern and the first characters of the text string
        for (int i = pattern.length()-1; i >= 0; --i) {
            patternHash += pattern.charAt(i) * pow;
            textHash += text.charAt(i) * pow;
            pow *= 10;
        }
        pow /= 10;

        // actual search
        if (patternHash == textHash && areEqual(text, 0, pattern)) {
            return 0;
        } else {
            for (int i = 1; i < text.length()-pattern.length()+1; ++i) {
                textHash -= text.charAt(i-1)*pow;
                textHash *= 10;
                textHash += text.charAt(i+pattern.length()-1);
                if (textHash == patternHash && areEqual(text, i, pattern)) {
                    return i;
                }
            }
            return -1;
        }
    }
}

A partir de alguns testes preliminares, minha hipótese parece ser empiricamente precisa, mas ainda não a vi escrita em lugar algum, por isso fico pensando ..

Estou esquecendo de algo?

elísio devorado
fonte
2
O grande problema é provavelmente que queremos fazer todos os módulos de computação Q, presumivelmente, um grande prime próximo ao MAXINT. Presumivelmente, isso deve resultar em uma melhor função de hash. No entanto, é difícil saber, pois não sei qual é o seu algoritmo de referência - existem muitas variantes de Rabin-Karp. Também prefiro não ler código Java. Certamente você pode resumir seu algoritmo em pseudocódigo.
Yuval Filmus

Respostas:

10

Sim, na prática, você pode se dar bem apenas deixando os cálculos transbordarem. Você está efetivamente trabalhando módulo232.. Ele também tem a vantagem de não exigir um cálculo de módulo (caro). No entanto, falta algumas das garantias teóricas de desempenho. Você precisa ter muito cuidado com a escolha da base (neste caso:10) em relação ao módulo.

Em particular, sua escolha de 10é muito pobre. Observe que1032.=232.532., tão 1032. mod 232.=0 0. Isso significa que apenas o último32. caracteres da string são levados em consideração no hash, para que se possa construir uma entrada na qual seu algoritmo tenha um desempenho muito ruim.

Deixe o palheiro ser uma sequência de m 1 1é 1111111 e a agulha uma corda consistindo de n 1 1é um 0 0, e depois 32. 1 1's. Como a sequência termina com32. 1 1todas as posições resultarão em um golpe falso, e o algoritmo precisará passar por cima n 1 1antes de encontrar um zero, o que significa que você receberá um Ω(nm) tempo de execução.

Testei seu algoritmo em uma entrada em que n=3000,m=n2=9106. Levou18 segundos para executar em uma entrada que terminou em 32. 1, mas apenas 200ms para uma sequência terminada em 31 1 1's.

O problema é que 10não é relativamente primordial para o módulo. Por exemplo, tomar9 como a base melhora o desempenho do seu programa, levando apenas 200ms para o caso com 32. 1 1's. Obviamente, tomar um módulo primo resolverá parcialmente esse problema, já que a base será automaticamente relativamente privilegiada. No entanto, este não é o único motivo para preferir um módulo principal.

Agora, mesmo que o módulo n e base bsão relativamente excelentes, coisas indesejáveis ​​ainda podem acontecer. Por exemplo, há umk para qual bk=1 1 mod n. É indesejável parak ser pequeno, pois a função hash não pode distinguir todos os Euº personagem de todos Eu+kºpersonagem. Em termos matemáticos, você deseja a ordem deb mod n ser o maior possível.

A ordem de b mod n é sempre no máximo a função Euler-Phi ϕ(n). Para um primop, ϕ(p)=p-1 1 enquanto para não primos nserá menor. Então, tomandon ser primo permitirá mais dos valores de bkSer útil". Idealmente, deve-se tomarb ser um módulo raiz primitivo nfazendo isso bk=1 1 mod n não vale para nenhum valor de 0 0<k<ϕ(n).

Observe que você sempre pode construir instâncias para as quais o desempenho é ruim e, para se proteger contra "ataques" de um adversário, é necessário que a base e o módulo sejam valores aleatórios.

Tom van der Zanden
fonte
Uma excelente resposta. Eu gostaria de acrescentar isso, porQ=2k, existe a string Thue-Morse : para arbitráriop, possui substrings curtos, indistinguíveis por hash polinomial. Por exemplo, comQ=264, os substrings que terminam em múltiplos de 4096=212 todos terão zero hashes, independentemente de p. Aqui está uma explicação popular.
Gassa