Muss ich mich beim Rabin-Karp wirklich darum kümmern, eine Mod-Q-Operation auf die rollenden Hashes anzuwenden?


8

Ich habe über den Rabin Karp-Algorithmus gelesen und mich immer wieder gefragt, was die große Sache ist, wenn unsere rollenden Hashes durch einen Wert Q begrenzt werden.

Ich hatte gedacht, dass unsere Ganzzahlendarstellung auf dem typischen Computer 2-Komplement ist, was genau der Begrenzung aller unserer Operationen über die rollenden Hashes durch 2 ^ 31 entspricht, sodass es mich mit anderen Worten einfach nicht interessieren sollte. Je kleiner wir binden oder haschen, desto mehr Kollisionen hätten wir. Ein größeres Q sollte also gleichbedeutend mit einer verbesserten Leistung sein!

Ich habe versucht, eine einfache (Java) Implementierung zu codieren:

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;
        }
    }
}

Nach einigen vorläufigen Tests scheint meine Hypothese empirisch korrekt zu sein, aber ich habe sie nirgendwo geschrieben gesehen, also wundere ich mich.

Vermisse ich etwas


2
Die große Sache ist wahrscheinlich, dass wir alle Berechnungen modulo machen wollen Q., vermutlich eine große Primzahl in der Nähe von MAXINT. Das sollte vermutlich zu einer besseren Hash-Funktion führen. Es ist jedoch schwer zu wissen, da ich nicht weiß, was Ihr Referenzalgorithmus ist - es gibt viele Varianten von Rabin-Karp. Ich lese auch lieber keinen Java-Code. Sicherlich können Sie Ihren Algorithmus stattdessen im Pseudocode zusammenfassen.
Yuval Filmus

Antworten:


10

Ja, in der Praxis kommt man gut zurecht, wenn man nur die Berechnungen überlaufen lässt. Sie arbeiten effektiv Modulo232. Es hat auch den Vorteil, dass keine (teure) Modulo-Berechnung erforderlich ist. Es fehlen jedoch einige der theoretischen Leistungsgarantien. Sie müssen bei der Auswahl der Basis sehr vorsichtig sein (in diesem Fall:10) in Bezug auf den Modul.

Insbesondere Ihre Wahl von 10ist sehr arm. Beachten Sie, dass1032=232532, damit 1032 mod 232=0. Dies bedeutet, dass nur die letzten32 Zeichen der Zeichenfolge werden im Hash berücksichtigt, sodass eine Eingabe erstellt werden kann, für die Ihr Algorithmus eine sehr schlechte Leistung erbringt.

Lassen Sie den Heuhaufen eine Kette von sein m 1's, dh 1111111 und die Nadel eine Schnur bestehend aus n 1eins 0, und dann 32 1's. Weil die Zeichenfolge mit endet32 1Jede Position führt zu einem falschen Treffer, und der Algorithmus muss eine Schleife durchlaufen n 1Bevor Sie auf eine Null stoßen, erhalten Sie eine Ω(nm) Laufzeit.

Ich habe Ihren Algorithmus an einer Eingabe getestet, an der n=3000,m=n2=9106. Es dauerte18 Sekunden, um auf einem Eingang zu laufen, der mit endete 32 Einsen, aber nur 200ms für eine Zeichenfolge, die auf endet 31 1's.

Das Problem ist, dass 10ist nicht relativ prim zum Modul. Zum Beispiel nehmen9 da die Basis Ihr Programm viel besser macht, nur nehmen 200ms für den Fall mit 32 1's. Natürlich löst die Verwendung eines Primmoduls dieses Problem teilweise, da die Basis automatisch relativ prim ist. Dies ist jedoch nicht der einzige Grund, einen Primzahlmodul zu bevorzugen.

Nun, auch wenn der Modul n und Basis bsind relativ erstklassig, unerwünschte Dinge können immer noch passieren. Zum Beispiel gibt es einek für welche bk=1 mod n. Es ist unerwünscht fürk klein sein, da die Hash-Funktion nicht jeden unterscheiden kann ichth Charakter von jedem ich+kthCharakter. In mathematischen Begriffen möchten Sie die Reihenfolge vonb mod n so groß wie möglich sein.

Die Reihenfolge von b mod n ist immer höchstens die Euler-Phi-Funktion ϕ(n). Für eine Primzahlp, ϕ(p)=p- -1 während für Nicht-Primzahlen nes wird kleiner sein. Also nehmenn eine Primzahl zu sein, erlaubt mehr von den Werten von bkum nützlich zu sein". Idealerweise sollte man nehmenb ein primitives Wurzelmodulo sein n, das zu machen bk=1 mod n gilt nicht für einen Wert von 0<k<ϕ(n).

Beachten Sie, dass Sie immer Instanzen erstellen können, für die die Leistung schlecht ist. Um sich vor "Angriffen" durch einen Gegner zu schützen, müssen Sie die Basis und den Modul als zufällige Werte verwenden.


Eine ausgezeichnete Antwort. Ich möchte das hinzufügen, zQ.=2kgibt es die Thue-Morse- Zeichenfolge: für beliebigepEs hat kurze Teilzeichenfolgen, die durch Polynom-Hashing nicht zu unterscheiden sind. Zum Beispiel mitQ.=264, die Teilzeichenfolgen, die auf Vielfachen von enden 4096=212 haben alle null Hashes, unabhängig davon p. Hier ist eine beliebte Erklärung.
Gassa
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.