Wie überprüfe ich, ob zwei Zeichenfolgen Permutationen voneinander sind, indem ich O (1) zusätzlichen Raum verwende?


13

Wie können Sie bei zwei gegebenen Zeichenfolgen überprüfen, ob sie eine Permutation voneinander sind, indem Sie den O (1) -Raum verwenden? Das Ändern der Zeichenfolgen ist in keiner Weise zulässig.
Anmerkung: O (1) Abstand in Bezug sowohl auf die Zeichenkettenlänge als auch auf die Größe des Alphabets.


3
Was denkst du? Was hast du versucht und wo steckst du fest? Befinden sich die Zeichenfolgen über einem Alphabet mit konstanter Größe? Haben Sie versucht, ihre Histogramme zu berechnen?
Yuval Filmus

@YuvalFilmus es sollte O (1) Leerzeichen sowohl für die Länge der Zeichenfolge als auch für die Größe des Alphabets sein
Anonym

Dies scheint eindeutig unmöglich. Jeder Algorithmus benötigt zusätzlichen Speicherplatz, um mindestens eine Position in einer Zeichenfolge oder einem einzelnen Zeichen zu speichern. Keines dieser Dinge ist O (1).
David Schwartz

@DavidSchwartz - wie? O (1) bedeutet konstant, nicht ein Buten. Es spielt keine Rolle, wie lang die Zeichenfolge ist, die Position darin ist eine Zahl.
Davor

Es hängt vom Maschinenmodell ab, offensichtlich kein Problem bei einheitlichen Modellen. In einem logarithmischen Kostenmodell wird der Index O(log n)für Zeichenfolgen der Länge n gespeichert, die weder über die Länge noch über die Alphabetgröße konstant ist. Wenn die Zeichenfolgen vorübergehend geändert werden können, gibt es meines Erachtens eine Lösung mit vergrößertem Alphabet, das in der Alphabetgröße linear ist, in einem logarithmischen Modell jedoch eine konstante Zeichenfolgenlänge aufweist.
kap

Antworten:


7

Der naive Ansatz würde darin bestehen, Histogramme beider Zeichenfolgen zu erstellen und zu überprüfen, ob sie gleich sind. Da wir keine Datenstruktur speichern dürfen (deren Größe linear zur Größe des Alphabets wäre), die in einem Durchgang berechnet werden könnte, müssen wir die Vorkommen der einzelnen möglichen Symbole nacheinander zählen:

function count(letter, string)
    var count := 0
    foreach element in string
        if letter = element
            count++
    return count

function samePermutation(stringA, stringB)
    foreach s in alphabet
        if count(s, stringA) != count(s, stringB)
            return false
    return true

Dies setzt natürlich voraus, dass die Zählwerte und Iteratorindizes ganze Zahlen konstanter Größe sind, anstatt von der Länge der Zeichenfolgen abhängig zu sein.


Als Optimierung können Sie ein Array durchgehen und nur die Histogramme der Buchstaben berechnen, auf die Sie stoßen. Auf diese Weise wird die Komplexität unabhängig von der Alphabetgröße.
Yuval Filmus

Um den @YuvalFilmus-Kommentar zu erweitern, müssen Sie auch 1) überprüfen, ob die Zeichenfolgenlängen gleich sind, oder 2) über beide Eingabezeichenfolgen iterieren. Du brauchst eines davon, da es möglich ist, dass einige Buchstaben in einem nicht im anderen sind. Option 1 sollte weniger Berechnungen enthalten.
BurnsBA

@YuvalFilmus Ich wollte vermeiden, dass das Alphabet kleiner als die durchschnittliche Zeichenfolge ist, da dies eine quadratische Zeitkomplexität bedeuten würde. Für kleine Zeichenfolgen und ein geordnetes Alphabet würde ich in Betracht ziehen, das nächstkleinere aktuelle Symbol zusammen mit der Anzahl in der inneren Schleife zu berechnen, damit ein paar Iterationen der Alphabetschleife übersprungen werden können - mit einer Komplexität von O(n * min(n, |Σ|)). Hm, jetzt, wo ich darüber nachdenke, hört sich das nach der "darf wiederholen" -Lösung aus Ihrer Antwort an, nicht wahr?
Bergi

countist nicht O(1)(dh es kann überlaufen)
Reinierpost

1
@Eternalcode Ich habe nie gesagt, dass countdas ein war int:-) Ja, es würde nicht funktionieren, aber in Java kann das sowieso nicht passieren
Bergi

12

Bezeichnen Sie die Arrays mit und nehmen Sie an, dass sie die Länge n haben .EIN,Bn

Nehmen wir zunächst an, dass die Werte in jedem Array unterschiedlich sind. Hier ist ein Algorithmus, der den -Raum verwendet:Ö(1)

  1. Berechnen Sie die Mindestwerte beider Arrays und überprüfen Sie, ob sie identisch sind.

  2. Berechnen Sie die zweiten Mindestwerte beider Arrays und überprüfen Sie, ob sie identisch sind.

  3. Und so weiter.

Bei der Berechnung des Mindestwerts eines Arrays wird eindeutig der Abstand verwendet. Ausgehend von dem k- ten kleinsten Element können wir ( k + 1 findenÖ(1)kindemden Minimalwert finden, der größer als das k- te kleinste Element ist (hier verwenden wir die Tatsache, dass alle Elemente verschieden sind).(k+1)k

Wenn sich Elemente wiederholen dürfen, ändern wir den Algorithmus wie folgt:

  1. Berechnen Sie die Mindestwerte beider Arrays, zählen Sie, wie oft sie jeweils auftreten, und überprüfen Sie, ob m A , 1 = m B istmEIN,1,mB,1 und dass die Zählungen identisch sind.mEIN,1=mB,1

  2. Berechnen Sie die Mindestwerte größer als m A , 1 , m B , 1 in den beiden Arrays sind, und zählen Sie, wie oft sie jeweils auftreten. Stellen Sie sicher, dass m A , 2 = m B , 2 ist und dass die Zählungen identisch sind.mEIN,2,mB,2mEIN,1,mB,1mEIN,2=mB,2

  3. Und so weiter.


1
Wäre dieser Ansatz da der einzige Weg, das Element min im O ( 1 ) -Raum zu finden und schreibgeschützt auf das Array zuzugreifen, darin besteht, alle Elemente zu durchlaufen? Ö(n2)Ö(1)
Ryan

4
Dies erfordert eine Sortierung nach dem Alphabet, obwohl es einfach ist, den Algorithmus zu ändern, um dies nicht zu erfordern. In dem Fall "hat Duplikate" erfordert dies jedoch Raum, nicht O ( 1 ) . Zählen braucht Platz. Ö(lgn)Ö(1)
Derek Elkins verließ SE am

7
Das Zählen benötigt zwar (logarithmischen) Speicherplatz, aber - nach dieser Definition der Speicherplatznutzung - auch das Iterieren über das Array. Unter der strengen Bedeutung der Raumnutzung gibt es daher keine Möglichkeit, dies im konstanten Raum zu tun.
Daniel Jour

4
@ DanielJour, es hängt davon ab, welches Kostenmodell Sie verwenden. Bei gleichmäßigen Kosten ist dies bei konstantem Raum möglich.
Ryan

7
Wenn Sie nur eine konstante Anzahl von Bits haben, können Sie nur Alphabete mit konstanter Größe verarbeiten (dies folgt aus der Theorie der regulären Sprachen).
Yuval Filmus

2

Definieren Sie eine Funktion f (c), die ein Zeichen c einer eindeutigen Primzahl zuordnet (a = 2, b = 3, c = 5 usw.).

set checksum = 1
set count = 0 <-- this is probably not even necessary, but it's another level of check
for character c in string 1
    checksum = checksum * f(c)
    count = count + 1
for character c in string 2
    checksum = checksum / f(c)
    count = count = 1

permutation = count == 0 and checksum == 1

Nur zu erklären, dass Sie eine Primzahlzuordnungsfunktion verwenden können, ist ein bisschen handwavey, und am wahrscheinlichsten, wenn ein Problem auftreten würde, Raum zu halten.Ö(1)


Mit einer Schranke am Alphabet sollte den O ( 1 ) -Raum verwenden, sonst würde es meiner Meinung nach kein konstanter Raum sein. Wenn Sie es außerdem im O ( 1 ) -Raum berechnen würden, wäre es auf der Grundlage der aktuellen Ergebnisse äußerst ineffizient . Trotzdem +1 für den Primalitätsansatz. f(c)Ö(1)Ö(1)
Ryan

Ein weiteres Problem, das ich nach dem Posten festgestellt habe, ist, dass die Prüfsumme für große Zeichenfolgen eine gigantische Zahl sein wird, insofern, als sie für sich genommen den Platzbedarf von O (1) verletzen könnte. Dies kann gelöst werden, indem Floats verwendet und durch ein Zeichen in einer Zeichenfolge multipliziert und die andere geteilt wird. Dann muss die Prüfsumme nahe 1 liegen. Die Zeichenfolgen müssten wirklich gigantisch sein, damit ein Gleitkommafehler ein Problem darstellt.
Alex Stasse

4
Solche Antworten sind der Grund, warum wir unser Berechnungsmodell sorgfältig durcharbeiten müssen. Das übliche Modell, das wir bei der Analyse von Algorithmen verwenden, zählt den Speicher in Einheiten von Maschinenwörtern , die Bits der Größe . Sie können die Berechnung also nicht in ganzen Zahlen durchführen. Wenn Sie auf Gleitkomma umschalten, schlägt Ihr Algorithmus möglicherweise auch dann fehl, wenn die beiden Zeichenfolgen Permutationen voneinander sind, und gibt umgekehrt nicht unbedingt die richtige Antwort, wenn dies nicht der Fall ist. Ö(Logn)
Yuval Filmus

4
Dies belegt keinen konstanten Raum. Selbst für ein festes Alphabet, die Größe der ganzen Zahl Prüfsumme wird sein Bits für die Eingaben der Länge n . Θ(n)n
David Richerby

0

Sie können dies tun O(nlogn). Sortieren Sie die beiden Zeichenfolgen und vergleichen Sie sie indexweise. Wenn sie sich irgendwo unterscheiden, sind sie keine Permutationen voneinander.

Für eine O(n)Lösung könnte Hashing verwendet werden. Diese Hash-Funktion würde funktionieren, und efür jeden Buchstaben wäre dies der ASCII-Wert. Wenn sich die beiden Hashes der Zeichenfolgen unterscheiden, sind sie keine Permutationen voneinander.

Die Hash-Funktion im Link:

Ein möglicher Kandidat könnte dies sein. Legen Sie eine ungerade Ganzzahl R fest. Berechnen Sie für jedes Element e, das Sie hashen möchten, den Faktor (R + 2 * e). Berechnen Sie dann das Produkt all dieser Faktoren. Teilen Sie das Produkt zum Schluss durch 2, um den Hash zu erhalten.

Der Faktor 2 in (R + 2e) garantiert, dass alle Faktoren ungerade sind, wodurch vermieden wird, dass das Produkt jemals zu 0 wird. Die Division durch 2 am Ende liegt daran, dass das Produkt immer ungerade ist, sodass die Division nur ein konstantes Bit entfernt .

ZB wähle ich R = 1779033703. Dies ist eine willkürliche Wahl. Einige Experimente sollten zeigen, ob ein gegebenes R gut oder schlecht ist. Angenommen, Ihre Werte sind [1, 10, 3, 18]. Das Produkt (berechnet mit 32-Bit-Ints) ist

(R + 2) * (R + 20) * (R + 6) * (R + 36) = 3376724311 Daher wäre der Hash

3376724311/2 = 1688362155.

Durch die Verwendung von Double-Hashing (oder für noch mehr Overkill) durch Ändern des Werts von R würden sie erfolgreich als Permutationen mit sehr hoher Wahrscheinlichkeit identifiziert .


1
Sie können die Zeichenfolgen nicht sortieren, da Sie sie nicht ändern dürfen. Beim Hashing handelt es sich um einen zufälligen Algorithmus, der die falsche Antwort geben kann.
Yuval Filmus

0

Angenommen, Sie haben zwei Zeichenfolgen mit den Namen s und t.

Sie können Heuristiken verwenden, um sicherzustellen, dass sie nicht ungleich sind.

  1. Länge == Länge
  2. Summe der Zeichen von s == Summe der Zeichen in t
  3. [das selbe wie in 2. aber mit xor statt sum]

Danach können Sie leicht einen Algorithmus ausführen, um zu beweisen, dass die Zeichenfolge gleich ist.

  1. sortiere einen String so, dass er gleich dem anderen ist und vergleiche (O (n ^ 2))
  2. sortiere beide und vergleiche (O (2n log (n))
  3. überprüfe für jedes Zeichen in s, ob es in beiden Zeichenketten die gleichen Beträge gibt (O (n ^ 2))

Natürlich können Sie nicht so schnell sortieren, wenn Sie keinen zusätzlichen Speicherplatz verwenden dürfen. Es spielt also keine Rolle, welchen Algorithmus Sie wählen - jeder benötigte Algorithmus wird in O (n ^ 2) ausgeführt, wenn nur O (1) Platz vorhanden ist und die Heuristik nicht beweisen konnte, dass sie nicht gleich sein können.


3
"Das Ändern der Zeichenfolgen ist in keiner Weise zulässig. "
Bergi

0

Im C-Code für die gesamte Routine:

for (int i = 0; i < n; i++) {
   int k = -1;
   next: for (int j = 0; j <= i; j++)
       if (A[j] == A[i]) {
          while (++k < n)
              if (B[k] == A[i])
                  continue next;
          return false; // note at this point j == i
       }
}
return true; 

Oder in sehr ausführlichem Pseudocode (mit 1-basierter Indizierung)

// our loop invariant is that B contains a permutation of the letters
// in A[1]..A[i-1]
for i=1..n
   if !checkLetters(A, B, i)
      return false
return true

Dabei prüft die Funktion checkLetters (A, B, i), ob in A [1] .. A [i] M Kopien von A [i] vorhanden sind. In B sind dann mindestens M Kopien von A [i] vorhanden:

checkLetters(A,B,i)
    k = 0 // scan index into B
    for j=1..i
      if A[j] = A[i]
         k = findNextValue(B, k+1, A[i])
         if k > n
            return false
    return true

Die Funktion findNextValue sucht in B nach einem Wert, der von einem Index ausgeht, und gibt den Index an der Stelle zurück, an der er gefunden wurde (oder n + 1, wenn er nicht gefunden wurde).

Die Idee hier ist, dass wir bereits bestätigt haben, dass wir eine Permutation der Zeichen vor i in der äußeren Schleife haben. In der j-Schleife durchlaufen wir alle Zeichen, die mit A [i] übereinstimmen, und müssen in der Lage sein, eindeutige Indizes in B zu finden, die mit ihnen übereinstimmen (einschließlich der i-ten Position). Daher müssen wir für jeden von ihnen unseren Scan um B vorwärts bewegen. Die Zeit ist O (n2).


Können Sie bitte Ihren C-Code in Pseudocode umwandeln? Dies ist keine Programmierseite.
Yuval Filmus

Dies scheint eine andere Variante von Bergis Antwort zu sein (mit einigen unwichtigen Unterschieden).
Yuval Filmus

Es ist ähnlich, aber keine Variante. Bergis Antwort istÖ(nm)Wobei m = Alphabetgröße. Das istÖ(n2).
MotiN

0

Ich denke das ist der einfachste Algorithmus (mit Ö(n3) Zeit, n Länge der Saiten)

Durchlaufe string1und string2, überprüfe für jeden Charakter, wie oft er in string1und zu finden iststring2 . Wenn ein Zeichen in einer Zeichenfolge häufiger vorkommt als in der anderen, handelt es sich nicht um eine Permutation. Wenn die Frequenzen aller Zeichen gleich sind, sind die Zeichenfolgen Permutationen voneinander.

Hier ist ein Stück Python, um dies zu präzisieren

s1="abcaba"
s2="aadbba"

def check_if_permutations(string1, string2):
  for string in [string1, string2]:
    # string references string1 
    #  string2, it is not a copy
    for char in string:
      count1=0
      for char1 in string1:
        if  char==char1:
          count1+=1
      count2=0
      for char2 in string2:
        if  char==char2:
          count2+=1
      if count1!=count2:
        print('unbalanced character',char)
        return()
  print ("permutations")
  return()

check_if_permutations(s1,s2)

Das Programm benötigt einige Hinweise auf Strings ( string, string1, string2, char, char1, char2) und Variablen der GrößeÖ(Logn)zum Zählen ( count1, count2). Es muss überprüft werden, ob die Zeichen gleich sind oder nicht, aber es muss keine Reihenfolge für diese Zeichen festgelegt werden. Vielleicht braucht es einige Variablen für kleine ganze Zahlen (zB um boolesche Werte zu speichern oder die Position von stringin darzustellen[string1, string2] .

Natürlich brauchen Sie nicht einmal die Zählvariablen, sondern können Zeiger verwenden.

s1="abcaba"
s2="aadbba"

def check_if_permutations(string1, string2):
  for string in [string1, string2]:
    # string references one of string1 
    # or string2, it is not a copy
    for char in string:
      # p1 and p2 should be views as pointers
      p1=0
      p2=0
      while (p1<len(string1)) and (p2<len(string2)):
        # p1>=len(string1): p1 points to beyond end of string
        while (p1<len(string1)) and (string1[p1]!=char) :
          p1+=1
        while(p2<len(string2)) and (string2[p2]!=char):
          p2+=1
        if (p1<len(string1)) != (p2<len(string2)):
          print('unbalanced character',char)
          return()
        p1+=1
        p2+=1
  print ("permutations")
  return()

check_if_permutations(s1,s2)

Dieses zweite Programm benötigt ähnliche Variablen wie das erste, außer dass es das nicht benötigt Ö(Log(n))-size Variablen zum Speichern der Zählwerte.

Also kommt es eigentlich nicht darauf an n oder die Größe des Alphabets.


Dies ist die gleiche Lösung wie bei Bergi.
Yuval Filmus

@YuvalFilmus Nein, es wird nicht über das gesamte Alphabet iteriert, daher hängt die Laufzeit nicht von der Alphabetgröße ab. Es werden nur die beiden zu testenden Zeichenfolgen verwendet. Auch das zweite Programm vermeidet das Zählen.
miracle173

@YuvalFilmus Ich sehe jetzt, dass Ihre und andere Kommentare in die Richtung weisen, wie ich sie in meinem Programm verwendet habe.
miracle173
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.